From e5a9175254086cf9ba3e839239b96e5881d69fa6 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Sun, 5 Jul 2026 13:19:23 +0300 Subject: [PATCH] refactor: remove internal/layout/Engine + update CHANGELOG (ADR-032 CACHE-030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine (centralized layout cache) has 0 production usage — confirmed by codebase audit. Per-widget layout caching is now handled by widget.LayoutChild on WidgetBase (ADR-032 Phase 2b, #160). - Delete engine.go + engine_test.go (-769 LOC) - Extract Layoutable interface to layoutable.go (used by Flex/Grid/Stack) - Extract mockLayoutable to mock_test.go - Remove Engine cache benchmarks from bench_test.go - Update doc.go and CHANGELOG --- CHANGELOG.md | 4 + internal/layout/bench_test.go | 49 ---- internal/layout/doc.go | 23 +- internal/layout/engine.go | 290 ----------------------- internal/layout/engine_test.go | 413 --------------------------------- internal/layout/layoutable.go | 19 ++ internal/layout/mock_test.go | 24 ++ 7 files changed, 52 insertions(+), 770 deletions(-) delete mode 100644 internal/layout/engine.go delete mode 100644 internal/layout/engine_test.go create mode 100644 internal/layout/layoutable.go create mode 100644 internal/layout/mock_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 71846ef..741b8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Collapsible animation glitch on Wayland** ([#152](https://github.com/gogpu/ui/issues/152)) — force final redraw after animation completes + full-window `wl_surface.damage_buffer` on root repaint. Confirmed by @porjo. +### Removed + +- **`internal/layout/Engine`** (ADR-032 CACHE-030) — centralized layout engine with 0 production usage. Per-widget layout caching is now fully handled by `widget.LayoutChild` on `WidgetBase`. -769 lines of dead code. + ### Changed - **deps:** gg v0.50.1 → v0.50.2, gogpu v0.43.1 → v0.43.4, wgpu v0.30.8 → v0.30.9, goffi v0.5.5 → v0.5.6 diff --git a/internal/layout/bench_test.go b/internal/layout/bench_test.go index fc6694b..3cb1f67 100644 --- a/internal/layout/bench_test.go +++ b/internal/layout/bench_test.go @@ -18,14 +18,6 @@ func (b *benchLayoutable) Layout(_ geometry.Constraints) geometry.Size { func (b *benchLayoutable) Children() []Layoutable { return nil } func (b *benchLayoutable) ID() uint64 { return 0 } -// cachableLayoutable is a Layoutable with a non-zero ID for cache benchmarks. -type cachableLayoutable struct { - benchLayoutable - id uint64 -} - -func (c *cachableLayoutable) ID() uint64 { return c.id } - // --- Flex layout benchmarks --- func newFlexWithChildren(n int) *FlexContainer { @@ -158,44 +150,3 @@ func BenchmarkGridLayout10x10(b *testing.B) { grid.Layout(constraints) } } - -// --- Layout engine cache benchmarks --- - -func BenchmarkLayoutCacheHit(b *testing.B) { - engine := NewEngine() - engine.EnableCache(true) - - child := &cachableLayoutable{ - benchLayoutable: benchLayoutable{preferredSize: geometry.Sz(100, 50)}, - id: 42, - } - constraints := geometry.BoxConstraints(0, 800, 0, 600) - - // Warm up cache with one layout call. - engine.Layout(child, constraints) - - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - engine.Layout(child, constraints) - } -} - -func BenchmarkLayoutCacheMiss(b *testing.B) { - engine := NewEngine() - engine.EnableCache(true) - - child := &cachableLayoutable{ - benchLayoutable: benchLayoutable{preferredSize: geometry.Sz(100, 50)}, - id: 42, - } - constraints := geometry.BoxConstraints(0, 800, 0, 600) - - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - // Mark dirty before each layout to force recomputation. - engine.MarkDirty(child.ID()) - engine.Layout(child, constraints) - } -} diff --git a/internal/layout/doc.go b/internal/layout/doc.go index 32f313d..cf61486 100644 --- a/internal/layout/doc.go +++ b/internal/layout/doc.go @@ -1,4 +1,4 @@ -// Package layout provides the internal layout engine implementation for gogpu/ui. +// Package layout provides internal layout algorithm implementations for gogpu/ui. // // This package is INTERNAL and not intended for public use. It implements // constraint-based layout algorithms used by the widget system. @@ -7,11 +7,14 @@ // // The layout package provides several layout algorithms: // -// - [Engine]: Manages layout passes with caching and dirty tracking // - [FlexContainer]: CSS Flexbox-style layout (row, column, wrap) // - [VStack], [HStack], [ZStack]: Simplified stack layouts // - [GridContainer]: Basic grid layout with rows and columns // +// Per-widget layout caching is handled by [widget.LayoutChild] (ADR-032), +// not by this package. Container widgets call LayoutChild instead of +// child.Layout directly, which checks the per-widget cache on WidgetBase. +// // # Constraint-Based Layout // // Layout follows a constraint-passing model similar to Flutter: @@ -25,15 +28,6 @@ // forces a specific size (min == max), while a "loose" constraint allows // flexibility (min = 0). // -// # Layout Engine -// -// The Engine manages layout passes efficiently: -// -// - Single-pass layout for simple hierarchies -// - Multi-pass layout for intrinsic sizing -// - Caching of layout results to avoid redundant calculations -// - Dirty tracking for incremental layout updates -// // # Flexbox Layout // // FlexContainer implements a simplified CSS Flexbox model: @@ -70,11 +64,4 @@ // // This package is used internally by the UI framework. Application code should // use the public layout widgets instead of directly using this package. -// -// // Internal framework usage -// engine := layout.NewEngine() -// flex := layout.NewFlexContainer(layout.Row, layout.JustifyStart, layout.AlignStretch) -// flex.AddChild(child1, layout.FlexItem{Grow: 1}) -// flex.AddChild(child2, layout.FlexItem{Grow: 2}) -// size := engine.Layout(flex, constraints) package layout diff --git a/internal/layout/engine.go b/internal/layout/engine.go deleted file mode 100644 index 8b70a6d..0000000 --- a/internal/layout/engine.go +++ /dev/null @@ -1,290 +0,0 @@ -package layout - -import ( - "github.com/gogpu/ui/geometry" -) - -// Layoutable represents an element that can be laid out. -// -// This interface abstracts over widgets and layout containers, -// allowing the engine to work with any layoutable element. -type Layoutable interface { - // Layout calculates size given constraints and returns the computed size. - // The implementation should also position any children. - Layout(constraints geometry.Constraints) geometry.Size - - // Children returns child layoutables for traversal. - // Returns nil for leaf elements. - Children() []Layoutable - - // ID returns a unique identifier for caching purposes. - // Return 0 if caching is not needed for this element. - ID() uint64 -} - -// LayoutResult stores the computed layout for an element. -type LayoutResult struct { - // Size is the computed size after layout. - Size geometry.Size - - // Position is the offset from parent origin. - Position geometry.Point - - // Constraints used for this layout pass. - Constraints geometry.Constraints -} - -// Engine manages layout passes with optional caching and dirty tracking. -// -// The engine supports: -// - Single-pass layout for most cases -// - Multi-pass layout for intrinsic sizing -// - Caching to avoid redundant layout calculations -// - Dirty tracking for incremental updates -// -// Engine is NOT thread-safe. -type Engine struct { - // Cache stores layout results by element ID and constraints hash. - cache map[cacheKey]LayoutResult - - // dirtySet tracks elements that need re-layout. - dirtySet map[uint64]struct{} - - // stats tracks layout statistics for debugging. - stats LayoutStats - - // enableCache controls whether caching is active. - enableCache bool -} - -// cacheKey combines element ID and constraints for cache lookup. -type cacheKey struct { - id uint64 - constraints geometry.Constraints -} - -// LayoutStats provides layout performance metrics. -type LayoutStats struct { - // LayoutCalls is the number of Layout calls made. - LayoutCalls int - - // CacheHits is the number of times a cached result was used. - CacheHits int - - // CacheMisses is the number of times layout was computed. - CacheMisses int -} - -// NewEngine creates a new layout engine. -// -// By default, caching is disabled. Use EnableCache to enable it. -func NewEngine() *Engine { - return &Engine{ - cache: make(map[cacheKey]LayoutResult), - dirtySet: make(map[uint64]struct{}), - enableCache: false, - } -} - -// EnableCache enables or disables layout caching. -// -// When enabled, the engine caches layout results and reuses them -// when the same element is laid out with the same constraints. -func (e *Engine) EnableCache(enable bool) { - e.enableCache = enable - if !enable { - e.ClearCache() - } -} - -// IsCacheEnabled returns whether caching is enabled. -func (e *Engine) IsCacheEnabled() bool { - return e.enableCache -} - -// Layout performs a layout pass on the given element. -// -// This is the main entry point for layout. It delegates to the element's -// Layout method, optionally using cached results. -func (e *Engine) Layout(element Layoutable, constraints geometry.Constraints) geometry.Size { - e.stats.LayoutCalls++ - - if element == nil { - return geometry.Size{} - } - - // Normalize constraints to ensure validity - constraints = constraints.Normalize() - - // Try cache lookup - if cachedSize, ok := e.tryCache(element, constraints); ok { - return cachedSize - } - - // Perform actual layout - size := element.Layout(constraints) - - // Store result in cache - e.storeInCache(element, constraints, size) - - return size -} - -// tryCache attempts to retrieve a cached layout result. -func (e *Engine) tryCache(element Layoutable, constraints geometry.Constraints) (geometry.Size, bool) { - if !e.enableCache { - return geometry.Size{}, false - } - - id := element.ID() - if id == 0 { - return geometry.Size{}, false - } - - key := cacheKey{id: id, constraints: constraints} - result, ok := e.cache[key] - if !ok { - e.stats.CacheMisses++ - return geometry.Size{}, false - } - - // Check if element is dirty - if _, dirty := e.dirtySet[id]; dirty { - e.stats.CacheMisses++ - return geometry.Size{}, false - } - - e.stats.CacheHits++ - return result.Size, true -} - -// storeInCache stores a layout result in the cache. -func (e *Engine) storeInCache(element Layoutable, constraints geometry.Constraints, size geometry.Size) { - if !e.enableCache { - return - } - - id := element.ID() - if id == 0 { - return - } - - key := cacheKey{id: id, constraints: constraints} - e.cache[key] = LayoutResult{ - Size: size, - Constraints: constraints, - } - delete(e.dirtySet, id) -} - -// MarkDirty marks an element as needing re-layout. -// -// This invalidates the cache for the element and all ancestors. -// Call this when an element's content or properties change. -func (e *Engine) MarkDirty(id uint64) { - if id != 0 { - e.dirtySet[id] = struct{}{} - } -} - -// MarkDirtyWithAncestors marks an element and all provided ancestor IDs as dirty. -func (e *Engine) MarkDirtyWithAncestors(id uint64, ancestorIDs []uint64) { - e.MarkDirty(id) - for _, ancestorID := range ancestorIDs { - e.MarkDirty(ancestorID) - } -} - -// IsDirty returns whether an element needs re-layout. -func (e *Engine) IsDirty(id uint64) bool { - _, dirty := e.dirtySet[id] - return dirty -} - -// ClearDirty removes the dirty flag for an element. -func (e *Engine) ClearDirty(id uint64) { - delete(e.dirtySet, id) -} - -// ClearAllDirty removes all dirty flags. -func (e *Engine) ClearAllDirty() { - e.dirtySet = make(map[uint64]struct{}) -} - -// ClearCache removes all cached layout results. -// -// Call this when the layout tree structure changes significantly. -func (e *Engine) ClearCache() { - e.cache = make(map[cacheKey]LayoutResult) -} - -// ClearCacheFor removes cached results for a specific element. -func (e *Engine) ClearCacheFor(id uint64) { - // Remove all cache entries for this ID (any constraints) - for key := range e.cache { - if key.id == id { - delete(e.cache, key) - } - } -} - -// Stats returns current layout statistics. -func (e *Engine) Stats() LayoutStats { - return e.stats -} - -// ResetStats resets layout statistics to zero. -func (e *Engine) ResetStats() { - e.stats = LayoutStats{} -} - -// CacheSize returns the number of cached layout results. -func (e *Engine) CacheSize() int { - return len(e.cache) -} - -// DirtyCount returns the number of dirty elements. -func (e *Engine) DirtyCount() int { - return len(e.dirtySet) -} - -// LayoutWithIntrinsics performs layout with intrinsic size calculation. -// -// This is a two-pass layout: -// 1. First pass: measure intrinsic sizes with loose constraints -// 2. Second pass: layout with actual constraints using intrinsic info -// -// Use this for elements that need to know their content size before layout. -func (e *Engine) LayoutWithIntrinsics(element Layoutable, constraints geometry.Constraints) geometry.Size { - if element == nil { - return geometry.Size{} - } - - // First pass: measure with loose constraints - looseConstraints := constraints.Loosen() - intrinsicSize := element.Layout(looseConstraints) - - // Second pass: layout with actual constraints, using intrinsic size as hint - // For tight constraints, use them directly - if constraints.IsTight() { - return element.Layout(constraints) - } - - // For loose constraints, tighten to intrinsic size (clamped to constraints) - tightened := constraints.Tighten(intrinsicSize) - return element.Layout(tightened) -} - -// LayoutTree performs layout on an entire tree of elements. -// -// This recursively lays out all children before the parent, ensuring -// that child sizes are known when positioning them. -func (e *Engine) LayoutTree(root Layoutable, constraints geometry.Constraints) geometry.Size { - if root == nil { - return geometry.Size{} - } - - // The root element's Layout is responsible for recursively - // laying out its children. We just call Layout on root. - return e.Layout(root, constraints) -} diff --git a/internal/layout/engine_test.go b/internal/layout/engine_test.go deleted file mode 100644 index f7b7f8a..0000000 --- a/internal/layout/engine_test.go +++ /dev/null @@ -1,413 +0,0 @@ -package layout - -import ( - "testing" - - "github.com/gogpu/ui/geometry" -) - -// mockLayoutable is a simple layoutable for testing. -type mockLayoutable struct { - id uint64 - preferredSize geometry.Size - layoutCalls int - children []Layoutable -} - -func (m *mockLayoutable) ID() uint64 { - return m.id -} - -func (m *mockLayoutable) Layout(constraints geometry.Constraints) geometry.Size { - m.layoutCalls++ - return constraints.Constrain(m.preferredSize) -} - -func (m *mockLayoutable) Children() []Layoutable { - return m.children -} - -func TestNewEngine(t *testing.T) { - engine := NewEngine() - - if engine == nil { - t.Fatal("NewEngine returned nil") - } - - if engine.IsCacheEnabled() { - t.Error("cache should be disabled by default") - } - - if engine.CacheSize() != 0 { - t.Errorf("cache size = %d, want 0", engine.CacheSize()) - } - - if engine.DirtyCount() != 0 { - t.Errorf("dirty count = %d, want 0", engine.DirtyCount()) - } -} - -func TestEngine_Layout(t *testing.T) { - engine := NewEngine() - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - constraints := geometry.Loose(geometry.Sz(200, 200)) - size := engine.Layout(mock, constraints) - - if size != mock.preferredSize { - t.Errorf("Layout() = %v, want %v", size, mock.preferredSize) - } - - if mock.layoutCalls != 1 { - t.Errorf("layoutCalls = %d, want 1", mock.layoutCalls) - } -} - -func TestEngine_LayoutNil(t *testing.T) { - engine := NewEngine() - - size := engine.Layout(nil, geometry.Expand()) - - if !size.IsZero() { - t.Errorf("Layout(nil) = %v, want zero size", size) - } -} - -func TestEngine_CacheEnabled(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - if !engine.IsCacheEnabled() { - t.Error("cache should be enabled") - } - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - constraints := geometry.Loose(geometry.Sz(200, 200)) - - // First layout - size1 := engine.Layout(mock, constraints) - if mock.layoutCalls != 1 { - t.Errorf("first layout: layoutCalls = %d, want 1", mock.layoutCalls) - } - - // Second layout with same constraints should use cache - size2 := engine.Layout(mock, constraints) - if mock.layoutCalls != 1 { - t.Errorf("second layout: layoutCalls = %d, want 1 (cached)", mock.layoutCalls) - } - - if size1 != size2 { - t.Errorf("cached size mismatch: %v != %v", size1, size2) - } - - // Check stats - stats := engine.Stats() - if stats.CacheHits != 1 { - t.Errorf("cache hits = %d, want 1", stats.CacheHits) - } - if stats.CacheMisses != 1 { - t.Errorf("cache misses = %d, want 1", stats.CacheMisses) - } -} - -func TestEngine_CacheDifferentConstraints(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - // Layout with different constraints - _ = engine.Layout(mock, geometry.Loose(geometry.Sz(200, 200))) - _ = engine.Layout(mock, geometry.Loose(geometry.Sz(300, 300))) - - if mock.layoutCalls != 2 { - t.Errorf("layoutCalls = %d, want 2 (different constraints)", mock.layoutCalls) - } -} - -func TestEngine_DirtyTracking(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - constraints := geometry.Loose(geometry.Sz(200, 200)) - - // First layout - _ = engine.Layout(mock, constraints) - if mock.layoutCalls != 1 { - t.Errorf("first layout: layoutCalls = %d, want 1", mock.layoutCalls) - } - - // Mark dirty - engine.MarkDirty(mock.id) - - if !engine.IsDirty(mock.id) { - t.Error("element should be marked dirty") - } - - // Layout again - should re-layout because dirty - _ = engine.Layout(mock, constraints) - if mock.layoutCalls != 2 { - t.Errorf("after dirty: layoutCalls = %d, want 2", mock.layoutCalls) - } - - // Should no longer be dirty - if engine.IsDirty(mock.id) { - t.Error("element should not be dirty after layout") - } -} - -func TestEngine_ClearCache(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - constraints := geometry.Loose(geometry.Sz(200, 200)) - - // Fill cache - _ = engine.Layout(mock, constraints) - - if engine.CacheSize() != 1 { - t.Errorf("cache size = %d, want 1", engine.CacheSize()) - } - - // Clear cache - engine.ClearCache() - - if engine.CacheSize() != 0 { - t.Errorf("after clear: cache size = %d, want 0", engine.CacheSize()) - } -} - -func TestEngine_DisableCache(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - constraints := geometry.Loose(geometry.Sz(200, 200)) - - // Fill cache - _ = engine.Layout(mock, constraints) - - // Disable cache - engine.EnableCache(false) - - if engine.IsCacheEnabled() { - t.Error("cache should be disabled") - } - - if engine.CacheSize() != 0 { - t.Errorf("cache should be cleared when disabled") - } -} - -func TestEngine_MarkDirtyWithAncestors(t *testing.T) { - engine := NewEngine() - - engine.MarkDirtyWithAncestors(1, []uint64{2, 3, 4}) - - if !engine.IsDirty(1) { - t.Error("element 1 should be dirty") - } - if !engine.IsDirty(2) { - t.Error("ancestor 2 should be dirty") - } - if !engine.IsDirty(3) { - t.Error("ancestor 3 should be dirty") - } - if !engine.IsDirty(4) { - t.Error("ancestor 4 should be dirty") - } - if engine.IsDirty(5) { - t.Error("element 5 should not be dirty") - } -} - -func TestEngine_ClearDirty(t *testing.T) { - engine := NewEngine() - - engine.MarkDirty(1) - engine.MarkDirty(2) - - if engine.DirtyCount() != 2 { - t.Errorf("dirty count = %d, want 2", engine.DirtyCount()) - } - - engine.ClearDirty(1) - - if engine.IsDirty(1) { - t.Error("element 1 should not be dirty after clear") - } - if !engine.IsDirty(2) { - t.Error("element 2 should still be dirty") - } - - engine.ClearAllDirty() - - if engine.DirtyCount() != 0 { - t.Errorf("after ClearAllDirty: dirty count = %d, want 0", engine.DirtyCount()) - } -} - -func TestEngine_ResetStats(t *testing.T) { - engine := NewEngine() - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - _ = engine.Layout(mock, geometry.Expand()) - - stats := engine.Stats() - if stats.LayoutCalls == 0 { - t.Error("LayoutCalls should be non-zero") - } - - engine.ResetStats() - - stats = engine.Stats() - if stats.LayoutCalls != 0 { - t.Errorf("after reset: LayoutCalls = %d, want 0", stats.LayoutCalls) - } -} - -func TestEngine_LayoutWithIntrinsics(t *testing.T) { - engine := NewEngine() - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - constraints := geometry.Loose(geometry.Sz(200, 200)) - size := engine.LayoutWithIntrinsics(mock, constraints) - - // Should call layout twice: once loose, once tight - if mock.layoutCalls != 2 { - t.Errorf("layoutCalls = %d, want 2", mock.layoutCalls) - } - - if size != mock.preferredSize { - t.Errorf("size = %v, want %v", size, mock.preferredSize) - } -} - -func TestEngine_LayoutWithIntrinsics_TightConstraints(t *testing.T) { - engine := NewEngine() - - mock := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - } - - // With tight constraints, intrinsic measurement is skipped - constraints := geometry.Tight(geometry.Sz(80, 40)) - size := engine.LayoutWithIntrinsics(mock, constraints) - - // Should call layout twice: first loose measurement, then tight layout - if mock.layoutCalls != 2 { - t.Errorf("layoutCalls = %d, want 2", mock.layoutCalls) - } - - // Size should be constrained to 80x40 - expected := geometry.Sz(80, 40) - if size != expected { - t.Errorf("size = %v, want %v", size, expected) - } -} - -func TestEngine_LayoutTree(t *testing.T) { - engine := NewEngine() - - child := &mockLayoutable{ - id: 2, - preferredSize: geometry.Sz(50, 30), - } - - parent := &mockLayoutable{ - id: 1, - preferredSize: geometry.Sz(100, 50), - children: []Layoutable{child}, - } - - _ = engine.LayoutTree(parent, geometry.Expand()) - - // Only parent's Layout is called (children are responsibility of parent) - if parent.layoutCalls != 1 { - t.Errorf("parent layoutCalls = %d, want 1", parent.layoutCalls) - } -} - -func TestEngine_ClearCacheFor(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - mock1 := &mockLayoutable{id: 1, preferredSize: geometry.Sz(100, 50)} - mock2 := &mockLayoutable{id: 2, preferredSize: geometry.Sz(200, 100)} - - _ = engine.Layout(mock1, geometry.Expand()) - _ = engine.Layout(mock2, geometry.Expand()) - - if engine.CacheSize() != 2 { - t.Errorf("cache size = %d, want 2", engine.CacheSize()) - } - - engine.ClearCacheFor(1) - - if engine.CacheSize() != 1 { - t.Errorf("after clear for 1: cache size = %d, want 1", engine.CacheSize()) - } - - // Layout mock1 again - should not use cache - _ = engine.Layout(mock1, geometry.Expand()) - if mock1.layoutCalls != 2 { - t.Errorf("mock1 layoutCalls = %d, want 2", mock1.layoutCalls) - } -} - -func TestEngine_ZeroIDNoCache(t *testing.T) { - engine := NewEngine() - engine.EnableCache(true) - - // Element with zero ID should not be cached - mock := &mockLayoutable{ - id: 0, - preferredSize: geometry.Sz(100, 50), - } - - _ = engine.Layout(mock, geometry.Expand()) - _ = engine.Layout(mock, geometry.Expand()) - - // Should layout twice since ID=0 bypasses cache - if mock.layoutCalls != 2 { - t.Errorf("layoutCalls = %d, want 2 (ID=0 not cached)", mock.layoutCalls) - } - - if engine.CacheSize() != 0 { - t.Errorf("cache size = %d, want 0 (ID=0 not cached)", engine.CacheSize()) - } -} diff --git a/internal/layout/layoutable.go b/internal/layout/layoutable.go new file mode 100644 index 0000000..8e4ac7b --- /dev/null +++ b/internal/layout/layoutable.go @@ -0,0 +1,19 @@ +package layout + +import "github.com/gogpu/ui/geometry" + +// Layoutable represents an element that can be laid out. +// +// This interface abstracts over widgets and layout containers, +// allowing layout algorithms (Flex, Stack, Grid) to work with +// any layoutable element. +type Layoutable interface { + // Layout calculates size given constraints and returns the computed size. + Layout(constraints geometry.Constraints) geometry.Size + + // Children returns child layoutables for traversal. + Children() []Layoutable + + // ID returns a unique identifier for caching purposes. + ID() uint64 +} diff --git a/internal/layout/mock_test.go b/internal/layout/mock_test.go new file mode 100644 index 0000000..cb4371a --- /dev/null +++ b/internal/layout/mock_test.go @@ -0,0 +1,24 @@ +package layout + +import "github.com/gogpu/ui/geometry" + +// mockLayoutable is a simple layoutable for testing. +type mockLayoutable struct { + id uint64 + preferredSize geometry.Size + layoutCalls int + children []Layoutable +} + +func (m *mockLayoutable) ID() uint64 { + return m.id +} + +func (m *mockLayoutable) Layout(constraints geometry.Constraints) geometry.Size { + m.layoutCalls++ + return constraints.Constrain(m.preferredSize) +} + +func (m *mockLayoutable) Children() []Layoutable { + return m.children +}