container.go 6.0 KB

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