container.go 6.0 KB

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