container.go 5.8 KB

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