flex.go 5.8 KB

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