diff --git a/LAYOUT.md b/LAYOUT.md new file mode 100644 index 0000000..773bc77 --- /dev/null +++ b/LAYOUT.md @@ -0,0 +1,391 @@ +# Advanced Layout System for TV + +This document describes the new advanced layout system for the TV terminal library. The new system provides flexible, powerful layout capabilities while maintaining backward compatibility with the existing simple split functions. + +## Overview + +The new layout system supports: + +- **Flexible Layouts**: Flexbox-style layouts with grow/shrink factors +- **Ratio-based Layouts**: Proportional sizing using ratios +- **Fixed Layouts**: Absolute pixel-based sizing +- **Grid Layouts**: CSS Grid-style 2D layouts +- **Spacing and Alignment**: Margins, padding, gaps, and alignment options +- **Responsive Design**: Layouts that adapt to different screen sizes + +## Core Concepts + +### Size Constraints + +Size constraints define how layout items should be sized: + +```go +// Fixed size - always the same number of pixels +width := tv.FixedSize(100) + +// Ratio size - percentage of available space (0.0 to 1.0) +width := tv.RatioSize(0.3) // 30% of available width + +// Flexible size - grows/shrinks based on available space +width := tv.FlexSize{ + Grow: 1, // How much to grow (default 1) + Shrink: 1, // How much to shrink (default 1) + Basis: nil, // Initial size before flex (optional) +} + +// Min/max constraints - bounds for other constraints +width := tv.MinMaxSize{ + Constraint: tv.FlexSize{Grow: 1}, + Min: 50, // Minimum 50 pixels + Max: 200, // Maximum 200 pixels +} +``` + +### Layout Items + +Layout items represent individual elements in a layout: + +```go +item := tv.LayoutItem{ + Width: tv.FixedSize(100), + Height: tv.FixedSize(50), + Margin: tv.Uniform(5), // 5px margin on all sides + Padding: tv.HorizontalVertical(10, 5), // 10px horizontal, 5px vertical +} + +// Helper function for creating items +item := tv.Item(tv.FixedSize(100), tv.FixedSize(50)) +``` + +### Spacing + +Spacing can be applied as margins and padding: + +```go +// Uniform spacing (same on all sides) +spacing := tv.Uniform(10) + +// Different horizontal and vertical spacing +spacing := tv.HorizontalVertical(20, 15) // 20px horizontal, 15px vertical + +// Custom spacing for each side +spacing := tv.Spacing{ + Top: 10, + Right: 20, + Bottom: 10, + Left: 20, +} +``` + +## Linear Layouts + +Linear layouts arrange items in a single direction (horizontal or vertical). + +### Basic Linear Layout + +```go +// Horizontal layout +layout := tv.NewHorizontalLayout( + tv.Item(tv.FixedSize(100), tv.FixedSize(50)), + tv.Item(tv.FixedSize(150), tv.FixedSize(50)), +) + +// Vertical layout +layout := tv.NewVerticalLayout( + tv.Item(tv.FixedSize(100), tv.FixedSize(50)), + tv.Item(tv.FixedSize(100), tv.FixedSize(75)), +) + +// Calculate layout for a given area +area := tv.Rect(0, 0, 300, 100) +result := layout.Calculate(area) + +// Access individual item areas +for i, itemArea := range result.Areas { + fmt.Printf("Item %d: %+v\n", i, itemArea) +} +``` + +### Layout with Gaps and Alignment + +```go +layout := &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FixedSize(100), tv.FixedSize(50)), + tv.Item(tv.FixedSize(100), tv.FixedSize(50)), + }, + Gap: 20, // 20px gap between items + + // Cross-axis alignment (perpendicular to direction) + CrossAxisAlignment: tv.CrossAxisCenter, + + // Main-axis alignment (along the direction) + MainAxisAlignment: tv.MainAxisSpaceBetween, +} +``` + +### Alignment Options + +**Cross-axis alignment** (perpendicular to layout direction): +- `CrossAxisStart`: Align to start +- `CrossAxisCenter`: Center alignment +- `CrossAxisEnd`: Align to end +- `CrossAxisStretch`: Stretch to fill + +**Main-axis alignment** (along layout direction): +- `MainAxisStart`: Align to start +- `MainAxisCenter`: Center alignment +- `MainAxisEnd`: Align to end +- `MainAxisSpaceBetween`: Space between items +- `MainAxisSpaceAround`: Space around items +- `MainAxisSpaceEvenly`: Even spacing + +## Flexible Layouts + +Flexible layouts automatically distribute available space among items. + +### Basic Flex Layout + +```go +layout := tv.NewFlexLayout(tv.Horizontal, + tv.Item(tv.FixedSize(100), nil), // Fixed 100px width + tv.FlexItem(1, 1, nil), // Takes remaining space + tv.Item(tv.FixedSize(100), nil), // Fixed 100px width +) +``` + +### Advanced Flex Layout + +```go +layout := &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + // Fixed sidebar + tv.Item(tv.FixedSize(200), nil), + + // Flexible content area + tv.Item(tv.FlexSize{Grow: 2}, nil), // Grows twice as much + + // Flexible sidebar + tv.Item(tv.FlexSize{Grow: 1}, nil), // Grows normally + }, +} +``` + +### Flex with Basis + +```go +// Flex item with initial size +flexItem := tv.Item( + tv.FlexSize{ + Grow: 1, + Shrink: 1, + Basis: tv.FixedSize(200), // Start at 200px, then grow/shrink + }, + nil, +) +``` + +## Grid Layouts + +Grid layouts arrange items in a 2D grid structure. + +### Basic Grid + +```go +grid := tv.NewGrid(3, // 3 columns + tv.GridItemAt(1, 1, tv.Item(nil, nil)), // Column 1, Row 1 + tv.GridItemAt(2, 1, tv.Item(nil, nil)), // Column 2, Row 1 + tv.GridItemAt(3, 1, tv.Item(nil, nil)), // Column 3, Row 1 + tv.GridItemAt(1, 2, tv.Item(nil, nil)), // Column 1, Row 2 +) + +area := tv.Rect(0, 0, 300, 200) +result := grid.Calculate(area) +``` + +### Grid with Gaps + +```go +grid := &tv.Grid{ + Columns: 3, + ColumnGap: 10, // 10px gap between columns + RowGap: 20, // 20px gap between rows + Items: []tv.GridItem{ + tv.GridItemAt(1, 1, tv.Item(nil, nil)), + tv.GridItemAt(2, 1, tv.Item(nil, nil)), + tv.GridItemAt(3, 1, tv.Item(nil, nil)), + }, +} +``` + +### Grid with Spanning + +```go +grid := tv.NewGrid(4, + // Item spanning 2 columns and 1 row + tv.GridItemSpan(1, 1, 2, 1, tv.Item(nil, nil)), + + // Regular items + tv.GridItemAt(3, 1, tv.Item(nil, nil)), + tv.GridItemAt(4, 1, tv.Item(nil, nil)), + + // Item spanning 1 column and 2 rows + tv.GridItemSpan(1, 2, 1, 2, tv.Item(nil, nil)), +) +``` + +### Auto-placement Grid + +```go +// Items without explicit positions are auto-placed +grid := tv.NewGrid(3, + tv.GridItemAt(0, 0, tv.Item(nil, nil)), // Auto-placed + tv.GridItemAt(0, 0, tv.Item(nil, nil)), // Auto-placed + tv.GridItemAt(0, 0, tv.Item(nil, nil)), // Auto-placed +) +``` + +## Practical Examples + +### Dashboard Layout + +```go +func createDashboard(area tv.Rectangle) tv.LayoutResult { + // Main vertical layout: header, body, footer + mainLayout := tv.NewVerticalLayout( + tv.Item(nil, tv.FixedSize(60)), // Header + tv.Item(nil, tv.FlexSize{Grow: 1}), // Body + tv.Item(nil, tv.FixedSize(30)), // Footer + ) + mainResult := mainLayout.Calculate(area) + + // Body horizontal layout: sidebar, content + bodyLayout := tv.NewHorizontalLayout( + tv.Item(tv.FixedSize(250), nil), // Sidebar + tv.Item(tv.FlexSize{Grow: 1}, nil), // Content + ) + bodyLayout.Gap = 10 + bodyResult := bodyLayout.Calculate(mainResult.Areas[1]) + + // Content grid for widgets + contentGrid := tv.NewGrid(3, + tv.GridItemSpan(1, 1, 2, 1, tv.Item(nil, tv.FixedSize(200))), // Large widget + tv.GridItemAt(3, 1, tv.Item(nil, tv.FixedSize(200))), + tv.GridItemAt(1, 2, tv.Item(nil, tv.FixedSize(150))), + tv.GridItemAt(2, 2, tv.Item(nil, tv.FixedSize(150))), + tv.GridItemAt(3, 2, tv.Item(nil, tv.FixedSize(150))), + ) + contentGrid.ColumnGap = 15 + contentGrid.RowGap = 15 + + return contentGrid.Calculate(bodyResult.Areas[1]) +} +``` + +### Responsive Layout + +```go +func createResponsiveLayout(area tv.Rectangle) tv.LayoutResult { + width := area.Dx() + + if width < 600 { + // Mobile layout: single column + return tv.NewVerticalLayout( + tv.Item(nil, tv.FixedSize(100)), // Header + tv.Item(nil, tv.FlexSize{Grow: 1}), // Content + tv.Item(nil, tv.FixedSize(50)), // Footer + ).Calculate(area) + } else if width < 1200 { + // Tablet layout: sidebar below header + mainLayout := tv.NewVerticalLayout( + tv.Item(nil, tv.FixedSize(80)), // Header + tv.Item(nil, tv.FlexSize{Grow: 1}), // Body + tv.Item(nil, tv.FixedSize(50)), // Footer + ) + mainResult := mainLayout.Calculate(area) + + bodyLayout := tv.NewVerticalLayout( + tv.Item(nil, tv.FixedSize(200)), // Sidebar + tv.Item(nil, tv.FlexSize{Grow: 1}), // Content + ) + bodyLayout.Gap = 10 + bodyResult := bodyLayout.Calculate(mainResult.Areas[1]) + + return tv.LayoutResult{ + Areas: append([]tv.Rectangle{mainResult.Areas[0]}, + append(bodyResult.Areas, mainResult.Areas[2])...), + Size: area, + } + } else { + // Desktop layout: sidebar on left + return createDashboard(area) + } +} +``` + +## Migration from Old System + +The new system maintains backward compatibility: + +```go +// Old way +left, right := tv.SplitVertical(area, 0.3) + +// New way (equivalent) +layout := tv.NewHorizontalLayout( + tv.Item(tv.RatioSize(0.3), nil), + tv.Item(tv.RatioSize(0.7), nil), +) +result := layout.Calculate(area) +left, right := result.Areas[0], result.Areas[1] + +// Or using the updated functions (same API) +left, right := tv.SplitVertical(area, 0.3) // Uses new system internally +``` + +## Performance Considerations + +- Layout calculations are O(n) where n is the number of items +- Grid layouts are O(n) for positioning, O(1) for cell size calculation +- Flex layouts require two passes: fixed sizing, then flex distribution +- Cache layout results when the area hasn't changed +- Use fixed sizes when possible for better performance + +## Best Practices + +1. **Use semantic layouts**: Choose the layout type that best represents your UI structure +2. **Prefer constraints over absolute sizes**: Use ratios and flex for responsive designs +3. **Group related items**: Use nested layouts for complex UIs +4. **Consider performance**: Cache layout results and avoid unnecessary recalculations +5. **Test different screen sizes**: Ensure your layouts work across various terminal sizes +6. **Use margins and padding consistently**: Establish a spacing system for your application + +## Helper Functions + +The system provides several helper functions for common patterns: + +```go +// Create items +item := tv.Item(width, height) +flexItem := tv.FlexItem(grow, shrink, basis) + +// Create layouts +hLayout := tv.NewHorizontalLayout(items...) +vLayout := tv.NewVerticalLayout(items...) +flexLayout := tv.NewFlexLayout(direction, items...) + +// Create grids +grid := tv.NewGrid(columns, items...) + +// Create grid items +gridItem := tv.GridItemAt(col, row, item) +spanItem := tv.GridItemSpan(col, row, colSpan, rowSpan, item) + +// Create spacing +uniform := tv.Uniform(size) +hvSpacing := tv.HorizontalVertical(h, v) +``` + +This new layout system provides the flexibility and power needed for modern terminal applications while maintaining the simplicity and performance that TV is known for. \ No newline at end of file diff --git a/examples/flexbox/README.md b/examples/flexbox/README.md new file mode 100644 index 0000000..19d64ae --- /dev/null +++ b/examples/flexbox/README.md @@ -0,0 +1,102 @@ +# Flexbox Layout Demo + +This example demonstrates the comprehensive flexbox capabilities of the TV layout system. + +## Features Demonstrated + +### 1. Basic Horizontal Flex +- Three items with equal flex grow factors (1:1:1) +- Shows how items automatically distribute available space + +### 2. Proportional Flex Growth +- Items with different grow factors (1:2:3) +- Demonstrates how grow factors control space distribution + +### 3. Fixed + Flex Layout +- Common pattern: fixed sidebars with flexible content area +- Shows mixing fixed and flexible sizing + +### 4. Flex with Basis +- Items with initial size (basis) that then grow +- Demonstrates how basis affects final sizing + +### 5. Vertical Flex Layout +- Vertical flexbox with different grow factors +- Shows flexbox works in both directions + +### 6. Cross-Axis Alignment +- Horizontal flex with center cross-axis alignment +- Items of different heights aligned to center + +### 7. Main-Axis Alignment +- Fixed-size items with space-between alignment +- Shows how items are distributed along main axis + +### 8. Space Around Alignment +- Fixed-size items with space-around alignment +- Equal space around each item + +### 9. Complex Nested Layout +- Multiple nested flexbox containers +- Demonstrates building complex layouts + +### 10. Margin and Padding Demo +- Flex items with various margin and padding configurations +- Shows how spacing affects layout + +## Controls + +- **← →** or **h l**: Navigate between demos +- **Space**: Next demo +- **1-9**: Jump to specific demo +- **r**: Reset to first demo +- **q** or **Ctrl+C**: Quit + +## Key Concepts Demonstrated + +### Flex Grow +```go +tv.FlexSize{Grow: 2} // Takes twice as much space as Grow: 1 +``` + +### Flex with Basis +```go +tv.FlexSize{ + Grow: 1, + Basis: tv.FixedSize(100), // Start at 100px, then grow +} +``` + +### Cross-Axis Alignment +```go +layout.CrossAxisAlignment = tv.CrossAxisCenter // Center items perpendicular to main axis +``` + +### Main-Axis Alignment +```go +layout.MainAxisAlignment = tv.MainAxisSpaceBetween // Distribute space between items +``` + +### Spacing +```go +item.Margin = tv.Uniform(5) // 5px on all sides +item.Margin = tv.HorizontalVertical(10, 5) // 10px horizontal, 5px vertical +item.Margin = tv.Spacing{Top: 2, Right: 8, Bottom: 2, Left: 8} // Custom spacing +``` + +## Running the Demo + +```bash +cd examples/flexbox +go run main.go +``` + +The demo is fully interactive and responsive - resize your terminal to see how the layouts adapt to different screen sizes. + +## Layout System Benefits + +1. **Intuitive**: Similar to CSS Flexbox, familiar to web developers +2. **Powerful**: Handles complex layouts with simple declarations +3. **Responsive**: Automatically adapts to different screen sizes +4. **Composable**: Nest layouts to build complex UIs +5. **Performance**: Efficient O(n) layout calculations \ No newline at end of file diff --git a/examples/flexbox/main.go b/examples/flexbox/main.go new file mode 100644 index 0000000..d3cc03d --- /dev/null +++ b/examples/flexbox/main.go @@ -0,0 +1,417 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/tv" + "github.com/charmbracelet/tv/component/styledstring" + "github.com/charmbracelet/x/ansi" +) + +type FlexDemo struct { + currentDemo int + demos []FlexDemoConfig +} + +type FlexDemoConfig struct { + title string + description string + layout *tv.Layout +} + +func main() { + t := tv.DefaultTerminal() + if err := t.Start(); err != nil { + log.Fatalf("starting program: %v", err) + } + + physicalWidth, physicalHeight, err := t.GetSize() + if err != nil { + log.Fatalf("getting size: %v", err) + } + + // Use altscreen mode + t.EnterAltScreen() + defer t.ExitAltScreen() + + // Enable mouse events + modes := []ansi.Mode{ + ansi.ButtonEventMouseMode, + ansi.SgrExtMouseMode, + } + + os.Stdout.WriteString(ansi.SetMode(modes...)) //nolint:errcheck + defer os.Stdout.WriteString(ansi.ResetMode(modes...)) //nolint:errcheck + + // Create styles + headerStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#7D56F4")). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(0, 1). + Bold(true) + + titleStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#FF6B6B")). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(0, 1). + Bold(true) + + descStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#4ECDC4")). + Foreground(lipgloss.Color("#000000")). + Padding(0, 1) + + footerStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#555555")). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(0, 1) + + // Flex item styles with different colors + itemStyles := []lipgloss.Style{ + lipgloss.NewStyle().Background(lipgloss.Color("#FF9F43")).Foreground(lipgloss.Color("#000000")).Padding(1).Bold(true), + lipgloss.NewStyle().Background(lipgloss.Color("#10AC84")).Foreground(lipgloss.Color("#FFFFFF")).Padding(1).Bold(true), + lipgloss.NewStyle().Background(lipgloss.Color("#5F27CD")).Foreground(lipgloss.Color("#FFFFFF")).Padding(1).Bold(true), + lipgloss.NewStyle().Background(lipgloss.Color("#EE5A24")).Foreground(lipgloss.Color("#FFFFFF")).Padding(1).Bold(true), + lipgloss.NewStyle().Background(lipgloss.Color("#0984E3")).Foreground(lipgloss.Color("#FFFFFF")).Padding(1).Bold(true), + } + + // Create flex demo configurations + flexDemo := &FlexDemo{ + currentDemo: 0, + demos: []FlexDemoConfig{ + { + title: "Basic Horizontal Flex", + description: "Three items with equal flex grow (1:1:1)", + layout: &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FlexSize{Grow: 1}, nil), + }, + Gap: 5, + }, + }, + { + title: "Proportional Flex Growth", + description: "Items with different grow factors (1:2:3)", + layout: &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FlexSize{Grow: 2}, nil), + tv.Item(tv.FlexSize{Grow: 3}, nil), + }, + Gap: 5, + }, + }, + { + title: "Fixed + Flex Layout", + description: "Fixed sidebar (200px) + flexible content + fixed sidebar (150px)", + layout: &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FixedSize(200), nil), + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FixedSize(150), nil), + }, + Gap: 10, + }, + }, + { + title: "Flex with Basis", + description: "Items with initial size (basis) that then grow", + layout: &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FlexSize{Grow: 1, Basis: tv.FixedSize(100)}, nil), + tv.Item(tv.FlexSize{Grow: 2, Basis: tv.FixedSize(50)}, nil), + tv.Item(tv.FlexSize{Grow: 1, Basis: tv.FixedSize(200)}, nil), + }, + Gap: 5, + }, + }, + { + title: "Vertical Flex Layout", + description: "Vertical flexbox with different grow factors", + layout: &tv.Layout{ + Direction: tv.Vertical, + Items: []tv.LayoutItem{ + tv.Item(nil, tv.FixedSize(60)), + tv.Item(nil, tv.FlexSize{Grow: 2}), + tv.Item(nil, tv.FlexSize{Grow: 1}), + tv.Item(nil, tv.FixedSize(40)), + }, + Gap: 5, + }, + }, + { + title: "Cross-Axis Alignment", + description: "Horizontal flex with center cross-axis alignment", + layout: &tv.Layout{ + Direction: tv.Horizontal, + CrossAxisAlignment: tv.CrossAxisCenter, + Items: []tv.LayoutItem{ + tv.Item(tv.FlexSize{Grow: 1}, tv.FixedSize(60)), + tv.Item(tv.FlexSize{Grow: 1}, tv.FixedSize(100)), + tv.Item(tv.FlexSize{Grow: 1}, tv.FixedSize(80)), + }, + Gap: 5, + }, + }, + { + title: "Main-Axis Alignment", + description: "Fixed-size items with space-between alignment", + layout: &tv.Layout{ + Direction: tv.Horizontal, + MainAxisAlignment: tv.MainAxisSpaceBetween, + Items: []tv.LayoutItem{ + tv.Item(tv.FixedSize(120), nil), + tv.Item(tv.FixedSize(120), nil), + tv.Item(tv.FixedSize(120), nil), + }, + }, + }, + { + title: "Space Around Alignment", + description: "Fixed-size items with space-around alignment", + layout: &tv.Layout{ + Direction: tv.Horizontal, + MainAxisAlignment: tv.MainAxisSpaceAround, + Items: []tv.LayoutItem{ + tv.Item(tv.FixedSize(100), nil), + tv.Item(tv.FixedSize(100), nil), + tv.Item(tv.FixedSize(100), nil), + tv.Item(tv.FixedSize(100), nil), + }, + }, + }, + { + title: "Complex Nested Layout", + description: "Vertical layout with horizontal flex containers", + layout: &tv.Layout{ + Direction: tv.Vertical, + Items: []tv.LayoutItem{ + tv.Item(nil, tv.FixedSize(80)), + tv.Item(nil, tv.FlexSize{Grow: 1}), + tv.Item(nil, tv.FixedSize(60)), + }, + Gap: 5, + }, + }, + { + title: "Margin and Padding Demo", + description: "Flex items with margins and padding", + layout: &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + { + Width: tv.FlexSize{Grow: 1}, + Margin: tv.Uniform(5), + Padding: tv.Uniform(3), + }, + { + Width: tv.FlexSize{Grow: 2}, + Margin: tv.HorizontalVertical(10, 5), + Padding: tv.HorizontalVertical(5, 2), + }, + { + Width: tv.FlexSize{Grow: 1}, + Margin: tv.Spacing{Top: 2, Right: 8, Bottom: 2, Left: 8}, + Padding: tv.Uniform(4), + }, + }, + Gap: 0, // No gap since we're using margins + }, + }, + }, + } + + // Create main layout areas + mainArea := tv.Rect(0, 0, physicalWidth, physicalHeight) + mainLayout := tv.NewVerticalLayout( + tv.Item(nil, tv.FixedSize(3)), // Header + tv.Item(nil, tv.FixedSize(5)), // Title and description + tv.Item(nil, tv.FlexSize{Grow: 1}), // Demo area + tv.Item(nil, tv.FixedSize(3)), // Footer + ) + + // Create frame + f := &tv.Frame{ + Buffer: tv.NewBuffer(physicalWidth, physicalHeight), + Viewport: tv.FullViewport{}, + Area: mainArea, + } + + display := func() { + f.Buffer.Clear() + + // Calculate main layout + mainResult := mainLayout.Calculate(mainArea) + headerArea := mainResult.Areas[0] + titleArea := mainResult.Areas[1] + demoArea := mainResult.Areas[2] + footerArea := mainResult.Areas[3] + + // Render header + headerText := headerStyle.Width(headerArea.Dx()).Render("Flexbox Layout System Demo") + headerSs := styledstring.New(ansi.WcWidth, headerText) + f.RenderComponent(headerSs, headerArea) //nolint:errcheck + + // Render title and description + currentConfig := flexDemo.demos[flexDemo.currentDemo] + titleText := titleStyle.Width(titleArea.Dx()).Render( + fmt.Sprintf("[%d/%d] %s", flexDemo.currentDemo+1, len(flexDemo.demos), currentConfig.title)) + titleSs := styledstring.New(ansi.WcWidth, titleText) + f.RenderComponent(titleSs, tv.Rect(titleArea.Min.X, titleArea.Min.Y, titleArea.Dx(), 1)) //nolint:errcheck + + descText := descStyle.Width(titleArea.Dx()).Render(currentConfig.description) + descSs := styledstring.New(ansi.WcWidth, descText) + f.RenderComponent(descSs, tv.Rect(titleArea.Min.X, titleArea.Min.Y+1, titleArea.Dx(), titleArea.Dy()-1)) //nolint:errcheck + + // Calculate and render the current demo layout + demoResult := currentConfig.layout.Calculate(demoArea) + + // Handle nested layout for demo 9 (Complex Nested Layout) + if flexDemo.currentDemo == 8 { + // Top section + topLayout := &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FlexSize{Grow: 2}, nil), + }, + Gap: 5, + } + topResult := topLayout.Calculate(demoResult.Areas[0]) + + // Middle section (main flex demo) + middleLayout := &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FixedSize(150), nil), + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FlexSize{Grow: 2}, nil), + tv.Item(tv.FixedSize(100), nil), + }, + Gap: 5, + } + middleResult := middleLayout.Calculate(demoResult.Areas[1]) + + // Bottom section + bottomLayout := &tv.Layout{ + Direction: tv.Horizontal, + Items: []tv.LayoutItem{ + tv.Item(tv.FlexSize{Grow: 3}, nil), + tv.Item(tv.FlexSize{Grow: 1}, nil), + tv.Item(tv.FlexSize{Grow: 1}, nil), + }, + Gap: 5, + } + bottomResult := bottomLayout.Calculate(demoResult.Areas[2]) + + // Render all sections + allAreas := append(topResult.Areas, append(middleResult.Areas, bottomResult.Areas...)...) + for i, area := range allAreas { + if i < len(itemStyles) { + content := fmt.Sprintf("Item %d\n%dx%d", i+1, area.Dx(), area.Dy()) + itemText := itemStyles[i%len(itemStyles)]. + Width(area.Dx() - 2). + Height(area.Dy() - 2). + Render(content) + itemSs := styledstring.New(ansi.WcWidth, itemText) + f.RenderComponent(itemSs, area) //nolint:errcheck + } + } + } else { + // Regular demo rendering + for i, area := range demoResult.Areas { + if i < len(itemStyles) { + var content string + if flexDemo.currentDemo == 9 { // Margin and padding demo + content = fmt.Sprintf("Item %d\nMargin+Padding\n%dx%d", i+1, area.Dx(), area.Dy()) + } else { + content = fmt.Sprintf("Item %d\n%dx%d", i+1, area.Dx(), area.Dy()) + } + + itemText := itemStyles[i]. + Width(area.Dx() - 2). + Height(area.Dy() - 2). + Render(content) + itemSs := styledstring.New(ansi.WcWidth, itemText) + f.RenderComponent(itemSs, area) //nolint:errcheck + } + } + } + + // Render footer + footerText := footerStyle.Width(footerArea.Dx()).Render( + "← → Navigate demos | q Quit | r Reset to demo 1") + footerSs := styledstring.New(ansi.WcWidth, footerText) + f.RenderComponent(footerSs, footerArea) //nolint:errcheck + + t.Display(f) + } + + // Set terminal to raw mode + if err := t.MakeRaw(); err != nil { + log.Fatalf("making raw: %v", err) + } + defer t.Restore() //nolint:errcheck + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // First display + display() + + // Event loop + for ev := range t.Events(ctx) { + switch ev := ev.(type) { + case tv.WindowSizeEvent: + // Mark screen to be redrawn. + t.ClearScreen() + + // Recalculate layout on resize + mainArea = tv.Rect(0, 0, ev.Width, ev.Height) + f.Area = mainArea + f.Buffer.Resize(ev.Width, ev.Height) + t.Resize(ev.Width, ev.Height) + + case tv.KeyPressEvent: + switch { + case ev.MatchStrings("ctrl+c", "q"): + cancel() + case ev.MatchStrings("right", "l", "space"): + flexDemo.currentDemo = (flexDemo.currentDemo + 1) % len(flexDemo.demos) + case ev.MatchStrings("left", "h"): + flexDemo.currentDemo = (flexDemo.currentDemo - 1 + len(flexDemo.demos)) % len(flexDemo.demos) + case ev.MatchStrings("r"): + flexDemo.currentDemo = 0 + case ev.MatchStrings("1", "2", "3", "4", "5", "6", "7", "8", "9"): + if demoNum := int(ev.Code - '1'); demoNum < len(flexDemo.demos) { + flexDemo.currentDemo = demoNum + } + } + } + + display() + } + + // Shutdown gracefully + if err := t.Shutdown(ctx); err != nil { + log.Fatalf("shutting down program: %v", err) + } +} + +func init() { + f, err := os.OpenFile("flexbox_demo.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) + if err != nil { + log.Fatal(err) + } + log.SetOutput(f) +} diff --git a/examples/layout_new/main.go b/examples/layout_new/main.go new file mode 100644 index 0000000..ab53498 --- /dev/null +++ b/examples/layout_new/main.go @@ -0,0 +1,224 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/tv" + "github.com/charmbracelet/tv/component/styledstring" + "github.com/charmbracelet/x/ansi" +) + +func main() { + t := tv.DefaultTerminal() + if err := t.Start(); err != nil { + log.Fatalf("starting program: %v", err) + } + + physicalWidth, physicalHeight, err := t.GetSize() + if err != nil { + log.Fatalf("getting size: %v", err) + } + + // Use altscreen mode + t.EnterAltScreen() + defer t.ExitAltScreen() + + // Enable mouse events + modes := []ansi.Mode{ + ansi.ButtonEventMouseMode, + ansi.SgrExtMouseMode, + } + + os.Stdout.WriteString(ansi.SetMode(modes...)) //nolint:errcheck + defer os.Stdout.WriteString(ansi.ResetMode(modes...)) //nolint:errcheck + + // Create styles + headerStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#7D56F4")). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(0, 1). + Bold(true) + + sidebarStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#383838")). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(1) + + contentStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#F0F0F0")). + Foreground(lipgloss.Color("#000000")). + Padding(1) + + footerStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#555555")). + Foreground(lipgloss.Color("#FFFFFF")). + Padding(0, 1) + + cardStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#FFFFFF")). + Foreground(lipgloss.Color("#000000")). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#CCCCCC")). + Padding(1). + Margin(1) + + // Create the main layout using the new layout system + mainArea := tv.Rect(0, 0, physicalWidth, physicalHeight) + + // Main vertical layout: header, body, footer + mainLayout := tv.NewVerticalLayout( + tv.Item(nil, tv.FixedSize(3)), // Header + tv.Item(nil, tv.FlexSize{Grow: 1}), // Body (flexible) + tv.Item(nil, tv.FixedSize(3)), // Footer + ) + mainResult := mainLayout.Calculate(mainArea) + + headerArea := mainResult.Areas[0] + bodyArea := mainResult.Areas[1] + footerArea := mainResult.Areas[2] + + // Body layout: sidebar and content + bodyLayout := tv.NewHorizontalLayout( + tv.Item(tv.FixedSize(20), nil), // Sidebar + tv.Item(tv.FlexSize{Grow: 1}, nil), // Content (flexible) + ) + bodyLayout.Gap = 1 + bodyResult := bodyLayout.Calculate(bodyArea) + + sidebarArea := bodyResult.Areas[0] + contentArea := bodyResult.Areas[1] + + // Content area with grid layout for cards + cardGrid := tv.NewGrid(3, + tv.GridItemAt(1, 1, tv.Item(nil, tv.FixedSize(8))), + tv.GridItemAt(2, 1, tv.Item(nil, tv.FixedSize(8))), + tv.GridItemAt(3, 1, tv.Item(nil, tv.FixedSize(8))), + tv.GridItemSpan(1, 2, 2, 1, tv.Item(nil, tv.FixedSize(8))), // Spans 2 columns + tv.GridItemAt(3, 2, tv.Item(nil, tv.FixedSize(8))), + ) + cardGrid.ColumnGap = 2 + cardGrid.RowGap = 1 + gridResult := cardGrid.Calculate(contentArea) + + // Create frame + f := &tv.Frame{ + Buffer: tv.NewBuffer(physicalWidth, physicalHeight), + Viewport: tv.FullViewport{}, + Area: mainArea, + } + + display := func() { + f.Buffer.Clear() + + // Render header + headerText := headerStyle.Width(headerArea.Dx()).Render("New Layout System Demo") + headerSs := styledstring.New(ansi.WcWidth, headerText) + f.RenderComponent(headerSs, headerArea) //nolint:errcheck + + // Render sidebar + sidebarContent := sidebarStyle. + Width(sidebarArea.Dx() - 2). + Height(sidebarArea.Dy() - 2). + Render("Navigation\n\n• Home\n• About\n• Contact\n• Settings") + sidebarSs := styledstring.New(ansi.WcWidth, sidebarContent) + f.RenderComponent(sidebarSs, sidebarArea) //nolint:errcheck + + // Render main content area background + contentText := contentStyle. + Width(contentArea.Dx() - 2). + Height(contentArea.Dy() - 2). + Render("Main Content Area") + contentSs := styledstring.New(ansi.WcWidth, contentText) + f.RenderComponent(contentSs, contentArea) //nolint:errcheck + + // Render content cards using grid + cardContents := []string{ + "Card 1\n\nThis is the first card with some content.", + "Card 2\n\nThis is the second card with different content.", + "Card 3\n\nThis is the third card in the top row.", + "Large Card\n\nThis card spans two columns and shows how grid spanning works in the new layout system.", + "Card 5\n\nThis is the last card in the grid.", + } + + for i, cardArea := range gridResult.Areas { + if i < len(cardContents) { + cardText := cardStyle. + Width(cardArea.Dx() - 4). + Height(cardArea.Dy() - 4). + Render(cardContents[i]) + cardSs := styledstring.New(ansi.WcWidth, cardText) + f.RenderComponent(cardSs, cardArea) //nolint:errcheck + } + } + + // Render footer + footerText := footerStyle.Width(footerArea.Dx()).Render("Press 'q' to quit | Arrow keys to navigate") + footerSs := styledstring.New(ansi.WcWidth, footerText) + f.RenderComponent(footerSs, footerArea) //nolint:errcheck + + t.Display(f) + } + + // Set terminal to raw mode + if err := t.MakeRaw(); err != nil { + log.Fatalf("making raw: %v", err) + } + defer t.Restore() //nolint:errcheck + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // First display + display() + + // Event loop + for ev := range t.Events(ctx) { + switch ev := ev.(type) { + case tv.WindowSizeEvent: + // Mark screen to be redrawn. + t.ClearScreen() + + // Recalculate layout on resize + mainArea = tv.Rect(0, 0, ev.Width, ev.Height) + f.Area = mainArea + f.Buffer.Resize(ev.Width, ev.Height) + t.Resize(ev.Width, ev.Height) + + // Recalculate all layouts + mainResult = mainLayout.Calculate(mainArea) + headerArea = mainResult.Areas[0] + bodyArea = mainResult.Areas[1] + footerArea = mainResult.Areas[2] + + bodyResult = bodyLayout.Calculate(bodyArea) + sidebarArea = bodyResult.Areas[0] + contentArea = bodyResult.Areas[1] + + gridResult = cardGrid.Calculate(contentArea) + + case tv.KeyPressEvent: + switch { + case ev.MatchStrings("ctrl+c", "q"): + cancel() + } + } + + display() + } + + // Shutdown gracefully + if err := t.Shutdown(ctx); err != nil { + log.Fatalf("shutting down program: %v", err) + } +} + +func init() { + f, err := os.OpenFile("layout_demo.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) + if err != nil { + log.Fatal(err) + } + log.SetOutput(f) +} diff --git a/layout.go b/layout.go index 66322b6..3ad22e0 100644 --- a/layout.go +++ b/layout.go @@ -1,313 +1 @@ package tv - -// Direction represents a direction in a coordinate system. -type Direction uint8 - -// Direction constants. -const ( - // Vertical direction. - Vertical Direction = iota - // Horizontal direction. - Horizontal -) - -// Constraint represents a constraint on a rectangle. -type Constraint interface { - // Apply applies the constraint to the given rectangle. - Apply(length int) int -} - -// Length is a constraint that splits a rectangle by a fixed length. -type Length int - -// Apply applies the length constraint to the given rectangle. -func (l Length) Apply(length int) int { - if l < 0 { - return 0 - } - return int(l) -} - -// Percent is a constraint that splits a rectangle by a percentage of its -// length. -type Percent int - -// Apply applies the percent constraint to the given rectangle. -func (p Percent) Apply(length int) int { - if p < 0 { - return 0 - } - return int(p) * length / 100 -} - -// Ratio returns a [Constraint] that splits a rectangle by a ratio of its -// length. This is a convenience function for creating a [Percent] constraint. -func Ratio(n, d int) Constraint { - if n <= 0 || d <= 0 { - return Length(0) - } - return Percent(n * 100 / d) -} - -// Layout is a set of [Constraint]s and a [Direction] that can be applied to a -// [Rectangle] to split it into smaller ones. -type Layout struct { - // dir is the direction of the layout. - dir Direction - // consts is a list of constraints to apply to the rectangle. - consts []Constraint - // The layout margins. - marginTop, marginRight, marginBot, marginLeft int - // The space between the split rectangles. Positive values indicate - // additional space between the rectangles. Negative values indicate - // overlapping rectangles. Zero means no space between the rectangles. - spacing int -} - -// NewLayout creates a new [Layout] with the given direction and constraints. -// The constrains are applied in the order they are received. -func NewLayout(direction Direction, constraints ...Constraint) *Layout { - return &Layout{ - dir: direction, - consts: constraints, - } -} - -// NewVerticalLayout creates a new vertical [Layout] with the given -// constraints. -func NewVerticalLayout(constraints ...Constraint) *Layout { - return NewLayout(Vertical, constraints...) -} - -// NewHorizontalLayout creates a new horizontal [Layout] with the given -// constraints. -func NewHorizontalLayout(constraints ...Constraint) *Layout { - return NewLayout(Horizontal, constraints...) -} - -// Split splits the given rectangle into smaller ones based on the layout -// constraints and direction. The resulting rectangles are returned as a slice. -func (l *Layout) Split(r Rectangle) []Rectangle { - if l.dir == Horizontal { - rects, _, _ := l.splitHorizontal(r) - return rects - } - rects, _, _ := l.splitVertical(r) - return rects -} - -// Areas splits the given rectangle into smaller ones based on the layout -// constraints and direction. The resulting rectangles are returned as a two -// slices: the first slice contains the rectangles, the second slice contains -// any margin rectangles, and the third slice contains any spacing rectangles. -func (l *Layout) Areas(r Rectangle) ([]Rectangle, []Rectangle, []Rectangle) { - if l.dir == Horizontal { - return l.splitHorizontal(r) - } - return l.splitVertical(r) -} - -// Spacing sets the spacing between the split rectangles. Positive values -// indicate additional space between the rectangles. Negative values indicate -// overlapping rectangles. Zero means no space between the rectangles. -func (l *Layout) Spacing(s int) *Layout { - l.spacing = s - return l -} - -// Margin sets the margins for the layout. One value sets all margins to the -// same value, two values set the vertical and horizontal margins, three values -// set the top, horizontal, and bottom margins, and four values set the top, -// right, bottom, and left margins. More than four values are ignored. -func (l *Layout) Margin(m ...int) *Layout { - switch len(m) { - case 1: - return l.MarginTop(m[0]).MarginRight(m[0]).MarginBottom(m[0]).MarginLeft(m[0]) - case 2: - return l.MarginTop(m[0]).MarginRight(m[1]).MarginBottom(m[0]).MarginLeft(m[1]) - case 3: - return l.MarginTop(m[0]).MarginRight(m[1]).MarginBottom(m[2]).MarginLeft(m[1]) - case 4: - return l.MarginTop(m[0]).MarginRight(m[1]).MarginBottom(m[2]).MarginLeft(m[3]) - default: - return l - } -} - -// MarginTop sets the top margin for the layout. -func (l *Layout) MarginTop(m int) *Layout { - l.marginTop = m - return l -} - -// MarginRight sets the right margin for the layout. -func (l *Layout) MarginRight(m int) *Layout { - l.marginRight = m - return l -} - -// MarginBottom sets the bottom margin for the layout. -func (l *Layout) MarginBottom(m int) *Layout { - l.marginBot = m - return l -} - -// MarginLeft sets the left margin for the layout. -func (l *Layout) MarginLeft(m int) *Layout { - l.marginLeft = m - return l -} - -// splitHorizontal splits a rectangle into smaller ones. The resulting -// rectangles are returned as three slices: the first slice contains the area -// rectangles, the second slice contains the margin rectangles, and the third -// slice contains the spacing rectangles. -// Each constraint is applied to the remaining rectangle in the order they are -// received. -func (l *Layout) splitHorizontal(r Rectangle) ([]Rectangle, []Rectangle, []Rectangle) { - var areas, margins, spacings []Rectangle - - // Apply margins to the rectangle - if l.marginTop > 0 { - margins = append(margins, Rect(r.Min.X, r.Min.Y, r.Dx(), l.marginTop)) - } - if l.marginRight > 0 { - margins = append(margins, Rect(r.Max.X-l.marginRight, r.Min.Y+l.marginTop, l.marginRight, r.Dy()-l.marginTop-l.marginBot)) - } - if l.marginBot > 0 { - margins = append(margins, Rect(r.Min.X, r.Max.Y-l.marginBot, r.Dx(), l.marginBot)) - } - if l.marginLeft > 0 { - margins = append(margins, Rect(r.Min.X, r.Min.Y+l.marginTop, l.marginLeft, r.Dy()-l.marginTop-l.marginBot)) - } - - // Apply the margins to the rectangle - innerRect := Rect( - r.Min.X+l.marginLeft, - r.Min.Y+l.marginTop, - r.Dx()-l.marginLeft-l.marginRight, - r.Dy()-l.marginTop-l.marginBot, - ) - - // If there are no constraints, return the rectangle as is - if len(l.consts) == 0 { - areas = append(areas, innerRect) - return areas, margins, spacings - } - - // Apply the constraints sequentially to the remaining area - x := innerRect.Min.X - remainingWidth := innerRect.Dx() - - for i, c := range l.consts { - width := min(c.Apply(remainingWidth), remainingWidth) - - // Add the area - areas = append(areas, Rect(x, innerRect.Min.Y, width, innerRect.Dy())) - - // Update position and remaining width - x += width - remainingWidth -= width - - // Add spacing if not the last constraint and we have room - if i < len(l.consts)-1 && l.spacing != 0 && remainingWidth > l.spacing { - spacings = append(spacings, Rect(x, innerRect.Min.Y, l.spacing, innerRect.Dy())) - x += l.spacing - remainingWidth -= l.spacing - } - - // If we've run out of space, stop processing constraints - if remainingWidth <= 0 { - break - } - } - - // If there's remaining width after all constraints, add it as an additional area - if remainingWidth > 0 { - if l.spacing != 0 && remainingWidth > l.spacing { - spacings = append(spacings, Rect(x, innerRect.Min.Y, l.spacing, innerRect.Dy())) - x += l.spacing - remainingWidth -= l.spacing - } - areas = append(areas, Rect(x, innerRect.Min.Y, remainingWidth, innerRect.Dy())) - } - - return areas, margins, spacings -} - -// splitVertical splits a rectangle into smaller ones. The resulting -// rectangles are returned as three slices: the first slice contains the area -// rectangles, the second slice contains the margin rectangles, and the third -// slice contains the spacing rectangles. -// Each constraint is applied to the remaining rectangle in the order they are -// received. -func (l *Layout) splitVertical(r Rectangle) ([]Rectangle, []Rectangle, []Rectangle) { - var areas, margins, spacings []Rectangle - - // Apply margins to the rectangle - if l.marginTop > 0 { - margins = append(margins, Rect(r.Min.X, r.Min.Y, r.Dx(), l.marginTop)) - } - if l.marginRight > 0 { - margins = append(margins, Rect(r.Max.X-l.marginRight, r.Min.Y+l.marginTop, l.marginRight, r.Dy()-l.marginTop-l.marginBot)) - } - if l.marginBot > 0 { - margins = append(margins, Rect(r.Min.X, r.Max.Y-l.marginBot, r.Dx(), l.marginBot)) - } - if l.marginLeft > 0 { - margins = append(margins, Rect(r.Min.X, r.Min.Y+l.marginTop, l.marginLeft, r.Dy()-l.marginTop-l.marginBot)) - } - - // Apply the margins to the rectangle - innerRect := Rect( - r.Min.X+l.marginLeft, - r.Min.Y+l.marginTop, - r.Dx()-l.marginLeft-l.marginRight, - r.Dy()-l.marginTop-l.marginBot, - ) - - // If there are no constraints, return the rectangle as is - if len(l.consts) == 0 { - areas = append(areas, innerRect) - return areas, margins, spacings - } - - // Apply the constraints sequentially to the remaining area - y := innerRect.Min.Y - remainingHeight := innerRect.Dy() - - for i, c := range l.consts { - height := min(c.Apply(remainingHeight), remainingHeight) - - // Add the area - areas = append(areas, Rect(innerRect.Min.X, y, innerRect.Dx(), height)) - - // Update position and remaining height - y += height - remainingHeight -= height - - // Add spacing if not the last constraint and we have room - if i < len(l.consts)-1 && l.spacing != 0 && remainingHeight > l.spacing { - spacings = append(spacings, Rect(innerRect.Min.X, y, innerRect.Dx(), l.spacing)) - y += l.spacing - remainingHeight -= l.spacing - } - - // If we've run out of space, stop processing constraints - if remainingHeight <= 0 { - break - } - } - - // If there's remaining height after all constraints, add it as an additional area - if remainingHeight > 0 { - if l.spacing != 0 && remainingHeight > l.spacing { - spacings = append(spacings, Rect(innerRect.Min.X, y, innerRect.Dx(), l.spacing)) - y += l.spacing - remainingHeight -= l.spacing - } - areas = append(areas, Rect(innerRect.Min.X, y, innerRect.Dx(), remainingHeight)) - } - - return areas, margins, spacings -} diff --git a/layout_new.go b/layout_new.go new file mode 100644 index 0000000..7243800 --- /dev/null +++ b/layout_new.go @@ -0,0 +1,666 @@ +package tv + +import ( + "math" +) + +// LayoutDirection represents the direction of layout flow. +type LayoutDirection int + +const ( + // Horizontal layouts flow left to right. + Horizontal LayoutDirection = iota + // Vertical layouts flow top to bottom. + Vertical +) + +// SizeConstraint represents how a layout item should be sized. +type SizeConstraint interface { + // Calculate returns the size in pixels given the available space. + Calculate(available int) int + // IsFlexible returns true if this constraint can grow/shrink. + IsFlexible() bool +} + +// FixedSize represents a fixed size constraint. +type FixedSize int + +func (f FixedSize) Calculate(available int) int { + return int(f) +} + +func (f FixedSize) IsFlexible() bool { + return false +} + +// RatioSize represents a proportional size constraint (0.0 to 1.0). +type RatioSize float64 + +func (r RatioSize) Calculate(available int) int { + if r < 0 { + r = 0 + } else if r > 1 { + r = 1 + } + return int(float64(available) * float64(r)) +} + +func (r RatioSize) IsFlexible() bool { + return false +} + +// FlexSize represents a flexible size constraint with optional grow/shrink factors. +type FlexSize struct { + // Grow factor (default 1). Higher values take more space. + Grow float64 + // Shrink factor (default 1). Higher values shrink more when space is limited. + Shrink float64 + // Basis is the initial size before growing/shrinking. + Basis SizeConstraint +} + +func (f FlexSize) Calculate(available int) int { + if f.Basis != nil { + return f.Basis.Calculate(available) + } + return 0 +} + +func (f FlexSize) IsFlexible() bool { + return true +} + +// MinMaxSize wraps another constraint with minimum and maximum bounds. +type MinMaxSize struct { + Constraint SizeConstraint + Min int + Max int +} + +func (m MinMaxSize) Calculate(available int) int { + size := m.Constraint.Calculate(available) + if m.Min > 0 && size < m.Min { + size = m.Min + } + if m.Max > 0 && size > m.Max { + size = m.Max + } + return size +} + +func (m MinMaxSize) IsFlexible() bool { + return m.Constraint.IsFlexible() +} + +// LayoutItem represents a single item in a layout. +type LayoutItem struct { + // Width constraint for this item. + Width SizeConstraint + // Height constraint for this item. + Height SizeConstraint + // Margin around the item. + Margin Spacing + // Padding inside the item. + Padding Spacing +} + +// Spacing represents spacing values for all four sides. +type Spacing struct { + Top, Right, Bottom, Left int +} + +// Uniform creates spacing with the same value on all sides. +func Uniform(size int) Spacing { + return Spacing{Top: size, Right: size, Bottom: size, Left: size} +} + +// Horizontal creates spacing with horizontal and vertical values. +func HorizontalVertical(horizontal, vertical int) Spacing { + return Spacing{Top: vertical, Right: horizontal, Bottom: vertical, Left: horizontal} +} + +// Layout represents a container that arranges child items. +type Layout struct { + // Direction of the layout flow. + Direction LayoutDirection + // Items in this layout. + Items []LayoutItem + // Gap between items. + Gap int + // Alignment of items in the cross axis. + CrossAxisAlignment CrossAxisAlignment + // Alignment of items in the main axis. + MainAxisAlignment MainAxisAlignment + // Whether items should wrap to new lines/columns. + Wrap bool +} + +// CrossAxisAlignment represents alignment perpendicular to the main axis. +type CrossAxisAlignment int + +const ( + // CrossAxisStart aligns items to the start of the cross axis. + CrossAxisStart CrossAxisAlignment = iota + // CrossAxisCenter centers items on the cross axis. + CrossAxisCenter + // CrossAxisEnd aligns items to the end of the cross axis. + CrossAxisEnd + // CrossAxisStretch stretches items to fill the cross axis. + CrossAxisStretch +) + +// MainAxisAlignment represents alignment along the main axis. +type MainAxisAlignment int + +const ( + // MainAxisStart aligns items to the start of the main axis. + MainAxisStart MainAxisAlignment = iota + // MainAxisCenter centers items on the main axis. + MainAxisCenter + // MainAxisEnd aligns items to the end of the main axis. + MainAxisEnd + // MainAxisSpaceBetween distributes items with space between them. + MainAxisSpaceBetween + // MainAxisSpaceAround distributes items with space around them. + MainAxisSpaceAround + // MainAxisSpaceEvenly distributes items with even spacing. + MainAxisSpaceEvenly +) + +// LayoutResult represents the result of a layout calculation. +type LayoutResult struct { + // Areas for each item in the layout. + Areas []Rectangle + // Total size used by the layout. + Size Rectangle +} + +// Calculate computes the layout for the given available area. +func (l *Layout) Calculate(area Rectangle) LayoutResult { + if len(l.Items) == 0 { + return LayoutResult{Size: area} + } + + switch l.Direction { + case Horizontal: + return l.calculateHorizontal(area) + case Vertical: + return l.calculateVertical(area) + default: + return LayoutResult{Size: area} + } +} + +func (l *Layout) calculateHorizontal(area Rectangle) LayoutResult { + availableWidth := area.Dx() + availableHeight := area.Dy() + + // Calculate total gap space + totalGap := l.Gap * (len(l.Items) - 1) + if totalGap < 0 { + totalGap = 0 + } + + // Calculate widths for each item + widths := l.calculateSizes(availableWidth-totalGap, true) + + // Calculate heights for each item + heights := make([]int, len(l.Items)) + for i, item := range l.Items { + if item.Height != nil { + heights[i] = item.Height.Calculate(availableHeight) + } else { + heights[i] = availableHeight + } + } + + // Position items + areas := make([]Rectangle, len(l.Items)) + x := area.Min.X + + for i, item := range l.Items { + width := widths[i] + height := heights[i] + + // Apply margin + marginWidth := item.Margin.Left + item.Margin.Right + marginHeight := item.Margin.Top + item.Margin.Bottom + + // Calculate y position based on cross-axis alignment + y := l.calculateCrossAxisPosition(area.Min.Y, availableHeight, height+marginHeight) + + // Create the area including margin + itemArea := Rect(x+item.Margin.Left, y+item.Margin.Top, + width-marginWidth, height-marginHeight) + areas[i] = itemArea + + x += width + l.Gap + } + + // Apply main axis alignment + l.applyMainAxisAlignment(areas, area, true) + + return LayoutResult{ + Areas: areas, + Size: area, + } +} + +func (l *Layout) calculateVertical(area Rectangle) LayoutResult { + availableWidth := area.Dx() + availableHeight := area.Dy() + + // Calculate total gap space + totalGap := l.Gap * (len(l.Items) - 1) + if totalGap < 0 { + totalGap = 0 + } + + // Calculate heights for each item + heights := l.calculateSizes(availableHeight-totalGap, false) + + // Calculate widths for each item + widths := make([]int, len(l.Items)) + for i, item := range l.Items { + if item.Width != nil { + widths[i] = item.Width.Calculate(availableWidth) + } else { + widths[i] = availableWidth + } + } + + // Position items + areas := make([]Rectangle, len(l.Items)) + y := area.Min.Y + + for i, item := range l.Items { + width := widths[i] + height := heights[i] + + // Apply margin + marginWidth := item.Margin.Left + item.Margin.Right + marginHeight := item.Margin.Top + item.Margin.Bottom + + // Calculate x position based on cross-axis alignment + x := l.calculateCrossAxisPosition(area.Min.X, availableWidth, width+marginWidth) + + // Create the area including margin + itemArea := Rect(x+item.Margin.Left, y+item.Margin.Top, + width-marginWidth, height-marginHeight) + areas[i] = itemArea + + y += height + l.Gap + } + + // Apply main axis alignment + l.applyMainAxisAlignment(areas, area, false) + + return LayoutResult{ + Areas: areas, + Size: area, + } +} + +func (l *Layout) calculateSizes(available int, isWidth bool) []int { + sizes := make([]int, len(l.Items)) + flexItems := make([]int, 0) + usedSpace := 0 + + // First pass: calculate fixed and ratio sizes + for i, item := range l.Items { + var constraint SizeConstraint + if isWidth { + constraint = item.Width + } else { + constraint = item.Height + } + + if constraint == nil { + // No constraint means flexible + flexItems = append(flexItems, i) + continue + } + + if constraint.IsFlexible() { + flexItems = append(flexItems, i) + if flex, ok := constraint.(FlexSize); ok && flex.Basis != nil { + sizes[i] = flex.Basis.Calculate(available) + usedSpace += sizes[i] + } + } else { + sizes[i] = constraint.Calculate(available) + usedSpace += sizes[i] + } + } + + // Second pass: distribute remaining space to flexible items + remainingSpace := available - usedSpace + if remainingSpace > 0 && len(flexItems) > 0 { + l.distributeFlex(sizes, flexItems, remainingSpace, isWidth) + } + + return sizes +} + +func (l *Layout) distributeFlex(sizes []int, flexItems []int, remainingSpace int, isWidth bool) { + totalGrow := 0.0 + + // Calculate total grow factor + for _, i := range flexItems { + var constraint SizeConstraint + if isWidth { + constraint = l.Items[i].Width + } else { + constraint = l.Items[i].Height + } + + if constraint == nil { + totalGrow += 1.0 // Default grow factor + } else if flex, ok := constraint.(FlexSize); ok { + if flex.Grow > 0 { + totalGrow += flex.Grow + } else { + totalGrow += 1.0 + } + } else { + totalGrow += 1.0 + } + } + + // Distribute space proportionally + for _, i := range flexItems { + var constraint SizeConstraint + if isWidth { + constraint = l.Items[i].Width + } else { + constraint = l.Items[i].Height + } + + grow := 1.0 + if constraint != nil { + if flex, ok := constraint.(FlexSize); ok && flex.Grow > 0 { + grow = flex.Grow + } + } + + additionalSpace := int(float64(remainingSpace) * (grow / totalGrow)) + sizes[i] += additionalSpace + } +} + +func (l *Layout) calculateCrossAxisPosition(start, available, itemSize int) int { + switch l.CrossAxisAlignment { + case CrossAxisStart: + return start + case CrossAxisCenter: + return start + (available-itemSize)/2 + case CrossAxisEnd: + return start + available - itemSize + case CrossAxisStretch: + return start + default: + return start + } +} + +func (l *Layout) applyMainAxisAlignment(areas []Rectangle, container Rectangle, isHorizontal bool) { + if l.MainAxisAlignment == MainAxisStart { + return // Already positioned correctly + } + + var totalItemSize, availableSize int + if isHorizontal { + for _, area := range areas { + totalItemSize += area.Dx() + } + totalItemSize += l.Gap * (len(areas) - 1) + availableSize = container.Dx() + } else { + for _, area := range areas { + totalItemSize += area.Dy() + } + totalItemSize += l.Gap * (len(areas) - 1) + availableSize = container.Dy() + } + + extraSpace := availableSize - totalItemSize + if extraSpace <= 0 { + return + } + + var offset int + var spacing int + + switch l.MainAxisAlignment { + case MainAxisCenter: + offset = extraSpace / 2 + case MainAxisEnd: + offset = extraSpace + case MainAxisSpaceBetween: + if len(areas) > 1 { + spacing = extraSpace / (len(areas) - 1) + } + case MainAxisSpaceAround: + spacing = extraSpace / len(areas) + offset = spacing / 2 + case MainAxisSpaceEvenly: + spacing = extraSpace / (len(areas) + 1) + offset = spacing + } + + // Apply the adjustments + for i := range areas { + if isHorizontal { + areas[i] = areas[i].Add(Pos(offset+i*spacing, 0)) + } else { + areas[i] = areas[i].Add(Pos(0, offset+i*spacing)) + } + } +} + +// Grid represents a grid layout system. +type Grid struct { + // Number of columns in the grid. + Columns int + // Number of rows in the grid (0 means auto). + Rows int + // Gap between grid items. + ColumnGap int + RowGap int + // Items in the grid. + Items []GridItem +} + +// GridItem represents an item in a grid layout. +type GridItem struct { + // Grid position (1-based, 0 means auto). + Column, Row int + // How many columns/rows this item spans. + ColumnSpan, RowSpan int + // Layout item properties. + LayoutItem +} + +// Calculate computes the grid layout for the given available area. +func (g *Grid) Calculate(area Rectangle) LayoutResult { + if len(g.Items) == 0 || g.Columns <= 0 { + return LayoutResult{Size: area} + } + + // Determine number of rows + rows := g.Rows + if rows <= 0 { + rows = int(math.Ceil(float64(len(g.Items)) / float64(g.Columns))) + } + + // Calculate cell dimensions + totalColumnGap := g.ColumnGap * (g.Columns - 1) + totalRowGap := g.RowGap * (rows - 1) + + cellWidth := (area.Dx() - totalColumnGap) / g.Columns + cellHeight := (area.Dy() - totalRowGap) / rows + + areas := make([]Rectangle, len(g.Items)) + + for i, item := range g.Items { + // Determine grid position + col := item.Column + row := item.Row + + if col <= 0 || row <= 0 { + // Auto-placement + autoCol := i % g.Columns + autoRow := i / g.Columns + if col <= 0 { + col = autoCol + 1 + } + if row <= 0 { + row = autoRow + 1 + } + } + + // Convert to 0-based + col-- + row-- + + // Calculate spans + colSpan := item.ColumnSpan + rowSpan := item.RowSpan + if colSpan <= 0 { + colSpan = 1 + } + if rowSpan <= 0 { + rowSpan = 1 + } + + // Calculate position and size + x := area.Min.X + col*(cellWidth+g.ColumnGap) + y := area.Min.Y + row*(cellHeight+g.RowGap) + + width := cellWidth*colSpan + g.ColumnGap*(colSpan-1) + height := cellHeight*rowSpan + g.RowGap*(rowSpan-1) + + // Apply margin + x += item.Margin.Left + y += item.Margin.Top + width -= item.Margin.Left + item.Margin.Right + height -= item.Margin.Top + item.Margin.Bottom + + areas[i] = Rect(x, y, width, height) + } + + return LayoutResult{ + Areas: areas, + Size: area, + } +} + +// Helper functions for creating common layouts + +// NewHorizontalLayout creates a new horizontal layout. +func NewHorizontalLayout(items ...LayoutItem) *Layout { + return &Layout{ + Direction: Horizontal, + Items: items, + } +} + +// NewVerticalLayout creates a new vertical layout. +func NewVerticalLayout(items ...LayoutItem) *Layout { + return &Layout{ + Direction: Vertical, + Items: items, + } +} + +// NewFlexLayout creates a flexible layout with the given direction. +func NewFlexLayout(direction LayoutDirection, items ...LayoutItem) *Layout { + return &Layout{ + Direction: direction, + Items: items, + CrossAxisAlignment: CrossAxisStretch, + } +} + +// NewGrid creates a new grid layout. +func NewGrid(columns int, items ...GridItem) *Grid { + return &Grid{ + Columns: columns, + Items: items, + } +} + +// Item creates a layout item with the given constraints. +func Item(width, height SizeConstraint) LayoutItem { + return LayoutItem{ + Width: width, + Height: height, + } +} + +// FlexItem creates a flexible layout item. +func FlexItem(grow, shrink float64, basis SizeConstraint) LayoutItem { + return LayoutItem{ + Width: FlexSize{ + Grow: grow, + Shrink: shrink, + Basis: basis, + }, + Height: FlexSize{ + Grow: grow, + Shrink: shrink, + Basis: basis, + }, + } +} + +// GridItemAt creates a grid item at the specified position. +func GridItemAt(col, row int, item LayoutItem) GridItem { + return GridItem{ + Column: col, + Row: row, + ColumnSpan: 1, + RowSpan: 1, + LayoutItem: item, + } +} + +// GridItemSpan creates a grid item that spans multiple cells. +func GridItemSpan(col, row, colSpan, rowSpan int, item LayoutItem) GridItem { + return GridItem{ + Column: col, + Row: row, + ColumnSpan: colSpan, + RowSpan: rowSpan, + LayoutItem: item, + } +} + +// Convenience functions for backward compatibility + +// SplitVertical splits a rectangle into two rectangles vertically using the new layout system. +func SplitVertical(r Rectangle, ratio float64) (Rectangle, Rectangle) { + layout := NewHorizontalLayout( + Item(RatioSize(ratio), nil), + Item(RatioSize(1-ratio), nil), + ) + result := layout.Calculate(r) + if len(result.Areas) >= 2 { + return result.Areas[0], result.Areas[1] + } + return r, Rect(0, 0, 0, 0) +} + +// SplitHorizontal splits a rectangle into two rectangles horizontally using the new layout system. +func SplitHorizontal(r Rectangle, ratio float64) (Rectangle, Rectangle) { + layout := NewVerticalLayout( + Item(nil, RatioSize(ratio)), + Item(nil, RatioSize(1-ratio)), + ) + result := layout.Calculate(r) + if len(result.Areas) >= 2 { + return result.Areas[0], result.Areas[1] + } + return r, Rect(0, 0, 0, 0) +} + diff --git a/layout_new_test.go b/layout_new_test.go new file mode 100644 index 0000000..ab91f15 --- /dev/null +++ b/layout_new_test.go @@ -0,0 +1,489 @@ +package tv + +import ( + "testing" +) + +func TestFixedSize(t *testing.T) { + size := FixedSize(100) + if size.Calculate(200) != 100 { + t.Errorf("Expected 100, got %d", size.Calculate(200)) + } + if size.IsFlexible() { + t.Error("FixedSize should not be flexible") + } +} + +func TestRatioSize(t *testing.T) { + tests := []struct { + ratio RatioSize + available int + expected int + }{ + {RatioSize(0.5), 100, 50}, + {RatioSize(0.25), 200, 50}, + {RatioSize(-0.1), 100, 0}, // Clamped to 0 + {RatioSize(1.5), 100, 100}, // Clamped to 1 + } + + for _, test := range tests { + result := test.ratio.Calculate(test.available) + if result != test.expected { + t.Errorf("RatioSize(%f).Calculate(%d) = %d, expected %d", + test.ratio, test.available, result, test.expected) + } + } + + if RatioSize(0.5).IsFlexible() { + t.Error("RatioSize should not be flexible") + } +} + +func TestFlexSize(t *testing.T) { + flex := FlexSize{Grow: 1, Shrink: 1, Basis: FixedSize(50)} + if flex.Calculate(100) != 50 { + t.Errorf("Expected 50, got %d", flex.Calculate(100)) + } + if !flex.IsFlexible() { + t.Error("FlexSize should be flexible") + } +} + +func TestMinMaxSize(t *testing.T) { + constraint := MinMaxSize{ + Constraint: FixedSize(100), + Min: 50, + Max: 150, + } + + if constraint.Calculate(200) != 100 { + t.Errorf("Expected 100, got %d", constraint.Calculate(200)) + } + + // Test with constraint that would be too small + smallConstraint := MinMaxSize{ + Constraint: FixedSize(25), + Min: 50, + Max: 150, + } + if smallConstraint.Calculate(200) != 50 { + t.Errorf("Expected 50 (min), got %d", smallConstraint.Calculate(200)) + } + + // Test with constraint that would be too large + largeConstraint := MinMaxSize{ + Constraint: FixedSize(200), + Min: 50, + Max: 150, + } + if largeConstraint.Calculate(300) != 150 { + t.Errorf("Expected 150 (max), got %d", largeConstraint.Calculate(300)) + } +} + +func TestSpacing(t *testing.T) { + uniform := Uniform(10) + expected := Spacing{Top: 10, Right: 10, Bottom: 10, Left: 10} + if uniform != expected { + t.Errorf("Uniform(10) = %+v, expected %+v", uniform, expected) + } + + hv := HorizontalVertical(20, 15) + expected = Spacing{Top: 15, Right: 20, Bottom: 15, Left: 20} + if hv != expected { + t.Errorf("HorizontalVertical(20, 15) = %+v, expected %+v", hv, expected) + } +} + +func TestHorizontalLayout(t *testing.T) { + layout := NewHorizontalLayout( + Item(FixedSize(100), FixedSize(50)), + Item(FixedSize(150), FixedSize(75)), + ) + + area := Rect(0, 0, 300, 100) + result := layout.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + // First item should be at (0,0) with size 100x50 + expected1 := Rect(0, 0, 100, 50) + if result.Areas[0] != expected1 { + t.Errorf("First area = %+v, expected %+v", result.Areas[0], expected1) + } + + // Second item should be at (100,0) with size 150x75 + expected2 := Rect(100, 0, 150, 75) + if result.Areas[1] != expected2 { + t.Errorf("Second area = %+v, expected %+v", result.Areas[1], expected2) + } +} + +func TestVerticalLayout(t *testing.T) { + layout := NewVerticalLayout( + Item(FixedSize(100), FixedSize(50)), + Item(FixedSize(150), FixedSize(75)), + ) + + area := Rect(0, 0, 200, 200) + result := layout.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + // First item should be at (0,0) with size 100x50 + expected1 := Rect(0, 0, 100, 50) + if result.Areas[0] != expected1 { + t.Errorf("First area = %+v, expected %+v", result.Areas[0], expected1) + } + + // Second item should be at (0,50) with size 150x75 + expected2 := Rect(0, 50, 150, 75) + if result.Areas[1] != expected2 { + t.Errorf("Second area = %+v, expected %+v", result.Areas[1], expected2) + } +} + +func TestLayoutWithGap(t *testing.T) { + layout := &Layout{ + Direction: Horizontal, + Items: []LayoutItem{ + Item(FixedSize(100), FixedSize(50)), + Item(FixedSize(100), FixedSize(50)), + }, + Gap: 20, + } + + area := Rect(0, 0, 300, 100) + result := layout.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + // First item should be at (0,0) + expected1 := Rect(0, 0, 100, 50) + if result.Areas[0] != expected1 { + t.Errorf("First area = %+v, expected %+v", result.Areas[0], expected1) + } + + // Second item should be at (120,0) due to 20px gap + expected2 := Rect(120, 0, 100, 50) + if result.Areas[1] != expected2 { + t.Errorf("Second area = %+v, expected %+v", result.Areas[1], expected2) + } +} + +func TestLayoutWithMargin(t *testing.T) { + item := Item(FixedSize(100), FixedSize(50)) + item.Margin = Uniform(10) + + layout := NewHorizontalLayout(item) + area := Rect(0, 0, 200, 100) + result := layout.Calculate(area) + + if len(result.Areas) != 1 { + t.Fatalf("Expected 1 area, got %d", len(result.Areas)) + } + + // Item should be at (10,10) with size 80x30 (reduced by margin) + expected := Rect(10, 10, 80, 30) + if result.Areas[0] != expected { + t.Errorf("Area = %+v, expected %+v", result.Areas[0], expected) + } +} + +func TestFlexLayout(t *testing.T) { + layout := &Layout{ + Direction: Horizontal, + Items: []LayoutItem{ + Item(FixedSize(100), FixedSize(50)), + Item(FlexSize{Grow: 1}, FixedSize(50)), + Item(FixedSize(100), FixedSize(50)), + }, + } + + area := Rect(0, 0, 400, 100) + result := layout.Calculate(area) + + if len(result.Areas) != 3 { + t.Fatalf("Expected 3 areas, got %d", len(result.Areas)) + } + + // First item: fixed 100px + expected1 := Rect(0, 0, 100, 50) + if result.Areas[0] != expected1 { + t.Errorf("First area = %+v, expected %+v", result.Areas[0], expected1) + } + + // Second item: should take remaining space (200px) + expected2 := Rect(100, 0, 200, 50) + if result.Areas[1] != expected2 { + t.Errorf("Second area = %+v, expected %+v", result.Areas[1], expected2) + } + + // Third item: fixed 100px + expected3 := Rect(300, 0, 100, 50) + if result.Areas[2] != expected3 { + t.Errorf("Third area = %+v, expected %+v", result.Areas[2], expected3) + } +} + +func TestRatioLayout(t *testing.T) { + layout := NewHorizontalLayout( + Item(RatioSize(0.3), FixedSize(50)), + Item(RatioSize(0.7), FixedSize(50)), + ) + + area := Rect(0, 0, 200, 100) + result := layout.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + // First item: 30% of 200 = 60px + expected1 := Rect(0, 0, 60, 50) + if result.Areas[0] != expected1 { + t.Errorf("First area = %+v, expected %+v", result.Areas[0], expected1) + } + + // Second item: 70% of 200 = 140px + expected2 := Rect(60, 0, 140, 50) + if result.Areas[1] != expected2 { + t.Errorf("Second area = %+v, expected %+v", result.Areas[1], expected2) + } +} + +func TestGrid(t *testing.T) { + grid := NewGrid(2, + GridItemAt(1, 1, Item(nil, nil)), + GridItemAt(2, 1, Item(nil, nil)), + GridItemAt(1, 2, Item(nil, nil)), + GridItemAt(2, 2, Item(nil, nil)), + ) + + area := Rect(0, 0, 200, 100) + result := grid.Calculate(area) + + if len(result.Areas) != 4 { + t.Fatalf("Expected 4 areas, got %d", len(result.Areas)) + } + + // Each cell should be 100x50 + expected := []Rectangle{ + Rect(0, 0, 100, 50), // (1,1) + Rect(100, 0, 100, 50), // (2,1) + Rect(0, 50, 100, 50), // (1,2) + Rect(100, 50, 100, 50), // (2,2) + } + + for i, expectedArea := range expected { + if result.Areas[i] != expectedArea { + t.Errorf("Grid area %d = %+v, expected %+v", i, result.Areas[i], expectedArea) + } + } +} + +func TestGridWithGap(t *testing.T) { + grid := &Grid{ + Columns: 2, + ColumnGap: 10, + RowGap: 20, + Items: []GridItem{ + GridItemAt(1, 1, Item(nil, nil)), + GridItemAt(2, 1, Item(nil, nil)), + }, + } + + area := Rect(0, 0, 210, 100) // 200 + 10 gap + result := grid.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + // Each cell should be 100x100 with 10px gap between columns + expected1 := Rect(0, 0, 100, 100) + expected2 := Rect(110, 0, 100, 100) // 100 + 10 gap + + if result.Areas[0] != expected1 { + t.Errorf("First grid area = %+v, expected %+v", result.Areas[0], expected1) + } + if result.Areas[1] != expected2 { + t.Errorf("Second grid area = %+v, expected %+v", result.Areas[1], expected2) + } +} + +func TestGridSpan(t *testing.T) { + grid := &Grid{ + Columns: 3, + Items: []GridItem{ + GridItemSpan(1, 1, 2, 1, Item(nil, nil)), // Spans 2 columns + GridItemAt(3, 1, Item(nil, nil)), + }, + } + + area := Rect(0, 0, 300, 100) + result := grid.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + // First item spans 2 columns (200px wide) + expected1 := Rect(0, 0, 200, 100) + if result.Areas[0] != expected1 { + t.Errorf("Spanning grid area = %+v, expected %+v", result.Areas[0], expected1) + } + + // Second item is in third column + expected2 := Rect(200, 0, 100, 100) + if result.Areas[1] != expected2 { + t.Errorf("Third column area = %+v, expected %+v", result.Areas[1], expected2) + } +} + +func TestMainAxisAlignment(t *testing.T) { + tests := []struct { + alignment MainAxisAlignment + expected []Rectangle + }{ + { + MainAxisStart, + []Rectangle{ + Rect(0, 0, 50, 100), + Rect(50, 0, 50, 100), + }, + }, + { + MainAxisCenter, + []Rectangle{ + Rect(100, 0, 50, 100), // Centered with 100px offset + Rect(150, 0, 50, 100), + }, + }, + { + MainAxisEnd, + []Rectangle{ + Rect(200, 0, 50, 100), // Right-aligned + Rect(250, 0, 50, 100), + }, + }, + } + + for _, test := range tests { + layout := &Layout{ + Direction: Horizontal, + MainAxisAlignment: test.alignment, + Items: []LayoutItem{ + Item(FixedSize(50), FixedSize(100)), + Item(FixedSize(50), FixedSize(100)), + }, + } + + area := Rect(0, 0, 300, 100) // Extra space for alignment + result := layout.Calculate(area) + + if len(result.Areas) != 2 { + t.Fatalf("Expected 2 areas, got %d", len(result.Areas)) + } + + for i, expected := range test.expected { + if result.Areas[i] != expected { + t.Errorf("Alignment %d, area %d = %+v, expected %+v", + test.alignment, i, result.Areas[i], expected) + } + } + } +} + +func TestCrossAxisAlignment(t *testing.T) { + layout := &Layout{ + Direction: Horizontal, + CrossAxisAlignment: CrossAxisCenter, + Items: []LayoutItem{ + Item(FixedSize(100), FixedSize(50)), // Smaller height + }, + } + + area := Rect(0, 0, 200, 100) // Taller container + result := layout.Calculate(area) + + if len(result.Areas) != 1 { + t.Fatalf("Expected 1 area, got %d", len(result.Areas)) + } + + // Item should be vertically centered + expected := Rect(0, 25, 100, 50) // Y offset = (100-50)/2 = 25 + if result.Areas[0] != expected { + t.Errorf("Centered area = %+v, expected %+v", result.Areas[0], expected) + } +} + +func TestBackwardCompatibility(t *testing.T) { + // Test that the new SplitVertical works the same as the old one + area := Rect(0, 0, 200, 100) + left, right := SplitVertical(area, 0.3) + + expectedLeft := Rect(0, 0, 60, 100) // 30% of 200 + expectedRight := Rect(60, 0, 140, 100) // 70% of 200 + + if left != expectedLeft { + t.Errorf("SplitVertical left = %+v, expected %+v", left, expectedLeft) + } + if right != expectedRight { + t.Errorf("SplitVertical right = %+v, expected %+v", right, expectedRight) + } + + // Test SplitHorizontal + top, bottom := SplitHorizontal(area, 0.4) + + expectedTop := Rect(0, 0, 200, 40) // 40% of 100 + expectedBottom := Rect(0, 40, 200, 60) // 60% of 100 + + if top != expectedTop { + t.Errorf("SplitHorizontal top = %+v, expected %+v", top, expectedTop) + } + if bottom != expectedBottom { + t.Errorf("SplitHorizontal bottom = %+v, expected %+v", bottom, expectedBottom) + } +} + +func TestHelperFunctions(t *testing.T) { + // Test Item helper + item := Item(FixedSize(100), FixedSize(50)) + if item.Width.Calculate(200) != 100 { + t.Error("Item helper width not set correctly") + } + if item.Height.Calculate(200) != 50 { + t.Error("Item helper height not set correctly") + } + + // Test FlexItem helper + flexItem := FlexItem(2, 1, FixedSize(50)) + if !flexItem.Width.IsFlexible() { + t.Error("FlexItem should create flexible width") + } + if !flexItem.Height.IsFlexible() { + t.Error("FlexItem should create flexible height") + } + + // Test GridItemAt helper + gridItem := GridItemAt(2, 3, item) + if gridItem.Column != 2 || gridItem.Row != 3 { + t.Error("GridItemAt position not set correctly") + } + if gridItem.ColumnSpan != 1 || gridItem.RowSpan != 1 { + t.Error("GridItemAt spans not set correctly") + } + + // Test GridItemSpan helper + spanItem := GridItemSpan(1, 1, 2, 3, item) + if spanItem.ColumnSpan != 2 || spanItem.RowSpan != 3 { + t.Error("GridItemSpan spans not set correctly") + } +} \ No newline at end of file