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}