diff --git a/CHANGELOG.md b/CHANGELOG.md index fb2cf6f..725a87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **refactor(widget): decouple widget/ from gg/scene** (ADR-036 Phase 1) — replaced `*scene.Scene` with `SceneCache` interface in widget package. Third-party widget authors importing `widget/` + `geometry/` + `event/` now compile **71 packages instead of 124** (zero gg/wgpu/naga in the dependency chain). `SceneFactory` registration pattern follows existing `SceneRecorder` DI. + - New: `widget.SceneCache` interface (`Reset()`, `IsEmpty()`), `widget.SceneFactory`, `widget.RegisterSceneFactory()`, `widget.NewSceneCache()` + - Changed: `Canvas.ReplayScene(*scene.Scene)` → `Canvas.ReplayScene(SceneCache)` + - Changed: `WidgetBase.CachedScene()` / `SetCachedScene()` types → `SceneCache` + - Changed: `SceneRecorder` function signature uses `SceneCache` + - 92 files changed, rendering layer uses type assertions where concrete `*scene.Scene` needed +- **deps:** gg v0.50.11 → v0.50.12, gogpu v0.48.5 → v0.50.0, wgpu v0.30.35 → v0.30.36 + - **gogpu v0.50.0:** outgoing drag-and-drop, per-pixel-alpha transparency, window Show/Hide/SetPosition/SetSize API, macOS menu Role+Action fix. + - **wgpu v0.30.36:** Vulkan present semaphore fix. + ## [0.1.50] — 2026-08-03 ### Fixed diff --git a/app/boundary_visibility_test.go b/app/boundary_visibility_test.go index 0ae86bb..e324300 100644 --- a/app/boundary_visibility_test.go +++ b/app/boundary_visibility_test.go @@ -133,8 +133,8 @@ func TestIsBoundaryVisible_AboveClip_Invisible(t *testing.T) { func setupSceneRecorder(t *testing.T) func() { t.Helper() prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) return func() { widget.RegisterSceneRecorder(prev) } diff --git a/app/compositor_test.go b/app/compositor_test.go index 9c54bc4..856def4 100644 --- a/app/compositor_test.go +++ b/app/compositor_test.go @@ -189,8 +189,8 @@ func (w *containerTestWidget) Draw(_ widget.Context, canvas widget.Canvas) { func (w *containerTestWidget) Event(_ widget.Context, _ event.Event) bool { return false } func (w *containerTestWidget) Children() []widget.Widget { return w.children } -// testSceneRecorder creates a SceneCanvas for recording into scene.Scene. -func testSceneRecorder(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) +// testSceneRecorder creates a SceneCanvas for recording into SceneCache. +func testSceneRecorder(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close } diff --git a/app/drawchild_skip_test.go b/app/drawchild_skip_test.go index d7e37f0..8a4d363 100644 --- a/app/drawchild_skip_test.go +++ b/app/drawchild_skip_test.go @@ -62,8 +62,8 @@ func (w *itemWidget) Children() []widget.Widget { return nil } func TestDrawChildSkip_ListViewItemBoundaries(t *testing.T) { // Register SceneRecorder factory (required for boundary recording). prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -154,7 +154,7 @@ func TestDrawChildSkip_ListViewItemBoundaries(t *testing.T) { t.Logf("item[%d]: bounds=%v (%.0fx%.0f)", i, bounds, bounds.Width(), bounds.Height()) // 5c: Item has cached scene (recorded by PaintBoundaryLayers recursion). - sc, ok := item.(interface{ CachedScene() *scene.Scene }) + sc, ok := item.(interface{ CachedScene() widget.SceneCache }) if !ok { t.Errorf("item[%d]: does not implement CachedScene()", i) continue @@ -187,8 +187,8 @@ func TestDrawChildSkip_ListViewItemBoundaries(t *testing.T) { // Items should NOT appear in the root's scene -- they have their own scenes. func TestDrawChildSkip_RootRecordingSkipsItems(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -239,7 +239,7 @@ func TestDrawChildSkip_RootRecordingSkipsItems(t *testing.T) { // After PaintBoundaryLayers, item boundaries should also have scenes. items := collectBoundaryDescendants(root) for i, item := range items { - if sc, ok := item.(interface{ CachedScene() *scene.Scene }); ok { + if sc, ok := item.(interface{ CachedScene() widget.SceneCache }); ok { cs := sc.CachedScene() if cs == nil { t.Errorf("item[%d]: CachedScene nil after PaintBoundaryLayers", i) @@ -255,8 +255,8 @@ func TestDrawChildSkip_RootRecordingSkipsItems(t *testing.T) { // though they were created dynamically during VirtualContent.Draw(). func TestDrawChildSkip_ItemsExistAfterRootRecording(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -324,8 +324,8 @@ func TestDrawChildSkip_ItemsExistAfterRootRecording(t *testing.T) { // both the Box background and the Text content. func TestDrawChildSkip_BoxTextItems_ProductionScenario(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -379,7 +379,7 @@ func TestDrawChildSkip_BoxTextItems_ProductionScenario(t *testing.T) { } for i, item := range items { - sc, ok := item.(interface{ CachedScene() *scene.Scene }) + sc, ok := item.(interface{ CachedScene() widget.SceneCache }) if !ok { t.Errorf("item[%d]: does not implement CachedScene()", i) continue @@ -505,7 +505,7 @@ func walkTreeDiag(t *testing.T, w widget.Widget, depth int) { if sd, ok := w.(interface{ IsSceneDirty() bool }); ok { sceneDirty = sd.IsSceneDirty() } - if sc, ok := w.(interface{ CachedScene() *scene.Scene }); ok { + if sc, ok := w.(interface{ CachedScene() widget.SceneCache }); ok { hasScene = sc.CachedScene() != nil } diff --git a/app/first_frame_test.go b/app/first_frame_test.go index 8cf2665..31c077a 100644 --- a/app/first_frame_test.go +++ b/app/first_frame_test.go @@ -65,7 +65,7 @@ func (c *trackingCanvas) PopTransform() {} func (c *trackingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *trackingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *trackingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *trackingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *trackingCanvas) ReplayScene(_ widget.SceneCache) {} // Compile-time check. var _ widget.Canvas = (*trackingCanvas)(nil) diff --git a/app/hover_boundary_e2e_test.go b/app/hover_boundary_e2e_test.go index 665e99b..66d96e5 100644 --- a/app/hover_boundary_e2e_test.go +++ b/app/hover_boundary_e2e_test.go @@ -23,8 +23,8 @@ import ( func TestHoverE2E_ButtonInBoundary_DirtyPropagation(t *testing.T) { // Register SceneRecorder factory for boundary recording. prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -124,8 +124,8 @@ func TestHoverE2E_ButtonInBoundary_DirtyPropagation(t *testing.T) { // multiple levels of nesting. Button inside Box inside Box inside root boundary. func TestHoverE2E_DeepNesting_PropagatesUpward(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -205,8 +205,8 @@ func TestHoverE2E_DeepNesting_PropagatesUpward(t *testing.T) { // Window.HandleEvent → updateHover → hitTest → MouseEnter → dirty propagation. func TestHoverE2E_WindowHandleEvent_FullChain(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) diff --git a/app/layer_tree.go b/app/layer_tree.go index 06351e4..ba9f8ff 100644 --- a/app/layer_tree.go +++ b/app/layer_tree.go @@ -1,7 +1,6 @@ package app import ( - "github.com/gogpu/gg/scene" "github.com/gogpu/gpucontext" "github.com/gogpu/ui/compositor" "github.com/gogpu/ui/geometry" @@ -31,8 +30,8 @@ type boundaryInfo interface { widget.Widget IsRepaintBoundary() bool IsSceneDirty() bool - CachedScene() *scene.Scene - SetCachedScene(*scene.Scene) + CachedScene() widget.SceneCache + SetCachedScene(widget.SceneCache) ClearSceneDirty() SceneCacheSize() (int, int) SetSceneCacheSize(int, int) @@ -519,7 +518,7 @@ func recordBoundary(bi boundaryInfo, ctx widget.Context) { cachedScene := bi.CachedScene() if cachedScene == nil { - cachedScene = scene.NewScene() + cachedScene = widget.NewSceneCache() } cachedScene.Reset() diff --git a/app/overlay_damage_tracking_test.go b/app/overlay_damage_tracking_test.go index e1168d8..d4433cf 100644 --- a/app/overlay_damage_tracking_test.go +++ b/app/overlay_damage_tracking_test.go @@ -43,8 +43,8 @@ func TestOverlayBoundary_SceneDirtyAfterHover(t *testing.T) { // Record initial scene to clear dirty state. prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -94,8 +94,8 @@ func TestOverlayBoundary_RecordClearsButVersionIncrements(t *testing.T) { menu.SetScreenOrigin(geometry.Pt(100, 200)) prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -140,8 +140,8 @@ func TestOverlayBoundary_SyncPictureLayerDetectsVersionChange(t *testing.T) { menu.SetScreenOrigin(geometry.Pt(100, 200)) prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -201,8 +201,8 @@ func TestOverlayBoundary_SyncPictureLayerDetectsVersionChange(t *testing.T) { // should still trigger re-render. func TestOverlayBoundary_FullPipeline_HoverGeneratesDamage(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -339,8 +339,8 @@ func TestOverlayBoundary_FullPipeline_HoverGeneratesDamage(t *testing.T) { // version does not change, and the PictureLayer stays clean. func TestOverlayBoundary_CleanHover_NoRender(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -396,8 +396,8 @@ func TestOverlayBoundary_StandalonePropagation(t *testing.T) { menu.SetScreenOrigin(geometry.Pt(100, 200)) prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) @@ -434,8 +434,8 @@ func TestOverlayBoundary_OnBoundaryDirtyCallback(t *testing.T) { menu.SetScreenOrigin(geometry.Pt(100, 200)) prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) diff --git a/app/scene_recorder.go b/app/scene_recorder.go index 2d9a82a..c7ab71c 100644 --- a/app/scene_recorder.go +++ b/app/scene_recorder.go @@ -7,13 +7,19 @@ import ( ) func init() { + // Register the SceneFactory so that widget.DrawTree can create new + // SceneCache instances for WidgetBase-based repaint boundaries (ADR-024). + widget.RegisterSceneFactory(func() widget.SceneCache { + return scene.NewScene() + }) + // Register the SceneRecorder factory so that widget.DrawTree can create // recording canvases for WidgetBase-based repaint boundaries (ADR-024). // // The widget package cannot import internal/render (circular dep), so // we inject the factory here during package initialization. - widget.RegisterSceneRecorder(func(s *scene.Scene, width, height int) (widget.Canvas, func()) { - recorder := internalRender.NewSceneCanvas(s, width, height) + widget.RegisterSceneRecorder(func(s widget.SceneCache, width, height int) (widget.Canvas, func()) { + recorder := internalRender.NewSceneCanvas(s.(*scene.Scene), width, height) return recorder, recorder.Close }) } diff --git a/app/screen_origin_test.go b/app/screen_origin_test.go index 12d0ecb..001b60d 100644 --- a/app/screen_origin_test.go +++ b/app/screen_origin_test.go @@ -141,7 +141,7 @@ func (w *screenOriginLeaf) Children() []widget.Widget { return // testSceneRecorder is defined in compositor_test.go but redeclared here // for this test file. Uses the same pattern. -func testSceneRecorderForOriginTests(s *scene.Scene, w, h int) (widget.Canvas, func()) { //nolint:unused // retained for future screen origin test variants - rec := internalRender.NewSceneCanvas(s, w, h) +func testSceneRecorderForOriginTests(s widget.SceneCache, w, h int) (widget.Canvas, func()) { //nolint:unused // retained for future screen origin test variants + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close } diff --git a/app/spinner_e2e_test.go b/app/spinner_e2e_test.go index 9b63341..b86fa32 100644 --- a/app/spinner_e2e_test.go +++ b/app/spinner_e2e_test.go @@ -37,8 +37,8 @@ func (w *boxContainer) Children() []widget.Widget { return w.ki // Verifies spinner is found by PaintBoundaryLayers AND visible in composed scene. func TestSpinnerE2E_VisibleInCompositor(t *testing.T) { prev := widget.GetSceneRecorderFactory() - widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { - rec := internalRender.NewSceneCanvas(s, w, h) + widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { + rec := internalRender.NewSceneCanvas(s.(*scene.Scene), w, h) return rec, rec.Close }) defer widget.RegisterSceneRecorder(prev) diff --git a/app/window_draw_test.go b/app/window_draw_test.go index 70d2e0e..9588c13 100644 --- a/app/window_draw_test.go +++ b/app/window_draw_test.go @@ -1,7 +1,6 @@ package app import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -49,7 +48,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // drawTrackingWidget tracks whether Draw was called and has configurable bounds. type drawTrackingWidget struct { diff --git a/app/window_test.go b/app/window_test.go index 530bc9a..346fb89 100644 --- a/app/window_test.go +++ b/app/window_test.go @@ -1,7 +1,6 @@ package app import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -458,7 +457,7 @@ func (m *mockCanvas) PopTransform() {} func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Retained-mode rendering tests --- diff --git a/compositor/compositor.go b/compositor/compositor.go index 20f87ca..772373b 100644 --- a/compositor/compositor.go +++ b/compositor/compositor.go @@ -53,8 +53,10 @@ func (c *Compositor) composeLayer(layer Layer, parentX, parentY float32) { // PictureLayer: append its scene at accumulated offset. if po, ok := layer.(PictureOwner); ok { pic := po.Picture() - if pic != nil && !pic.IsEmpty() { - c.composed.AppendWithTranslation(pic, x, y) + if pic != nil { + if sc, ok2 := pic.(*scene.Scene); ok2 && !sc.IsEmpty() { + c.composed.AppendWithTranslation(sc, x, y) + } } layer.ClearNeedsCompositing() return diff --git a/compositor/layer.go b/compositor/layer.go index 5502990..f9b2a3b 100644 --- a/compositor/layer.go +++ b/compositor/layer.go @@ -1,8 +1,8 @@ package compositor import ( - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" + "github.com/gogpu/ui/widget" ) // Layer is a node in the compositor layer tree. @@ -55,17 +55,17 @@ type ContainerLayer interface { RemoveAll() } -// PictureOwner is implemented by layers that own a scene.Scene (display list). +// PictureOwner is implemented by layers that own a SceneCache (display list). // // Flutter equivalent: PictureLayer.picture. type PictureOwner interface { - // Picture returns the scene.Scene owned by this layer. + // Picture returns the SceneCache owned by this layer. // Returns nil if the layer has not been recorded yet. - Picture() *scene.Scene + Picture() widget.SceneCache // SetPicture stores a recorded scene. Called after recording a // RepaintBoundary's subtree via SceneCanvas. - SetPicture(s *scene.Scene) + SetPicture(s widget.SceneCache) // IsDirty reports whether the picture needs re-recording. IsDirty() bool @@ -155,7 +155,7 @@ func (l *OffsetLayerImpl) Append(child Layer) { l.MarkNeedsCompositing() } -// PictureLayerImpl owns a scene.Scene display list. Leaf node. +// PictureLayerImpl owns a SceneCache display list. Leaf node. // // Flutter equivalent: PictureLayer. Contains the recorded draw // commands from a RepaintBoundary's subtree. @@ -165,7 +165,7 @@ func (l *OffsetLayerImpl) Append(child Layer) { // BuildLayerTree populates them; compositeTexturesFromTree reads them. type PictureLayerImpl struct { layerBase - picture *scene.Scene + picture widget.SceneCache dirty bool boundaryCacheKey uint64 // Links to per-boundary texture cache (renderLoop.boundaryTextures). isRoot bool // True for the root boundary (uses DrawGPUTextureBase). @@ -183,11 +183,11 @@ func NewPictureLayer() *PictureLayerImpl { return &PictureLayerImpl{dirty: true} } -func (l *PictureLayerImpl) Picture() *scene.Scene { return l.picture } -func (l *PictureLayerImpl) SetPicture(s *scene.Scene) { l.picture = s; l.MarkNeedsCompositing() } -func (l *PictureLayerImpl) IsDirty() bool { return l.dirty } -func (l *PictureLayerImpl) MarkDirty() { l.dirty = true; l.MarkNeedsCompositing() } -func (l *PictureLayerImpl) ClearDirty() { l.dirty = false } +func (l *PictureLayerImpl) Picture() widget.SceneCache { return l.picture } +func (l *PictureLayerImpl) SetPicture(s widget.SceneCache) { l.picture = s; l.MarkNeedsCompositing() } +func (l *PictureLayerImpl) IsDirty() bool { return l.dirty } +func (l *PictureLayerImpl) MarkDirty() { l.dirty = true; l.MarkNeedsCompositing() } +func (l *PictureLayerImpl) ClearDirty() { l.dirty = false } // BoundaryCacheKey returns the unique ID linking this layer to the // per-boundary GPU texture cache. Set by BuildLayerTree. diff --git a/core/button/button_test.go b/core/button/button_test.go index 5e039c8..d6352f1 100644 --- a/core/button/button_test.go +++ b/core/button/button_test.go @@ -1,7 +1,6 @@ package button_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -603,7 +602,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- mockCanvas for non-recording tests --- @@ -637,7 +636,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Lifecycle Tests --- diff --git a/core/button/internal_test.go b/core/button/internal_test.go index 04e15ac..d3a8278 100644 --- a/core/button/internal_test.go +++ b/core/button/internal_test.go @@ -1,7 +1,6 @@ package button import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1152,7 +1151,7 @@ func (c *internalMockCanvas) PopTransform() {} func (c *internalMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalMockCanvas) ReplayScene(_ widget.SceneCache) {} // --- ReadonlySignal Tests --- diff --git a/core/checkbox/checkbox_test.go b/core/checkbox/checkbox_test.go index 5624dff..6bc680f 100644 --- a/core/checkbox/checkbox_test.go +++ b/core/checkbox/checkbox_test.go @@ -1,7 +1,6 @@ package checkbox_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -610,7 +609,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- mockCanvas for non-recording tests --- @@ -644,7 +643,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Signal Binding Tests --- diff --git a/core/checkbox/internal_test.go b/core/checkbox/internal_test.go index 55f3a73..5a41e44 100644 --- a/core/checkbox/internal_test.go +++ b/core/checkbox/internal_test.go @@ -1,7 +1,6 @@ package checkbox import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1096,7 +1095,7 @@ func (c *internalMockCanvas) PopTransform() {} func (c *internalMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalMockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Signal Binding Tests --- diff --git a/core/collapsible/collapsible_test.go b/core/collapsible/collapsible_test.go index 0dedba9..22a2851 100644 --- a/core/collapsible/collapsible_test.go +++ b/core/collapsible/collapsible_test.go @@ -1,7 +1,6 @@ package collapsible_test import ( - "github.com/gogpu/gg/scene" "image" "testing" "time" @@ -1136,7 +1135,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- recordingCanvas records draw calls for detailed verification --- @@ -1213,7 +1212,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- TitleSignal Tests --- diff --git a/core/collapsible/internal_test.go b/core/collapsible/internal_test.go index 8d1375f..251ef4b 100644 --- a/core/collapsible/internal_test.go +++ b/core/collapsible/internal_test.go @@ -1,7 +1,6 @@ package collapsible import ( - "github.com/gogpu/gg/scene" "image" "testing" "time" @@ -693,7 +692,7 @@ func (c *internalMockCanvas) PopTransform() {} func (c *internalMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalMockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Granular Invalidation Tests (TASK-UI-INVAL-001g) --- // diff --git a/core/datatable/datatable_test.go b/core/datatable/datatable_test.go index 261863f..b9d3ccc 100644 --- a/core/datatable/datatable_test.go +++ b/core/datatable/datatable_test.go @@ -2,7 +2,6 @@ package datatable import ( "fmt" - "github.com/gogpu/gg/scene" "image" "testing" @@ -55,7 +54,7 @@ func (m *mockCanvas) PopTransform() { m.transform func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} // testColumns returns a standard set of test columns. func testColumns() []Column { diff --git a/core/dialog/dialog_test.go b/core/dialog/dialog_test.go index 545012a..da09538 100644 --- a/core/dialog/dialog_test.go +++ b/core/dialog/dialog_test.go @@ -1,7 +1,6 @@ package dialog_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -498,7 +497,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- mockOverlayManager records overlay operations --- diff --git a/core/dialog/internal_test.go b/core/dialog/internal_test.go index 9904731..8507c9c 100644 --- a/core/dialog/internal_test.go +++ b/core/dialog/internal_test.go @@ -1,7 +1,6 @@ package dialog import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1000,7 +999,7 @@ func (c *internalRecordingCanvas) ScreenOriginBase() geometry.Point func (c *internalRecordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalRecordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalRecordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- internalMockOverlayManager --- diff --git a/core/docking/docking_test.go b/core/docking/docking_test.go index 818902c..7849004 100644 --- a/core/docking/docking_test.go +++ b/core/docking/docking_test.go @@ -1,7 +1,6 @@ package docking import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1217,4 +1216,4 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/dropdown/dropdown_test.go b/core/dropdown/dropdown_test.go index 947d458..1651646 100644 --- a/core/dropdown/dropdown_test.go +++ b/core/dropdown/dropdown_test.go @@ -1,7 +1,6 @@ package dropdown_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -940,7 +939,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- Mock Canvas --- @@ -974,7 +973,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Lifecycle Tests --- diff --git a/core/gridview/gridview_test.go b/core/gridview/gridview_test.go index 4824e50..42632a9 100644 --- a/core/gridview/gridview_test.go +++ b/core/gridview/gridview_test.go @@ -1,7 +1,6 @@ package gridview_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1256,7 +1255,7 @@ func (m *mockCanvas) PopTransform() {} func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} type testPainter struct { cellBackgroundCalls int diff --git a/core/gridview/internal_test.go b/core/gridview/internal_test.go index 49ac491..edc61aa 100644 --- a/core/gridview/internal_test.go +++ b/core/gridview/internal_test.go @@ -1,7 +1,6 @@ package gridview import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -956,4 +955,4 @@ func (m *mockCanvas) PopTransform() {} func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/linechart/linechart_test.go b/core/linechart/linechart_test.go index a1ec846..7c46f65 100644 --- a/core/linechart/linechart_test.go +++ b/core/linechart/linechart_test.go @@ -1,7 +1,6 @@ package linechart import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -754,7 +753,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} type mockPainter struct { called bool diff --git a/core/listview/internal_test.go b/core/listview/internal_test.go index e634c58..2f73873 100644 --- a/core/listview/internal_test.go +++ b/core/listview/internal_test.go @@ -4,7 +4,6 @@ import ( "image" "testing" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/cdk" "github.com/gogpu/ui/event" "github.com/gogpu/ui/geometry" @@ -888,7 +887,7 @@ func (m *mockCanvas) PopTransform() {} func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Decorator + RepaintBoundary integration tests --- diff --git a/core/listview/listview_test.go b/core/listview/listview_test.go index a2bbe14..8ae1eda 100644 --- a/core/listview/listview_test.go +++ b/core/listview/listview_test.go @@ -1,7 +1,6 @@ package listview_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1618,7 +1617,7 @@ func (m *mockCanvas) PopTransform() {} func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} type mockPainter struct { dividerCalls int diff --git a/core/menu/menu_test.go b/core/menu/menu_test.go index 37f0760..1f8cdee 100644 --- a/core/menu/menu_test.go +++ b/core/menu/menu_test.go @@ -1,7 +1,6 @@ package menu_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -2004,7 +2003,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- Mock Canvas --- @@ -2038,4 +2037,4 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/popover/popover_test.go b/core/popover/popover_test.go index f8c6e36..896c17e 100644 --- a/core/popover/popover_test.go +++ b/core/popover/popover_test.go @@ -1,7 +1,6 @@ package popover_test import ( - "github.com/gogpu/gg/scene" "image" "testing" "time" @@ -2050,7 +2049,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // mockCanvas is a no-op canvas for testing. type mockCanvas struct{} @@ -2083,4 +2082,4 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/progress/progress_test.go b/core/progress/progress_test.go index 72a5617..ecb3c0e 100644 --- a/core/progress/progress_test.go +++ b/core/progress/progress_test.go @@ -1,7 +1,6 @@ package progress_test import ( - "github.com/gogpu/gg/scene" "image" "testing" "time" @@ -1047,7 +1046,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- ADR-024 RepaintBoundary Propagation Tests --- diff --git a/core/progressbar/progressbar_test.go b/core/progressbar/progressbar_test.go index 28ebcb0..7cefb05 100644 --- a/core/progressbar/progressbar_test.go +++ b/core/progressbar/progressbar_test.go @@ -1,7 +1,6 @@ package progressbar_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -803,4 +802,4 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/radio/internal_test.go b/core/radio/internal_test.go index b308279..9d5c00b 100644 --- a/core/radio/internal_test.go +++ b/core/radio/internal_test.go @@ -1,7 +1,6 @@ package radio import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1470,7 +1469,7 @@ func (c *internalMockCanvas) PopTransform() {} func (c *internalMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalMockCanvas) ReplayScene(_ widget.SceneCache) {} // --- onChange dedup test --- diff --git a/core/radio/radio_test.go b/core/radio/radio_test.go index 3f341a6..bd6f100 100644 --- a/core/radio/radio_test.go +++ b/core/radio/radio_test.go @@ -1,7 +1,6 @@ package radio_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -989,7 +988,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- mockCanvas for non-recording tests --- @@ -1023,7 +1022,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Signal Binding Tests (public API) --- diff --git a/core/scrollview/internal_test.go b/core/scrollview/internal_test.go index 45722f9..7969eeb 100644 --- a/core/scrollview/internal_test.go +++ b/core/scrollview/internal_test.go @@ -1,7 +1,6 @@ package scrollview import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -2070,7 +2069,7 @@ func (c *internalMockCanvas) PopTransform() { func (c *internalMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalMockCanvas) ReplayScene(_ widget.SceneCache) {} // --- ScrollView MarkRedrawLocal vs SetNeedsRedraw Tests (ADR-024 regression) --- // diff --git a/core/scrollview/scrollview_test.go b/core/scrollview/scrollview_test.go index 144e683..aa227f3 100644 --- a/core/scrollview/scrollview_test.go +++ b/core/scrollview/scrollview_test.go @@ -1,7 +1,6 @@ package scrollview_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -83,7 +82,7 @@ func (c *stubCanvas) PopTransform() { func (c *stubCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *stubCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *stubCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *stubCanvas) ReplayScene(_ *scene.Scene) {} +func (c *stubCanvas) ReplayScene(_ widget.SceneCache) {} // --- Construction Tests --- diff --git a/core/slider/internal_test.go b/core/slider/internal_test.go index 7032445..ad4f13d 100644 --- a/core/slider/internal_test.go +++ b/core/slider/internal_test.go @@ -1,7 +1,6 @@ package slider import ( - "github.com/gogpu/gg/scene" "image" "math" "testing" @@ -1872,7 +1871,7 @@ func (c *internalMockCanvas) PopTransform() {} func (c *internalMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *internalMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *internalMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *internalMockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Granular Invalidation Tests (TASK-UI-INVAL-001d) --- diff --git a/core/slider/slider_test.go b/core/slider/slider_test.go index b1cde4c..8757a31 100644 --- a/core/slider/slider_test.go +++ b/core/slider/slider_test.go @@ -1,7 +1,6 @@ package slider_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -362,4 +361,4 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/splitview/splitview_test.go b/core/splitview/splitview_test.go index 6da4aa8..5540cfd 100644 --- a/core/splitview/splitview_test.go +++ b/core/splitview/splitview_test.go @@ -1,7 +1,6 @@ package splitview_test import ( - "github.com/gogpu/gg/scene" "image" "testing" "time" @@ -77,7 +76,7 @@ func (c *canvasRecorder) PopTransform() {} func (c *canvasRecorder) TransformOffset() geometry.Point { return geometry.Point{} } func (c *canvasRecorder) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *canvasRecorder) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *canvasRecorder) ReplayScene(_ *scene.Scene) {} +func (c *canvasRecorder) ReplayScene(_ widget.SceneCache) {} // Compile-time check. var _ widget.Canvas = (*canvasRecorder)(nil) diff --git a/core/tabview/tabview_test.go b/core/tabview/tabview_test.go index 57e5efc..361d10d 100644 --- a/core/tabview/tabview_test.go +++ b/core/tabview/tabview_test.go @@ -1,7 +1,6 @@ package tabview_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1095,7 +1094,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- mockCanvas for non-recording tests --- @@ -1129,4 +1128,4 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/textfield/textfield_test.go b/core/textfield/textfield_test.go index 6442b5b..4cfcf1d 100644 --- a/core/textfield/textfield_test.go +++ b/core/textfield/textfield_test.go @@ -1,7 +1,6 @@ package textfield_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1261,7 +1260,7 @@ func (c *recordingCanvas) PopTransform() {} func (c *recordingCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordingCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordingCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordingCanvas) ReplayScene(_ widget.SceneCache) {} // --- mockCanvas for non-recording tests --- @@ -1295,7 +1294,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // --- Lifecycle Tests --- diff --git a/core/titlebar/titlebar_test.go b/core/titlebar/titlebar_test.go index 47b2b1c..0a4c51b 100644 --- a/core/titlebar/titlebar_test.go +++ b/core/titlebar/titlebar_test.go @@ -1,7 +1,6 @@ package titlebar import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1374,7 +1373,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // RenderSVG implements widget.SVGRenderer for SVG icon testing. func (c *mockCanvas) RenderSVG(svgXML []byte, bounds geometry.Rect, color widget.Color) { diff --git a/core/toolbar/toolbar_test.go b/core/toolbar/toolbar_test.go index 9e5f6c1..8b46038 100644 --- a/core/toolbar/toolbar_test.go +++ b/core/toolbar/toolbar_test.go @@ -1,7 +1,6 @@ package toolbar import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1431,4 +1430,4 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/core/treeview/treeview_test.go b/core/treeview/treeview_test.go index 0f3b2bf..ef2d954 100644 --- a/core/treeview/treeview_test.go +++ b/core/treeview/treeview_test.go @@ -1,7 +1,6 @@ package treeview import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -1996,7 +1995,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} type mockScheduler struct{} diff --git a/desktop/desktop.go b/desktop/desktop.go index 345321b..bd6f7c4 100644 --- a/desktop/desktop.go +++ b/desktop/desktop.go @@ -636,7 +636,8 @@ func (rl *renderLoop) renderSingleBoundaryFromLayer(pic *compositor.PictureLayer entry := rl.ensureBoundaryTexture(pic.BoundaryCacheKey(), bw, bh, cc) // Detect fresh recordings via scene version. Skip re-rendering clean textures. - cachedScene := pic.Picture() + cachedSceneRaw := pic.Picture() + cachedScene, _ := cachedSceneRaw.(*scene.Scene) if rl.isBoundaryClean(entry, pic, cachedScene) { rl.updateClipRect(entry, pic) return diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 74efa4f..17d12c9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1527,7 +1527,7 @@ This lets the same widget render as Material 3, Fluent, or Cupertino by swapping **LayoutMetrics (ADR-034):** Widgets define an optional `LayoutMetrics` interface that painters can implement to control spatial metrics (height, padding, font size, corner radius). Widgets query via type assertion with DefaultPainter fallback. This is the Qt `QStyle::pixelMetric` pattern — themes control dimensions without touching behavior. -**ThemeBundle:** `theme.Bundle` interface packages all painters for complete theme installation. Community themes implement `Bundle` to provide a full design system via `app.WithThemeBundle()`. +**ThemeBundle:** `theme.Bundle` interface (defined in `theme/bundle.go`) packages all painters for complete theme installation. Interface is defined but built-in implementations (M3, DevTools, Fluent, Cupertino) and `app.WithThemeBundle()` are not yet available (ADR-034 Phase 3 remaining work). ### 6. Opt-in Lifecycle for Signal Binding diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md index 7400457..2ad93a3 100644 --- a/docs/EXTENSIONS.md +++ b/docs/EXTENSIONS.md @@ -597,12 +597,22 @@ import _ "github.com/yourname/ui-extension" | gogpu/ui | Extension API | |----------|---------------| -| Phase 1.x | Stable (registry, theme, layout, plugin) | -| Phase 2.x | Stable + interactive widgets (button, checkbox, radio, textfield, dropdown) + Painter pattern + overlay | -| Phase 3.x | Stable + slider, dialog, scrollview, tabview, animation, RepaintBoundary, scene.Scene tile-parallel rendering | -| Phase 4.x | Stable + 22 interactive widgets, 3 design systems (M3/Fluent/Cupertino), i18n, dnd, icon, uitest | - -The extension API is stable and will remain compatible across future releases. +| v0.1.x (current) | Stable: registry, theme, layout, plugin, 26 interactive widgets, 4 design systems (M3/DevTools/Fluent/Cupertino), i18n, dnd, icon, uitest, LayoutMetrics (ADR-034), Painter pattern, overlay, RepaintBoundary, Layer Tree compositor | + +**Implemented extension points:** +- Widget Registry (`registry/`) — register/create widgets by name +- ThemeExtension (`theme/extension.go`) — extend themes with custom properties +- Public Layout API (`layout/`) — pluggable layout algorithms +- Plugin System (`plugin/`) — bundle widgets, themes, layouts with dependency resolution +- Content\[C\] CDK (`cdk/`) — polymorphic content rendering +- Pluggable Painter (`core/*/painter.go`) — design-system-agnostic rendering +- LayoutMetrics (`core/*/painter.go`) — painters control spatial metrics (7 widgets) + +**Planned (ADR-034 Phase 3 remaining):** +- ThemeBundle (`theme/bundle.go`) — interface defined, no built-in implementations yet +- `app.WithThemeBundle()` — one-call theme installation + +The extension API is stable within v0.x and will remain compatible across future releases. --- diff --git a/focus/focus_test.go b/focus/focus_test.go index ce5291c..b9c5a41 100644 --- a/focus/focus_test.go +++ b/focus/focus_test.go @@ -1,7 +1,6 @@ package focus_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -98,7 +97,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // testTree holds widget references from buildTree for easy access. type testTree struct { diff --git a/go.mod b/go.mod index 57bd669..82a795e 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.25.0 require ( github.com/coregx/signals v0.1.1 - github.com/gogpu/gg v0.50.11 - github.com/gogpu/gogpu v0.48.5 + github.com/gogpu/gg v0.50.12 + github.com/gogpu/gogpu v0.50.0 github.com/gogpu/gpucontext v0.24.0 github.com/gogpu/gputypes v0.5.1 - github.com/gogpu/wgpu v0.30.35 + github.com/gogpu/wgpu v0.30.36 golang.org/x/image v0.44.0 ) diff --git a/go.sum b/go.sum index abe2c25..5026f8e 100644 --- a/go.sum +++ b/go.sum @@ -4,18 +4,18 @@ github.com/go-webgpu/goffi v0.6.3 h1:p4gKGikHBAQ/8iUiew9MV4C5M1ZGIsk8QGFGuJjMj+A github.com/go-webgpu/goffi v0.6.3/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM= github.com/go-webgpu/webgpu v0.5.5 h1:pIrXzRg0LRlNjNmR+ZNo/ERN88YaObzbCY5oYhir5SA= github.com/go-webgpu/webgpu v0.5.5/go.mod h1:vgIuNTa1UlZ4njCGY6Pmp/0c5T5o2Ml2fQ0znLc7B98= -github.com/gogpu/gg v0.50.11 h1:kw1Lh0QIso0HCpvI2j0A/FBT2P/y0PwGF0nhVxdUlgg= -github.com/gogpu/gg v0.50.11/go.mod h1:4kNutSanGhCNSKxbUp9izb0BkMCpROdSz0C+5N7oTp0= -github.com/gogpu/gogpu v0.48.5 h1:ZcCGHzH5rWj1XTmGrqFqqbmLA0C7sbnY0wYaXU5O6mA= -github.com/gogpu/gogpu v0.48.5/go.mod h1:Vt8YmokZ/j0nK7DOoWEiQk1ksZMVRvNIcw3LMlHXYpY= +github.com/gogpu/gg v0.50.12 h1:rPhZGx5uTvd4AsARSfi3Uj7bLyYoXpUOUZ09uibg45M= +github.com/gogpu/gg v0.50.12/go.mod h1:cV+vVHM75pf7qPDIKPLSFUQuazolClb8xSW9tdQTJRU= +github.com/gogpu/gogpu v0.50.0 h1:qyUi5vBte/m7NDd49aN+YJffn36V4jGJydXCg37NR9E= +github.com/gogpu/gogpu v0.50.0/go.mod h1:hIEFjkuVV2+eOkayR7gS38h7UrnUOZfcQXXQbxt36c0= github.com/gogpu/gpucontext v0.24.0 h1:YQ0FxGqXEO8LQB+6/TOqZOV1fn0mO9UDYR0obSU3R6o= github.com/gogpu/gpucontext v0.24.0/go.mod h1:OrT137boh5yPhqBEhF4UQKIOu1Jq74SLQzYa/m6JbNo= github.com/gogpu/gputypes v0.5.1 h1:X38OPcP6umQqqubzzJYL6Nm1tXHSNQj6TRSAoxdAJmg= github.com/gogpu/gputypes v0.5.1/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw= github.com/gogpu/naga v0.18.0 h1:2y83HUcAnlwEZMuHOiYTMdNYM9J8rev40gkI4zJ96Uk= github.com/gogpu/naga v0.18.0/go.mod h1:15sQaHKkbqXcwTN+hHYGLsA0WBBnkmYzne/eF5p5WEg= -github.com/gogpu/wgpu v0.30.35 h1:qUARm/jiRDYlxyd+UVLBt1JHXdt4936p8VrOOZacMy0= -github.com/gogpu/wgpu v0.30.35/go.mod h1:fi3zxQmJnjPRMqqA5wl2kta2Z6kxdf8HQxEORNT1Lt4= +github.com/gogpu/wgpu v0.30.36 h1:8r4tYLCo8qG0ZaOQxsCh4BpZJ5u8CL3Ab/XDOwMH3co= +github.com/gogpu/wgpu v0.30.36/go.mod h1:fi3zxQmJnjPRMqqA5wl2kta2Z6kxdf8HQxEORNT1Lt4= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/icon/icon_test.go b/icon/icon_test.go index 7f92e9c..1816196 100644 --- a/icon/icon_test.go +++ b/icon/icon_test.go @@ -1,7 +1,6 @@ package icon import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -287,7 +286,7 @@ func (m *mockCanvas) PopTransform() {} func (m *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (m *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (m *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (m *mockCanvas) ReplayScene(_ widget.SceneCache) {} func TestDraw_EmptyOps(t *testing.T) { c := &mockCanvas{} diff --git a/internal/render/canvas.go b/internal/render/canvas.go index a2a3c08..70bb352 100644 --- a/internal/render/canvas.go +++ b/internal/render/canvas.go @@ -693,15 +693,19 @@ func (c *Canvas) FillSVGPath(svgData string, viewBox float32, bounds geometry.Re c.dc.Pop() } -// ReplayScene renders a previously recorded scene.Scene display list into +// ReplayScene renders a previously recorded scene display list into // this canvas. The scene commands are decoded and routed through gg.Context's // GPU accelerator, which auto-selects GPU or CPU rendering per shape. // // This is the retained-mode replay path (ADR-007): RepaintBoundary caches // child drawing as a scene.Scene and replays it on cache hit instead of // re-executing child.Draw(). -func (c *Canvas) ReplayScene(s *scene.Scene) { - if s == nil || s.IsEmpty() { +func (c *Canvas) ReplayScene(s widget.SceneCache) { + if s == nil { + return + } + sc, ok := s.(*scene.Scene) + if !ok || sc.IsEmpty() { return } // Apply current canvas offset so the scene renders at the correct @@ -721,7 +725,7 @@ func (c *Canvas) ReplayScene(s *scene.Scene) { oy := float64(c.currentOffset.Y) c.dc.Translate(ox, oy) renderer := scene.NewGPUSceneRenderer(c.dc) - _ = renderer.RenderScene(s) + _ = renderer.RenderScene(sc) c.dc.Pop() } diff --git a/internal/render/scene_canvas.go b/internal/render/scene_canvas.go index 28517f3..86b13dc 100644 --- a/internal/render/scene_canvas.go +++ b/internal/render/scene_canvas.go @@ -582,7 +582,7 @@ func (c *SceneCanvas) ClipBounds() geometry.Rect { return c.currentClip } -// ReplayScene merges a child scene.Scene into this canvas's parent scene +// ReplayScene merges a child scene into this canvas's parent scene // with translation offset. This is the scene-concatenation path (ADR-007) // used when a RepaintBoundary replays its cached display list inside // another SceneCanvas (nested boundaries). @@ -590,11 +590,15 @@ func (c *SceneCanvas) ClipBounds() geometry.Rect { // The child scene was recorded in local coordinates (0,0 = boundary origin). // AppendWithTranslation offsets all path coordinates by the current cumulative // transform offset, following the Vello pattern (encoding.rs:162-169). -func (c *SceneCanvas) ReplayScene(s *scene.Scene) { - if s == nil || s.IsEmpty() { +func (c *SceneCanvas) ReplayScene(s widget.SceneCache) { + if s == nil { return } - c.sc.AppendWithTranslation(s, c.currentOffset.X, c.currentOffset.Y) + sc, ok := s.(*scene.Scene) + if !ok || sc.IsEmpty() { + return + } + c.sc.AppendWithTranslation(sc, c.currentOffset.X, c.currentOffset.Y) } // --- Internal helpers --- diff --git a/internal/textmetrics/textmetrics_test.go b/internal/textmetrics/textmetrics_test.go index f126b9b..1415641 100644 --- a/internal/textmetrics/textmetrics_test.go +++ b/internal/textmetrics/textmetrics_test.go @@ -4,7 +4,6 @@ import ( "image" "testing" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" "github.com/gogpu/ui/widget" ) @@ -42,7 +41,7 @@ func (c *mockCanvas) PopTransform() {} func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} func newMetrics() *Metrics { return &Metrics{ diff --git a/primitives/box_test.go b/primitives/box_test.go index 4405333..4d30049 100644 --- a/primitives/box_test.go +++ b/primitives/box_test.go @@ -1,7 +1,6 @@ package primitives_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -950,7 +949,7 @@ func (c *mockCanvas) PopTransform() { c.popTransf func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // eventConsumer is a mock widget that optionally consumes events. type eventConsumer struct { diff --git a/primitives/repaint_boundary.go b/primitives/repaint_boundary.go index 0d6270b..193ccc7 100644 --- a/primitives/repaint_boundary.go +++ b/primitives/repaint_boundary.go @@ -18,7 +18,7 @@ import ( var nextCacheKey atomic.Uint64 // RepaintBoundary is a display widget that caches its child subtree as a -// scene.Scene display list. When the child subtree is clean (no dirty +// display list (SceneCache). When the child subtree is clean (no dirty // widgets), the cached display list is replayed into the parent canvas // instead of re-executing Draw on every descendant. // @@ -52,10 +52,10 @@ type RepaintBoundary struct { // into the Window's dirty boundary set. Assigned once at creation time. cacheKey uint64 - // cachedScene holds the recorded display list (scene.Scene) for the - // child subtree. On cache hit, this is replayed into the parent canvas - // via Canvas.ReplayScene — no child.Draw() re-execution needed. - cachedScene *scene.Scene + // cachedScene holds the recorded display list for the child subtree. + // On cache hit, this is replayed into the parent canvas via + // Canvas.ReplayScene — no child.Draw() re-execution needed. + cachedScene widget.SceneCache // cacheVersion is a monotonic counter incremented each time the cache // is refreshed. Used for observability and diagnostics. @@ -120,7 +120,7 @@ func WithDebugLabel(label string) Option { } // NewRepaintBoundary creates a RepaintBoundary that caches the rendering -// of the given child widget as a scene.Scene display list. +// of the given child widget as a display list (SceneCache). // // If child is nil, the boundary renders nothing and reports zero size. // @@ -248,8 +248,8 @@ func (rb *RepaintBoundary) ConsecutiveHits() int { // boundary. Used for diagnostics, benchmarks, and compositor decisions. func (rb *RepaintBoundary) RasterCacheStats() RasterCacheStats { var tagCount int - if rb.cachedScene != nil { - tagCount = len(rb.cachedScene.Encoding().Tags()) + if sc, ok := rb.cachedScene.(*scene.Scene); ok { + tagCount = len(sc.Encoding().Tags()) } return RasterCacheStats{ @@ -284,10 +284,11 @@ func (rb *RepaintBoundary) evaluatePromotion(w, h int) { return } - if rb.cachedScene == nil { + sc, ok := rb.cachedScene.(*scene.Scene) + if !ok { return } - tagCount := len(rb.cachedScene.Encoding().Tags()) + tagCount := len(sc.Encoding().Tags()) if tagCount < cfg.MinComplexity { return } @@ -366,11 +367,11 @@ func (rb *RepaintBoundary) Layout(ctx widget.Context, constraints geometry.Const // Draw renders the child subtree, using the scene cache when possible. // // On cache hit (boundary not dirty, cached scene exists): replays the -// cached scene.Scene into the canvas via Canvas.ReplayScene — no child +// cached SceneCache into the canvas via Canvas.ReplayScene — no child // re-execution. This is O(commands) via Encoding.Append or GPU dispatch. // // On cache miss (boundary dirty or first draw): records child.Draw into -// a new scene.Scene via SceneCanvas, then replays the result. +// a new SceneCache via SceneCanvas, then replays the result. // // This is the ADR-007 retained-mode pattern: display list per boundary. func (rb *RepaintBoundary) Draw(ctx widget.Context, canvas widget.Canvas) { @@ -425,7 +426,7 @@ func (rb *RepaintBoundary) Draw(ctx widget.Context, canvas widget.Canvas) { } rb.cachedScene.Reset() - recorder := internalRender.NewSceneCanvas(rb.cachedScene, w, h) + recorder := internalRender.NewSceneCanvas(rb.cachedScene.(*scene.Scene), w, h) rb.child.Draw(ctx, recorder) recorder.Close() diff --git a/primitives/repaint_boundary_cache_test.go b/primitives/repaint_boundary_cache_test.go index 7ea8ee6..668363a 100644 --- a/primitives/repaint_boundary_cache_test.go +++ b/primitives/repaint_boundary_cache_test.go @@ -3,7 +3,6 @@ package primitives_test import ( "testing" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" "github.com/gogpu/ui/primitives" "github.com/gogpu/ui/widget" @@ -15,10 +14,10 @@ import ( type replayRecordingCanvas struct { mockCanvas replayCount int - replayScenes []*scene.Scene + replayScenes []widget.SceneCache } -func (c *replayRecordingCanvas) ReplayScene(s *scene.Scene) { +func (c *replayRecordingCanvas) ReplayScene(s widget.SceneCache) { c.replayCount++ c.replayScenes = append(c.replayScenes, s) } diff --git a/primitives/repaint_boundary_test.go b/primitives/repaint_boundary_test.go index d1115ba..d16cd7b 100644 --- a/primitives/repaint_boundary_test.go +++ b/primitives/repaint_boundary_test.go @@ -4,7 +4,6 @@ import ( "image" "testing" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/a11y" "github.com/gogpu/ui/event" "github.com/gogpu/ui/geometry" @@ -44,7 +43,7 @@ var _ widget.Widget = (*drawCountingWidget)(nil) type imageRecordingCanvas struct { mockCanvas drawImageCalls []drawImageCall - replaySceneCalls []*scene.Scene + replaySceneCalls []widget.SceneCache } type drawImageCall struct { @@ -56,7 +55,7 @@ func (c *imageRecordingCanvas) DrawImage(img image.Image, at geometry.Point) { c.drawImageCalls = append(c.drawImageCalls, drawImageCall{img: img, at: at}) } -func (c *imageRecordingCanvas) ReplayScene(s *scene.Scene) { +func (c *imageRecordingCanvas) ReplayScene(s widget.SceneCache) { c.replaySceneCalls = append(c.replaySceneCalls, s) } diff --git a/primitives/text_test.go b/primitives/text_test.go index a7e6f8f..7296332 100644 --- a/primitives/text_test.go +++ b/primitives/text_test.go @@ -5,7 +5,6 @@ import ( "image" "testing" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/a11y" "github.com/gogpu/ui/event" "github.com/gogpu/ui/geometry" @@ -58,7 +57,7 @@ func (c *styledMockCanvas) ScreenOriginBase() geometry.Point { retur func (c *styledMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *styledMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *styledMockCanvas) ReplayScene(_ widget.SceneCache) {} // StyledTextDrawer implementation. func (c *styledMockCanvas) DrawStyledText(text string, _ geometry.Rect, style widget.TextStyle) { diff --git a/theme/cupertino/badge_test.go b/theme/cupertino/badge_test.go index bbc6994..0155b7f 100644 --- a/theme/cupertino/badge_test.go +++ b/theme/cupertino/badge_test.go @@ -1,7 +1,6 @@ package cupertino import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -227,4 +226,4 @@ func (c *cupBadgeMockCanvas) PopTransform() {} func (c *cupBadgeMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *cupBadgeMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *cupBadgeMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *cupBadgeMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *cupBadgeMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/cupertino/cupertino_test.go b/theme/cupertino/cupertino_test.go index fd9adf7..aa848b2 100644 --- a/theme/cupertino/cupertino_test.go +++ b/theme/cupertino/cupertino_test.go @@ -1,7 +1,6 @@ package cupertino_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -97,7 +96,7 @@ func (c *recordCanvas) PopTransform() {} func (c *recordCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordCanvas) ReplayScene(_ widget.SceneCache) {} // Method name constants to satisfy goconst. const ( diff --git a/theme/devtools/badge_test.go b/theme/devtools/badge_test.go index d845274..206cfb9 100644 --- a/theme/devtools/badge_test.go +++ b/theme/devtools/badge_test.go @@ -1,7 +1,6 @@ package devtools import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -225,4 +224,4 @@ func (c *bgMockCanvas) PopTransform() {} func (c *bgMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *bgMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *bgMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *bgMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *bgMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/devtools/painters_test.go b/theme/devtools/painters_test.go index ddff231..8c44568 100644 --- a/theme/devtools/painters_test.go +++ b/theme/devtools/painters_test.go @@ -1,7 +1,6 @@ package devtools_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -97,7 +96,7 @@ func (c *recordCanvas) PopTransform() {} func (c *recordCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordCanvas) ReplayScene(_ widget.SceneCache) {} // Method name constants to satisfy goconst. const ( diff --git a/theme/devtools/titlebar_test.go b/theme/devtools/titlebar_test.go index 944a904..7532d0d 100644 --- a/theme/devtools/titlebar_test.go +++ b/theme/devtools/titlebar_test.go @@ -1,7 +1,6 @@ package devtools import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -259,4 +258,4 @@ func (c *tbMockCanvas) PopTransform() { func (c *tbMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *tbMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *tbMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *tbMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *tbMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/fluent/badge_test.go b/theme/fluent/badge_test.go index 4649fce..dc14954 100644 --- a/theme/fluent/badge_test.go +++ b/theme/fluent/badge_test.go @@ -1,7 +1,6 @@ package fluent import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -225,4 +224,4 @@ func (c *flBadgeMockCanvas) PopTransform() {} func (c *flBadgeMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *flBadgeMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *flBadgeMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *flBadgeMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *flBadgeMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/fluent/fluent_test.go b/theme/fluent/fluent_test.go index 43d45d5..f8ad1fd 100644 --- a/theme/fluent/fluent_test.go +++ b/theme/fluent/fluent_test.go @@ -1,7 +1,6 @@ package fluent_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -97,7 +96,7 @@ func (c *recordCanvas) PopTransform() {} func (c *recordCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordCanvas) ReplayScene(_ widget.SceneCache) {} // Method name constants to satisfy goconst. const ( diff --git a/theme/material3/button_test.go b/theme/material3/button_test.go index 9f595de..8f68f6b 100644 --- a/theme/material3/button_test.go +++ b/theme/material3/button_test.go @@ -1,7 +1,6 @@ package material3_test import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -92,7 +91,7 @@ func (c *recordCanvas) PopTransform() {} func (c *recordCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *recordCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *recordCanvas) ReplayScene(_ *scene.Scene) {} +func (c *recordCanvas) ReplayScene(_ widget.SceneCache) {} // Method name constants to satisfy goconst. const ( diff --git a/theme/material3/collapsible_test.go b/theme/material3/collapsible_test.go index 751c620..04d256a 100644 --- a/theme/material3/collapsible_test.go +++ b/theme/material3/collapsible_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -253,4 +252,4 @@ func (c *colMockCanvas) PopTransform() {} func (c *colMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *colMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *colMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *colMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *colMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/datatable_test.go b/theme/material3/datatable_test.go index 94dc4aa..67e41a4 100644 --- a/theme/material3/datatable_test.go +++ b/theme/material3/datatable_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -217,4 +216,4 @@ func (c *tableMockCanvas) PopTransform() {} func (c *tableMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *tableMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *tableMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *tableMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *tableMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/dialog_test.go b/theme/material3/dialog_test.go index a57954d..957c899 100644 --- a/theme/material3/dialog_test.go +++ b/theme/material3/dialog_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -232,4 +231,4 @@ func (c *dialogMockCanvas) PopTransform() {} func (c *dialogMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *dialogMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *dialogMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *dialogMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *dialogMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/docking_test.go b/theme/material3/docking_test.go index 31ee835..3392f00 100644 --- a/theme/material3/docking_test.go +++ b/theme/material3/docking_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -216,4 +215,4 @@ func (c *dockingMockCanvas) PopTransform() {} func (c *dockingMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *dockingMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *dockingMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *dockingMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *dockingMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/gridview_test.go b/theme/material3/gridview_test.go index b9f6b62..a3344cf 100644 --- a/theme/material3/gridview_test.go +++ b/theme/material3/gridview_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -288,4 +287,4 @@ func (c *gvMockCanvas) PopTransform() {} func (c *gvMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *gvMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *gvMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *gvMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *gvMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/linechart_test.go b/theme/material3/linechart_test.go index 8e30c1b..fa5d3cb 100644 --- a/theme/material3/linechart_test.go +++ b/theme/material3/linechart_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -200,4 +199,4 @@ func (c *chartMockCanvas) PopTransform() {} func (c *chartMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *chartMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *chartMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *chartMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *chartMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/menu_test.go b/theme/material3/menu_test.go index a97f81a..ac047a3 100644 --- a/theme/material3/menu_test.go +++ b/theme/material3/menu_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -239,4 +238,4 @@ func (c *menuMockCanvas) PopTransform() {} func (c *menuMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *menuMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *menuMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *menuMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *menuMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/popover_test.go b/theme/material3/popover_test.go index 20e1928..ea09b84 100644 --- a/theme/material3/popover_test.go +++ b/theme/material3/popover_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -188,4 +187,4 @@ func (c *popMockCanvas) PopTransform() {} func (c *popMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *popMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *popMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *popMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *popMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/progress_test.go b/theme/material3/progress_test.go index 063dcbe..4607b93 100644 --- a/theme/material3/progress_test.go +++ b/theme/material3/progress_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -209,4 +208,4 @@ func (c *cpMockCanvas) PopTransform() {} func (c *cpMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *cpMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *cpMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *cpMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *cpMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/progressbar_test.go b/theme/material3/progressbar_test.go index 8b42344..f42f765 100644 --- a/theme/material3/progressbar_test.go +++ b/theme/material3/progressbar_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -233,4 +232,4 @@ func (c *pbMockCanvas) PopTransform() {} func (c *pbMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *pbMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *pbMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *pbMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *pbMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/scrollbar_test.go b/theme/material3/scrollbar_test.go index bbbdb3e..e65b876 100644 --- a/theme/material3/scrollbar_test.go +++ b/theme/material3/scrollbar_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -182,4 +181,4 @@ func (c *scrollbarMockCanvas) PopTransform() {} func (c *scrollbarMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *scrollbarMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *scrollbarMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *scrollbarMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *scrollbarMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/slider_test.go b/theme/material3/slider_test.go index 11d82a9..996af9f 100644 --- a/theme/material3/slider_test.go +++ b/theme/material3/slider_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -366,4 +365,4 @@ func (c *sliderMockCanvas) PopTransform() {} func (c *sliderMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *sliderMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *sliderMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *sliderMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *sliderMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/splitview_test.go b/theme/material3/splitview_test.go index 39fa387..e0d54a2 100644 --- a/theme/material3/splitview_test.go +++ b/theme/material3/splitview_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -247,4 +246,4 @@ func (c *svMockCanvas) PopTransform() {} func (c *svMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *svMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *svMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *svMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *svMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/tabview_test.go b/theme/material3/tabview_test.go index e87ff7c..8f7e52a 100644 --- a/theme/material3/tabview_test.go +++ b/theme/material3/tabview_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -266,4 +265,4 @@ func (c *tabMockCanvas) PopTransform() {} func (c *tabMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *tabMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *tabMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *tabMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *tabMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/toolbar_test.go b/theme/material3/toolbar_test.go index 1f5b811..62c5e21 100644 --- a/theme/material3/toolbar_test.go +++ b/theme/material3/toolbar_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -225,4 +224,4 @@ func (c *toolbarMockCanvas) PopTransform() {} func (c *toolbarMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *toolbarMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *toolbarMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *toolbarMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *toolbarMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/theme/material3/treeview_test.go b/theme/material3/treeview_test.go index 5e15410..65e2b28 100644 --- a/theme/material3/treeview_test.go +++ b/theme/material3/treeview_test.go @@ -1,7 +1,6 @@ package material3 import ( - "github.com/gogpu/gg/scene" "image" "testing" @@ -264,4 +263,4 @@ func (c *treeMockCanvas) PopTransform() {} func (c *treeMockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *treeMockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *treeMockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *treeMockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *treeMockCanvas) ReplayScene(_ widget.SceneCache) {} diff --git a/transition/transition_test.go b/transition/transition_test.go index f1614ea..bd641bb 100644 --- a/transition/transition_test.go +++ b/transition/transition_test.go @@ -1,7 +1,6 @@ package transition import ( - "github.com/gogpu/gg/scene" "image" "testing" "time" @@ -84,7 +83,7 @@ func (c *mockCanvas) PopTransform() { func (c *mockCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *mockCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *mockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *mockCanvas) ReplayScene(_ widget.SceneCache) {} // opacityCanvas extends mockCanvas with OpacityPusher support. type opacityCanvas struct { diff --git a/uitest/canvas.go b/uitest/canvas.go index 4671e27..2b06aeb 100644 --- a/uitest/canvas.go +++ b/uitest/canvas.go @@ -3,7 +3,6 @@ package uitest import ( "image" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" "github.com/gogpu/ui/widget" ) @@ -328,7 +327,7 @@ func (c *MockCanvas) ClipBounds() geometry.Rect { } // ReplayScene is a no-op for the mock canvas. -func (c *MockCanvas) ReplayScene(_ *scene.Scene) {} +func (c *MockCanvas) ReplayScene(_ widget.SceneCache) {} // Reset clears all recorded calls, returning the canvas to its initial state. func (c *MockCanvas) Reset() { diff --git a/widget/base.go b/widget/base.go index 62e4cd7..d7dc76a 100644 --- a/widget/base.go +++ b/widget/base.go @@ -3,7 +3,6 @@ package widget import ( "sync" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" ) @@ -69,18 +68,18 @@ type WidgetBase struct { mounted bool // Whether widget is currently mounted // --- RepaintBoundary property (ADR-024) --- - // When isRepaintBoundary is true, this widget owns a scene.Scene that + // When isRepaintBoundary is true, this widget owns a SceneCache that // caches its subtree rendering. Clean boundaries replay cached content // instead of re-executing Draw on every descendant. isRepaintBoundary bool - boundaryCacheKey uint64 // Unique ID for dirty-set deduplication - cachedScene *scene.Scene // Recorded display list for the subtree - sceneDirty bool // Whether the cached scene needs re-recording - sceneCacheVersion uint64 // Monotonic counter (increments on re-record) - sceneCacheWidth int // Cache dimensions for size-change detection - sceneCacheHeight int // Cache dimensions for size-change detection - onBoundaryDirty func() // Callback when boundary transitions to dirty - suppressDirtyCallback bool // Suppressed during Draw recording (animation defers render) + boundaryCacheKey uint64 // Unique ID for dirty-set deduplication + cachedScene SceneCache // Recorded display list for the subtree + sceneDirty bool // Whether the cached scene needs re-recording + sceneCacheVersion uint64 // Monotonic counter (increments on re-record) + sceneCacheWidth int // Cache dimensions for size-change detection + sceneCacheHeight int // Cache dimensions for size-change detection + onBoundaryDirty func() // Callback when boundary transitions to dirty + suppressDirtyCallback bool // Suppressed during Draw recording (animation defers render) // --- Compositor clip (for per-boundary GPU textures) --- // When this boundary is skipped during parent BoundaryRecording (DrawChild), diff --git a/widget/boundary.go b/widget/boundary.go index 76720c1..8f152fc 100644 --- a/widget/boundary.go +++ b/widget/boundary.go @@ -2,8 +2,6 @@ package widget import ( "sync/atomic" - - "github.com/gogpu/gg/scene" ) // nextBoundaryCacheKey is a monotonic counter for generating unique cache keys. @@ -21,7 +19,7 @@ var nextBoundaryCacheKey atomic.Uint64 // SetRepaintBoundary marks this widget as a repaint boundary. // -// When enabled, the widget owns a scene.Scene display list that caches +// When enabled, the widget owns a SceneCache display list that caches // its subtree rendering. Clean boundaries replay their cached scene // instead of re-executing Draw on every descendant. // @@ -58,7 +56,7 @@ func (w *WidgetBase) SetRepaintBoundary(enabled bool) { // IsRepaintBoundary reports whether this widget is a repaint boundary. // -// Repaint boundaries own a scene.Scene that caches their subtree rendering. +// Repaint boundaries own a SceneCache that caches their subtree rendering. // The DrawTree function checks this property and replays the cached scene // when the boundary is clean, avoiding re-execution of the child Draw methods. func (w *WidgetBase) IsRepaintBoundary() bool { @@ -129,7 +127,7 @@ func (w *WidgetBase) IsSceneDirty() bool { // CachedScene returns the boundary's cached scene, or nil if no cache exists. // This is used by DrawTree to replay the scene when the boundary is clean. -func (w *WidgetBase) CachedScene() *scene.Scene { +func (w *WidgetBase) CachedScene() SceneCache { w.mu.RLock() defer w.mu.RUnlock() return w.cachedScene @@ -137,7 +135,7 @@ func (w *WidgetBase) CachedScene() *scene.Scene { // SetCachedScene stores the recorded scene for this boundary. // Called by the render system after recording the subtree. -func (w *WidgetBase) SetCachedScene(s *scene.Scene) { +func (w *WidgetBase) SetCachedScene(s SceneCache) { w.mu.Lock() defer w.mu.Unlock() w.cachedScene = s diff --git a/widget/boundary_draw.go b/widget/boundary_draw.go index 1d8c841..2dc5873 100644 --- a/widget/boundary_draw.go +++ b/widget/boundary_draw.go @@ -1,12 +1,11 @@ package widget import ( - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" ) // SceneRecorder creates a recording Canvas that writes draw commands into a -// scene.Scene. This is the dependency-injection point for ADR-024 Phase 2: +// SceneCache. This is the dependency-injection point for ADR-024 Phase 2: // the widget package cannot import internal/render (circular dep), so the // app layer registers a factory function that creates SceneCanvas instances. // @@ -14,12 +13,12 @@ import ( // After recording, the scene can be replayed via Canvas.ReplayScene. // // Parameters: -// - s: the scene.Scene to record into (must not be nil) +// - s: the SceneCache to record into (must not be nil) // - width, height: dimensions of the recording canvas // // Returns a Canvas that records into s, and a cleanup function that must // be called after recording is complete (e.g., SceneCanvas.Close). -type SceneRecorder func(s *scene.Scene, width, height int) (Canvas, func()) +type SceneRecorder func(s SceneCache, width, height int) (Canvas, func()) // sceneRecorderFactory holds the registered SceneRecorder factory. // Set by the app layer during initialization via RegisterSceneRecorder. @@ -31,8 +30,8 @@ var sceneRecorderFactory SceneRecorder // // Example (from app package): // -// widget.RegisterSceneRecorder(func(s *scene.Scene, w, h int) (widget.Canvas, func()) { -// recorder := render.NewSceneCanvas(s, w, h) +// widget.RegisterSceneRecorder(func(s widget.SceneCache, w, h int) (widget.Canvas, func()) { +// recorder := render.NewSceneCanvas(s.(*scene.Scene), w, h) // return recorder, recorder.Close // }) func RegisterSceneRecorder(factory SceneRecorder) { @@ -51,8 +50,8 @@ type boundaryWidget interface { Widget IsRepaintBoundary() bool IsSceneDirty() bool - CachedScene() *scene.Scene - SetCachedScene(*scene.Scene) + CachedScene() SceneCache + SetCachedScene(SceneCache) ClearSceneDirty() SceneCacheSize() (int, int) SetSceneCacheSize(int, int) @@ -138,7 +137,7 @@ func drawBoundaryWidget(w Widget, ctx Context, canvas Canvas, stats *DrawStats) // Cache miss: record child drawing into a scene. cachedScene := bw.CachedScene() if cachedScene == nil { - cachedScene = scene.NewScene() + cachedScene = NewSceneCache() } cachedScene.Reset() diff --git a/widget/boundary_test.go b/widget/boundary_test.go index 6a0d5b7..80e88b7 100644 --- a/widget/boundary_test.go +++ b/widget/boundary_test.go @@ -182,7 +182,9 @@ func TestPropagateDirtyUpward_LegacyBoundary(t *testing.T) { func TestDrawBoundaryWidget_FallbackOnZeroBounds(t *testing.T) { // Widget with boundary enabled but zero bounds should fall back to normal draw. - RegisterSceneRecorder(func(s *scene.Scene, w, h int) (Canvas, func()) { + RegisterSceneFactory(func() SceneCache { return scene.NewScene() }) + defer RegisterSceneFactory(nil) + RegisterSceneRecorder(func(s SceneCache, w, h int) (Canvas, func()) { return &noopCanvas{}, func() {} }) defer RegisterSceneRecorder(nil) @@ -204,7 +206,9 @@ func TestDrawBoundaryWidget_FallbackOnZeroBounds(t *testing.T) { } func TestDrawBoundaryWidget_CacheHit(t *testing.T) { - RegisterSceneRecorder(func(s *scene.Scene, w, h int) (Canvas, func()) { + RegisterSceneFactory(func() SceneCache { return scene.NewScene() }) + defer RegisterSceneFactory(nil) + RegisterSceneRecorder(func(s SceneCache, w, h int) (Canvas, func()) { return &noopCanvas{}, func() {} }) defer RegisterSceneRecorder(nil) @@ -240,7 +244,9 @@ func TestDrawBoundaryWidget_CacheHit(t *testing.T) { } func TestDrawBoundaryWidget_CacheInvalidation(t *testing.T) { - RegisterSceneRecorder(func(s *scene.Scene, w, h int) (Canvas, func()) { + RegisterSceneFactory(func() SceneCache { return scene.NewScene() }) + defer RegisterSceneFactory(nil) + RegisterSceneRecorder(func(s SceneCache, w, h int) (Canvas, func()) { return &noopCanvas{}, func() {} }) defer RegisterSceneRecorder(nil) diff --git a/widget/canvas.go b/widget/canvas.go index 3227f96..3a42bb1 100644 --- a/widget/canvas.go +++ b/widget/canvas.go @@ -3,7 +3,6 @@ package widget import ( "image" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/geometry" ) @@ -190,7 +189,7 @@ type Canvas interface { // preventing offscreen widgets from ticking animations. ClipBounds() geometry.Rect - // ReplayScene renders a previously recorded scene.Scene display list + // ReplayScene renders a previously recorded SceneCache display list // into this canvas. Used by RepaintBoundary to replay cached content // without re-executing the child widget's Draw method. // @@ -200,7 +199,7 @@ type Canvas interface { // Scene.Append (O(commands), zero re-encoding). // // If s is nil or empty, this is a no-op. - ReplayScene(s *scene.Scene) + ReplayScene(s SceneCache) } // DamageController can suppress damage tracking during rendering. @@ -211,7 +210,7 @@ type DamageController interface { } // BoundaryRecorder is implemented by canvases that record into a boundary's -// scene.Scene. When DrawChild encounters a child that IS a boundary, it skips +// SceneCache. When DrawChild encounters a child that IS a boundary, it skips // drawing — the child boundary has its own PictureLayer in the compositor. // // Flutter equivalent: PaintingContext knows it's recording into a boundary's diff --git a/widget/compositor_clip_test.go b/widget/compositor_clip_test.go index abb8e64..c71d71f 100644 --- a/widget/compositor_clip_test.go +++ b/widget/compositor_clip_test.go @@ -4,7 +4,6 @@ import ( "image" "testing" - "github.com/gogpu/gg/scene" "github.com/gogpu/ui/event" "github.com/gogpu/ui/geometry" "github.com/gogpu/ui/widget" @@ -193,7 +192,7 @@ func (c *clipStampCanvas) PopTransform() {} func (c *clipStampCanvas) TransformOffset() geometry.Point { return c.transformOffset } func (c *clipStampCanvas) ScreenOriginBase() geometry.Point { return c.screenOriginBase } func (c *clipStampCanvas) ClipBounds() geometry.Rect { return c.clipBounds } -func (c *clipStampCanvas) ReplayScene(*scene.Scene) {} +func (c *clipStampCanvas) ReplayScene(_ widget.SceneCache) {} // --- BoundaryRecorder interface --- func (c *clipStampCanvas) IsBoundaryRecording() bool { return c.isBoundary } diff --git a/widget/draw_test.go b/widget/draw_test.go index f686857..af81985 100644 --- a/widget/draw_test.go +++ b/widget/draw_test.go @@ -376,7 +376,7 @@ func (c *noopCanvas) PopTransform() {} func (c *noopCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *noopCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *noopCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 10000, 10000) } -func (c *noopCanvas) ReplayScene(_ *scene.Scene) {} +func (c *noopCanvas) ReplayScene(_ SceneCache) {} var _ Canvas = (*noopCanvas)(nil) @@ -825,6 +825,8 @@ func (w *animatingWidget) Children() []Widget { return nil } // boundary widget (spinner) remains dirty after drawBoundaryWidget completes. // Without this, spinner freezes after first frame (cache hit forever). func TestAnimatingBoundary_ReDirtiesSelfDuringDraw(t *testing.T) { + RegisterSceneFactory(func() SceneCache { return scene.NewScene() }) + defer RegisterSceneFactory(nil) RegisterSceneRecorder(stubSceneRecorder) defer RegisterSceneRecorder(nil) @@ -876,6 +878,8 @@ func TestAnimatingBoundary_ReDirtiesSelfDuringDraw(t *testing.T) { // widget's SetNeedsRedraw during Draw does NOT propagate to parent boundary. // Parent boundary must stay clean — only the animated widget re-records. func TestAnimatingBoundary_DoesNotDirtyParent(t *testing.T) { + RegisterSceneFactory(func() SceneCache { return scene.NewScene() }) + defer RegisterSceneFactory(nil) RegisterSceneRecorder(stubSceneRecorder) defer RegisterSceneRecorder(nil) @@ -920,6 +924,8 @@ func TestAnimatingBoundary_DoesNotDirtyParent(t *testing.T) { // a boundary and a child IS also a boundary, DrawTree reaches the child. // This is the gallery scenario: root boundary + spinner boundary. func TestDrawTree_RootBoundary_ChildBoundaryReached(t *testing.T) { + RegisterSceneFactory(func() SceneCache { return scene.NewScene() }) + defer RegisterSceneFactory(nil) RegisterSceneRecorder(stubSceneRecorder) defer RegisterSceneRecorder(nil) @@ -975,7 +981,7 @@ func TestDrawTree_RootBoundary_ChildBoundaryReached(t *testing.T) { // --- Test helpers for boundary draw --- // stubSceneRecorder creates a minimal scene recording canvas for tests. -func stubSceneRecorder(s *scene.Scene, _, _ int) (Canvas, func()) { +func stubSceneRecorder(_ SceneCache, _, _ int) (Canvas, func()) { return &stubReplayCanvas{}, func() {} } @@ -1006,7 +1012,7 @@ func (c *stubReplayCanvas) PopTransform() {} func (c *stubReplayCanvas) TransformOffset() geometry.Point { return geometry.Point{} } func (c *stubReplayCanvas) ScreenOriginBase() geometry.Point { return geometry.Point{} } func (c *stubReplayCanvas) ClipBounds() geometry.Rect { return geometry.NewRect(0, 0, 9999, 9999) } -func (c *stubReplayCanvas) ReplayScene(s *scene.Scene) { c.replayCount++ } +func (c *stubReplayCanvas) ReplayScene(s SceneCache) { c.replayCount++ } var _ Canvas = (*stubReplayCanvas)(nil) diff --git a/widget/scene_cache.go b/widget/scene_cache.go new file mode 100644 index 0000000..ee48cc9 --- /dev/null +++ b/widget/scene_cache.go @@ -0,0 +1,34 @@ +package widget + +// SceneCache is an opaque retained-mode display list for RepaintBoundary caching. +// The concrete implementation is *scene.Scene from gg, but widget code treats it +// as a black box — storing, passing, resetting, and querying emptiness only. +type SceneCache interface { + Reset() + IsEmpty() bool +} + +// SceneFactory creates new SceneCache instances for RepaintBoundary recording. +// Registered by the rendering layer (app package) at initialization. +type SceneFactory func() SceneCache + +// defaultSceneFactory holds the registered SceneFactory. +// Set by the app layer during initialization via RegisterSceneFactory. +var defaultSceneFactory SceneFactory + +// RegisterSceneFactory registers the factory function for creating SceneCache +// instances. This must be called by the app layer before any boundary draws +// occur (typically in package init or Window creation). +func RegisterSceneFactory(factory SceneFactory) { + defaultSceneFactory = factory +} + +// NewSceneCache creates a new SceneCache via the registered factory. +// Panics if no factory has been registered — the app layer MUST register +// a factory before the first frame. +func NewSceneCache() SceneCache { + if defaultSceneFactory == nil { + panic("widget: no SceneFactory registered — app layer must call widget.RegisterSceneFactory before first frame") + } + return defaultSceneFactory() +}