flex.go 5.8 KB

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