agg-gui 0.5.1

Immediate-mode Rust GUI library with AGG rendering, Y-up layout, widgets, text, SVG, and native/WASM adapters
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
//! Widget-tree traversal helpers: dirty marking and hit-test path lookup.
//!
//! [`mark_subtree_dirty`] is called by the frame loop after off-event-path
//! async work finishes (font load, image decode). The hit-test family —
//! [`hit_test_subtree`], [`active_modal_path`], [`global_overlay_hit_path`]
//! — returns a `Vec<usize>` path of child indices identifying the deepest
//! widget that claims a position; the App event router uses that path to
//! dispatch mouse events to the right subtree.
//!
//! # Coordinate system
//!
//! All positions in this module are **logical Y-up**, origin at the
//! bottom-left, expressed in the receiving widget's own local space (the
//! caller has already subtracted ancestor `bounds().x/y`). The App
//! pre-converts platform Y-down input coordinates via `App::flip_y`
//! before calling in — do not flip Y here.

use super::*;

/// Map a point from a parent's local space into the local space of its child
/// at index `idx`, honouring the parent's optional
/// [`Widget::child_transform`](crate::widget::Widget::child_transform).
///
/// The child transform (if any) is inverted first — it maps child→parent, so
/// its inverse maps the incoming parent-local point back into child space —
/// and only then is the child's `bounds()` offset removed.  Bounds are thus
/// interpreted *inside* the transform, matching how [`paint_subtree`] applies
/// the transform to the whole child group before translating per child.
fn child_local_pos(parent: &dyn Widget, child_bounds: Rect, pos_in_parent: Point) -> Point {
    let mut x = pos_in_parent.x;
    let mut y = pos_in_parent.y;
    if let Some(t) = parent.child_transform() {
        t.inverse_transform(&mut x, &mut y);
    }
    Point::new(x - child_bounds.x, y - child_bounds.y)
}

/// Recursively call `mark_dirty` on `widget` and every visible
/// descendant.  Used by the host frame loop after an async data
/// source (image fetch + decode, font load, etc.) finishes outside
/// the normal event-dispatch path that would otherwise mark widgets
/// dirty as the event bubbles.  Called explicitly at the top of the
/// frame so the user-visible "freshly-decoded data lands in stale
/// FBO contents" bug never opens a one-frame race window.
pub fn mark_subtree_dirty(widget: &mut dyn Widget) {
    widget.mark_dirty();
    for child in widget.children_mut().iter_mut() {
        mark_subtree_dirty(child.as_mut());
    }
}

/// Walk the subtree rooted at `widget` and return the path (list of child
/// indices) to the deepest widget that passes `hit_test` at `local_pos`.
///
/// `local_pos` is expressed in `widget`'s coordinate space (not including
/// `widget.bounds().x/y` — the caller has already accounted for that).
///
/// Returns `Some(vec![])` if `widget` itself is hit but no child is.
/// Returns `None` if nothing is hit.
pub fn hit_test_subtree(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
    if !widget.is_visible() || !widget.hit_test(local_pos) {
        return None;
    }
    // Let overlays (e.g. a floating scrollbar) claim the pointer before any
    // child that happens to cover the same pixels, and let a disabled scope
    // swallow the pointer so clicks never reach its (non-interactive) children.
    if widget.claims_pointer_exclusively(local_pos) || widget.blocks_child_interaction() {
        return Some(vec![]);
    }
    // Check children in reverse order (last drawn = topmost = highest priority).
    for (i, child) in widget.children().iter().enumerate().rev() {
        let child_local = child_local_pos(widget, child.bounds(), local_pos);
        if let Some(mut sub_path) = hit_test_subtree(child.as_ref(), child_local) {
            sub_path.insert(0, i);
            return Some(sub_path);
        }
    }
    Some(vec![]) // hit this widget, no child claimed it
}

/// Return the path to the topmost active modal subtree, ignoring normal
/// hit-testing bounds. Modal overlays paint at app level, so their event
/// routing must also bypass regular child clipping/window hit regions.
pub fn active_modal_path(widget: &dyn Widget) -> Option<Vec<usize>> {
    if !widget.is_visible() {
        return None;
    }
    for (i, child) in widget.children().iter().enumerate().rev() {
        if let Some(mut sub_path) = active_modal_path(child.as_ref()) {
            sub_path.insert(0, i);
            return Some(sub_path);
        }
    }
    if widget.has_active_modal() {
        Some(vec![])
    } else {
        None
    }
}

