Skip to main content

agg_gui/
widget.rs

1//! Widget trait, tree traversal, and the top-level [`App`] struct.
2//!
3//! # Coordinate system
4//!
5//! Widget bounds are expressed in **parent-local** first-quadrant (Y-up)
6//! coordinates. A widget at `bounds.x = 10, bounds.y = 20` is drawn 10 units
7//! right and 20 units up from its parent's bottom-left corner.
8//!
9//! OS/browser mouse events arrive in Y-down screen coordinates. The single
10//! conversion `y_up = viewport_height - y_down` happens inside
11//! [`App::on_mouse_move`] / [`App::on_mouse_down`] / [`App::on_mouse_up`].
12//! All widget code sees Y-up coordinates only.
13//!
14//! # Tree traversal
15//!
16//! Paint: root → leaves (children painted on top of parents).
17//! Hit test: root → leaves (deepest child under cursor wins).
18//! Event dispatch: leaf → root (events bubble up; any widget can consume).
19
20use std::sync::Arc;
21
22use crate::draw_ctx::DrawCtx;
23use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
24use crate::framebuffer::Framebuffer;
25use crate::lcd_coverage::LcdBuffer;
26use crate::geometry::{Point, Rect, Size};
27use crate::gfx_ctx::GfxCtx;
28use crate::layout_props::{HAnchor, Insets, VAnchor};
29
30// ---------------------------------------------------------------------------
31// Widget backbuffer — CPU bitmap cache per widget, invalidated via a dirty flag.
32// ---------------------------------------------------------------------------
33//
34// Any widget can opt into a cached CPU backbuffer by returning `Some(&mut ...)`
35// from [`Widget::backbuffer_cache_mut`].  The framework's `paint_subtree`
36// handles caching transparently: when the widget is dirty (or has no bitmap
37// yet) it allocates a fresh `Framebuffer`, runs `widget.paint` + all children
38// into it via a software `GfxCtx`, and caches the resulting RGBA8 pixels as a
39// shared `Arc<Vec<u8>>`.  Every subsequent frame that finds the widget clean
40// just blits the cached pixels through `ctx.draw_image_rgba_arc` — zero AGG
41// cost in steady state.  On the GL backend the `Arc`'s pointer identity keys
42// the GPU texture cache (see `arc_texture_cache`), so the hardware texture
43// is also reused across frames and dropped when the bitmap drops.
44//
45// The pattern is the one MatterCAD / AggSharp use: every widget CAN be
46// backbuffered, each owns its bitmap, and a single `dirty` flag drives
47// re-rasterisation.
48//
49// LCD subpixel rendering works naturally inside a backbuffer: the widget
50// paints its own background first (so text has a solid dst) and then any
51// `fill_text` call composites the per-channel coverage mask onto that
52// destination.  No walk / sample / bg-declaration needed.
53
54/// How a widget's backbuffer stores pixels.
55///
56/// The choice controls what the framework allocates as the render
57/// target during `paint_subtree_backbuffered` and how the cached
58/// bitmap is composited back onto the parent.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum BackbufferMode {
61    /// 8-bit straight-alpha RGBA.  Standard Porter-Duff `SRC_ALPHA,
62    /// ONE_MINUS_SRC_ALPHA` composite on blit.  Works for any widget,
63    /// including ones with transparent areas.  Text inside is grayscale
64    /// AA (no LCD subpixel).
65    Rgba,
66    /// 3 bytes-per-pixel **composited opaque RGB** — no alpha channel.
67    /// Every fill (rects, strokes, text, etc.) inside the buffer goes
68    /// through the 3× horizontal supersample + 5-tap filter + per-channel
69    /// src-over pipeline described in `lcd-subpixel-compositing.md`.
70    /// The buffer is blitted as an opaque RGB texture.
71    ///
72    /// **Contract:** the widget is responsible for painting content
73    /// that covers its full bounds with opaque fills (starting with a
74    /// bg rect).  Uncovered pixels land as black on the parent because
75    /// there is no alpha channel to carry "no paint here."
76    LcdCoverage,
77}
78
79/// A CPU bitmap owned by a widget that opts into backbuffer caching.
80///
81/// Set `dirty = true` from the widget's setter methods whenever the widget's
82/// visual output could change (text, colour, bounds, hover/press state, …).
83/// The framework re-rasterises on the next paint and clears the flag.
84pub struct BackbufferCache {
85    /// In **Rgba** mode: top-row-first RGBA8 pixels, straight alpha.
86    /// Blitted via [`DrawCtx::draw_image_rgba_arc`].
87    ///
88    /// In **LcdCoverage** mode: top-row-first **colour plane** — 3
89    /// bytes/pixel (R_premult, G_premult, B_premult) matching the
90    /// convention of [`crate::lcd_coverage::LcdBuffer::color_plane`]
91    /// flipped to top-down.  The companion alpha plane lives in
92    /// [`Self::lcd_alpha`].
93    pub pixels: Option<Arc<Vec<u8>>>,
94    /// `LcdCoverage`-mode companion to `pixels`: top-row-first per-channel
95    /// **alpha plane** (3 bytes/pixel, `(R_alpha, G_alpha, B_alpha)`).
96    /// `None` means this is a plain Rgba cache.  When `Some`, the blit
97    /// step uses [`DrawCtx::draw_lcd_backbuffer_arc`] to preserve the
98    /// per-channel subpixel information through to the destination —
99    /// required for LCD chroma to survive the cache round-trip.
100    pub lcd_alpha: Option<Arc<Vec<u8>>>,
101    pub width:  u32,
102    pub height: u32,
103    /// When true, the next paint will re-rasterise rather than reusing
104    /// `pixels`.  Widgets set this from their mutation paths
105    /// (`set_text`, `set_color`, focus/hover changes, etc.) and the
106    /// framework clears it after a successful re-raster.
107    pub dirty:  bool,
108    /// Visuals epoch (see [`crate::theme::current_visuals_epoch`]) recorded
109    /// the last time this cache was populated.  `paint_subtree_backbuffered`
110    /// compares it against the live epoch and forces a re-raster on mismatch,
111    /// so widgets whose text/fill colours come from `ctx.visuals()` refresh
112    /// automatically on a dark/light theme flip without needing every widget
113    /// to subscribe to theme-change events.
114    pub theme_epoch: u64,
115    /// Typography epoch (see
116    /// [`crate::font_settings::current_typography_epoch`]) — same
117    /// pattern as `theme_epoch` but for font / size scale / LCD /
118    /// hinting / gamma / width / interval / faux-* globals.  Lets a
119    /// slider drag in the LCD Subpixel demo invalidate every cached
120    /// `Label` bitmap without bespoke hooks per widget.
121    pub typography_epoch: u64,
122}
123
124impl BackbufferCache {
125    pub fn new() -> Self {
126        Self {
127            pixels: None, lcd_alpha: None,
128            width: 0, height: 0, dirty: true,
129            theme_epoch: 0, typography_epoch: 0,
130        }
131    }
132
133    /// Mark the cache dirty so the next paint re-rasterises.
134    pub fn invalidate(&mut self) { self.dirty = true; }
135}
136
137impl Default for BackbufferCache {
138    fn default() -> Self { Self::new() }
139}
140
141// ---------------------------------------------------------------------------
142// Widget trait
143// ---------------------------------------------------------------------------
144
145/// Every visible element in the UI is a widget.
146///
147/// Implementors handle their own painting and event handling. The framework
148/// takes care of tree traversal, coordinate translation, and focus management.
149pub trait Widget {
150    /// Bounding rectangle in **parent-local** Y-up coordinates.
151    fn bounds(&self) -> Rect;
152
153    /// Set the bounding rectangle. Called by the parent during layout.
154    fn set_bounds(&mut self, bounds: Rect);
155
156    /// Immutable access to child widgets.
157    fn children(&self) -> &[Box<dyn Widget>];
158
159    /// Mutable access to child widgets (required for event dispatch + layout).
160    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>>;
161
162    /// Compute desired size given available space, and update internal layout.
163    ///
164    /// The parent passes the space it can offer; the widget returns the size it
165    /// actually wants to occupy. The parent uses the returned size to set this
166    /// widget's bounds before calling `layout` on the next sibling.
167    fn layout(&mut self, available: Size) -> Size;
168
169    /// Paint this widget's own content into `ctx`.
170    ///
171    /// The framework has already translated `ctx` so that `(0, 0)` is this
172    /// widget's bottom-left corner. **Do not paint children here** — the
173    /// framework recurses into them automatically after `paint` returns.
174    ///
175    /// `ctx` is a `&mut dyn DrawCtx`; the concrete type is either a software
176    /// `GfxCtx` (back-buffer path) or a `GlGfxCtx` (hardware GL path).
177    fn paint(&mut self, ctx: &mut dyn DrawCtx);
178
179    /// Return `true` if `local_pos` (in this widget's local coordinates) falls
180    /// inside this widget's interactive area. Default: axis-aligned rect test.
181    fn hit_test(&self, local_pos: Point) -> bool {
182        let b = self.bounds();
183        local_pos.x >= 0.0 && local_pos.x <= b.width
184            && local_pos.y >= 0.0 && local_pos.y <= b.height
185    }
186
187    /// When `true`, `hit_test_subtree` stops recursing into this widget's
188    /// children and returns this widget as the hit target.  Used for floating
189    /// overlays (e.g. a scrollbar painted above its content) that must claim
190    /// the pointer before children that happen to share the same pixels.
191    /// Default: `false`.
192    fn claims_pointer_exclusively(&self, _local_pos: Point) -> bool { false }
193
194    /// Handle an event. The event's positions are already in **local** Y-up
195    /// coordinates. Return [`EventResult::Consumed`] to stop bubbling.
196    fn on_event(&mut self, event: &Event) -> EventResult;
197
198    /// Whether this widget can receive keyboard focus. Default: false.
199    fn is_focusable(&self) -> bool {
200        false
201    }
202
203    /// A static name for this widget type, used by the inspector. Default: "Widget".
204    fn type_name(&self) -> &'static str {
205        "Widget"
206    }
207
208    /// Optional human-readable identifier for this widget instance.
209    ///
210    /// Distinct from [`type_name`] (which is per-type and constant):
211    /// `id` lets external code look up a specific *instance* — used
212    /// today by the demo's z-order persistence to match a saved title
213    /// against a live `Window` in the canvas `Stack`.  Default
214    /// implementation returns `None`; widgets that want to be
215    /// identifiable (e.g. `Window` returning its title) override.
216    fn id(&self) -> Option<&str> {
217        None
218    }
219
220    /// Return `false` to suppress painting this widget **and all its children**.
221    /// The widget's own `paint()` will not be called.  Default: `true`.
222    fn is_visible(&self) -> bool {
223        true
224    }
225
226    /// Return type-specific properties for the inspector properties pane.
227    ///
228    /// Each entry is `(name, display_value)`.  The default returns an empty
229    /// list; widgets override this to expose their state to the inspector.
230    fn properties(&self) -> Vec<(&'static str, String)> {
231        vec![]
232    }
233
234    /// Whether this widget renders into its own offscreen buffer before
235    /// compositing into the parent.
236    ///
237    /// When `true`, `paint_subtree` wraps the widget (and all its descendants)
238    /// in `ctx.push_layer` / `ctx.pop_layer`.  The widget and its children draw
239    /// into a fresh transparent framebuffer; when complete, the buffer is
240    /// SrcOver-composited back into the parent render target.  This enables
241    /// per-widget alpha compositing, caching, and isolation.
242    ///
243    /// Default: `false` (pass-through rendering).
244    fn has_backbuffer(&self) -> bool {
245        false
246    }
247
248    /// Opt into per-widget CPU bitmap caching with a dirty flag.
249    ///
250    /// Widgets that return `Some(&mut cache)` get their paint +
251    /// children cached as a `Vec<u8>` of RGBA8 pixels.  `paint_subtree`
252    /// re-rasterises via AGG only when `cache.dirty` is true; otherwise
253    /// it blits the existing bitmap.  GL backends key their texture
254    /// cache on the `Arc`'s pointer identity so the uploaded GPU
255    /// texture is also reused across frames.
256    ///
257    /// The widget is responsible for calling `cache.invalidate()` (or
258    /// setting `cache.dirty = true`) from any mutation that could
259    /// change the rendered output — text/color setters, focus/hover
260    /// state changes, layout size changes, etc.  The framework clears
261    /// the flag after a successful re-raster.
262    ///
263    /// Default: `None` (no caching — paint every frame directly).
264    fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
265        None
266    }
267
268    /// Storage format for this widget's backbuffer.  Ignored unless
269    /// [`backbuffer_cache_mut`] returns `Some`.  Default
270    /// [`BackbufferMode::Rgba`] — correct for any widget.
271    /// Opt into [`BackbufferMode::LcdCoverage`] only when the widget
272    /// paints opaque content covering its full bounds.
273    fn backbuffer_mode(&self) -> BackbufferMode {
274        BackbufferMode::Rgba
275    }
276
277    /// Whether the inspector should recurse into this widget's children.
278    ///
279    /// Returns `false` for widgets that are part of the inspector infrastructure
280    /// (e.g. the inspector's own `TreeView`) to prevent the inspector from
281    /// showing itself recursively, which would grow the node list every frame.
282    ///
283    /// The widget itself is still included in the inspector snapshot — only
284    /// its subtree is suppressed.
285    fn contributes_children_to_inspector(&self) -> bool {
286        true
287    }
288
289    /// Return `false` to hide this widget (and its subtree) from the inspector
290    /// node snapshot entirely.  Intended for zero-size utility widgets such
291    /// as layout-time watchers / tickers / invisible composers — they bloat
292    /// the inspector tree without providing user-relevant information and,
293    /// at scale, can make the inspector's per-frame tree rebuild expensive.
294    fn show_in_inspector(&self) -> bool { true }
295
296    /// Per-widget LCD subpixel preference for backbuffered text rendering.
297    ///
298    /// - `Some(true)`  — always raster text with LCD subpixel.
299    /// - `Some(false)` — always use grayscale AA.
300    /// - `None`        — defer to the global `font_settings::lcd_enabled()`.
301    ///
302    /// Only widgets that raster text into an offscreen backbuffer act on
303    /// this flag (today: `Label`).  Defaulting to `None` means every such
304    /// widget follows the global toggle unless the instance explicitly
305    /// opts in or out.
306    fn lcd_preference(&self) -> Option<bool> { None }
307
308    /// Paint decorations that must appear **on top of all children**.
309    ///
310    /// Called by [`paint_subtree`] after all children have been painted.
311    /// The default implementation is a no-op; override in widgets that need
312    /// to draw overlays (e.g. resize handles, drag previews) that must not
313    /// be occluded by child content.
314    fn paint_overlay(&mut self, _ctx: &mut dyn DrawCtx) {}
315
316    /// Return a clip rectangle (in local coordinates) that constrains all child
317    /// painting.  `paint_subtree` applies this clip before recursing into
318    /// children, then restores the previous clip state afterward.  The clip does
319    /// **not** affect `paint_overlay`, which runs after the clip is removed.
320    ///
321    /// The default clips children to this widget's own bounds, preventing
322    /// overflow.  Override to return a narrower rect (e.g. Window clips to the
323    /// content area below the title bar, or an empty rect when collapsed).
324    fn clip_children_rect(&self) -> Option<(f64, f64, f64, f64)> {
325        let b = self.bounds();
326        Some((0.0, 0.0, b.width, b.height))
327    }
328
329    // -------------------------------------------------------------------------
330    // Layout properties (universal — every widget carries these)
331    // -------------------------------------------------------------------------
332
333    /// Outer margin around this widget in logical units.
334    ///
335    /// The parent layout reads this to compute spacing and position.
336    /// Default: [`Insets::ZERO`].
337    fn margin(&self) -> Insets { Insets::ZERO }
338
339    /// Horizontal anchor: how this widget sizes/positions itself horizontally
340    /// within the slot the parent assigns.
341    /// Default: [`HAnchor::FIT`] (take natural content width).
342    fn h_anchor(&self) -> HAnchor { HAnchor::FIT }
343
344    /// Vertical anchor: how this widget sizes/positions itself vertically
345    /// within the slot the parent assigns.
346    /// Default: [`VAnchor::FIT`] (take natural content height).
347    fn v_anchor(&self) -> VAnchor { VAnchor::FIT }
348
349    /// Minimum size constraint (logical units).
350    ///
351    /// The parent will never assign a slot smaller than this.
352    /// Default: [`Size::ZERO`] (no minimum).
353    fn min_size(&self) -> Size { Size::ZERO }
354
355    /// Maximum size constraint (logical units).
356    ///
357    /// The parent will never assign a slot larger than this.
358    /// Default: [`Size::MAX`] (no maximum).
359    fn max_size(&self) -> Size { Size::MAX }
360
361    /// Whether [`paint_subtree`] should snap this widget's incoming
362    /// translation to the physical pixel grid.
363    ///
364    /// Defaults to the process-wide
365    /// [`pixel_bounds::default_enforce_integer_bounds`](crate::pixel_bounds::default_enforce_integer_bounds)
366    /// flag so the common case — crisp UI text + strokes — works without
367    /// ceremony.  Widgets with a [`WidgetBase`] should delegate to
368    /// `self.base().enforce_integer_bounds` so per-instance overrides take
369    /// effect; widgets that genuinely want sub-pixel positioning (smooth
370    /// scroll markers, zoomed canvases) override to return `false`.
371    ///
372    /// Mirrors MatterCAD's `GuiWidget.EnforceIntegerBounds` accessor.
373    fn enforce_integer_bounds(&self) -> bool {
374        crate::pixel_bounds::default_enforce_integer_bounds()
375    }
376
377    /// Container widgets (notably [`crate::widgets::Stack`]) call this on each
378    /// child at the start of `layout()`.  A widget that returns `true` is
379    /// moved to the END of its parent's child list — painted last, i.e.
380    /// raised to the top of the z-order.  `take_` semantics: the call is
381    /// also expected to **clear** the request so the child doesn't keep
382    /// getting raised every frame.
383    ///
384    /// Default: no raise ever requested.  `Window` overrides to fire on the
385    /// false→true visibility transition (see its `with_visible_cell`), so
386    /// toggling a demo checkbox on in the sidebar automatically pops that
387    /// window to the front.
388    fn take_raise_request(&mut self) -> bool { false }
389
390    // -------------------------------------------------------------------------
391    // Visibility-gated repaint propagation
392    // -------------------------------------------------------------------------
393    //
394    // The host render loop walks the widget tree from the root to decide
395    // whether a new frame is needed.  A widget with in-flight animation,
396    // pending hover transition, or scheduled cursor-blink reports `true` via
397    // [`needs_paint`] or a deadline via [`next_paint_deadline`].  Parents
398    // aggregate these over their **visible** children — invisible subtrees
399    // (collapsed Window, non-selected TabView tab, off-viewport content)
400    // must NOT contribute, so an animation inside a hidden part of the UI
401    // cannot cause the screen to redraw.  This is the tree-walk equivalent
402    // of a global "dirty" flag; going through the tree lets the framework
403    // honour the visibility contract without trusting every widget author
404    // to check it manually.
405
406    /// Return `true` if this widget, or any visible descendant, has state
407    /// that requires a repaint (hover change, tween in flight, etc.).
408    ///
409    /// The default walks visible children.  Widgets with their own pending
410    /// state OR that state with the default walk — see `WidgetBase` helpers.
411    fn needs_paint(&self) -> bool {
412        if !self.is_visible() { return false; }
413        self.children().iter().any(|c| c.needs_paint())
414    }
415
416    /// Return the earliest wall-clock instant at which this widget (or any
417    /// visible descendant) wants the next paint.  `None` = no scheduled wake.
418    /// The host loop turns a `Some(t)` into `ControlFlow::WaitUntil(t)` so
419    /// e.g. a cursor blink fires without continuous polling.
420    ///
421    /// Same visibility contract as [`needs_paint`]: hidden subtrees return
422    /// `None` regardless of what the widget *would* ask for if shown.
423    fn next_paint_deadline(&self) -> Option<web_time::Instant> {
424        if !self.is_visible() { return None; }
425        let mut best: Option<web_time::Instant> = None;
426        for c in self.children() {
427            if let Some(t) = c.next_paint_deadline() {
428                best = Some(match best { Some(b) if b <= t => b, _ => t });
429            }
430        }
431        best
432    }
433}
434
435// ---------------------------------------------------------------------------
436// Tree traversal helpers (free functions operating on &mut dyn Widget)
437// ---------------------------------------------------------------------------
438
439/// Paint `widget` and all its descendants. The caller must ensure `ctx` is
440/// already translated so that (0,0) maps to `widget`'s bottom-left corner.
441///
442/// If the widget returns `Some` from [`Widget::backbuffer_cache_mut`], the
443/// whole subtree (widget + children + overlay) is rendered once into a CPU
444/// [`Framebuffer`] via a software [`GfxCtx`], cached as an
445/// `Arc<Vec<u8>>` on the widget, and blitted through
446/// [`DrawCtx::draw_image_rgba_arc`].  Subsequent frames that find
447/// `cache.dirty == false` skip the re-raster entirely and just blit the
448/// existing bitmap — identical fast path to MatterCAD's `DoubleBuffer`.
449pub fn paint_subtree(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
450    if !widget.is_visible() { return; }
451
452    // Snap CTM at paint_subtree ENTRY — see the commentary preserved
453    // below inside `paint_subtree_direct` for the full rationale.  The
454    // backbuffer path bypasses this because the bitmap is already at
455    // integer texel positions by construction.
456    if widget.backbuffer_cache_mut().is_some() {
457        paint_subtree_backbuffered(widget, ctx);
458    } else {
459        paint_subtree_direct(widget, ctx);
460    }
461}
462
463/// Direct (non-cached) paint: widget and its children paint onto `ctx`
464/// at the current CTM.  This is the default path for widgets that don't
465/// opt into backbuffer caching via `Widget::backbuffer_cache_mut`.
466fn paint_subtree_direct(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
467    paint_subtree_direct_inner(widget, ctx, true);
468}
469
470/// Cache-building variant: paints body + children into the given ctx
471/// WITHOUT calling `paint_overlay`.  The overlay is what `TextField` uses
472/// for its blinking cursor — if we baked the overlay into the cache bitmap,
473/// the drawn cursor would stay visible forever on blit while a second
474/// (blinking) overlay was being drawn on top of it every frame, producing
475/// two cursors.  Overlay runs only on the outer ctx in
476/// `paint_subtree_backbuffered` after the cache blit.
477fn paint_subtree_direct_no_overlay(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
478    paint_subtree_direct_inner(widget, ctx, false);
479}
480
481fn paint_subtree_direct_inner(
482    widget:          &mut dyn Widget,
483    ctx:             &mut dyn DrawCtx,
484    include_overlay: bool,
485) {
486    let snap_this = widget.enforce_integer_bounds();
487    if snap_this {
488        ctx.save();
489        ctx.snap_to_pixel();
490    }
491
492    widget.paint(ctx);
493
494    let b = widget.bounds();
495    let (cx, cy, cw, ch) = widget.clip_children_rect()
496        .unwrap_or((0.0, 0.0, b.width, b.height));
497    ctx.save();
498    ctx.clip_rect(cx, cy, cw, ch);
499
500    let n = widget.children().len();
501    for i in 0..n {
502        let child_bounds  = widget.children()[i].bounds();
503        let snap_to_pixel = widget.children()[i].enforce_integer_bounds();
504        ctx.save();
505        if snap_to_pixel {
506            ctx.translate(child_bounds.x.round(), child_bounds.y.round());
507        } else {
508            ctx.translate(child_bounds.x, child_bounds.y);
509        }
510        let child = &mut widget.children_mut()[i];
511        paint_subtree(child.as_mut(), ctx);
512        ctx.restore();
513    }
514
515    ctx.restore(); // lifts the children clip before paint_overlay
516    if include_overlay {
517        widget.paint_overlay(ctx);
518    }
519
520    if snap_this {
521        ctx.restore();
522    }
523}
524
525/// Backbuffered paint: re-raster through AGG if dirty, blit the cached
526/// bitmap via `draw_image_rgba_arc` regardless.
527///
528/// # HiDPI
529///
530/// The backing bitmap is allocated at **physical pixel** dimensions
531/// (`bounds × device_scale`) and the sub-ctx running the widget's paint has
532/// a matching `scale(dps, dps)` applied.  This means glyph outlines are
533/// rasterised at the physical grid — "true" HiDPI rendering, not pixel
534/// doubling — and the outer blit then draws the physical-sized image at the
535/// widget's logical rect, which the outer CTM (also scaled by dps) maps 1:1
536/// back to physical pixels.  Net: logical layout, physical rasterisation,
537/// zero upscale blur.
538fn paint_subtree_backbuffered(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
539    // Snap the outer CTM to the pixel grid BEFORE blitting the cached
540    // bitmap.  `draw_image_rgba_arc` uses a NEAREST filter for Arc-keyed
541    // textures (1:1 blit lane), so a fractional CTM translation shifts
542    // every screen pixel by a sub-texel amount — reading back interpolated
543    // near-black/near-white instead of the crisp AGG output.  Snapping
544    // here restores the "AGG rasterised it, show it at the pixel grid"
545    // contract the old pre-refactor code preserved.
546    ctx.save();
547    ctx.snap_to_pixel();
548
549    let b   = widget.bounds();
550    let dps = crate::device_scale::device_scale().max(1e-6);
551    // Physical pixel dimensions of the offscreen render target.
552    let w_phys = (b.width  * dps).ceil().max(1.0) as u32;
553    let h_phys = (b.height * dps).ceil().max(1.0) as u32;
554    // Logical dimensions used as the blit destination rect.  **Must** be
555    // derived from `w_phys / dps` rather than `b.width` so the quad the
556    // bitmap is drawn into matches the bitmap's actual pixel extent.  If
557    // `b.width` is non-integer (e.g. 19.5 for a sidebar Label), using
558    // it as `dst_w` stretches a 20-pixel bitmap into a 19.5-pixel quad —
559    // sub-pixel shrink that drops partial-coverage rows at the edges,
560    // which reads as a faint fade along the top / bottom of the glyph.
561    // Pre-HiDPI the blit used the bitmap's integer pixel size directly;
562    // this restores that contract for the logical-units pipeline.
563    let w_logical = w_phys as f64 / dps;
564    let h_logical = h_phys as f64 / dps;
565
566    // Decide whether to re-raster.  Size change invalidates; so does a
567    // mode swap — if the cache holds `Rgba` bytes but the widget now
568    // wants `LcdCoverage` (or vice versa) we must re-raster through the
569    // correct pipeline.  Mode membership is recorded implicitly by
570    // `cache.lcd_alpha`: `Some` means LCD cache, `None` means Rgba.
571    let mode = widget.backbuffer_mode();
572    let mode_is_lcd = matches!(mode, BackbufferMode::LcdCoverage);
573    let theme_epoch       = crate::theme::current_visuals_epoch();
574    let typography_epoch  = crate::font_settings::current_typography_epoch();
575    let (needs_raster, has_bitmap) = {
576        let cache = widget.backbuffer_cache_mut()
577            .expect("backbuffered widget must return Some from backbuffer_cache_mut");
578        let cache_is_lcd = cache.lcd_alpha.is_some();
579        let needs = cache.dirty
580            || cache.pixels.is_none()
581            || cache.width != w_phys
582            || cache.height != h_phys
583            || cache_is_lcd != mode_is_lcd
584            || cache.theme_epoch != theme_epoch
585            || cache.typography_epoch != typography_epoch;
586        (needs, cache.pixels.is_some())
587    };
588
589    if needs_raster {
590        // Allocate a fresh render target whose format matches the
591        // widget's chosen backbuffer mode, paint the subtree into it,
592        // then convert to top-down RGBA for the cache (the blit lane
593        // expects `(R, G, B, A)` rows top-first).
594        //
595        // `LcdCoverage` mode now uses an `LcdGfxCtx` over an `LcdBuffer`
596        // — every primitive (fill, stroke, text, image) flows through
597        // the per-channel LCD pipeline, so child widgets that paint
598        // into this widget's backbuffer compose correctly with
599        // LCD-treated text instead of breaking the per-channel
600        // coverage at the first non-text fill (the alpha bug the
601        // search-box screenshot showed before this change).
602        // Each branch produces `(pixels, lcd_alpha)` top-down:
603        //   - `Rgba`: `pixels` = straight-alpha RGBA8; `lcd_alpha` = None.
604        //   - `LcdCoverage`: `pixels` = premultiplied colour plane (3 B/px);
605        //     `lcd_alpha` = per-channel alpha plane (3 B/px).  The blit
606        //     step below picks a compositor based on which is present.
607        let (pixels_bytes, lcd_alpha_bytes): (Vec<u8>, Option<Vec<u8>>) = match mode {
608            BackbufferMode::Rgba => {
609                let mut fb = Framebuffer::new(w_phys, h_phys);
610                {
611                    let mut sub = GfxCtx::new(&mut fb);
612                    sub.set_lcd_mode(false);   // RGBA mode never uses LCD text
613                    if (dps - 1.0).abs() > 1e-6 {
614                        // Widgets paint in logical coords — scale the sub ctx
615                        // so their drawing lands on the physical pixel grid.
616                        sub.scale(dps, dps);
617                    }
618                    paint_subtree_direct_no_overlay(widget, &mut sub);
619                }
620                // Two conversions to make the bitmap directly blittable:
621                //   1. Row order — Framebuffer is Y-up, blit lane is top-down.
622                //   2. Alpha format — AGG writes premultiplied; the blend
623                //      function expects straight alpha so that half-coverage
624                //      AA edges composite without the dark-fringe artifact.
625                let mut pixels = fb.pixels_flipped();
626                crate::framebuffer::unpremultiply_rgba_inplace(&mut pixels);
627                (pixels, None)
628            }
629            BackbufferMode::LcdCoverage => {
630                // The LCD pipeline is strictly WRITE-only.  The buffer
631                // starts at zero coverage everywhere; the widget paints
632                // opaque content covering its full bounds (the contract
633                // for this mode) into it via an `LcdGfxCtx`; then the
634                // two planes (premultiplied colour + per-channel alpha)
635                // are cached and composited onto the destination at
636                // blit time via `draw_lcd_backbuffer_arc` — which
637                // preserves LCD per-channel chroma through the cache.
638                //
639                // We deliberately do NOT read from any destination —
640                // seeding the buffer from the parent's pixels would
641                // tie the cache's validity to the widget's current
642                // screen position (stale on scroll / reparent), stall
643                // the GPU pipeline on GL (glReadPixels is sync), and
644                // break on backends that can't read their own target.
645                // Widgets that can't paint their own opaque bg should
646                // use `Rgba` mode or paint through the parent's ctx
647                // directly instead.
648                let mut buf = LcdBuffer::new(w_phys, h_phys);
649                {
650                    let mut sub = crate::lcd_gfx_ctx::LcdGfxCtx::new(&mut buf);
651                    if (dps - 1.0).abs() > 1e-6 {
652                        // Match the RGBA branch: widgets paint in logical
653                        // coords; the sub ctx's scale transforms them into
654                        // the physical-pixel LCD buffer.
655                        sub.scale(dps, dps);
656                    }
657                    paint_subtree_direct_no_overlay(widget, &mut sub);
658                }
659                (buf.color_plane_flipped(), Some(buf.alpha_plane_flipped()))
660            }
661        };
662        let pixels     = Arc::new(pixels_bytes);
663        let lcd_alpha  = lcd_alpha_bytes.map(Arc::new);
664
665        let cache = widget.backbuffer_cache_mut().unwrap();
666        cache.pixels    = Some(Arc::clone(&pixels));
667        cache.lcd_alpha = lcd_alpha.as_ref().map(Arc::clone);
668        cache.width             = w_phys;
669        cache.height            = h_phys;
670        cache.dirty             = false;
671        cache.theme_epoch       = theme_epoch;
672        cache.typography_epoch  = typography_epoch;
673    }
674
675    // Blit the cached bitmap onto the outer ctx.  Two paths:
676    //
677    //   - `Rgba` cache (no `lcd_alpha`): a single RGBA8 texture via the
678    //     standard image-blit lane.  Alpha-aware SrcOver at the blend
679    //     stage handles transparency.
680    //
681    //   - `LcdCoverage` cache (`lcd_alpha` is `Some`): two 3-byte/pixel
682    //     planes — premultiplied colour + per-channel alpha.  The
683    //     backend's `draw_lcd_backbuffer_arc` composites them with
684    //     per-channel src-over, preserving LCD chroma through the
685    //     cache round-trip (grayscale AA on backends that fall back
686    //     to the default trait impl).
687    let cache = widget.backbuffer_cache_mut().unwrap();
688    // Image is physical-sized; dst is logical.  The outer CTM already has
689    // `scale(dps, dps)` active, so logical dst × dps == physical dst ==
690    // bitmap size, giving a 1:1 texel-to-pixel blit (no up/downscale blur).
691    let img_w = cache.width;
692    let img_h = cache.height;
693    match (cache.pixels.as_ref(), cache.lcd_alpha.as_ref()) {
694        (Some(color), Some(alpha)) => {
695            ctx.draw_lcd_backbuffer_arc(
696                color, alpha, img_w, img_h,
697                0.0, 0.0, w_logical, h_logical,
698            );
699        }
700        (Some(bmp), None) => {
701            ctx.draw_image_rgba_arc(bmp, img_w, img_h, 0.0, 0.0, w_logical, h_logical);
702        }
703        _ => {}
704    }
705    let _ = has_bitmap;
706
707    // Overlay paint runs AFTER the cache blit and paints directly onto
708    // the outer ctx.  Widgets use this for content that changes too
709    // often to be worth caching — the canonical case is `TextField`'s
710    // blinking cursor, which flips twice per second and would otherwise
711    // invalidate the cache 2×/s.  With overlay, cursor is drawn fresh
712    // each frame onto the already-blitted bg+text; the cache only
713    // invalidates when the text/focus/selection actually changes.
714    //
715    // `paint_subtree_direct` has the same overlay call after children
716    // (see its own body); this keeps the two paint paths consistent.
717    widget.paint_overlay(ctx);
718
719    ctx.restore(); // pops the snap_to_pixel save above.
720}
721
722/// Walk the subtree rooted at `widget` and return the path (list of child
723/// indices) to the deepest widget that passes `hit_test` at `local_pos`.
724///
725/// `local_pos` is expressed in `widget`'s coordinate space (not including
726/// `widget.bounds().x/y` — the caller has already accounted for that).
727///
728/// Returns `Some(vec![])` if `widget` itself is hit but no child is.
729/// Returns `None` if nothing is hit.
730pub fn hit_test_subtree(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
731    if !widget.is_visible() || !widget.hit_test(local_pos) {
732        return None;
733    }
734    // Let overlays (e.g. a floating scrollbar) claim the pointer before any
735    // child that happens to cover the same pixels.
736    if widget.claims_pointer_exclusively(local_pos) {
737        return Some(vec![]);
738    }
739    // Check children in reverse order (last drawn = topmost = highest priority).
740    for (i, child) in widget.children().iter().enumerate().rev() {
741        let child_local = Point::new(
742            local_pos.x - child.bounds().x,
743            local_pos.y - child.bounds().y,
744        );
745        if let Some(mut sub_path) = hit_test_subtree(child.as_ref(), child_local) {
746            sub_path.insert(0, i);
747            return Some(sub_path);
748        }
749    }
750    Some(vec![]) // hit this widget, no child claimed it
751}
752
753/// Dispatch `event` through a path (list of child indices from the root).
754/// The event bubbles leaf → root; returns `Consumed` if any widget consumed it.
755///
756/// `pos_in_root` is the event position in the root widget's coordinate space.
757/// The function translates it down through each level of the path.
758pub fn dispatch_event(
759    root: &mut Box<dyn Widget>,
760    path: &[usize],
761    event: &Event,
762    pos_in_root: Point,
763) -> EventResult {
764    if path.is_empty() {
765        return root.on_event(event);
766    }
767    let idx = path[0];
768    // Path can become stale between when it was captured (hit-test or
769    // previous-frame hovered/focus) and when it is dispatched — e.g. a
770    // CollapsingHeader collapsed since then and dropped its child.  Rather
771    // than panic, just stop descending and deliver the event at this level.
772    if idx >= root.children().len() {
773        return root.on_event(event);
774    }
775    let child_bounds = root.children()[idx].bounds();
776    let child_pos = Point::new(pos_in_root.x - child_bounds.x, pos_in_root.y - child_bounds.y);
777    let translated_event = translate_event(event, child_pos);
778
779    let child_result = dispatch_event(
780        &mut root.children_mut()[idx],
781        &path[1..],
782        &translated_event,
783        child_pos,
784    );
785    if child_result == EventResult::Consumed {
786        return EventResult::Consumed;
787    }
788    // Bubble: deliver to this widget too (with original pos_in_root coords).
789    root.on_event(event)
790}
791
792/// Produce a version of `event` with mouse positions replaced by `new_pos`.
793/// Non-mouse events (key, focus) are returned unchanged.
794fn translate_event(event: &Event, new_pos: Point) -> Event {
795    match event {
796        Event::MouseMove { .. } => Event::MouseMove { pos: new_pos },
797        Event::MouseDown { button, modifiers, .. } => Event::MouseDown {
798            pos: new_pos, button: *button, modifiers: *modifiers,
799        },
800        Event::MouseUp { button, modifiers, .. } => Event::MouseUp {
801            pos: new_pos, button: *button, modifiers: *modifiers,
802        },
803        Event::MouseWheel { delta_y, delta_x, .. } => Event::MouseWheel {
804            pos: new_pos, delta_y: *delta_y, delta_x: *delta_x,
805        },
806        other => other.clone(),
807    }
808}
809
810// ---------------------------------------------------------------------------
811// Inspector support
812// ---------------------------------------------------------------------------
813
814/// Flat snapshot of one widget for the inspector panel.
815#[derive(Clone)]
816pub struct InspectorNode {
817    pub type_name: &'static str,
818    /// Absolute screen bounds (Y-up), accumulated as the tree is walked.
819    pub screen_bounds: Rect,
820    pub depth: usize,
821    /// Type-specific display properties from [`Widget::properties`].
822    pub properties: Vec<(&'static str, String)>,
823}
824
825/// Walk the subtree rooted at `widget` and collect an `InspectorNode` per
826/// widget in DFS paint order (root first).
827///
828/// `screen_origin` is the accumulated parent offset in screen Y-up coords.
829pub fn collect_inspector_nodes(
830    widget: &dyn Widget,
831    depth: usize,
832    screen_origin: Point,
833    out: &mut Vec<InspectorNode>,
834) {
835    // Invisible widgets (and their entire subtrees) are excluded from the
836    // inspector — they are not part of the live rendered scene.
837    if !widget.is_visible() { return; }
838    // Utility widgets opt out of the inspector entirely.
839    if !widget.show_in_inspector() { return; }
840
841    let b = widget.bounds();
842    let abs = Rect::new(
843        screen_origin.x + b.x,
844        screen_origin.y + b.y,
845        b.width,
846        b.height,
847    );
848    // Build the properties vec — include the universal `backbuffer` flag
849    // first (so every widget shows it in a consistent location), then the
850    // widget-specific properties.
851    let mut props = vec![
852        ("backbuffer", if widget.has_backbuffer() { "true".to_string() }
853                       else                        { "false".to_string() }),
854    ];
855    props.extend(widget.properties());
856    out.push(InspectorNode {
857        type_name:  widget.type_name(),
858        screen_bounds: abs,
859        depth,
860        properties: props,
861    });
862
863    // Widgets that are part of the inspector infrastructure opt out of child
864    // recursion to prevent the inspector from growing its own node list every
865    // frame (exponential growth).  Their sub-trees are still visible in the
866    // inspector on the next frame through the normal layout snapshot.
867    if !widget.contributes_children_to_inspector() { return; }
868
869    let child_origin = Point::new(abs.x, abs.y);
870    for child in widget.children() {
871        collect_inspector_nodes(child.as_ref(), depth + 1, child_origin, out);
872    }
873}
874
875/// Collect all focusable widgets in paint order (DFS root → leaves).
876/// Returns their paths as `Vec<Vec<usize>>`.
877fn collect_focusable(widget: &dyn Widget, current_path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
878    if widget.is_focusable() {
879        out.push(current_path.clone());
880    }
881    for (i, child) in widget.children().iter().enumerate() {
882        current_path.push(i);
883        collect_focusable(child.as_ref(), current_path, out);
884        current_path.pop();
885    }
886}
887
888/// Get a mutable reference to the widget at the given path.
889fn widget_at_path<'a>(root: &'a mut Box<dyn Widget>, path: &[usize]) -> &'a mut dyn Widget {
890    if path.is_empty() {
891        return root.as_mut();
892    }
893    let idx = path[0];
894    widget_at_path(&mut root.children_mut()[idx], &path[1..])
895}
896
897// ---------------------------------------------------------------------------
898// App — top-level owner of the widget tree
899// ---------------------------------------------------------------------------
900
901/// Owns the widget tree, handles focus, and converts OS events to Y-up coords.
902///
903/// Create with [`App::new`], call [`App::layout`] every frame before
904/// [`App::paint`], and feed OS events through the `on_*` methods.
905pub struct App {
906    root: Box<dyn Widget>,
907    /// Current focus path (indices from root into children vec).
908    /// `None` means no widget has focus.
909    focus: Option<Vec<usize>>,
910    /// Path to the widget last seen under the cursor (for hover clearing).
911    hovered: Option<Vec<usize>>,
912    /// Mouse-captured widget path. Set when a widget consumes `MouseDown`;
913    /// cleared on `MouseUp`. While set, `MouseMove` events go to the captured
914    /// widget regardless of cursor position — enabling slider drag-outside-bounds.
915    captured: Option<Vec<usize>>,
916    /// Viewport height in pixels — used for Y-down → Y-up conversion.
917    viewport_height: f64,
918    /// Optional global key handler called *before* dispatching to the focused widget.
919    /// Returns `true` if the key was handled globally (suppresses focused dispatch).
920    global_key_handler: Option<Box<dyn FnMut(Key, Modifiers) -> bool>>,
921    /// Multi-touch gesture recogniser.  Platform shells feed raw touches
922    /// through [`App::on_touch_start/move/end/cancel`]; widgets read the
923    /// per-frame aggregate via [`crate::current_multi_touch`].
924    touch_state: crate::touch_state::TouchState,
925}
926
927impl App {
928    /// Create a new `App` with `root` as the root widget.
929    pub fn new(root: Box<dyn Widget>) -> Self {
930        Self {
931            root,
932            focus: None,
933            hovered: None,
934            captured: None,
935            viewport_height: 1.0,
936            global_key_handler: None,
937            touch_state: crate::touch_state::TouchState::new(),
938        }
939    }
940
941    /// Register a global key handler invoked before the focused widget receives
942    /// the key.  Return `true` to consume the event (suppress focused dispatch).
943    ///
944    /// # Example
945    /// ```ignore
946    /// app.set_global_key_handler(|key, mods| {
947    ///     if mods.ctrl && mods.shift && key == Key::O {
948    ///         organize_windows();
949    ///         return true;
950    ///     }
951    ///     false
952    /// });
953    /// ```
954    pub fn set_global_key_handler(&mut self, handler: impl FnMut(Key, Modifiers) -> bool + 'static) {
955        self.global_key_handler = Some(Box::new(handler));
956    }
957
958    /// Lay out the widget tree to fill `viewport`.  `viewport` is in **physical
959    /// pixels** (e.g. `window.inner_size()` on native, `canvas.width/height` on
960    /// wasm); this method divides by the current device scale factor so the
961    /// widget tree lays out in logical (device-independent) units.  Call once
962    /// per frame before [`paint`][Self::paint].
963    pub fn layout(&mut self, viewport: Size) {
964        let scale = crate::device_scale::device_scale().max(1e-6);
965        let logical = Size::new(viewport.width / scale, viewport.height / scale);
966        self.viewport_height = logical.height;
967        self.root.set_bounds(Rect::new(0.0, 0.0, logical.width, logical.height));
968        self.root.layout(logical);
969    }
970
971    /// Paint the entire widget tree into `ctx`. Call after [`layout`][Self::layout].
972    ///
973    /// Applies a `ctx.scale(dps, dps)` transform up-front so the whole tree —
974    /// widget dimensions, font sizes, margins — is rendered at physical pixel
975    /// density on HiDPI screens without any widget having to know about DPI.
976    ///
977    /// Also clears the animation tick flag so widgets can re-request it during
978    /// this paint if they need another frame; hosts read [`wants_animation_tick`]
979    /// after `paint` returns to decide whether to schedule continuous redraws.
980    pub fn paint(&mut self, ctx: &mut dyn DrawCtx) {
981        crate::animation::clear_tick();
982        // Recompute the multi-touch aggregate once per paint and publish
983        // to the thread-local — widgets read it during `on_event` or
984        // `paint` without an explicit `&App` reference.
985        self.touch_state.update_gesture();
986        crate::touch_state::set_current(self.touch_state.current());
987        let scale = crate::device_scale::device_scale();
988        if (scale - 1.0).abs() > 1e-6 {
989            ctx.save();
990            ctx.scale(scale, scale);
991            paint_subtree(self.root.as_mut(), ctx);
992            ctx.restore();
993        } else {
994            paint_subtree(self.root.as_mut(), ctx);
995        }
996    }
997
998    /// After a paint pass, returns `true` if any widget requested another frame
999    /// (e.g. an in-progress hover animation).  Hosts should use this to set
1000    /// their event-loop control flow to continuous polling while it's `true`.
1001    ///
1002    /// Combines the **tree-walk** signal — [`Widget::needs_paint`], which is
1003    /// visibility-gated: hidden subtrees cannot contribute — with the legacy
1004    /// thread-local [`crate::animation::wants_tick`] flag, which is retained
1005    /// as a transitional fallback for widgets that haven't yet moved their
1006    /// pending-repaint state into their own struct.  Widgets should prefer
1007    /// overriding `needs_paint` (visibility-safe) over calling the
1008    /// thread-local `request_tick` (fires even from hidden subtrees).
1009    pub fn wants_animation_tick(&self) -> bool {
1010        self.root.needs_paint() || crate::animation::wants_tick()
1011    }
1012
1013    /// Earliest scheduled repaint deadline across the visible widget tree.
1014    /// Hosts translate `Some(t)` into `ControlFlow::WaitUntil(t)` so that
1015    /// e.g. a text field's cursor blink wakes the loop exactly at the flip
1016    /// boundary.  Invisible subtrees contribute nothing.
1017    pub fn next_paint_deadline(&self) -> Option<web_time::Instant> {
1018        self.root.next_paint_deadline()
1019    }
1020
1021    // --- Platform event ingestion ---
1022    //
1023    // Hosts pass raw physical-pixel coordinates (e.g. `e.clientX * devicePixelRatio`
1024    // in wasm, or `WindowEvent::CursorMoved.position` on native).  These methods
1025    // divide by the current device scale factor and flip Y so widget code sees
1026    // logical Y-up coordinates matching the layout pass.
1027
1028    /// Mouse cursor moved. `screen_y` is Y-down physical pixels.
1029    pub fn on_mouse_move(&mut self, screen_x: f64, screen_y: f64) {
1030        // Reset cursor so the hovered widget can set it; Default if nothing sets it.
1031        crate::cursor::reset_cursor_icon();
1032        let pos = self.flip_y(screen_x, screen_y);
1033        self.dispatch_mouse_move(pos);
1034    }
1035
1036    /// Mouse button pressed. `screen_y` is Y-down physical pixels.
1037    pub fn on_mouse_down(&mut self, screen_x: f64, screen_y: f64, button: MouseButton, mods: Modifiers) {
1038        let pos = self.flip_y(screen_x, screen_y);
1039        let hit = self.compute_hit(pos);
1040
1041        // Click-to-focus: if the hit widget is focusable, give it focus.
1042        if let Some(ref path) = hit {
1043            let w = widget_at_path(&mut self.root, path);
1044            if w.is_focusable() {
1045                self.set_focus(Some(path.clone()));
1046            } else {
1047                self.set_focus(None);
1048            }
1049        } else {
1050            self.set_focus(None);
1051        }
1052
1053        let event = Event::MouseDown { pos, button, modifiers: mods };
1054        if let Some(mut path) = hit {
1055            let result = dispatch_event(&mut self.root, &path, &event, pos);
1056            if result == EventResult::Consumed {
1057                self.maybe_bring_to_front(&mut path);
1058                self.captured = Some(path);
1059            }
1060        }
1061        // NO blanket request_tick.  Mouse-down on an inert area must not
1062        // cause a repaint.  Each widget that changes visual state in
1063        // response to a MouseDown (button press, window raise, focus
1064        // indicator on the focus-gained widget, etc.) is responsible for
1065        // calling `crate::animation::request_tick` itself.
1066    }
1067
1068    /// Mouse button released. `screen_y` is Y-down.
1069    pub fn on_mouse_up(&mut self, screen_x: f64, screen_y: f64, button: MouseButton, mods: Modifiers) {
1070        let pos = self.flip_y(screen_x, screen_y);
1071        let event = Event::MouseUp { pos, button, modifiers: mods };
1072        // Deliver release to captured widget first (if any), then clear capture.
1073        if let Some(path) = self.captured.take() {
1074            dispatch_event(&mut self.root, &path, &event, pos);
1075        } else {
1076            let hit = self.compute_hit(pos);
1077            if let Some(path) = hit {
1078                dispatch_event(&mut self.root, &path, &event, pos);
1079            }
1080        }
1081    }
1082
1083    /// Key pressed. Delivered to the focused widget and bubbles up.
1084    ///
1085    /// If a global key handler was registered via [`set_global_key_handler`] and
1086    /// it returns `true`, the key is consumed and the focused widget does not
1087    /// receive it.
1088    pub fn on_key_down(&mut self, key: Key, mods: Modifiers) {
1089        if key == Key::Tab {
1090            self.advance_focus(!mods.shift);
1091            return;
1092        }
1093        // Call global handler first; bail out if it consumes the key.
1094        if let Some(ref mut handler) = self.global_key_handler {
1095            if handler(key.clone(), mods) {
1096                return;
1097            }
1098        }
1099        let event = Event::KeyDown { key, modifiers: mods };
1100        if let Some(path) = self.focus.clone() {
1101            dispatch_event(&mut self.root, &path, &event, Point::ORIGIN);
1102        }
1103    }
1104
1105    /// Key released. Delivered to the focused widget.
1106    pub fn on_key_up(&mut self, key: Key, mods: Modifiers) {
1107        let event = Event::KeyUp { key, modifiers: mods };
1108        if let Some(path) = self.focus.clone() {
1109            dispatch_event(&mut self.root, &path, &event, Point::ORIGIN);
1110        }
1111    }
1112
1113    /// Mouse wheel scrolled. `screen_y` is Y-down. `delta_y` positive = scroll up.
1114    /// `delta_x` positive = content moves right.
1115    pub fn on_mouse_wheel(&mut self, screen_x: f64, screen_y: f64, delta_y: f64) {
1116        self.on_mouse_wheel_xy(screen_x, screen_y, 0.0, delta_y);
1117    }
1118
1119    /// Mouse wheel with an explicit horizontal component (trackpad pan,
1120    /// shift+wheel via the platform harness).
1121    pub fn on_mouse_wheel_xy(
1122        &mut self,
1123        screen_x: f64, screen_y: f64,
1124        delta_x: f64, delta_y: f64,
1125    ) {
1126        let pos = self.flip_y(screen_x, screen_y);
1127        let hit = self.compute_hit(pos);
1128        let event = Event::MouseWheel { pos, delta_y, delta_x };
1129        if let Some(path) = hit {
1130            dispatch_event(&mut self.root, &path, &event, pos);
1131        }
1132    }
1133
1134    /// Snapshot the entire widget tree for the inspector.
1135    pub fn collect_inspector_nodes(&self) -> Vec<InspectorNode> {
1136        let mut out = Vec::new();
1137        collect_inspector_nodes(self.root.as_ref(), 0, Point::ORIGIN, &mut out);
1138        out
1139    }
1140
1141    /// Serialize the widget tree — types, bounds, depth, properties — as JSON.
1142    ///
1143    /// Produces a flat array of nodes in paint-order DFS.  Suitable for writing
1144    /// to a file and diffing between runs to verify layout stability.  Used by
1145    /// the demo harness's debug hotkey.
1146    pub fn dump_tree_json(&self) -> String {
1147        let nodes = self.collect_inspector_nodes();
1148        let mut s = String::from("[\n");
1149        for (i, n) in nodes.iter().enumerate() {
1150            let props_json = n.properties.iter()
1151                .map(|(k, v)| format!("{:?}: {:?}", k, v))
1152                .collect::<Vec<_>>()
1153                .join(", ");
1154            s.push_str(&format!(
1155                "  {{\"type\":{:?},\"depth\":{},\"x\":{:.2},\"y\":{:.2},\"w\":{:.2},\"h\":{:.2},\"props\":{{{}}}}}",
1156                n.type_name, n.depth,
1157                n.screen_bounds.x, n.screen_bounds.y,
1158                n.screen_bounds.width, n.screen_bounds.height,
1159                props_json,
1160            ));
1161            if i + 1 < nodes.len() { s.push(','); }
1162            s.push('\n');
1163        }
1164        s.push(']');
1165        s
1166    }
1167
1168    /// Returns `true` if any widget currently holds keyboard focus.
1169    /// Used by the render loop to schedule cursor-blink repaints.
1170    pub fn has_focus(&self) -> bool { self.focus.is_some() }
1171
1172    /// Call when the cursor leaves the window to clear hover state.
1173    pub fn on_mouse_leave(&mut self) {
1174        crate::cursor::reset_cursor_icon();
1175        self.dispatch_mouse_move(Point::new(-1.0, -1.0));
1176    }
1177
1178    // --- Touch ingestion ---
1179    //
1180    // Raw touches go into the multi-touch gesture recogniser; widgets
1181    // read `current_multi_touch()` each frame.  Platform shells ALSO
1182    // route the first finger through the existing `on_mouse_*` entry
1183    // points so widgets that only understand mouse input keep working
1184    // without changes.  Coordinates are the same physical-pixel Y-down
1185    // units the mouse entry points accept.
1186    pub fn on_touch_start(
1187        &mut self,
1188        device:  crate::touch_state::TouchDeviceId,
1189        id:      crate::touch_state::TouchId,
1190        screen_x: f64, screen_y: f64,
1191        force:    Option<f32>,
1192    ) {
1193        let pos = self.flip_y(screen_x, screen_y);
1194        self.touch_state.on_start(device, id, pos, force);
1195    }
1196    pub fn on_touch_move(
1197        &mut self,
1198        device:  crate::touch_state::TouchDeviceId,
1199        id:      crate::touch_state::TouchId,
1200        screen_x: f64, screen_y: f64,
1201        force:    Option<f32>,
1202    ) {
1203        let pos = self.flip_y(screen_x, screen_y);
1204        self.touch_state.on_move(device, id, pos, force);
1205    }
1206    pub fn on_touch_end(
1207        &mut self,
1208        device:  crate::touch_state::TouchDeviceId,
1209        id:      crate::touch_state::TouchId,
1210    ) {
1211        self.touch_state.on_end_or_cancel(device, id);
1212    }
1213    pub fn on_touch_cancel(
1214        &mut self,
1215        device:  crate::touch_state::TouchDeviceId,
1216        id:      crate::touch_state::TouchId,
1217    ) {
1218        self.touch_state.on_end_or_cancel(device, id);
1219    }
1220    /// Current number of fingers down across all devices.  Used by
1221    /// widgets that want to know the gesture has *begun* before the
1222    /// first frame has had a chance to produce a delta (where
1223    /// `current_multi_touch()` may still be `None`).
1224    pub fn active_touch_count(&self) -> usize {
1225        self.touch_state.active_count()
1226    }
1227
1228    // --- Private helpers ---
1229
1230    /// If the click path passes through a `Window` widget, move that window to
1231    /// the end of its parent's children list so it paints on top of siblings.
1232    /// All stored paths (focus, hovered, captured, plus the clicked path itself)
1233    /// are updated to reflect the new index.
1234    fn maybe_bring_to_front(&mut self, clicked_path: &mut Vec<usize>) {
1235        // Walk the clicked path and record the deepest Window encountered.
1236        // At each step we descend into children[idx]; after descending, if the
1237        // new node is a Window we record (parent_path, win_idx).  We keep
1238        // scanning so a nested Window (unlikely but possible) wins.
1239        let mut node: &dyn Widget = self.root.as_ref();
1240        let mut window_info: Option<(Vec<usize>, usize)> = None; // (parent_path, win_idx)
1241        for (depth, &idx) in clicked_path.iter().enumerate() {
1242            let children = node.children();
1243            if idx >= children.len() { break; }
1244            node = &*children[idx];
1245            if node.type_name() == "Window" {
1246                // parent_path = clicked_path[..depth], win_idx = idx
1247                window_info = Some((clicked_path[..depth].to_vec(), idx));
1248            }
1249        }
1250
1251        let (parent_path, win_idx) = match window_info { Some(x) => x, None => return };
1252
1253        // Check there's actually a sibling to leapfrog.
1254        let n = {
1255            let parent = widget_at_path(&mut self.root, &parent_path);
1256            parent.children().len()
1257        };
1258        if win_idx >= n - 1 { return; } // already at front
1259
1260        // Move the window to the end of its parent's children (mutable pass).
1261        {
1262            let parent = widget_at_path(&mut self.root, &parent_path);
1263            let child = parent.children_mut().remove(win_idx);
1264            parent.children_mut().push(child);
1265        }
1266        let new_idx = n - 1;
1267        let depth = parent_path.len(); // depth at which the window index sits
1268
1269        // Update any stored path whose element at `depth` was affected by the move.
1270        fn shift_path(p: &mut Vec<usize>, depth: usize, old: usize, new: usize) {
1271            if p.len() > depth {
1272                let i = p[depth];
1273                if i == old {
1274                    p[depth] = new;
1275                } else if i > old && i <= new {
1276                    // Siblings that were after the removed window shift left by 1.
1277                    p[depth] -= 1;
1278                }
1279            }
1280        }
1281        shift_path(clicked_path, depth, win_idx, new_idx);
1282        if let Some(ref mut p) = self.focus    { shift_path(p, depth, win_idx, new_idx); }
1283        if let Some(ref mut p) = self.hovered  { shift_path(p, depth, win_idx, new_idx); }
1284        if let Some(ref mut p) = self.captured { shift_path(p, depth, win_idx, new_idx); }
1285    }
1286
1287    #[inline]
1288    /// Convert a platform-supplied physical Y-down coordinate into the
1289    /// logical Y-up space the widget tree works in.  Divides by the current
1290    /// device scale factor (so mouse coords line up with the scaled paint
1291    /// transform) and flips Y against the cached logical viewport height.
1292    fn flip_y(&self, x: f64, y_down: f64) -> Point {
1293        let scale = crate::device_scale::device_scale().max(1e-6);
1294        let lx = x / scale;
1295        let ly_down = y_down / scale;
1296        Point::new(lx, self.viewport_height - ly_down)
1297    }
1298
1299    fn compute_hit(&self, pos: Point) -> Option<Vec<usize>> {
1300        hit_test_subtree(self.root.as_ref(), pos)
1301    }
1302
1303    fn dispatch_mouse_move(&mut self, pos: Point) {
1304        let new_hit = self.compute_hit(pos);
1305
1306        // If the hovered widget changed, clear the old one — but skip the clear
1307        // event when the old widget still has mouse capture (it should keep
1308        // receiving real positions, not a (-1,-1) sentinel that snaps state).
1309        if new_hit != self.hovered {
1310            if let Some(old_path) = self.hovered.take() {
1311                let is_captured = self.captured.as_ref() == Some(&old_path);
1312                if !is_captured {
1313                    let clear = Event::MouseMove { pos: Point::new(-1.0, -1.0) };
1314                    dispatch_event(&mut self.root, &old_path, &clear, Point::new(-1.0, -1.0));
1315                }
1316            }
1317            self.hovered = new_hit.clone();
1318        }
1319
1320        let event = Event::MouseMove { pos };
1321        if let Some(ref cap_path) = self.captured.clone() {
1322            // Captured widget always receives the real position, regardless of
1323            // whether the cursor is over it — this is what keeps a slider
1324            // tracking the cursor when dragged outside its bounds.
1325            dispatch_event(&mut self.root, cap_path, &event, pos);
1326        } else if let Some(path) = new_hit {
1327            dispatch_event(&mut self.root, &path, &event, pos);
1328        }
1329    }
1330
1331    /// Set focus to `new_path`, sending `FocusLost` / `FocusGained` as needed.
1332    fn set_focus(&mut self, new_path: Option<Vec<usize>>) {
1333        if self.focus == new_path {
1334            return;
1335        }
1336        if let Some(old) = self.focus.take() {
1337            dispatch_event(&mut self.root, &old, &Event::FocusLost, Point::ORIGIN);
1338        }
1339        self.focus = new_path.clone();
1340        if let Some(new) = new_path {
1341            dispatch_event(&mut self.root, &new, &Event::FocusGained, Point::ORIGIN);
1342        }
1343    }
1344
1345    /// Move focus to the next (or previous) focusable widget in paint order.
1346    fn advance_focus(&mut self, forward: bool) {
1347        let mut all: Vec<Vec<usize>> = Vec::new();
1348        collect_focusable(self.root.as_ref(), &mut vec![], &mut all);
1349        if all.is_empty() {
1350            return;
1351        }
1352        let current_idx = self.focus.as_ref()
1353            .and_then(|f| all.iter().position(|p| p == f));
1354        let next_idx = match current_idx {
1355            None => if forward { 0 } else { all.len() - 1 },
1356            Some(i) => {
1357                if forward {
1358                    (i + 1) % all.len()
1359                } else {
1360                    if i == 0 { all.len() - 1 } else { i - 1 }
1361                }
1362            }
1363        };
1364        let next_path = all[next_idx].clone();
1365        self.set_focus(Some(next_path));
1366    }
1367}