modal.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package modal
  2. import (
  3. "github.com/charmbracelet/lipgloss/v2"
  4. "github.com/sst/opencode/internal/layout"
  5. "github.com/sst/opencode/internal/styles"
  6. "github.com/sst/opencode/internal/theme"
  7. )
  8. // CloseModalMsg is a message to signal that the active modal should be closed.
  9. type CloseModalMsg struct{}
  10. // Modal is a reusable modal component that handles frame rendering and overlay placement
  11. type Modal struct {
  12. width int
  13. height int
  14. title string
  15. maxWidth int
  16. maxHeight int
  17. fitContent bool
  18. }
  19. // ModalOption is a function that configures a Modal
  20. type ModalOption func(*Modal)
  21. // WithTitle sets the modal title
  22. func WithTitle(title string) ModalOption {
  23. return func(m *Modal) {
  24. m.title = title
  25. }
  26. }
  27. // WithMaxWidth sets the maximum width
  28. func WithMaxWidth(width int) ModalOption {
  29. return func(m *Modal) {
  30. m.maxWidth = width
  31. m.fitContent = false
  32. }
  33. }
  34. // WithMaxHeight sets the maximum height
  35. func WithMaxHeight(height int) ModalOption {
  36. return func(m *Modal) {
  37. m.maxHeight = height
  38. }
  39. }
  40. func WithFitContent(fit bool) ModalOption {
  41. return func(m *Modal) {
  42. m.fitContent = fit
  43. }
  44. }
  45. // New creates a new Modal with the given options
  46. func New(opts ...ModalOption) *Modal {
  47. m := &Modal{
  48. maxWidth: 0,
  49. maxHeight: 0,
  50. fitContent: true,
  51. }
  52. for _, opt := range opts {
  53. opt(m)
  54. }
  55. return m
  56. }
  57. // Render renders the modal centered on the screen
  58. func (m *Modal) Render(contentView string, background string) string {
  59. t := theme.CurrentTheme()
  60. outerWidth := layout.Current.Container.Width - 8
  61. if m.maxWidth > 0 && outerWidth > m.maxWidth {
  62. outerWidth = m.maxWidth
  63. }
  64. if m.fitContent {
  65. titleWidth := lipgloss.Width(m.title)
  66. contentWidth := lipgloss.Width(contentView)
  67. largestWidth := max(titleWidth+2, contentWidth)
  68. outerWidth = largestWidth + 6
  69. }
  70. innerWidth := outerWidth - 4
  71. // Base style for the modal
  72. baseStyle := styles.BaseStyle().
  73. Background(t.BackgroundElement()).
  74. Foreground(t.TextMuted())
  75. // Add title if provided
  76. var finalContent string
  77. if m.title != "" {
  78. titleStyle := baseStyle.
  79. Foreground(t.Primary()).
  80. Bold(true).
  81. Width(innerWidth).
  82. Padding(0, 1)
  83. titleView := titleStyle.Render(m.title)
  84. finalContent = lipgloss.JoinVertical(
  85. lipgloss.Left,
  86. titleView,
  87. contentView,
  88. )
  89. } else {
  90. finalContent = contentView
  91. }
  92. modalStyle := baseStyle.
  93. PaddingTop(1).
  94. PaddingBottom(1).
  95. PaddingLeft(2).
  96. PaddingRight(2).
  97. BorderStyle(lipgloss.ThickBorder()).
  98. BorderLeft(true).
  99. BorderRight(true).
  100. BorderLeftForeground(t.BackgroundSubtle()).
  101. BorderLeftBackground(t.Background()).
  102. BorderRightForeground(t.BackgroundSubtle()).
  103. BorderRightBackground(t.Background())
  104. modalView := modalStyle.
  105. Width(outerWidth).
  106. Render(finalContent)
  107. // Calculate position for centering
  108. bgHeight := lipgloss.Height(background)
  109. bgWidth := lipgloss.Width(background)
  110. modalHeight := lipgloss.Height(modalView)
  111. modalWidth := lipgloss.Width(modalView)
  112. row := (bgHeight - modalHeight) / 2
  113. col := (bgWidth - modalWidth) / 2
  114. return layout.PlaceOverlay(
  115. col,
  116. row,
  117. modalView,
  118. background,
  119. )
  120. }