/// Path to the first visible widget, in depth-first paint order, that
/// answers `true` to `pred`. Subtrees that own an active modal are skipped
/// when `skip_modals` is set — the `App` uses that for the root-level
/// default/cancel action so a showing sheet's buttons (already offered the
/// key by the sheet itself) are never fired twice.
fn find_action_path(
    widget: &dyn Widget,
    pred: &dyn Fn(&dyn Widget) -> bool,
    skip_modals: bool,
) -> Option<Vec<usize>> {
    if !widget.is_visible() || (skip_modals && widget.has_active_modal()) {
        return None;
    }
    if pred(widget) {
        return Some(vec![]);
    }
    for (i, child) in widget.children().iter().enumerate() {
        if let Some(mut sub) = find_action_path(child.as_ref(), pred, skip_modals) {
            sub.insert(0, i);
            return Some(sub);
        }
    }
    None
}

/// Path to the first visible widget whose `WidgetBase` has
/// [`default_action`](crate::layout_props::WidgetBase::default_action)
/// set, or `None`. See `find_action_path` for `skip_modals`.
pub fn default_action_path(widget: &dyn Widget, skip_modals: bool) -> Option<Vec<usize>> {
    find_action_path(
        widget,
        &|w| w.widget_base().is_some_and(|b| b.default_action),
        skip_modals,
    )
}

/// Path to the first visible widget whose `WidgetBase` has
/// [`cancel_action`](crate::layout_props::WidgetBase::cancel_action) set,
/// or `None`.
pub fn cancel_action_path(widget: &dyn Widget, skip_modals: bool) -> Option<Vec<usize>> {
    find_action_path(
        widget,
        &|w| w.widget_base().is_some_and(|b| b.cancel_action),
        skip_modals,
    )
}

/// Activate the widget at `path` as a keyboard default/cancel action by
/// dispatching an `Enter` key-down to it (a `Button` fires its click and
/// consumes it). Returns the dispatch result — `Ignored` when the target
/// is disabled or otherwise declined — so the caller can fall back.
pub fn activate_action_at(root: &mut dyn Widget, path: &[usize]) -> EventResult {
    let event = Event::KeyDown {
        key: Key::Enter,
        modifiers: Modifiers::default(),
    };
    dispatch_event_dyn(root, path, &event, Point::ORIGIN)
}

/// Return the topmost widget whose app-level overlay contains `local_pos`.
///
/// This intentionally ignores ancestor `hit_test` bounds while descending:
/// global overlays such as ComboBox popups are painted outside their normal
/// parent clip/bounds, so their event routing must escape those bounds too.
pub fn global_overlay_hit_path(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
    if !widget.is_visible() {
        return None;
    }
    for (i, child) in widget.children().iter().enumerate().rev() {
        let child_local = child_local_pos(widget, child.bounds(), local_pos);
        if let Some(mut sub_path) = global_overlay_hit_path(child.as_ref(), child_local) {
            sub_path.insert(0, i);
            return Some(sub_path);
        }
    }
    if widget.hit_test_global_overlay(local_pos) {
        Some(vec![])
    } else {
        None
    }
}

/// Centralized event delivery: call the widget's `on_event` and apply the
/// framework's automatic-invalidation policy in ONE place, so no dispatch
/// path can silently forget it.  A [`EventResult::Consumed`] result schedules
/// a repaint via [`crate::animation::request_draw`];
/// [`EventResult::ConsumedQuiet`] and [`EventResult::Ignored`] do not.
///
/// This single hook makes "consume ⇒ repaint" the default, fixing the
/// recurring bug class where a widget mutates paint-affecting state on an
/// event but forgets to request a draw, leaving part of itself stale.
fn deliver(widget: &mut dyn Widget, event: &Event) -> EventResult {
    auto_request_draw(widget.on_event(event))
}

/// Apply the auto-invalidation policy to an already-computed [`EventResult`].
/// Shared by [`deliver`] (for `on_event`) and the unconsumed-key path (for
/// `on_unconsumed_key`) so every delivery route honours the same rule.
fn auto_request_draw(result: EventResult) -> EventResult {
    if result.requests_redraw() {
        crate::animation::request_draw();
    }
    result
}

