container.go 4.9 KB

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