mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
325 lines
7.4 KiB
Go
325 lines
7.4 KiB
Go
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...)
|
|
}
|