app/window.go
Functions
func AddDirtyBoundary
func (w *Window) AddDirtyBoundary(key uint64) {
if w.dirtyBoundaries == nil {
w.dirtyBoundaries = make(map[uint64]dirtyBoundaryEntry)
}
w.dirtyBoundaries[key] = dirtyBoundaryEntry{present: true}
}
func ClearAfterPaint
ClearAfterPaint clears dirty flags and frame state after a paint pass.
Called by the compositor path in desktop.draw after PaintBoundaryLayers
and overlay drawing are complete.
Flutter equivalent: flags are cleared at the end of flushPaint and
after compositeFrame. We consolidate cleanup into one call.
func (w *Window) ClearAfterPaint() {
// Do NOT call ClearRedrawInTree here. The paint pass (recordBoundary)
// clears dirty flags BEFORE each boundary's Draw, so widgets that
// re-dirty during Draw (spinner animation) keep their needsRedraw=true.
// ClearRedrawInTree here would erase that re-dirty → spinner not found
// by CollectDirtyRegions next frame → cyan overlay empty.
w.needsRedraw = false
w.needsFullRepaint = false
}
func ClearAnimationFrame
ClearAnimationFrame resets the animation frame flag. Called by
desktop.draw AFTER the frame skip check passes, ensuring the
flag is consumed exactly once per frame.
func (w *Window) ClearAnimationFrame() {
w.needsAnimationFrame = false
}
func ClearDirtyBoundaries
ClearDirtyBoundaries resets the dirty boundary set after painting.
Each boundary's ClearBoundaryDirty is NOT called here — that is the
responsibility of the PaintDirtyBoundaries method.
func (w *Window) ClearDirtyBoundaries() {
// Clear map efficiently: delete all entries but keep the allocated map.
for k := range w.dirtyBoundaries {
delete(w.dirtyBoundaries, k)
}
}
func ClearOverlayRedraw
ClearOverlayRedraw clears NeedsRedraw on all overlay widgets after
drawing with damage tracking enabled.
func (w *Window) ClearOverlayRedraw() {
if w.overlays == nil {
return
}
for _, o := range w.overlays.List() {
widget.ClearRedrawInTree(o)
}
}
func CollectDirtyRegions
CollectDirtyRegions runs the dirty collector on the widget tree to populate
the dirty tracker. Called by the compositor path in desktop.draw before
painting, so debug overlays and damage rects have correct data.
In the DrawTo path, this is called internally at the start of DrawTo.
The compositor path must call it explicitly since it bypasses DrawTo.
func (w *Window) CollectDirtyRegions() {
if w.root == nil {
return
}
w.dirtyTracker.Reset()
w.dirtyCollector.Collect(w.root)
// ADR-029: also collect dirty regions from overlay content widgets.
// Overlays are NOT in the root tree — without this, dirty overlay
// widgets (hover on dropdown menu items) are invisible to both
// cyan debug overlay (GOGPU_DEBUG_DIRTY) and green debug overlay
// (GOGPU_DEBUG_DAMAGE via TrackDamageRect from prePaintDirtyRegions).
// Use OverlayContentWidgets (not overlays.List) to reach the actual
// content widgets directly, bypassing Container which may not expose
// children through the standard Children() interface.
for _, cw := range w.OverlayContentWidgets() {
w.dirtyCollector.Collect(cw)
}
w.dirtyTracker.Optimize()
}
func Context
Context returns the widget context used for layout, draw, and events.
func (w *Window) Context() *widget.ContextImpl {
return w.ctx
}
func DirtyBoundaryCount
DirtyBoundaryCount returns the number of dirty RepaintBoundary instances.
func (w *Window) DirtyBoundaryCount() int {
return len(w.dirtyBoundaries)
}
func DirtyOverlayContentRects
DirtyOverlayContentRects returns the screen bounds of overlay CONTENT widgets
(not the full-window Container backdrop) that have NeedsRedraw=true.
This enables granular damage tracking for overlays. Without this, the Container
backdrop (full-window scrim for modal overlays) registers the entire window as
damage, causing GOGPU_DEBUG_DAMAGE green overlay on the full screen.
ADR-029: retained-mode overlays. Flutter pattern: ModalBarrier is event-only
(no draw contribution to damage), overlay content is in its own RepaintBoundary.
Our equivalent: suppress damage for Container backdrop, track only content rects.
For overlay types that implement ContentProvider (Container), the content widget's
bounds are returned. For other overlay types, the overlay's own bounds are used.
func (w *Window) DirtyOverlayContentRects() []geometry.Rect {
if w.overlays == nil || w.overlays.Len() == 0 {
return nil
}
var rects []geometry.Rect
for _, o := range w.overlays.List() {
if !widget.NeedsRedrawInTree(o) {
continue
}
// Try to get the content widget's bounds (Container pattern).
// Content bounds are tighter than Container bounds (full window).
type contentProvider interface {
Content() widget.Widget
}
if cp, ok := o.(contentProvider); ok {
content := cp.Content()
if content != nil {
type bounder interface{ Bounds() geometry.Rect }
if b, ok2 := content.(bounder); ok2 {
rects = append(rects, b.Bounds())
continue
}
}
}
// Fallback: use overlay's own bounds (non-Container overlays).
type bounder interface{ Bounds() geometry.Rect }
if b, ok := o.(bounder); ok {
rects = append(rects, b.Bounds())
}
}
return rects
}
func DirtyRegionCount
DirtyRegionCount returns the number of dirty regions from the most recent
DrawTo call. This reflects the state after Collector.Collect + Tracker.Optimize
in the last frame.
Returns 0 when the last frame was a full repaint (no incremental regions)
or when the frame was skipped (nothing dirty).
This is an observability hook for monitoring incremental rendering efficiency.
func (w *Window) DirtyRegionCount() int {
return w.dirtyTracker.RegionCount()
}
func DirtyRegions
DirtyRegions returns the list of dirty widget regions from the most
recent DrawTo call. Each region corresponds to a widget (or group of
nearby widgets) that needed redraw.
Used by desktop.Run for debug overlay (GOGPU_DEBUG_DIRTY=1) and for
passing damage rects to the OS compositor (SetDamageRects).
func (w *Window) DirtyRegions() []geometry.Rect {
regions := w.dirtyTracker.DirtyRegions()
rects := make([]geometry.Rect, len(regions))
for i, r := range regions {
rects[i] = r.Bounds
}
return rects
}
func DrawOverlayScrim
DrawOverlayScrim draws only the modal backdrop scrim for overlay Containers.
Non-modal overlays have no scrim. This is the minimal immediate-mode part
that remains after overlay content moves to the boundary pipeline.
Flutter equivalent: ModalBarrier.build() draws a full-screen gesture detector
with optional color. The barrier is event-only in damage terms — no paint
contribution. Our scrim draws a semi-transparent rect for visual feedback.
func (w *Window) DrawOverlayScrim(canvas widget.Canvas) {
if w.overlays == nil || w.overlays.Len() == 0 || canvas == nil {
return
}
for _, o := range w.overlays.List() {
type modalChecker interface {
Modal() bool
Bounds() geometry.Rect
}
mc, ok := o.(modalChecker)
if !ok || !mc.Modal() {
continue
}
scrim := widget.RGBA(0, 0, 0, 0.32)
canvas.DrawRect(mc.Bounds(), scrim)
}
}
func DrawOverlays
DrawOverlays draws overlay widgets (dropdowns, dialogs) on the given canvas.
In Flutter, overlays are part of the same widget tree. In our architecture,
they are managed separately by overlay.Stack and drawn after the main scene.
func (w *Window) DrawOverlays(canvas widget.Canvas) {
w.overlays.Draw(w.ctx, canvas)
}
func DrawTo
DrawTo performs the draw pass using the provided canvas.
Behavior depends on the [RenderMode]:
In RenderModeHostManaged (default):
- The host draws background before calling DrawTo.
- DrawTo does NOT call canvas.Clear (host already painted background).
- DrawTo always draws the full widget tree and returns true.
- dirty.Tracker is populated for RepaintBoundary Intersects fast path.
In RenderModeFrameworkManaged:
- Level 1 (Frame Skip): If nothing changed since the last draw,
returns false immediately — zero CPU, zero GPU upload.
- Level 2 (Dirty Region Redraw): Only dirty regions are redrawn on
the persistent pixmap. Clean regions retain previous pixels
(Qt QBackingStore pattern).
- Full Repaint: On first frame, resize, theme change, or SetRoot,
the entire pixmap is cleared and redrawn.
Returns true if rendering was performed, false if the draw was skipped.
func (w *Window) DrawTo(canvas widget.Canvas) bool {
if w.root == nil || canvas == nil {
return false
}
// Collect dirty regions (always — for RepaintBoundary Intersects fast path).
w.dirtyTracker.Reset()
w.dirtyCollector.Collect(w.root)
w.dirtyTracker.Optimize()
var drawn bool
switch w.renderMode {
case RenderModeFrameworkManaged:
drawn = w.drawFrameworkManaged(canvas)
default:
drawn = w.drawHostManaged(canvas)
}
if drawn {
ui.Logger().LogAttrs(context.Background(), slog.LevelDebug,
"DrawTo: rendered",
slog.Int("dirty", w.lastDrawStats.DirtyWidgets),
slog.Int("clean", w.lastDrawStats.CleanWidgets),
slog.Int("cached", w.lastDrawStats.CachedWidgets),
slog.Int("total", w.lastDrawStats.TotalWidgets),
slog.Int("dirtyRegions", w.dirtyTracker.RegionCount()),
slog.String("mode", w.renderMode.String()),
)
}
return drawn
}
func FocusManager
FocusManager returns the window's focus manager.
The focus manager handles Tab/Shift+Tab navigation between focusable
widgets and supports registering global keyboard shortcuts.
func (w *Window) FocusManager() *ifocus.Manager {
return w.focusMgr
}
func Frame
Frame performs one complete frame cycle.
The frame cycle consists of:
1. Update time tracking
2. Flush pending scheduler updates
3. Perform layout if needed
4. Draw the widget tree
5. Sync cursor state to platform
6. Clear invalidation flags
Frame is a no-op if there is no root widget.
func (w *Window) Frame() {
if w.root == nil {
return
}
frameStart := time.Now()
didLayout := w.needsLayout
// Begin frame timing. DeltaTime = time since last BeginFrame.
w.ctx.BeginFrame(frameStart)
// Cursor is managed in Event handlers (updateHover resets on target
// change, widgets set Pointer in MouseEnter/Default in MouseLeave).
// No ResetCursor here — Frame runs after Event and would overwrite
// the cursor set by the widget, causing flash on next syncCursor.
// Flush pending signal changes (may trigger new dirty marks).
// The scheduler's flushFn sets persistent needsRedraw flags on widgets.
const maxReflushes = 2
for i := 0; i <= maxReflushes; i++ {
w.scheduler.Flush()
if w.scheduler.PendingCount() == 0 {
break
}
}
// Update scale factor (may change between frames on multi-monitor setups).
w.updateScale()
// Update window size from provider.
w.updateWindowSize()
// Perform layout if needed.
// Layout changes always require a redraw since widget positions may shift.
var layoutDur time.Duration
if w.needsLayout {
ui.Logger().Info("[LAYOUT-TRIGGER]")
layoutStart := time.Now()
w.layout()
layoutDur = time.Since(layoutStart)
// Clear needsLayout only if no widget re-invalidated during layout.
// Animations call ctx.Invalidate() from tickAnimation() during layout,
// which sets needsLayout back to true — we must not clobber that.
if !w.ctx.IsInvalidated() {
w.needsLayout = false
}
// Layout completed — widgets with changed positions need redraw.
// ADR-028: do NOT MarkRedrawInTree(root) — that marks ALL widgets
// dirty → full screen repaint. Only widgets that actually changed
// should be dirty (they called SetNeedsRedraw during layout).
w.needsRedraw = true
}
// ADR-028 Phase C: O(1) dirty check. The needsRedraw flag is set by
// onInvalidate, onInvalidateRect, and scheduler.SetOnDirty callbacks.
// Boundary widgets populate dirtyBoundaries via RegisterDirtyBoundary.
// No O(n) NeedsRedrawInTreeNonBoundary tree walk needed.
// Draw the widget tree.
// In hosted mode (wp != nil), DrawTo() is called later by the host
// application with a real canvas — so we must NOT clear redraw flags here.
// In headless mode (wp == nil), there is no DrawTo() call, so we collect
// stats and clear flags ourselves.
drawStart := time.Now()
drawSkipped := !w.needsRedraw
if w.needsRedraw && w.wp == nil {
w.draw()
w.needsRedraw = false
}
drawDur := time.Since(drawStart)
// Sync cursor to platform.
w.syncCursor()
// Manage continuous rendering for animations.
// If a widget called Invalidate during this frame (e.g., animation tick),
// enter continuous (vsync) rendering mode via StartAnimation.
// When no more animations are active, stop continuous mode.
// Manage animation frame pumping.
// Start pumper when any animation is active (Invalidate from tickAnimation).
// Keep pumper running for a few extra frames to handle animation completion
// and prevent start/stop thrashing from periodic data updates.
if w.ctx.IsInvalidated() || !w.ctx.InvalidatedRect().IsEmpty() || w.needsAnimationFrame {
w.animIdleFrames = 0
if w.animToken == nil && w.wp != nil {
w.animToken = newAnimPumper(w.wp)
}
} else if w.animToken != nil {
w.animIdleFrames++
// Stop pumper after 3 consecutive idle frames (no Invalidate).
// This handles the case where data sim triggers periodic Invalidate
// but no animation is running.
if w.animIdleFrames > 3 {
w.animToken.Stop()
w.animToken = nil
w.animIdleFrames = 0
}
}
// Clear invalidation state.
w.ctx.ClearInvalidation()
// Report frame statistics if callback is set.
if w.frameCallback != nil {
w.frameCallback(FrameStats{
FrameStart: frameStart,
LayoutDuration: layoutDur,
DrawDuration: drawDur,
TotalDuration: time.Since(frameStart),
LayoutPerformed: didLayout,
DrawSkipped: drawSkipped,
DrawStats: w.lastDrawStats,
})
}
}
func HandleEvent
HandleEvent dispatches a single event to the widget tree.
Events are first offered to the overlay stack (top overlay has priority).
If no overlay consumes the event (and no modal overlay blocks it),
key events are offered to the focus manager for Tab/Shift+Tab navigation
and registered shortcuts. Finally, unconsumed events are propagated to
the root widget.
func (w *Window) HandleEvent(e event.Event) {
if w.root == nil || e == nil {
return
}
// Update context time for event processing.
w.ctx.SetNow(time.Now())
// Overlays get priority.
if w.overlays.HandleEvent(w.ctx, e) {
return
}
// Let focus manager intercept Tab/Shift+Tab and shortcuts.
if ke, ok := e.(*event.KeyEvent); ok {
w.syncContextFocusToManager()
if w.focusMgr.HandleKeyEvent(ke) {
w.syncManagerFocusToContext()
return
}
}
// ADR-031: Pointer capture — deliver mouse events directly to the
// captured widget, bypassing tree hit-testing. This enables drag
// operations (slider, splitview, scrollbar, text selection) to work
// when the mouse moves outside the widget's bounds.
// Auto-releases on MouseRelease when all buttons are up.
if me, ok := e.(*event.MouseEvent); ok && w.capturedWidget != nil {
switch me.MouseType {
case event.MouseMove, event.MouseRelease, event.MousePress:
consumed := w.capturedWidget.Event(w.ctx, me)
// Auto-release when all mouse buttons are up (drag ended).
if me.MouseType == event.MouseRelease && me.Buttons == 0 {
w.capturedWidget = nil
}
// Track mouse button state even during capture.
w.mouseButtonsHeld = me.Buttons
if consumed {
return
}
}
}
// Track widget-level hover for MouseEnter/MouseLeave dispatch.
// Skip hover updates during drag (button pressed) to prevent cursor
// from resetting when dragging over other widgets (e.g., SplitView divider).
if me, ok := e.(*event.MouseEvent); ok {
switch me.MouseType {
case event.MouseMove:
if me.Buttons == 0 {
w.updateHover(me.Position, me.Buttons, me.Modifiers())
}
case event.MouseLeave:
// Mouse left the window entirely — clear hover state.
// The window-level leave event still propagates to the root below.
w.clearHover(me.Buttons, me.Modifiers())
}
}
// Track mouse button state for drag cursor protection.
if me, ok := e.(*event.MouseEvent); ok {
w.mouseButtonsHeld = me.Buttons
}
// Dispatch event to root widget.
_ = w.root.Event(w.ctx, e)
// Sync cursor immediately after event dispatch so hover cursor
// changes are visible without waiting for the next Frame() tick.
if w.pp != nil {
w.syncCursor()
}
// After widget tree processes a mouse press, a widget may have called
// ctx.RequestFocus. Sync that to the focus manager so subsequent
// Tab navigation starts from the correct position.
if me, ok := e.(*event.MouseEvent); ok && me.MouseType == event.MousePress {
w.syncContextFocusToManager()
}
}
func HandleFocusChange
HandleFocusChange processes a window focus change.
func (w *Window) HandleFocusChange(focused bool) {
if w.root == nil {
return
}
var focusType event.FocusEventType
if focused {
focusType = event.FocusGained
} else {
focusType = event.FocusLost
}
e := event.NewFocusEvent(focusType)
_ = w.root.Event(w.ctx, e)
// Request redraw so widgets can update visual state (e.g. titlebar
// active/inactive appearance, focus rings).
w.needsRedraw = true
if w.wp != nil {
w.wp.RequestRedraw()
}
}
func HandleResize
HandleResize processes a window resize.
This updates the window size and marks layout as needing recalculation.
func (w *Window) HandleResize(width, height int) {
w.windowSize = geometry.Sz(float32(width), float32(height))
w.needsLayout = true
w.needsRedraw = true
w.needsFullRepaint = true
if w.root != nil {
widget.MarkRedrawInTree(w.root)
}
}
func HasDirtyBoundaries
HasDirtyBoundaries reports whether any RepaintBoundary has been marked
dirty since the last paint pass.
func (w *Window) HasDirtyBoundaries() bool {
return len(w.dirtyBoundaries) > 0
}
func HasDirtyBoundariesOrNeedsRedraw
HasDirtyBoundariesOrNeedsRedraw reports whether any rendering work is
needed: either dirty boundaries from upward propagation or full-frame
redraw flags (needsRedraw, needsFullRepaint).
func (w *Window) HasDirtyBoundariesOrNeedsRedraw() bool {
return w.HasDirtyBoundaries() || w.needsRedraw || w.needsFullRepaint
}
func HasDirtyOverlays
HasDirtyOverlays reports whether any overlay widget has NeedsRedraw=true.
Used to selectively enable damage tracking during DrawOverlays — unchanged
overlays suppress tracking (avoid permanent green debug overlay), while
changed overlays (hover) enable tracking for correct green flash.
func (w *Window) HasDirtyOverlays() bool {
if w.overlays == nil || w.overlays.Len() == 0 {
return false
}
for _, o := range w.overlays.List() {
if widget.NeedsRedrawInTree(o) {
return true
}
}
return false
}
func HasOverlays
HasOverlays reports whether any overlays (dropdowns, dialogs) are active.
func (w *Window) HasOverlays() bool {
return w.overlays != nil && w.overlays.Len() > 0
}
func HoveredWidget
HoveredWidget returns the widget currently under the mouse pointer,
or nil if no widget is hovered.
func (w *Window) HoveredWidget() widget.Widget {
return w.hoveredWidget
}
func LastDirtyUnion
LastDirtyUnion returns the union of all dirty regions from the most
recent dirty-region-only repaint. Returns a zero Rect when the last
frame was a full repaint or a frame skip.
Used by desktop.Run to pass the dirty region to ggcanvas for partial
texture upload — only the modified region is uploaded to the GPU
instead of the entire pixmap.
func (w *Window) LastDirtyUnion() geometry.Rect {
return w.lastDirtyUnion
}
func LastDrawStats
LastDrawStats returns the per-widget statistics from the most recent
draw traversal (either via [Window.DrawTo] or the headless draw path
inside [Window.Frame]).
When the last frame was skipped (no dirty widgets), the returned stats
are zero-valued.
func (w *Window) LastDrawStats() widget.DrawStats {
return w.lastDrawStats
}
func NeedsAnimationFrame
NeedsAnimationFrame reports whether an animated boundary requested
a frame via ScheduleAnimationFrame. This flag persists across
ClearAfterPaint (unlike needsRedraw) to prevent frame skip from
dropping animation frames. Flutter equivalent: _hasScheduledFrame.
func (w *Window) NeedsAnimationFrame() bool {
return w.needsAnimationFrame
}
func NeedsLayout
NeedsLayout returns true if layout needs recalculation.
func (w *Window) NeedsLayout() bool {
return w.needsLayout
}
func NeedsRedraw
NeedsRedraw reports whether the window-level redraw flag is set.
This is an O(1) check — the flag is set by onInvalidate, onInvalidateRect,
and scheduler.SetOnDirty callbacks. No tree walk is performed.
ADR-028 Phase C: removed O(n) NeedsRedrawInTreeNonBoundary fallback.
All dirty propagation paths now set this flag or populate dirtyBoundaries.
func (w *Window) NeedsRedraw() bool {
return w.needsRedraw
}
func OverlayContentWidgets
OverlayContentWidgets returns the content widgets from all active overlays.
For Container overlays, this returns the inner content widget (which is
marked as RepaintBoundary by PushOverlay). For non-Container overlays,
the overlay itself is returned.
Used by the compositor pipeline to include overlay boundaries in the
Layer Tree alongside the main widget tree boundaries.
func (w *Window) OverlayContentWidgets() []widget.Widget {
if w.overlays == nil || w.overlays.Len() == 0 {
return nil
}
result := make([]widget.Widget, 0, w.overlays.Len())
for _, o := range w.overlays.List() {
type contentProvider interface {
Content() widget.Widget
}
if cp, ok := o.(contentProvider); ok {
content := cp.Content()
if content != nil {
result = append(result, content)
continue
}
}
// Fallback: non-Container overlay is its own widget.
result = append(result, o)
}
return result
}
func OverlayCount
OverlayCount returns the number of active overlays.
func (w *Window) OverlayCount() int {
if w.overlays == nil {
return 0
}
return w.overlays.Len()
}
func Overlays
Overlays returns the window's overlay stack.
func (w *Window) Overlays() *overlay.Stack {
return w.overlays
}
func PaintDirtyBoundaries
PaintDirtyBoundaries clears the dirty boundary set after a frame.
RepaintBoundary.Draw() handles cache invalidation internally: when
boundaryDirty is true, it re-records child.Draw() into its scene.Scene.
The full DrawTree pass triggers re-recording automatically — dirty
boundaries re-record, clean ones replay cached scenes via ReplayScene.
This is the Flutter flushPaint pattern: only dirty RepaintBoundary nodes
re-record, clean ones replay cached scenes.
func (w *Window) PaintDirtyBoundaries() {
w.ClearDirtyBoundaries()
}
func PopOverlay
PopOverlay removes the topmost overlay.
func (m *windowOverlayManager) PopOverlay() {
m.window.overlays.Pop()
}
func PushOverlay
PushOverlay wraps the widget in an overlay.Container and pushes it.
The content widget is promoted to a RepaintBoundary via SetRepaintBoundary(true)
(ADR-024 WidgetBase property). No wrapper widget created — the content widget
itself becomes the boundary. Clean overlays = texture blit (zero re-render).
Dirty overlays (hover) = re-render only content texture.
func (m *windowOverlayManager) PushOverlay(w widget.Widget, onDismiss func()) {
// ADR-024 + ADR-029: promote content to RepaintBoundary for damage isolation.
type boundarySetter interface{ SetRepaintBoundary(bool) }
if bs, ok := w.(boundarySetter); ok {
bs.SetRepaintBoundary(true)
}
container := overlay.NewContainer(w, m.window.windowSize,
overlay.WithOnDismiss(func() {
if onDismiss != nil {
onDismiss()
}
}),
)
m.window.overlays.Push(container)
}
func RemoveOverlay
RemoveOverlay finds and removes the overlay containing the given widget.
func (m *windowOverlayManager) RemoveOverlay(w widget.Widget) {
for _, o := range m.window.overlays.List() {
if c, ok := o.(*overlay.Container); ok {
if c.Content() == w {
m.window.overlays.Remove(o)
return
}
}
}
}
func RenderMode
RenderMode returns the window's current rendering mode.
func (w *Window) RenderMode() RenderMode {
return w.renderMode
}
func Root
Root returns the current root widget, or nil if none is set.
func (w *Window) Root() widget.Widget {
return w.root
}
func SetRenderMode
SetRenderMode changes the window's rendering mode at runtime.
Switching to [RenderModeFrameworkManaged] enables frame skip and
dirty-region rendering. Switching to [RenderModeHostManaged] restores
full-tree draw every frame.
A full repaint is forced after the switch to ensure the persistent
pixmap is populated correctly in the new mode.
func (w *Window) SetRenderMode(mode RenderMode) {
w.renderMode = mode
w.needsFullRepaint = true
if w.root != nil {
widget.MarkRedrawInTree(w.root)
}
}
func SetRoot
SetRoot sets the root widget for this window.
Setting a new root triggers a full layout on the next Frame call.
The old root tree is unmounted and the new root tree is mounted,
which triggers signal binding setup/teardown via [widget.Lifecycle].
func (w *Window) SetRoot(root widget.Widget) {
// Unmount old tree (triggers RepaintBoundary.Unmount which invalidates
// individual cache entries from the shared ImageCache).
if w.root != nil {
widget.UnmountTree(w.root)
}
// Clear the entire image cache since the old widget tree is gone and
// the new tree will have different boundaries with different keys.
if w.imageCache != nil {
w.imageCache.InvalidateAll()
}
w.root = root
// ADR-007 Phase 5: Root IS boundary (Flutter RenderView.isRepaintBoundary).
// DrawChild skips child boundaries during recording (BoundaryRecorder).
// Compositor Layer Tree assembles all boundary scenes by reference.
type boundaryEnabler interface {
SetRepaintBoundary(bool)
}
if be, ok := root.(boundaryEnabler); ok {
be.SetRepaintBoundary(true)
}
w.needsLayout = true
w.needsRedraw = true
w.needsFullRepaint = true
// Clear references to old tree widgets to prevent pinning the
// entire unmounted tree in memory via parent/children links.
w.hoveredWidget = nil
w.capturedWidget = nil
// Clear references to old tree widgets to prevent pinning the
// entire unmounted tree in memory via parent/children links.
w.hoveredWidget = nil
w.capturedWidget = nil
// Update focus manager with new root so Tab navigation
// traverses the correct widget tree.
w.focusMgr.SetRoot(root)
// Mount new tree and mark all widgets as needing redraw.
if w.root != nil {
widget.MountTree(w.root, w.ctx)
widget.MarkRedrawInTree(w.root)
}
}
func Stop
func (p *animPumper) Stop() {
select {
case p.stop <- struct{}{}:
default:
}
}
func Theme
Theme returns the window's current theme.
func (w *Window) Theme() *theme.Theme {
return w.theme
}
func ThemeBackground
ThemeBackground returns the window background color from the current theme.
Falls back to white if no theme is configured.
func (w *Window) ThemeBackground() widget.Color {
if w.theme != nil {
return w.theme.Colors.Background
}
return widget.ColorWhite
}
func WasFullRepaint
WasFullRepaint returns true if the most recent DrawTo performed a full
repaint (first frame, resize, theme change, SetRoot). When true, the
entire pixmap was modified and needs full GPU upload.
func (w *Window) WasFullRepaint() bool {
return w.lastWasFullRepaint
}
func WindowSize
WindowSize returns the current window size in logical pixels.
func (w *Window) WindowSize() geometry.Size {
return w.windowSize
}
Structs
type Window struct
type Window struct {
root widget.Widget
ctx *widget.ContextImpl
wp gpucontext.WindowProvider
pp gpucontext.PlatformProvider
scheduler *state.Scheduler
theme *theme.Theme
overlays *overlay.Stack
focusMgr *ifocus.Manager
// renderMode controls background ownership and incremental rendering.
// See RenderMode documentation for details.
renderMode RenderMode
// animToken is non-nil while continuous rendering is active for animations.
animToken animationStopper
// animIdleFrames counts consecutive frames with no Invalidate call.
// Used to stop the animation pumper after animations complete.
animIdleFrames int
// needsAnimationFrame is set by ScheduleAnimationFrame (during Draw)
// and persists across ClearAfterPaint. Checked by desktop.draw frame
// skip to ensure animated boundary frames are not skipped.
// Flutter equivalent: _hasScheduledFrame.
needsAnimationFrame bool
// needsLayout indicates that layout should be recalculated.
needsLayout bool
// needsRedraw indicates that the draw pass should be performed.
// This is set when any widget in the tree needs re-rendering,
// and cleared after a successful draw. When false, DrawTo can
// skip rendering entirely because the previous frame's output
// is still valid in the GPU framebuffer.
needsRedraw bool
// needsFullRepaint forces a complete redraw of the entire widget tree
// on the next DrawTo call. Set on first frame, resize, theme change,
// and SetRoot. When true, DrawTo clears the entire pixmap and draws
// all widgets unconditionally instead of using incremental dirty regions.
needsFullRepaint bool
// dirtyTracker collects and merges dirty regions for incremental redraw.
// Populated by dirtyCollector before each DrawTo call.
dirtyTracker *dirty.Tracker
// dirtyCollector walks the widget tree to find dirty widgets and records
// their bounds in dirtyTracker.
dirtyCollector *dirty.Collector
// lastDrawStats holds per-widget statistics from the most recent
// draw traversal. Updated by DrawTo() and the headless draw() path.
lastDrawStats widget.DrawStats
// lastDirtyUnion is the union of all dirty regions from the most recent
// dirty-region-only repaint. Zero when the last frame was a full repaint
// or frame skip. Used by desktop.Run to pass dirty region to ggcanvas
// for partial texture upload.
lastDirtyUnion geometry.Rect
// lastWasFullRepaint indicates that the most recent DrawTo performed a
// full repaint (first frame, resize, theme change). When true, the
// entire pixmap was modified and needs full GPU upload.
lastWasFullRepaint bool
// hoveredWidget tracks the widget currently under the mouse pointer.
// Used for sending MouseEnter/MouseLeave events to individual widgets
// as the mouse moves across the widget tree.
hoveredWidget widget.Widget
// capturedWidget receives all mouse events directly during drag
// operations, bypassing tree hit-testing (ADR-031 PointerCapturer).
// Set by CapturePointer, cleared by ReleasePointer or auto-released
// on MouseRelease when all buttons are up.
// Enterprise references: Win32 SetCapture, Qt grabMouse.
capturedWidget widget.Widget
// mouseButtonsHeld tracks pressed mouse buttons to prevent cursor
// reset during drag operations (Frame.ResetCursor skipped while dragging).
mouseButtonsHeld event.ButtonState
// windowSize tracks the last known window size in physical pixels.
windowSize geometry.Size
// frameCallback, if set, is called after each frame with statistics.
frameCallback FrameCallback
// imageCache is the centralized LRU cache for RepaintBoundary pixel
// buffers. All RepaintBoundary instances in this window share this
// cache, which enforces a memory budget (default 64MB) and evicts
// least-recently-used entries. Phase 5, ADR-004.
imageCache *internalRender.ImageCache
// dirtyBoundaries collects RepaintBoundary instances marked dirty by
// upward propagation (ADR-007, Task 1e). Populated by the
// onBoundaryDirty callback wired during mount. Used by future Phase 2
// PaintDirtyBoundaries to repaint only changed boundaries.
dirtyBoundaries map[uint64]dirtyBoundaryEntry
}
AddDirtyBoundary registers a RepaintBoundary as dirty. Called by the
onBoundaryDirty callback during upward dirty propagation.
The key parameter is the boundary's unique cache key for deduplication.
If the boundary is already in the set, this is a no-op (O(1) guard).
This populates the flat dirty boundary set used by HasDirtyBoundaries
for O(1) frame skip decisions, replacing O(n) NeedsRedrawInTreeNonBoundary.