/// Dispatch `event` through a path (list of child indices from the root).
/// The event bubbles leaf → root; returns a consuming result if any widget
/// consumed it (preserving the `Consumed` vs `ConsumedQuiet` distinction).
///
/// `pos_in_root` is the event position in the root widget's coordinate space.
/// The function translates it down through each level of the path.
pub fn dispatch_event(
    root: &mut Box<dyn Widget>,
    path: &[usize],
    event: &Event,
    pos_in_root: Point,
) -> EventResult {
    if path.is_empty() {
        let before = crate::animation::invalidation_epoch();
        let result = deliver(root.as_mut(), event);
        if result.requests_redraw() || before != crate::animation::invalidation_epoch() {
            root.mark_dirty();
        }
        return result;
    }
    let idx = path[0];
    // Path can become stale between when it was captured (hit-test or
    // previous-frame hovered/focus) and when it is dispatched — e.g. a
    // CollapsingHeader collapsed since then and dropped its child.  Rather
    // than panic, just stop descending and deliver the event at this level.
    if idx >= root.children().len() {
        return deliver(root.as_mut(), event);
    }
    let child_bounds = root.children()[idx].bounds();
    let child_pos = child_local_pos(root.as_ref(), child_bounds, pos_in_root);
    let translated_event = translate_event(event, child_pos);

    let before_child = crate::animation::invalidation_epoch();
    let child_result = dispatch_event(
        &mut root.children_mut()[idx],
        &path[1..],
        &translated_event,
        child_pos,
    );
    // A child (or descendant) that requested a draw bumped the epoch —
    // invalidate our own cache so the retained backbuffer re-rasters. A
    // quiet consume bumps nothing, so it correctly leaves the cache alone.
    if before_child != crate::animation::invalidation_epoch() {
        root.mark_dirty();
    }
    if child_result.is_consumed() {
        return child_result;
    }
    // Bubble: deliver to this widget too (with original pos_in_root coords).
    let before_self = crate::animation::invalidation_epoch();
    let result = deliver(root.as_mut(), event);
    if result.requests_redraw() || before_self != crate::animation::invalidation_epoch() {
        root.mark_dirty();
    }
    result
}

/// Variant of [`dispatch_event`] that accepts `&mut dyn Widget` as the
/// root. Useful when a parent owns a sub-tree by concrete type (e.g.
/// `Window`'s `title_bar: WindowTitleBar`) and wants to route an event
/// into it via the framework's standard hit-test + bubble dispatch,
/// instead of running coordinate hit-tests inline.
pub fn dispatch_event_dyn(
    root: &mut dyn Widget,
    path: &[usize],
    event: &Event,
    pos_in_root: Point,
) -> EventResult {
    if path.is_empty() {
        let before = crate::animation::invalidation_epoch();
        let result = deliver(root, event);
        if result.requests_redraw() || before != crate::animation::invalidation_epoch() {
            root.mark_dirty();
        }
        return result;
    }
    let idx = path[0];
    if idx >= root.children().len() {
        return deliver(root, event);
    }
    let child_bounds = root.children()[idx].bounds();
    let child_pos = child_local_pos(root, child_bounds, pos_in_root);
    let translated_event = translate_event(event, child_pos);

    let before_child = crate::animation::invalidation_epoch();
    // After the first hop we're inside the Vec<Box<dyn Widget>>, so we
    // can fall back to the regular Box-based dispatcher.
    let child_result = dispatch_event(
        &mut root.children_mut()[idx],
        &path[1..],
        &translated_event,
        child_pos,
    );
    if before_child != crate::animation::invalidation_epoch() {
        root.mark_dirty();
    }
    if child_result.is_consumed() {
        return child_result;
    }
    let before_self = crate::animation::invalidation_epoch();
    let result = deliver(root, event);
    if result.requests_redraw() || before_self != crate::animation::invalidation_epoch() {
        root.mark_dirty();
    }
    result
}

/// Give visible widgets a chance to handle a key ignored by the focused path.
///
/// Traverses in reverse paint order so topmost windows/menu bars win.
pub fn dispatch_unconsumed_key(
    widget: &mut dyn Widget,
    key: &Key,
    modifiers: Modifiers,
) -> EventResult {
    if !widget.is_visible() {
        return EventResult::Ignored;
    }
    let mut consumed = None;
    for child in widget.children_mut().iter_mut().rev() {
        let r = dispatch_unconsumed_key(child.as_mut(), key, modifiers);
        if r.is_consumed() {
            consumed = Some(r);
            break;
        }
    }
    if let Some(r) = consumed {
        widget.mark_dirty();
        return r;
    }
    let before = crate::animation::invalidation_epoch();
    let result = auto_request_draw(widget.on_unconsumed_key(key, modifiers));
    if result.requests_redraw() || before != crate::animation::invalidation_epoch() {
        widget.mark_dirty();
    }
    result
}

