flex.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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 FlexDirection int
  8. const (
  9. FlexDirectionHorizontal FlexDirection = iota
  10. FlexDirectionVertical
  11. )
  12. type FlexPaneSize struct {
  13. Fixed bool
  14. Size int
  15. }
  16. var FlexPaneSizeGrow = FlexPaneSize{Fixed: false}
  17. func FlexPaneSizeFixed(size int) FlexPaneSize {
  18. return FlexPaneSize{Fixed: true, Size: size}
  19. }
  20. type FlexLayout interface {
  21. ModelWithView
  22. Sizeable
  23. SetPanes(panes []Container) tea.Cmd
  24. SetPaneSizes(sizes []FlexPaneSize) tea.Cmd
  25. SetDirection(direction FlexDirection) tea.Cmd
  26. }
  27. type flexLayout struct {
  28. width int
  29. height int
  30. direction FlexDirection
  31. panes []Container
  32. sizes []FlexPaneSize
  33. }
  34. type FlexLayoutOption func(*flexLayout)
  35. func (f *flexLayout) Init() tea.Cmd {
  36. var cmds []tea.Cmd
  37. for _, pane := range f.panes {
  38. if pane != nil {
  39. cmds = append(cmds, pane.Init())
  40. }
  41. }
  42. return tea.Batch(cmds...)
  43. }
  44. func (f *flexLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  45. var cmds []tea.Cmd
  46. switch msg := msg.(type) {
  47. case tea.WindowSizeMsg:
  48. return f, f.SetSize(msg.Width, msg.Height)
  49. }
  50. for i, pane := range f.panes {
  51. if pane != nil {
  52. u, cmd := pane.Update(msg)
  53. f.panes[i] = u.(Container)
  54. if cmd != nil {
  55. cmds = append(cmds, cmd)
  56. }
  57. }
  58. }
  59. return f, tea.Batch(cmds...)
  60. }
  61. func (f *flexLayout) View() string {
  62. if len(f.panes) == 0 {
  63. return ""
  64. }
  65. t := theme.CurrentTheme()
  66. views := make([]string, 0, len(f.panes))
  67. for i, pane := range f.panes {
  68. if pane == nil {
  69. continue
  70. }
  71. var paneWidth, paneHeight int
  72. if f.direction == FlexDirectionHorizontal {
  73. paneWidth, paneHeight = f.calculatePaneSize(i)
  74. view := lipgloss.PlaceHorizontal(
  75. paneWidth,
  76. pane.Alignment(),
  77. pane.View(),
  78. lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
  79. )
  80. views = append(views, view)
  81. } else {
  82. paneWidth, paneHeight = f.calculatePaneSize(i)
  83. view := lipgloss.Place(
  84. f.width,
  85. paneHeight,
  86. lipgloss.Center,
  87. pane.Alignment(),
  88. pane.View(),
  89. lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
  90. )
  91. views = append(views, view)
  92. }
  93. }
  94. if f.direction == FlexDirectionHorizontal {
  95. return lipgloss.JoinHorizontal(lipgloss.Center, views...)
  96. }
  97. return lipgloss.JoinVertical(lipgloss.Center, views...)
  98. }
  99. func (f *flexLayout) calculatePaneSize(index int) (width, height int) {
  100. if index >= len(f.panes) {
  101. return 0, 0
  102. }
  103. totalFixed := 0
  104. flexCount := 0
  105. for i, pane := range f.panes {
  106. if pane == nil {
  107. continue
  108. }
  109. if i < len(f.sizes) && f.sizes[i].Fixed {
  110. if f.direction == FlexDirectionHorizontal {
  111. totalFixed += f.sizes[i].Size
  112. } else {
  113. totalFixed += f.sizes[i].Size
  114. }
  115. } else {
  116. flexCount++
  117. }
  118. }
  119. if f.direction == FlexDirectionHorizontal {
  120. height = f.height
  121. if index < len(f.sizes) && f.sizes[index].Fixed {
  122. width = f.sizes[index].Size
  123. } else if flexCount > 0 {
  124. remainingSpace := f.width - totalFixed
  125. width = remainingSpace / flexCount
  126. }
  127. } else {
  128. width = f.width
  129. if index < len(f.sizes) && f.sizes[index].Fixed {
  130. height = f.sizes[index].Size
  131. } else if flexCount > 0 {
  132. remainingSpace := f.height - totalFixed
  133. height = remainingSpace / flexCount
  134. }
  135. }
  136. return width, height
  137. }
  138. func (f *flexLayout) SetSize(width, height int) tea.Cmd {
  139. f.width = width
  140. f.height = height
  141. var cmds []tea.Cmd
  142. currentX, currentY := 0, 0
  143. for i, pane := range f.panes {
  144. if pane != nil {
  145. paneWidth, paneHeight := f.calculatePaneSize(i)
  146. // Calculate actual position based on alignment
  147. actualX, actualY := currentX, currentY
  148. if f.direction == FlexDirectionHorizontal {
  149. // In horizontal layout, vertical alignment affects Y position
  150. // (lipgloss.Center is used for vertical alignment in JoinHorizontal)
  151. actualY = (f.height - paneHeight) / 2
  152. } else {
  153. // In vertical layout, horizontal alignment affects X position
  154. contentWidth := paneWidth
  155. if pane.MaxWidth() > 0 && contentWidth > pane.MaxWidth() {
  156. contentWidth = pane.MaxWidth()
  157. }
  158. switch pane.Alignment() {
  159. case lipgloss.Center:
  160. actualX = (f.width - contentWidth) / 2
  161. case lipgloss.Right:
  162. actualX = f.width - contentWidth
  163. case lipgloss.Left:
  164. actualX = 0
  165. }
  166. }
  167. // Set position if the pane is a *container
  168. if c, ok := pane.(*container); ok {
  169. c.x = actualX
  170. c.y = actualY
  171. }
  172. cmd := pane.SetSize(paneWidth, paneHeight)
  173. cmds = append(cmds, cmd)
  174. // Update position for next pane
  175. if f.direction == FlexDirectionHorizontal {
  176. currentX += paneWidth
  177. } else {
  178. currentY += paneHeight
  179. }
  180. }
  181. }
  182. return tea.Batch(cmds...)
  183. }
  184. func (f *flexLayout) GetSize() (int, int) {
  185. return f.width, f.height
  186. }
  187. func (f *flexLayout) SetPanes(panes []Container) tea.Cmd {
  188. f.panes = panes
  189. if f.width > 0 && f.height > 0 {
  190. return f.SetSize(f.width, f.height)
  191. }
  192. return nil
  193. }
  194. func (f *flexLayout) SetPaneSizes(sizes []FlexPaneSize) tea.Cmd {
  195. f.sizes = sizes
  196. if f.width > 0 && f.height > 0 {
  197. return f.SetSize(f.width, f.height)
  198. }
  199. return nil
  200. }
  201. func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
  202. f.direction = direction
  203. if f.width > 0 && f.height > 0 {
  204. return f.SetSize(f.width, f.height)
  205. }
  206. return nil
  207. }
  208. func NewFlexLayout(options ...FlexLayoutOption) FlexLayout {
  209. layout := &flexLayout{
  210. direction: FlexDirectionHorizontal,
  211. panes: []Container{},
  212. sizes: []FlexPaneSize{},
  213. }
  214. for _, option := range options {
  215. option(layout)
  216. }
  217. return layout
  218. }
  219. func WithDirection(direction FlexDirection) FlexLayoutOption {
  220. return func(f *flexLayout) {
  221. f.direction = direction
  222. }
  223. }
  224. func WithPanes(panes ...Container) FlexLayoutOption {
  225. return func(f *flexLayout) {
  226. f.panes = panes
  227. }
  228. }
  229. func WithPaneSizes(sizes ...FlexPaneSize) FlexLayoutOption {
  230. return func(f *flexLayout) {
  231. f.sizes = sizes
  232. }
  233. }