flex.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package layout
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. tea "github.com/charmbracelet/bubbletea"
  5. "github.com/charmbracelet/lipgloss"
  6. "github.com/sst/opencode/internal/theme"
  7. )
  8. type FlexDirection int
  9. const (
  10. FlexDirectionHorizontal FlexDirection = iota
  11. FlexDirectionVertical
  12. )
  13. type FlexPaneSize struct {
  14. Fixed bool
  15. Size int
  16. }
  17. var FlexPaneSizeGrow = FlexPaneSize{Fixed: false}
  18. func FlexPaneSizeFixed(size int) FlexPaneSize {
  19. return FlexPaneSize{Fixed: true, Size: size}
  20. }
  21. type FlexLayout interface {
  22. tea.Model
  23. Sizeable
  24. Bindings
  25. SetPanes(panes []Container) tea.Cmd
  26. SetPaneSizes(sizes []FlexPaneSize) tea.Cmd
  27. SetDirection(direction FlexDirection) tea.Cmd
  28. }
  29. type flexLayout struct {
  30. width int
  31. height int
  32. direction FlexDirection
  33. panes []Container
  34. sizes []FlexPaneSize
  35. }
  36. type FlexLayoutOption func(*flexLayout)
  37. func (f *flexLayout) Init() tea.Cmd {
  38. var cmds []tea.Cmd
  39. for _, pane := range f.panes {
  40. if pane != nil {
  41. cmds = append(cmds, pane.Init())
  42. }
  43. }
  44. return tea.Batch(cmds...)
  45. }
  46. func (f *flexLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  47. var cmds []tea.Cmd
  48. switch msg := msg.(type) {
  49. case tea.WindowSizeMsg:
  50. return f, f.SetSize(msg.Width, msg.Height)
  51. }
  52. for i, pane := range f.panes {
  53. if pane != nil {
  54. u, cmd := pane.Update(msg)
  55. f.panes[i] = u.(Container)
  56. if cmd != nil {
  57. cmds = append(cmds, cmd)
  58. }
  59. }
  60. }
  61. return f, tea.Batch(cmds...)
  62. }
  63. func (f *flexLayout) View() string {
  64. t := theme.CurrentTheme()
  65. if len(f.panes) == 0 {
  66. return ""
  67. }
  68. views := make([]string, 0, len(f.panes))
  69. for i, pane := range f.panes {
  70. if pane == nil {
  71. continue
  72. }
  73. var paneWidth, paneHeight int
  74. if f.direction == FlexDirectionHorizontal {
  75. paneWidth, paneHeight = f.calculatePaneSize(i)
  76. view := lipgloss.PlaceHorizontal(
  77. paneWidth,
  78. pane.Alignment(),
  79. pane.View(),
  80. lipgloss.WithWhitespaceBackground(t.Background()),
  81. )
  82. views = append(views, view)
  83. } else {
  84. paneWidth, paneHeight = f.calculatePaneSize(i)
  85. view := lipgloss.Place(
  86. f.width,
  87. paneHeight,
  88. lipgloss.Center,
  89. pane.Alignment(),
  90. pane.View(),
  91. lipgloss.WithWhitespaceBackground(t.Background()),
  92. )
  93. views = append(views, view)
  94. }
  95. }
  96. if f.direction == FlexDirectionHorizontal {
  97. return lipgloss.JoinHorizontal(lipgloss.Center, views...)
  98. }
  99. return lipgloss.JoinVertical(lipgloss.Center, views...)
  100. }
  101. func (f *flexLayout) calculatePaneSize(index int) (width, height int) {
  102. if index >= len(f.panes) {
  103. return 0, 0
  104. }
  105. totalFixed := 0
  106. flexCount := 0
  107. for i, pane := range f.panes {
  108. if pane == nil {
  109. continue
  110. }
  111. if i < len(f.sizes) && f.sizes[i].Fixed {
  112. if f.direction == FlexDirectionHorizontal {
  113. totalFixed += f.sizes[i].Size
  114. } else {
  115. totalFixed += f.sizes[i].Size
  116. }
  117. } else {
  118. flexCount++
  119. }
  120. }
  121. if f.direction == FlexDirectionHorizontal {
  122. height = f.height
  123. if index < len(f.sizes) && f.sizes[index].Fixed {
  124. width = f.sizes[index].Size
  125. } else if flexCount > 0 {
  126. remainingSpace := f.width - totalFixed
  127. width = remainingSpace / flexCount
  128. }
  129. } else {
  130. width = f.width
  131. if index < len(f.sizes) && f.sizes[index].Fixed {
  132. height = f.sizes[index].Size
  133. } else if flexCount > 0 {
  134. remainingSpace := f.height - totalFixed
  135. height = remainingSpace / flexCount
  136. }
  137. }
  138. return width, height
  139. }
  140. func (f *flexLayout) SetSize(width, height int) tea.Cmd {
  141. f.width = width
  142. f.height = height
  143. var cmds []tea.Cmd
  144. for i, pane := range f.panes {
  145. if pane != nil {
  146. paneWidth, paneHeight := f.calculatePaneSize(i)
  147. cmd := pane.SetSize(paneWidth, paneHeight)
  148. cmds = append(cmds, cmd)
  149. }
  150. }
  151. return tea.Batch(cmds...)
  152. }
  153. func (f *flexLayout) GetSize() (int, int) {
  154. return f.width, f.height
  155. }
  156. func (f *flexLayout) SetPanes(panes []Container) tea.Cmd {
  157. f.panes = panes
  158. if f.width > 0 && f.height > 0 {
  159. return f.SetSize(f.width, f.height)
  160. }
  161. return nil
  162. }
  163. func (f *flexLayout) SetPaneSizes(sizes []FlexPaneSize) tea.Cmd {
  164. f.sizes = sizes
  165. if f.width > 0 && f.height > 0 {
  166. return f.SetSize(f.width, f.height)
  167. }
  168. return nil
  169. }
  170. func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
  171. f.direction = direction
  172. if f.width > 0 && f.height > 0 {
  173. return f.SetSize(f.width, f.height)
  174. }
  175. return nil
  176. }
  177. func (f *flexLayout) BindingKeys() []key.Binding {
  178. keys := []key.Binding{}
  179. for _, pane := range f.panes {
  180. if pane != nil {
  181. if b, ok := pane.(Bindings); ok {
  182. keys = append(keys, b.BindingKeys()...)
  183. }
  184. }
  185. }
  186. return keys
  187. }
  188. func NewFlexLayout(options ...FlexLayoutOption) FlexLayout {
  189. layout := &flexLayout{
  190. direction: FlexDirectionHorizontal,
  191. panes: []Container{},
  192. sizes: []FlexPaneSize{},
  193. }
  194. for _, option := range options {
  195. option(layout)
  196. }
  197. return layout
  198. }
  199. func WithDirection(direction FlexDirection) FlexLayoutOption {
  200. return func(f *flexLayout) {
  201. f.direction = direction
  202. }
  203. }
  204. func WithPanes(panes ...Container) FlexLayoutOption {
  205. return func(f *flexLayout) {
  206. f.panes = panes
  207. }
  208. }
  209. func WithPaneSizes(sizes ...FlexPaneSize) FlexLayoutOption {
  210. return func(f *flexLayout) {
  211. f.sizes = sizes
  212. }
  213. }