/// Produce a version of `event` with mouse positions replaced by `new_pos`.
/// Non-mouse events (key, focus) are returned unchanged.
fn translate_event(event: &Event, new_pos: Point) -> Event {
    match event {
        Event::MouseMove { .. } => Event::MouseMove { pos: new_pos },
        Event::MouseDown {
            button, modifiers, ..
        } => Event::MouseDown {
            pos: new_pos,
            button: *button,
            modifiers: *modifiers,
        },
        Event::MouseUp {
            button, modifiers, ..
        } => Event::MouseUp {
            pos: new_pos,
            button: *button,
            modifiers: *modifiers,
        },
        Event::MouseWheel {
            delta_y,
            delta_x,
            modifiers,
            ..
        } => Event::MouseWheel {
            pos: new_pos,
            delta_y: *delta_y,
            delta_x: *delta_x,
            modifiers: *modifiers,
        },
        Event::FileDropped { paths, .. } => Event::FileDropped {
            pos: new_pos,
            paths: paths.clone(),
        },
        // Localize the centroid exactly like a mouse `pos`, but leave the
        // deltas untouched: `translation_delta` (and zoom/rotation) are
        // displacement vectors, invariant under the pure-translation change
        // of coordinate frame that descending the tree performs.
        Event::MultiTouch { info } => {
            let mut info = *info;
            info.center_pos = new_pos;
            Event::MultiTouch { info }
        }
        other => other.clone(),
    }
}

/// Offer `event` to every visible widget in the subtree, depth-first with
/// the same topmost-last-child priority as [`hit_test_subtree`], stopping
/// at the first consumer. Positions are translated into each widget's
/// local space on the way down, exactly like [`dispatch_event`].
///
/// This exists for events that must not be lost when the widget under
/// their position ignores them — file drops foremost: native shells can't
/// always produce an accurate drop position (winit's Windows backend
/// discards the OLE drop point and emits no CursorMoved during the drag),
/// so the position may point at chrome while the only interested handler
/// (a canvas) sits in a sibling subtree the bubble path never reaches.
pub fn dispatch_event_broadcast(
    root: &mut Box<dyn Widget>,
    event: &Event,
    pos_in_root: Point,
) -> EventResult {
    if !root.is_visible() {
        return EventResult::Ignored;
    }
    for i in (0..root.children().len()).rev() {
        let child_bounds = root.children()[i].bounds();
        let child_pos = child_local_pos(root.as_ref(), child_bounds, pos_in_root);
        let translated = translate_event(event, child_pos);
        let r = dispatch_event_broadcast(&mut root.children_mut()[i], &translated, child_pos);
        if r.is_consumed() {
            root.mark_dirty();
            return r;
        }
    }
    let result = deliver(root.as_mut(), event);
    if result.is_consumed() {
        root.mark_dirty();
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::touch_state::{MultiTouchInfo, TouchDeviceId};

    /// `translate_event` must localize a `MultiTouch` centroid exactly like a
    /// mouse `pos`, while leaving the displacement deltas (translation, zoom,
    /// rotation) untouched — they are frame-invariant vectors.
    #[test]
    fn multi_touch_localizes_center_but_not_deltas() {
        let info = MultiTouchInfo {
            device_id: TouchDeviceId(0),
            num_touches: 2,
            zoom_delta: 1.5,
            rotation_delta: 0.3,
            translation_delta: Point::new(10.0, 5.0),
            force: 0.0,
            center_pos: Point::new(100.0, 200.0),
        };
        let local = Point::new(7.0, 9.0);
        let out = translate_event(&Event::MultiTouch { info }, local);
        let Event::MultiTouch { info: out } = out else {
            panic!("translate_event changed the event kind");
        };
        // Centroid follows the localized position.
        assert_eq!(out.center_pos.x, 7.0);
        assert_eq!(out.center_pos.y, 9.0);
        // Displacement vector unchanged — NOT offset by the localization.
        assert_eq!(out.translation_delta.x, 10.0);
        assert_eq!(out.translation_delta.y, 5.0);
        assert_eq!(out.zoom_delta, 1.5);
        assert_eq!(out.rotation_delta, 0.3);
    }
}