flex.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. package layout
  2. import (
  3. "strings"
  4. "github.com/charmbracelet/lipgloss/v2"
  5. "github.com/charmbracelet/lipgloss/v2/compat"
  6. "github.com/sst/opencode/internal/styles"
  7. "github.com/sst/opencode/internal/theme"
  8. )
  9. type Direction int
  10. const (
  11. Row Direction = iota
  12. Column
  13. )
  14. type Justify int
  15. const (
  16. JustifyStart Justify = iota
  17. JustifyEnd
  18. JustifyCenter
  19. JustifySpaceBetween
  20. JustifySpaceAround
  21. )
  22. type Align int
  23. const (
  24. AlignStart Align = iota
  25. AlignEnd
  26. AlignCenter
  27. AlignStretch // Only applicable in the cross-axis
  28. )
  29. type FlexOptions struct {
  30. Background *compat.AdaptiveColor
  31. Direction Direction
  32. Justify Justify
  33. Align Align
  34. Width int
  35. Height int
  36. Gap int
  37. }
  38. type FlexItem struct {
  39. View string
  40. FixedSize int // Fixed size in the main axis (width for Row, height for Column)
  41. Grow bool // If true, the item will grow to fill available space
  42. }
  43. // Render lays out a series of view strings based on flexbox-like rules.
  44. func Render(opts FlexOptions, items ...FlexItem) string {
  45. if len(items) == 0 {
  46. return ""
  47. }
  48. t := theme.CurrentTheme()
  49. if opts.Background == nil {
  50. background := t.Background()
  51. opts.Background = &background
  52. }
  53. // Calculate dimensions for each item
  54. mainAxisSize := opts.Width
  55. crossAxisSize := opts.Height
  56. if opts.Direction == Column {
  57. mainAxisSize = opts.Height
  58. crossAxisSize = opts.Width
  59. }
  60. // Calculate total fixed size and count grow items
  61. totalFixedSize := 0
  62. growCount := 0
  63. for _, item := range items {
  64. if item.FixedSize > 0 {
  65. totalFixedSize += item.FixedSize
  66. } else if item.Grow {
  67. growCount++
  68. }
  69. }
  70. // Account for gaps between items
  71. totalGapSize := 0
  72. if len(items) > 1 && opts.Gap > 0 {
  73. totalGapSize = opts.Gap * (len(items) - 1)
  74. }
  75. // Calculate available space for grow items
  76. availableSpace := max(mainAxisSize-totalFixedSize-totalGapSize, 0)
  77. // Calculate size for each grow item
  78. growItemSize := 0
  79. if growCount > 0 && availableSpace > 0 {
  80. growItemSize = availableSpace / growCount
  81. }
  82. // Prepare sized views
  83. sizedViews := make([]string, len(items))
  84. actualSizes := make([]int, len(items))
  85. for i, item := range items {
  86. view := item.View
  87. // Determine the size for this item
  88. itemSize := 0
  89. if item.FixedSize > 0 {
  90. itemSize = item.FixedSize
  91. } else if item.Grow && growItemSize > 0 {
  92. itemSize = growItemSize
  93. } else {
  94. // No fixed size and not growing - use natural size
  95. if opts.Direction == Row {
  96. itemSize = lipgloss.Width(view)
  97. } else {
  98. itemSize = lipgloss.Height(view)
  99. }
  100. }
  101. // Apply size constraints
  102. if opts.Direction == Row {
  103. // For row direction, constrain width and handle height alignment
  104. if itemSize > 0 {
  105. view = styles.NewStyle().
  106. Background(*opts.Background).
  107. Width(itemSize).
  108. Height(crossAxisSize).
  109. Render(view)
  110. }
  111. // Apply cross-axis alignment
  112. switch opts.Align {
  113. case AlignCenter:
  114. view = lipgloss.PlaceVertical(
  115. crossAxisSize,
  116. lipgloss.Center,
  117. view,
  118. styles.WhitespaceStyle(*opts.Background),
  119. )
  120. case AlignEnd:
  121. view = lipgloss.PlaceVertical(
  122. crossAxisSize,
  123. lipgloss.Bottom,
  124. view,
  125. styles.WhitespaceStyle(*opts.Background),
  126. )
  127. case AlignStart:
  128. view = lipgloss.PlaceVertical(
  129. crossAxisSize,
  130. lipgloss.Top,
  131. view,
  132. styles.WhitespaceStyle(*opts.Background),
  133. )
  134. case AlignStretch:
  135. // Already stretched by Height setting above
  136. }
  137. } else {
  138. // For column direction, constrain height and handle width alignment
  139. if itemSize > 0 {
  140. style := styles.NewStyle().
  141. Background(*opts.Background).
  142. Height(itemSize)
  143. // Only set width for stretch alignment
  144. if opts.Align == AlignStretch {
  145. style = style.Width(crossAxisSize)
  146. }
  147. view = style.Render(view)
  148. }
  149. // Apply cross-axis alignment
  150. switch opts.Align {
  151. case AlignCenter:
  152. view = lipgloss.PlaceHorizontal(
  153. crossAxisSize,
  154. lipgloss.Center,
  155. view,
  156. styles.WhitespaceStyle(*opts.Background),
  157. )
  158. case AlignEnd:
  159. view = lipgloss.PlaceHorizontal(
  160. crossAxisSize,
  161. lipgloss.Right,
  162. view,
  163. styles.WhitespaceStyle(*opts.Background),
  164. )
  165. case AlignStart:
  166. view = lipgloss.PlaceHorizontal(
  167. crossAxisSize,
  168. lipgloss.Left,
  169. view,
  170. styles.WhitespaceStyle(*opts.Background),
  171. )
  172. case AlignStretch:
  173. // Already stretched by Width setting above
  174. }
  175. }
  176. sizedViews[i] = view
  177. if opts.Direction == Row {
  178. actualSizes[i] = lipgloss.Width(view)
  179. } else {
  180. actualSizes[i] = lipgloss.Height(view)
  181. }
  182. }
  183. // Calculate total actual size including gaps
  184. totalActualSize := 0
  185. for _, size := range actualSizes {
  186. totalActualSize += size
  187. }
  188. if len(items) > 1 && opts.Gap > 0 {
  189. totalActualSize += opts.Gap * (len(items) - 1)
  190. }
  191. // Apply justification
  192. remainingSpace := max(mainAxisSize-totalActualSize, 0)
  193. // Calculate spacing based on justification
  194. var spaceBefore, spaceBetween, spaceAfter int
  195. switch opts.Justify {
  196. case JustifyStart:
  197. spaceAfter = remainingSpace
  198. case JustifyEnd:
  199. spaceBefore = remainingSpace
  200. case JustifyCenter:
  201. spaceBefore = remainingSpace / 2
  202. spaceAfter = remainingSpace - spaceBefore
  203. case JustifySpaceBetween:
  204. if len(items) > 1 {
  205. spaceBetween = remainingSpace / (len(items) - 1)
  206. } else {
  207. spaceAfter = remainingSpace
  208. }
  209. case JustifySpaceAround:
  210. if len(items) > 0 {
  211. spaceAround := remainingSpace / (len(items) * 2)
  212. spaceBefore = spaceAround
  213. spaceAfter = spaceAround
  214. spaceBetween = spaceAround * 2
  215. }
  216. }
  217. // Build the final layout
  218. var parts []string
  219. spaceStyle := styles.NewStyle().Background(*opts.Background)
  220. // Add space before if needed
  221. if spaceBefore > 0 {
  222. if opts.Direction == Row {
  223. space := strings.Repeat(" ", spaceBefore)
  224. parts = append(parts, spaceStyle.Render(space))
  225. } else {
  226. // For vertical layout, add empty lines as separate parts
  227. for range spaceBefore {
  228. parts = append(parts, "")
  229. }
  230. }
  231. }
  232. // Add items with spacing
  233. for i, view := range sizedViews {
  234. parts = append(parts, view)
  235. // Add space between items (not after the last one)
  236. if i < len(sizedViews)-1 {
  237. // Add gap first, then any additional spacing from justification
  238. totalSpacing := opts.Gap + spaceBetween
  239. if totalSpacing > 0 {
  240. if opts.Direction == Row {
  241. space := strings.Repeat(" ", totalSpacing)
  242. parts = append(parts, spaceStyle.Render(space))
  243. } else {
  244. // For vertical layout, add empty lines as separate parts
  245. for range totalSpacing {
  246. parts = append(parts, "")
  247. }
  248. }
  249. }
  250. }
  251. }
  252. // Add space after if needed
  253. if spaceAfter > 0 {
  254. if opts.Direction == Row {
  255. space := strings.Repeat(" ", spaceAfter)
  256. parts = append(parts, spaceStyle.Render(space))
  257. } else {
  258. // For vertical layout, add empty lines as separate parts
  259. for range spaceAfter {
  260. parts = append(parts, "")
  261. }
  262. }
  263. }
  264. // Join the parts
  265. if opts.Direction == Row {
  266. return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
  267. } else {
  268. return lipgloss.JoinVertical(lipgloss.Left, parts...)
  269. }
  270. }
  271. // Helper function to create a simple vertical layout
  272. func Vertical(width, height int, items ...FlexItem) string {
  273. return Render(FlexOptions{
  274. Direction: Column,
  275. Width: width,
  276. Height: height,
  277. Justify: JustifyStart,
  278. Align: AlignStretch,
  279. }, items...)
  280. }
  281. // Helper function to create a simple horizontal layout
  282. func Horizontal(width, height int, items ...FlexItem) string {
  283. return Render(FlexOptions{
  284. Direction: Row,
  285. Width: width,
  286. Height: height,
  287. Justify: JustifyStart,
  288. Align: AlignStretch,
  289. }, items...)
  290. }