flex.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. package layout
  2. import (
  3. "strings"
  4. "github.com/charmbracelet/lipgloss/v2"
  5. "github.com/sst/opencode/internal/styles"
  6. )
  7. type Direction int
  8. const (
  9. Row Direction = iota
  10. Column
  11. )
  12. type Justify int
  13. const (
  14. JustifyStart Justify = iota
  15. JustifyEnd
  16. JustifyCenter
  17. JustifySpaceBetween
  18. JustifySpaceAround
  19. )
  20. type Align int
  21. const (
  22. AlignStart Align = iota
  23. AlignEnd
  24. AlignCenter
  25. AlignStretch // Only applicable in the cross-axis
  26. )
  27. type FlexOptions struct {
  28. Direction Direction
  29. Justify Justify
  30. Align Align
  31. Width int
  32. Height int
  33. }
  34. type FlexItem struct {
  35. View string
  36. FixedSize int // Fixed size in the main axis (width for Row, height for Column)
  37. Grow bool // If true, the item will grow to fill available space
  38. }
  39. // Render lays out a series of view strings based on flexbox-like rules.
  40. func Render(opts FlexOptions, items ...FlexItem) string {
  41. if len(items) == 0 {
  42. return ""
  43. }
  44. // Calculate dimensions for each item
  45. mainAxisSize := opts.Width
  46. crossAxisSize := opts.Height
  47. if opts.Direction == Column {
  48. mainAxisSize = opts.Height
  49. crossAxisSize = opts.Width
  50. }
  51. // Calculate total fixed size and count grow items
  52. totalFixedSize := 0
  53. growCount := 0
  54. for _, item := range items {
  55. if item.FixedSize > 0 {
  56. totalFixedSize += item.FixedSize
  57. } else if item.Grow {
  58. growCount++
  59. }
  60. }
  61. // Calculate available space for grow items
  62. availableSpace := max(mainAxisSize-totalFixedSize, 0)
  63. // Calculate size for each grow item
  64. growItemSize := 0
  65. if growCount > 0 && availableSpace > 0 {
  66. growItemSize = availableSpace / growCount
  67. }
  68. // Prepare sized views
  69. sizedViews := make([]string, len(items))
  70. actualSizes := make([]int, len(items))
  71. for i, item := range items {
  72. view := item.View
  73. // Determine the size for this item
  74. itemSize := 0
  75. if item.FixedSize > 0 {
  76. itemSize = item.FixedSize
  77. } else if item.Grow && growItemSize > 0 {
  78. itemSize = growItemSize
  79. } else {
  80. // No fixed size and not growing - use natural size
  81. if opts.Direction == Row {
  82. itemSize = lipgloss.Width(view)
  83. } else {
  84. itemSize = lipgloss.Height(view)
  85. }
  86. }
  87. // Apply size constraints
  88. if opts.Direction == Row {
  89. // For row direction, constrain width and handle height alignment
  90. if itemSize > 0 {
  91. view = styles.NewStyle().
  92. Width(itemSize).
  93. Height(crossAxisSize).
  94. Render(view)
  95. }
  96. // Apply cross-axis alignment
  97. switch opts.Align {
  98. case AlignCenter:
  99. view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Center, view)
  100. case AlignEnd:
  101. view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Bottom, view)
  102. case AlignStart:
  103. view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Top, view)
  104. case AlignStretch:
  105. // Already stretched by Height setting above
  106. }
  107. } else {
  108. // For column direction, constrain height and handle width alignment
  109. if itemSize > 0 {
  110. view = styles.NewStyle().
  111. Height(itemSize).
  112. Width(crossAxisSize).
  113. Render(view)
  114. }
  115. // Apply cross-axis alignment
  116. switch opts.Align {
  117. case AlignCenter:
  118. view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Center, view)
  119. case AlignEnd:
  120. view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Right, view)
  121. case AlignStart:
  122. view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Left, view)
  123. case AlignStretch:
  124. // Already stretched by Width setting above
  125. }
  126. }
  127. sizedViews[i] = view
  128. if opts.Direction == Row {
  129. actualSizes[i] = lipgloss.Width(view)
  130. } else {
  131. actualSizes[i] = lipgloss.Height(view)
  132. }
  133. }
  134. // Calculate total actual size
  135. totalActualSize := 0
  136. for _, size := range actualSizes {
  137. totalActualSize += size
  138. }
  139. // Apply justification
  140. remainingSpace := max(mainAxisSize-totalActualSize, 0)
  141. // Calculate spacing based on justification
  142. var spaceBefore, spaceBetween, spaceAfter int
  143. switch opts.Justify {
  144. case JustifyStart:
  145. spaceAfter = remainingSpace
  146. case JustifyEnd:
  147. spaceBefore = remainingSpace
  148. case JustifyCenter:
  149. spaceBefore = remainingSpace / 2
  150. spaceAfter = remainingSpace - spaceBefore
  151. case JustifySpaceBetween:
  152. if len(items) > 1 {
  153. spaceBetween = remainingSpace / (len(items) - 1)
  154. } else {
  155. spaceAfter = remainingSpace
  156. }
  157. case JustifySpaceAround:
  158. if len(items) > 0 {
  159. spaceAround := remainingSpace / (len(items) * 2)
  160. spaceBefore = spaceAround
  161. spaceAfter = spaceAround
  162. spaceBetween = spaceAround * 2
  163. }
  164. }
  165. // Build the final layout
  166. var parts []string
  167. // Add space before if needed
  168. if spaceBefore > 0 {
  169. if opts.Direction == Row {
  170. parts = append(parts, strings.Repeat(" ", spaceBefore))
  171. } else {
  172. parts = append(parts, strings.Repeat("\n", spaceBefore))
  173. }
  174. }
  175. // Add items with spacing
  176. for i, view := range sizedViews {
  177. parts = append(parts, view)
  178. // Add space between items (not after the last one)
  179. if i < len(sizedViews)-1 && spaceBetween > 0 {
  180. if opts.Direction == Row {
  181. parts = append(parts, strings.Repeat(" ", spaceBetween))
  182. } else {
  183. parts = append(parts, strings.Repeat("\n", spaceBetween))
  184. }
  185. }
  186. }
  187. // Add space after if needed
  188. if spaceAfter > 0 {
  189. if opts.Direction == Row {
  190. parts = append(parts, strings.Repeat(" ", spaceAfter))
  191. } else {
  192. parts = append(parts, strings.Repeat("\n", spaceAfter))
  193. }
  194. }
  195. // Join the parts
  196. if opts.Direction == Row {
  197. return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
  198. } else {
  199. return lipgloss.JoinVertical(lipgloss.Left, parts...)
  200. }
  201. }
  202. // Helper function to create a simple vertical layout
  203. func Vertical(width, height int, items ...FlexItem) string {
  204. return Render(FlexOptions{
  205. Direction: Column,
  206. Width: width,
  207. Height: height,
  208. Justify: JustifyStart,
  209. Align: AlignStretch,
  210. }, items...)
  211. }
  212. // Helper function to create a simple horizontal layout
  213. func Horizontal(width, height int, items ...FlexItem) string {
  214. return Render(FlexOptions{
  215. Direction: Row,
  216. Width: width,
  217. Height: height,
  218. Justify: JustifyStart,
  219. Align: AlignStretch,
  220. }, items...)
  221. }