flex.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package layout
  2. import (
  3. "github.com/charmbracelet/bubbles/v2/key"
  4. tea "github.com/charmbracelet/bubbletea/v2"
  5. "github.com/charmbracelet/lipgloss/v2"
  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. Bindings
  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. 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. )
  79. views = append(views, view)
  80. } else {
  81. paneWidth, paneHeight = f.calculatePaneSize(i)
  82. view := lipgloss.Place(
  83. f.width,
  84. paneHeight,
  85. lipgloss.Center,
  86. pane.Alignment(),
  87. pane.View(),
  88. )
  89. views = append(views, view)
  90. }
  91. }
  92. if f.direction == FlexDirectionHorizontal {
  93. return lipgloss.JoinHorizontal(lipgloss.Center, views...)
  94. }
  95. return lipgloss.JoinVertical(lipgloss.Center, views...)
  96. }
  97. func (f *flexLayout) calculatePaneSize(index int) (width, height int) {
  98. if index >= len(f.panes) {
  99. return 0, 0
  100. }
  101. totalFixed := 0
  102. flexCount := 0
  103. for i, pane := range f.panes {
  104. if pane == nil {
  105. continue
  106. }
  107. if i < len(f.sizes) && f.sizes[i].Fixed {
  108. if f.direction == FlexDirectionHorizontal {
  109. totalFixed += f.sizes[i].Size
  110. } else {
  111. totalFixed += f.sizes[i].Size
  112. }
  113. } else {
  114. flexCount++
  115. }
  116. }
  117. if f.direction == FlexDirectionHorizontal {
  118. height = f.height
  119. if index < len(f.sizes) && f.sizes[index].Fixed {
  120. width = f.sizes[index].Size
  121. } else if flexCount > 0 {
  122. remainingSpace := f.width - totalFixed
  123. width = remainingSpace / flexCount
  124. }
  125. } else {
  126. width = f.width
  127. if index < len(f.sizes) && f.sizes[index].Fixed {
  128. height = f.sizes[index].Size
  129. } else if flexCount > 0 {
  130. remainingSpace := f.height - totalFixed
  131. height = remainingSpace / flexCount
  132. }
  133. }
  134. return width, height
  135. }
  136. func (f *flexLayout) SetSize(width, height int) tea.Cmd {
  137. f.width = width
  138. f.height = height
  139. var cmds []tea.Cmd
  140. for i, pane := range f.panes {
  141. if pane != nil {
  142. paneWidth, paneHeight := f.calculatePaneSize(i)
  143. cmd := pane.SetSize(paneWidth, paneHeight)
  144. cmds = append(cmds, cmd)
  145. }
  146. }
  147. return tea.Batch(cmds...)
  148. }
  149. func (f *flexLayout) GetSize() (int, int) {
  150. return f.width, f.height
  151. }
  152. func (f *flexLayout) SetPanes(panes []Container) tea.Cmd {
  153. f.panes = panes
  154. if f.width > 0 && f.height > 0 {
  155. return f.SetSize(f.width, f.height)
  156. }
  157. return nil
  158. }
  159. func (f *flexLayout) SetPaneSizes(sizes []FlexPaneSize) tea.Cmd {
  160. f.sizes = sizes
  161. if f.width > 0 && f.height > 0 {
  162. return f.SetSize(f.width, f.height)
  163. }
  164. return nil
  165. }
  166. func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
  167. f.direction = direction
  168. if f.width > 0 && f.height > 0 {
  169. return f.SetSize(f.width, f.height)
  170. }
  171. return nil
  172. }
  173. func (f *flexLayout) BindingKeys() []key.Binding {
  174. keys := []key.Binding{}
  175. for _, pane := range f.panes {
  176. if pane != nil {
  177. if b, ok := pane.(Bindings); ok {
  178. keys = append(keys, b.BindingKeys()...)
  179. }
  180. }
  181. }
  182. return keys
  183. }
  184. func NewFlexLayout(options ...FlexLayoutOption) FlexLayout {
  185. layout := &flexLayout{
  186. direction: FlexDirectionHorizontal,
  187. panes: []Container{},
  188. sizes: []FlexPaneSize{},
  189. }
  190. for _, option := range options {
  191. option(layout)
  192. }
  193. return layout
  194. }
  195. func WithDirection(direction FlexDirection) FlexLayoutOption {
  196. return func(f *flexLayout) {
  197. f.direction = direction
  198. }
  199. }
  200. func WithPanes(panes ...Container) FlexLayoutOption {
  201. return func(f *flexLayout) {
  202. f.panes = panes
  203. }
  204. }
  205. func WithPaneSizes(sizes ...FlexPaneSize) FlexLayoutOption {
  206. return func(f *flexLayout) {
  207. f.sizes = sizes
  208. }
  209. }