container.go 6.0 KB

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