flex.go 5.6 KB

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