container.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. package layout
  2. import (
  3. tea "github.com/charmbracelet/bubbletea/v2"
  4. "github.com/charmbracelet/lipgloss/v2"
  5. "github.com/sst/opencode/internal/styles"
  6. "github.com/sst/opencode/internal/theme"
  7. )
  8. type Container interface {
  9. tea.Model
  10. tea.ViewModel
  11. Sizeable
  12. Focusable
  13. Alignable
  14. }
  15. type container struct {
  16. width int
  17. height int
  18. x int
  19. y int
  20. content tea.ViewModel
  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. if model, ok := c.content.(tea.Model); ok {
  36. return model.Init()
  37. }
  38. return nil
  39. }
  40. func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  41. if model, ok := c.content.(tea.Model); ok {
  42. u, cmd := model.Update(msg)
  43. c.content = u.(tea.ViewModel)
  44. return c, cmd
  45. }
  46. return c, nil
  47. }
  48. func (c *container) View() string {
  49. t := theme.CurrentTheme()
  50. style := styles.NewStyle().Background(t.Background())
  51. width := c.width
  52. height := c.height
  53. // Apply max width constraint if set
  54. if c.maxWidth > 0 && width > c.maxWidth {
  55. width = c.maxWidth
  56. }
  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. }