| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- package layout
- import (
- "strings"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/charmbracelet/lipgloss/v2/compat"
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
- )
- type Direction int
- const (
- Row Direction = iota
- Column
- )
- type Justify int
- const (
- JustifyStart Justify = iota
- JustifyEnd
- JustifyCenter
- JustifySpaceBetween
- JustifySpaceAround
- )
- type Align int
- const (
- AlignStart Align = iota
- AlignEnd
- AlignCenter
- AlignStretch // Only applicable in the cross-axis
- )
- type FlexOptions struct {
- Background *compat.AdaptiveColor
- Direction Direction
- Justify Justify
- Align Align
- Width int
- Height int
- Gap int
- }
- type FlexItem struct {
- View string
- FixedSize int // Fixed size in the main axis (width for Row, height for Column)
- Grow bool // If true, the item will grow to fill available space
- }
- // Render lays out a series of view strings based on flexbox-like rules.
- func Render(opts FlexOptions, items ...FlexItem) string {
- if len(items) == 0 {
- return ""
- }
- t := theme.CurrentTheme()
- if opts.Background == nil {
- background := t.Background()
- opts.Background = &background
- }
- // Calculate dimensions for each item
- mainAxisSize := opts.Width
- crossAxisSize := opts.Height
- if opts.Direction == Column {
- mainAxisSize = opts.Height
- crossAxisSize = opts.Width
- }
- // Calculate total fixed size and count grow items
- totalFixedSize := 0
- growCount := 0
- for _, item := range items {
- if item.FixedSize > 0 {
- totalFixedSize += item.FixedSize
- } else if item.Grow {
- growCount++
- }
- }
- // Account for gaps between items
- totalGapSize := 0
- if len(items) > 1 && opts.Gap > 0 {
- totalGapSize = opts.Gap * (len(items) - 1)
- }
- // Calculate available space for grow items
- availableSpace := max(mainAxisSize-totalFixedSize-totalGapSize, 0)
- // Calculate size for each grow item
- growItemSize := 0
- if growCount > 0 && availableSpace > 0 {
- growItemSize = availableSpace / growCount
- }
- // Prepare sized views
- sizedViews := make([]string, len(items))
- actualSizes := make([]int, len(items))
- for i, item := range items {
- view := item.View
- // Determine the size for this item
- itemSize := 0
- if item.FixedSize > 0 {
- itemSize = item.FixedSize
- } else if item.Grow && growItemSize > 0 {
- itemSize = growItemSize
- } else {
- // No fixed size and not growing - use natural size
- if opts.Direction == Row {
- itemSize = lipgloss.Width(view)
- } else {
- itemSize = lipgloss.Height(view)
- }
- }
- // Apply size constraints
- if opts.Direction == Row {
- // For row direction, constrain width and handle height alignment
- if itemSize > 0 {
- view = styles.NewStyle().
- Background(*opts.Background).
- Width(itemSize).
- Height(crossAxisSize).
- Render(view)
- }
- // Apply cross-axis alignment
- switch opts.Align {
- case AlignCenter:
- view = lipgloss.PlaceVertical(
- crossAxisSize,
- lipgloss.Center,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignEnd:
- view = lipgloss.PlaceVertical(
- crossAxisSize,
- lipgloss.Bottom,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStart:
- view = lipgloss.PlaceVertical(
- crossAxisSize,
- lipgloss.Top,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStretch:
- // Already stretched by Height setting above
- }
- } else {
- // For column direction, constrain height and handle width alignment
- if itemSize > 0 {
- style := styles.NewStyle().
- Background(*opts.Background).
- Height(itemSize)
- // Only set width for stretch alignment
- if opts.Align == AlignStretch {
- style = style.Width(crossAxisSize)
- }
- view = style.Render(view)
- }
- // Apply cross-axis alignment
- switch opts.Align {
- case AlignCenter:
- view = lipgloss.PlaceHorizontal(
- crossAxisSize,
- lipgloss.Center,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignEnd:
- view = lipgloss.PlaceHorizontal(
- crossAxisSize,
- lipgloss.Right,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStart:
- view = lipgloss.PlaceHorizontal(
- crossAxisSize,
- lipgloss.Left,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStretch:
- // Already stretched by Width setting above
- }
- }
- sizedViews[i] = view
- if opts.Direction == Row {
- actualSizes[i] = lipgloss.Width(view)
- } else {
- actualSizes[i] = lipgloss.Height(view)
- }
- }
- // Calculate total actual size including gaps
- totalActualSize := 0
- for _, size := range actualSizes {
- totalActualSize += size
- }
- if len(items) > 1 && opts.Gap > 0 {
- totalActualSize += opts.Gap * (len(items) - 1)
- }
- // Apply justification
- remainingSpace := max(mainAxisSize-totalActualSize, 0)
- // Calculate spacing based on justification
- var spaceBefore, spaceBetween, spaceAfter int
- switch opts.Justify {
- case JustifyStart:
- spaceAfter = remainingSpace
- case JustifyEnd:
- spaceBefore = remainingSpace
- case JustifyCenter:
- spaceBefore = remainingSpace / 2
- spaceAfter = remainingSpace - spaceBefore
- case JustifySpaceBetween:
- if len(items) > 1 {
- spaceBetween = remainingSpace / (len(items) - 1)
- } else {
- spaceAfter = remainingSpace
- }
- case JustifySpaceAround:
- if len(items) > 0 {
- spaceAround := remainingSpace / (len(items) * 2)
- spaceBefore = spaceAround
- spaceAfter = spaceAround
- spaceBetween = spaceAround * 2
- }
- }
- // Build the final layout
- var parts []string
- spaceStyle := styles.NewStyle().Background(*opts.Background)
- // Add space before if needed
- if spaceBefore > 0 {
- if opts.Direction == Row {
- space := strings.Repeat(" ", spaceBefore)
- parts = append(parts, spaceStyle.Render(space))
- } else {
- // For vertical layout, add empty lines as separate parts
- for range spaceBefore {
- parts = append(parts, "")
- }
- }
- }
- // Add items with spacing
- for i, view := range sizedViews {
- parts = append(parts, view)
- // Add space between items (not after the last one)
- if i < len(sizedViews)-1 {
- // Add gap first, then any additional spacing from justification
- totalSpacing := opts.Gap + spaceBetween
- if totalSpacing > 0 {
- if opts.Direction == Row {
- space := strings.Repeat(" ", totalSpacing)
- parts = append(parts, spaceStyle.Render(space))
- } else {
- // For vertical layout, add empty lines as separate parts
- for range totalSpacing {
- parts = append(parts, "")
- }
- }
- }
- }
- }
- // Add space after if needed
- if spaceAfter > 0 {
- if opts.Direction == Row {
- space := strings.Repeat(" ", spaceAfter)
- parts = append(parts, spaceStyle.Render(space))
- } else {
- // For vertical layout, add empty lines as separate parts
- for range spaceAfter {
- parts = append(parts, "")
- }
- }
- }
- // Join the parts
- if opts.Direction == Row {
- return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
- } else {
- return lipgloss.JoinVertical(lipgloss.Left, parts...)
- }
- }
- // Helper function to create a simple vertical layout
- func Vertical(width, height int, items ...FlexItem) string {
- return Render(FlexOptions{
- Direction: Column,
- Width: width,
- Height: height,
- Justify: JustifyStart,
- Align: AlignStretch,
- }, items...)
- }
- // Helper function to create a simple horizontal layout
- func Horizontal(width, height int, items ...FlexItem) string {
- return Render(FlexOptions{
- Direction: Row,
- Width: width,
- Height: height,
- Justify: JustifyStart,
- Align: AlignStretch,
- }, items...)
- }
|