container.go 5.9 KB

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