container.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package layout
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. tea "github.com/charmbracelet/bubbletea"
  5. "github.com/charmbracelet/lipgloss"
  6. "github.com/sst/opencode/internal/tui/theme"
  7. )
  8. type Container interface {
  9. tea.Model
  10. Sizeable
  11. Bindings
  12. Focus() // Add focus method
  13. Blur() // Add blur method
  14. }
  15. type container struct {
  16. width int
  17. height int
  18. content tea.Model
  19. // Style options
  20. paddingTop int
  21. paddingRight int
  22. paddingBottom int
  23. paddingLeft int
  24. borderTop bool
  25. borderRight bool
  26. borderBottom bool
  27. borderLeft bool
  28. borderStyle lipgloss.Border
  29. focused bool // Track focus state
  30. }
  31. func (c *container) Init() tea.Cmd {
  32. return c.content.Init()
  33. }
  34. func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  35. u, cmd := c.content.Update(msg)
  36. c.content = u
  37. return c, cmd
  38. }
  39. func (c *container) View() string {
  40. t := theme.CurrentTheme()
  41. style := lipgloss.NewStyle()
  42. width := c.width
  43. height := c.height
  44. style = style.Background(t.Background())
  45. // Apply border if any side is enabled
  46. if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
  47. // Adjust width and height for borders
  48. if c.borderTop {
  49. height--
  50. }
  51. if c.borderBottom {
  52. height--
  53. }
  54. if c.borderLeft {
  55. width--
  56. }
  57. if c.borderRight {
  58. width--
  59. }
  60. style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
  61. // Use primary color for border if focused
  62. if c.focused {
  63. style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
  64. } else {
  65. style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
  66. }
  67. }
  68. style = style.
  69. Width(width).
  70. Height(height).
  71. PaddingTop(c.paddingTop).
  72. PaddingRight(c.paddingRight).
  73. PaddingBottom(c.paddingBottom).
  74. PaddingLeft(c.paddingLeft)
  75. return style.Render(c.content.View())
  76. }
  77. func (c *container) SetSize(width, height int) tea.Cmd {
  78. c.width = width
  79. c.height = height
  80. // If the content implements Sizeable, adjust its size to account for padding and borders
  81. if sizeable, ok := c.content.(Sizeable); ok {
  82. // Calculate horizontal space taken by padding and borders
  83. horizontalSpace := c.paddingLeft + c.paddingRight
  84. if c.borderLeft {
  85. horizontalSpace++
  86. }
  87. if c.borderRight {
  88. horizontalSpace++
  89. }
  90. // Calculate vertical space taken by padding and borders
  91. verticalSpace := c.paddingTop + c.paddingBottom
  92. if c.borderTop {
  93. verticalSpace++
  94. }
  95. if c.borderBottom {
  96. verticalSpace++
  97. }
  98. // Set content size with adjusted dimensions
  99. contentWidth := max(0, width-horizontalSpace)
  100. contentHeight := max(0, height-verticalSpace)
  101. return sizeable.SetSize(contentWidth, contentHeight)
  102. }
  103. return nil
  104. }
  105. func (c *container) GetSize() (int, int) {
  106. return c.width, c.height
  107. }
  108. func (c *container) BindingKeys() []key.Binding {
  109. if b, ok := c.content.(Bindings); ok {
  110. return b.BindingKeys()
  111. }
  112. return []key.Binding{}
  113. }
  114. // Focus sets the container as focused
  115. func (c *container) Focus() {
  116. c.focused = true
  117. // Pass focus to content if it supports it
  118. if focusable, ok := c.content.(interface{ Focus() }); ok {
  119. focusable.Focus()
  120. }
  121. }
  122. // Blur removes focus from the container
  123. func (c *container) Blur() {
  124. c.focused = false
  125. // Remove focus from content if it supports it
  126. if blurable, ok := c.content.(interface{ Blur() }); ok {
  127. blurable.Blur()
  128. }
  129. }
  130. type ContainerOption func(*container)
  131. func NewContainer(content tea.Model, options ...ContainerOption) Container {
  132. c := &container{
  133. content: content,
  134. borderStyle: lipgloss.NormalBorder(),
  135. }
  136. for _, option := range options {
  137. option(c)
  138. }
  139. return c
  140. }
  141. // Padding options
  142. func WithPadding(top, right, bottom, left int) ContainerOption {
  143. return func(c *container) {
  144. c.paddingTop = top
  145. c.paddingRight = right
  146. c.paddingBottom = bottom
  147. c.paddingLeft = left
  148. }
  149. }
  150. func WithPaddingAll(padding int) ContainerOption {
  151. return WithPadding(padding, padding, padding, padding)
  152. }
  153. func WithPaddingHorizontal(padding int) ContainerOption {
  154. return func(c *container) {
  155. c.paddingLeft = padding
  156. c.paddingRight = padding
  157. }
  158. }
  159. func WithPaddingVertical(padding int) ContainerOption {
  160. return func(c *container) {
  161. c.paddingTop = padding
  162. c.paddingBottom = padding
  163. }
  164. }
  165. func WithBorder(top, right, bottom, left bool) ContainerOption {
  166. return func(c *container) {
  167. c.borderTop = top
  168. c.borderRight = right
  169. c.borderBottom = bottom
  170. c.borderLeft = left
  171. }
  172. }
  173. func WithBorderAll() ContainerOption {
  174. return WithBorder(true, true, true, true)
  175. }
  176. func WithBorderHorizontal() ContainerOption {
  177. return WithBorder(true, false, true, false)
  178. }
  179. func WithBorderVertical() ContainerOption {
  180. return WithBorder(false, true, false, true)
  181. }
  182. func WithBorderStyle(style lipgloss.Border) ContainerOption {
  183. return func(c *container) {
  184. c.borderStyle = style
  185. }
  186. }
  187. func WithRoundedBorder() ContainerOption {
  188. return WithBorderStyle(lipgloss.RoundedBorder())
  189. }
  190. func WithThickBorder() ContainerOption {
  191. return WithBorderStyle(lipgloss.ThickBorder())
  192. }
  193. func WithDoubleBorder() ContainerOption {
  194. return WithBorderStyle(lipgloss.DoubleBorder())
  195. }