smudge-core 0.2.0

Widget tree, reactive bindings, layout, paint and damage tracking for the Smudge GUI library
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
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
//! Keyboard focus: which node holds it, how it moves, and how keys route.
//!
//! The focused node hears key events before they bubble to its ancestors.
//! Focus moves by a press on a focusable node or by Tab navigation, and the
//! keyboard navigation mode records which of the two moved it last, so a
//! focus ring can draw for keyboard users alone. A pointer press turns the
//! mode off again.

use crate::geometry::{Point, Rect};
use crate::input::{Handled, Key, KeyEvent};
use crate::widget::Widget;

use super::interaction::Interaction;
use super::{FocusTrap, WidgetId, WidgetTree};

#[cfg(test)]
#[path = "focus_tests.rs"]
mod tests;

/// Returns whether `event` is a fresh press of Enter or Space, the keys that
/// activate a control.
///
/// Releases and key repeats activate nothing, so holding a key fires a button
/// once.
fn is_activation_key(event: &KeyEvent) -> bool {
    event.pressed && !event.repeat && matches!(event.key, Key::Enter | Key::Space)
}

impl WidgetTree {
    /// Routes a key event from the focused node through its ancestors.
    ///
    /// The focused node hears the key first, then its parent, and so on to the
    /// root, until a handler reports [`Handled::Yes`]. With nothing holding
    /// the focus, the key starts at the root, so a shortcut mounted there
    /// hears a key pressed anywhere in the window. While a focus trap is
    /// active, a key that would start outside it, because the focus is
    /// outside it or nothing holds the focus, starts at the trap instead and
    /// bubbles from there to the root. A press of Tab or an
    /// arrow key turns the keyboard navigation mode on, whether or not a
    /// handler took the key, and an unhandled Tab or Shift+Tab moves the focus
    /// to the next or previous focusable node in tree order, confined to the
    /// active focus trap when one is mounted. Releases and every other key
    /// only route.
    ///
    /// A focused node that became disabled or hidden since it took the focus
    /// loses it first, and its own handler does not run: a disabled button
    /// must not activate from the keyboard. The key still bubbles from it to
    /// the ancestors that can hear it, so shortcuts above it keep working.
    pub(crate) fn key(&mut self, event: &KeyEvent) {
        // A node that lost the focus by becoming disabled or hidden still
        // anchors the next key, so shortcuts above it keep working. The key
        // starts at its parent: the node itself no longer holds the focus,
        // even if it has been enabled again since. With no anchor either,
        // because nothing ever held the focus or a press consumed the last
        // one, the key starts at the root.
        let start = match self.focused {
            Some(focused) => Some(focused),
            None => self
                .stale_focus
                .take()
                .and_then(|anchor| self.nodes.get(anchor)?.parent)
                .or(self.root),
        };
        // An active trap holds the keyboard: a key that would start outside
        // it starts at the trap, so the trap's own handlers hear it before
        // the shortcuts above it, and a control behind it never does.
        let start = self.confine_to_trap(start);
        self.drop_invalid_focus();
        let handled = self.route_key(start, event);
        if !event.pressed {
            return;
        }
        match event.key {
            Key::Tab => {
                self.set_keyboard_nav(true);
                if handled != Handled::Yes {
                    self.move_focus(event.modifiers.shift);
                }
            }
            Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown => {
                self.set_keyboard_nav(true);
            }
            _ => {}
        }
    }

    /// Returns the active focus trap when `start` lies outside it or is
    /// `None`, and `start` otherwise, so routed input never starts behind an
    /// active trap.
    fn confine_to_trap(&self, start: Option<WidgetId>) -> Option<WidgetId> {
        match self.active_focus_trap() {
            Some(trap) if start.is_none_or(|start| !self.is_within(start, trap)) => Some(trap),
            _ => start,
        }
    }

    /// Delivers the event to `start` and its ancestors, deepest first, and
    /// returns whether a handler took it.
    ///
    /// `start` is the node the key routes from: the focused node, the parent
    /// of the node the focus fell off of, or the root when nothing anchors
    /// the key.
    fn route_key(&mut self, start: Option<WidgetId>, event: &KeyEvent) -> Handled {
        let mut next = start;
        while let Some(id) = next {
            next = self.nodes.get(id).and_then(|node| node.parent);
            // A disabled or hidden node's handlers never run, but the key keeps
            // bubbling, so shortcuts above it still work.
            if self.nodes.get(id).is_some_and(|node| {
                node.interaction.has(Interaction::DISABLED) || node.resolved.layout.hidden
            }) {
                continue;
            }
            // A control that activates on Enter or Space fires its press
            // handler before any key handler of its own hears the key, and a
            // handler that ran takes the key.
            if is_activation_key(event)
                && self
                    .nodes
                    .get(id)
                    .is_some_and(|node| node.handlers.activate_on_keys)
                && self.run_press(id)
            {
                return Handled::Yes;
            }
            let Some(owner) = self.owner(id).cloned() else {
                continue;
            };
            let mut handled = Handled::No;
            owner.with(|| {
                if let Some(on_key) = self
                    .nodes
                    .get_mut(id)
                    .and_then(|node| node.handlers.on_key.as_mut())
                {
                    handled = on_key(event);
                }
            });
            if handled == Handled::Yes {
                return Handled::Yes;
            }
        }
        Handled::No
    }

    /// Delivers committed input method text to the focused node.
    ///
    /// The commit walks up from the focused node, like a key event, and the
    /// first `on_ime_commit` handler that runs takes it. A committed string
    /// can arrive with no key event at all: a commit is how an input method
    /// delivers the text the user composed, so an editor that reads keys
    /// alone would lose it. A focused node that lost the focus by becoming
    /// disabled or hidden does not hear the commit, but its ancestors still
    /// can.
    pub(crate) fn ime_commit(&mut self, text: &str) {
        self.route_from_focus(|tree, id| {
            let Some(on_ime_commit) = tree
                .nodes
                .get_mut(id)
                .and_then(|node| node.handlers.on_ime_commit.as_mut())
            else {
                return false;
            };
            on_ime_commit(text);
            true
        });
    }

    /// Delivers a change of the composing text to the focused node.
    ///
    /// Non-empty text shows or updates the preedit, empty text clears it,
    /// which is how a canceled composition arrives. The routing is the
    /// commit's: the deepest `on_ime_preedit` handler in the focused chain
    /// takes it, and a node that can no longer hold the focus does not hear
    /// it while its ancestors still can.
    pub(crate) fn ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) {
        self.route_from_focus(|tree, id| {
            let Some(on_ime_preedit) = tree
                .nodes
                .get_mut(id)
                .and_then(|node| node.handlers.on_ime_preedit.as_mut())
            else {
                return false;
            };
            on_ime_preedit(text, cursor);
            true
        });
    }

    /// Runs `deliver` on the focused node and its ancestors, deepest first,
    /// and stops at the first handler that runs.
    ///
    /// The event starts at the node that held the focus when it arrived. A
    /// focused node that became disabled or hidden delivers nothing, but the
    /// event keeps bubbling past it, like a key does. While a focus trap is
    /// active, an event that would start outside it starts at the trap, as a
    /// key does, so text typed while a dialog is open never reaches an
    /// editor behind it.
    fn route_from_focus(&mut self, mut deliver: impl FnMut(&mut Self, WidgetId) -> bool) {
        let start = match self.focused {
            Some(focused) => Some(focused),
            None => self
                .stale_focus
                .take()
                .and_then(|anchor| self.nodes.get(anchor)?.parent),
        };
        let start = self.confine_to_trap(start);
        self.drop_invalid_focus();
        let mut next = start;
        while let Some(id) = next {
            next = self.nodes.get(id).and_then(|node| node.parent);
            if self.nodes.get(id).is_some_and(|node| {
                node.interaction.has(Interaction::DISABLED) || node.resolved.layout.hidden
            }) {
                continue;
            }
            let Some(owner) = self.owner(id).cloned() else {
                continue;
            };
            let mut ran = false;
            owner.with(|| {
                ran = deliver(self, id);
            });
            if ran {
                return;
            }
        }
    }

    /// Moves the focus to the next focusable node in tree order, or to the
    /// previous one when `backwards` is set, wrapping at both ends.
    ///
    /// When the walk finds nothing to move to while a node holds the focus,
    /// which happens when the active focus trap holds no focusable node, the
    /// focus stays where it is.
    fn move_focus(&mut self, backwards: bool) {
        let next = self.next_focusable(self.focused, backwards);
        if next.is_none() && self.focused.is_some() {
            return;
        }
        self.set_focus(next);
        self.scroll_focus_into_view();
    }

    /// Focuses `id`, or clears the focus when `id` is `None`.
    ///
    /// The node that loses the focus clears its focused bit and hears
    /// `on_focus` with `false` before the node that gains it is marked and
    /// hears `on_focus` with `true`. A node that already holds the focus keeps
    /// it without hearing `on_focus` again. The keyboard focus bit follows the
    /// keyboard navigation mode. Any change consumes the key anchor a dropped
    /// focus left behind, because the focus has moved on purpose.
    pub(super) fn set_focus(&mut self, id: Option<WidgetId>) {
        if self.focused == id {
            return;
        }
        let old = self.focused;
        self.stale_focus = None;
        if let Some(old) = self.focused {
            self.set_bit(old, Interaction::FOCUSED, false);
            self.set_bit(old, Interaction::KEYBOARD_FOCUS, false);
        }
        self.focused = id;
        if let Some(new) = id {
            self.set_bit(new, Interaction::FOCUSED, true);
            if self.keyboard_nav {
                self.set_bit(new, Interaction::KEYBOARD_FOCUS, true);
            }
        }
        if let Some(old) = old {
            self.push_a11y(old);
        }
        if let Some(new) = id {
            self.push_a11y(new);
        }
    }

    /// Focuses `id` on behalf of an accessibility action, and returns whether
    /// it took the focus.
    ///
    /// A node that is not focusable, or that cannot hold the focus because it
    /// is disabled or hidden, is left alone and `false` is returned. A node
    /// that already holds the focus keeps it.
    pub(crate) fn request_focus(&mut self, id: WidgetId) -> bool {
        if !self.focusable(id) || !self.can_hold_focus(id) {
            return false;
        }
        self.set_focus(Some(id));
        true
    }

    /// Turns the keyboard navigation mode on or off.
    ///
    /// The focused node carries the keyboard focus bit only while the mode is
    /// on, so a style can react to keyboard focus alone. A pointer press
    /// clears the mode, which clears the bit of a node that stays focused.
    pub(super) fn set_keyboard_nav(&mut self, on: bool) {
        if self.keyboard_nav == on {
            return;
        }
        self.keyboard_nav = on;
        if let Some(focused) = self.focused {
            self.set_bit(focused, Interaction::KEYBOARD_FOCUS, on);
        }
    }

    /// Returns the node that holds the keyboard focus, when one does.
    pub(crate) fn focused(&self) -> Option<WidgetId> {
        self.focused
    }

    /// Clears the focus when the node that holds it can no longer keep it.
    ///
    /// A node that became disabled or hidden loses the focus at once, so a
    /// focus style never draws on a control that is switched off, instead of
    /// holding it until the next key event notices. The node hears
    /// `on_focus` with `false` as it goes. The node stays as the anchor the
    /// next key event bubbles from, so a shortcut above it keeps working.
    pub(super) fn drop_invalid_focus(&mut self) {
        let Some(focused) = self
            .focused
            .filter(|focused| !self.can_hold_focus(*focused))
        else {
            return;
        };
        self.set_focus(None);
        self.stale_focus = Some(focused);
    }

    /// Returns whether the keyboard navigation mode is on.
    pub(crate) fn keyboard_nav(&self) -> bool {
        self.keyboard_nav
    }

    /// Returns the area the focused widget reports to the input method, when
    /// it reports one.
    ///
    /// The widget paints in paint coordinates, so the rectangle it reports is
    /// in paint coordinates too, and this translates it back into window
    /// coordinates with the node's shift. A focused node that has since
    /// become disabled or hidden reports none, as it would hear no key, and
    /// so does one outside an active focus trap, which hears no text.
    pub(crate) fn ime_area(&self) -> Option<Rect> {
        let focused = self.focused.filter(|&id| self.can_hold_focus(id))?;
        if self
            .active_focus_trap()
            .is_some_and(|trap| !self.is_within(focused, trap))
        {
            return None;
        }
        let node = self.nodes.get(focused)?;
        let shift = node.paint_shift;
        let widget: &dyn Widget = node.widget.as_ref();
        widget.ime_area().map(|area| {
            area.translated(Point {
                x: -shift.x,
                y: -shift.y,
            })
        })
    }

    /// Returns the next focusable node in tree order after `from`, or the
    /// previous one when `backwards` is set, wrapping at both ends.
    ///
    /// Without a focused node, forward navigation starts at the first
    /// focusable node and backward navigation at the last. A hidden node and
    /// its subtree are skipped, and a disabled node is skipped while its
    /// children are still visited, matching hit testing. While a focus trap
    /// is mounted, the order holds only the focusable nodes inside it, so a
    /// Tab press from outside the trap enters it and one inside it never
    /// leaves.
    fn next_focusable(&self, from: Option<WidgetId>, backwards: bool) -> Option<WidgetId> {
        let mut order = Vec::new();
        if let Some(scope) = self.active_focus_trap().or(self.root) {
            self.collect_focusable(scope, &mut order);
        }
        if order.is_empty() {
            return None;
        }
        match from.and_then(|id| order.iter().position(|&candidate| candidate == id)) {
            Some(index) if backwards => Some(order[(index + order.len() - 1) % order.len()]),
            Some(index) => Some(order[(index + 1) % order.len()]),
            None if backwards => order.last().copied(),
            None => order.first().copied(),
        }
    }

    /// Appends the focusable nodes of the subtree, in paint order.
    fn collect_focusable(&self, id: WidgetId, order: &mut Vec<WidgetId>) {
        let Some(node) = self.nodes.get(id) else {
            return;
        };
        if node.resolved.layout.hidden {
            return;
        }
        let focusable = node.interaction.has(Interaction::FOCUSABLE)
            && !node.interaction.has(Interaction::DISABLED);
        if focusable {
            order.push(id);
        }
        for &child in &node.children {
            self.collect_focusable(child, order);
        }
    }

    /// Returns whether `id` may keep the focus: it is not disabled, and
    /// neither it nor any ancestor is hidden.
    ///
    /// A node only clipped out of view, for example scrolled away, keeps it.
    fn can_hold_focus(&self, id: WidgetId) -> bool {
        let Some(node) = self.nodes.get(id) else {
            return false;
        };
        if node.interaction.has(Interaction::DISABLED) {
            return false;
        }
        let mut next = Some(id);
        while let Some(current) = next {
            let Some(node) = self.nodes.get(current) else {
                return false;
            };
            if node.resolved.layout.hidden {
                return false;
            }
            next = node.parent;
        }
        true
    }

    /// Queues an autofocus request for the node `id`, to resolve once a
    /// layout pass has laid it out visible.
    ///
    /// A node's element calls this at mount. The request is resolved once,
    /// when [`WidgetTree::resolve_autofocus`] runs and finds the node
    /// visible, and a later frame never asks again.
    pub(crate) fn queue_autofocus(&mut self, id: WidgetId) {
        self.pending_autofocus.push(id);
    }

    /// Focuses the node that mounted with autofocus and has been laid out
    /// visible.
    ///
    /// A request whose node is hidden, or sits under a hidden ancestor,
    /// stays queued until a layout pass finds it visible. The other requests
    /// are walked in paint order, and the last one that can hold the focus
    /// wins, so when several nodes show in one frame the node painted last
    /// takes it. While a focus trap is mounted, only a node inside it can
    /// win, so a node that mounts elsewhere cannot pull the focus out of the
    /// trap. A visible request that cannot hold the focus, because its node
    /// is disabled, is skipped and an earlier node then wins; a skipped
    /// request is dropped and never asked again.
    pub(super) fn resolve_autofocus(&mut self) {
        if self.pending_autofocus.is_empty() {
            return;
        }
        let mut pending = std::mem::take(&mut self.pending_autofocus);
        let mut ready = Vec::new();
        pending.retain(|&id| {
            let hidden = self.hidden_in_tree(id);
            if !hidden {
                ready.push(id);
            }
            hidden
        });
        self.pending_autofocus = pending;
        if ready.is_empty() {
            return;
        }
        let Some(root) = self.root else {
            return;
        };
        let trap = self.active_focus_trap();
        let mut order = Vec::new();
        self.collect_pending(root, &ready, &mut order);
        for id in order.into_iter().rev() {
            if trap.is_some_and(|trap| !self.is_within(id, trap)) {
                continue;
            }
            if self.request_focus(id) {
                self.scroll_focus_into_view();
                return;
            }
        }
    }

    /// Returns whether the live node `id` or one of its ancestors is hidden.
    ///
    /// A stale id counts as visible, so a caller that resolves it finds
    /// nothing there.
    fn hidden_in_tree(&self, id: WidgetId) -> bool {
        let mut next = Some(id);
        while let Some(current) = next {
            let Some(node) = self.nodes.get(current) else {
                return false;
            };
            if node.resolved.layout.hidden {
                return true;
            }
            next = node.parent;
        }
        false
    }

    /// Marks the mounted node `id` as a focus trap.
    ///
    /// While a trap is the last one mounted and not hidden, Tab movement is
    /// confined to its subtree, keys start there when the focus is outside
    /// it, and a press outside it does not move the focus out. Removing the
    /// node ends the trap, restores the one mounted before it, and returns
    /// the focus to the node that held it when the trap became active.
    /// Hiding the trap returns the focus the same way, and showing it again
    /// records the focus anew; see [`WidgetTree::resolve_focus_traps`].
    pub(crate) fn push_focus_trap(&mut self, id: WidgetId) {
        let active = self.can_hold_focus(id);
        self.focus_traps.push(FocusTrap {
            node: id,
            restore: if active { self.focused } else { None },
            active,
        });
    }

    /// Records the focus of every trap that became able to hold it since the
    /// last look, and gives it back for every trap that stopped being able
    /// to.
    ///
    /// A trap that shows, or whose disabled state or hidden ancestor no
    /// longer keeps it from the focus, records the node that holds the focus
    /// now. A trap that hides, or becomes disabled, returns the focus to the
    /// node it recorded, when the focus sits inside the trap or nowhere and
    /// that node can still take it. The layout pass calls this, because that
    /// is where a hidden state lands, before an autofocus request inside a
    /// trap that just showed moves the focus in.
    pub(super) fn resolve_focus_traps(&mut self) {
        for at in 0..self.focus_traps.len() {
            let trap = self.focus_traps[at];
            let active = self.can_hold_focus(trap.node);
            if active == trap.active {
                continue;
            }
            self.focus_traps[at].active = active;
            if active {
                self.focus_traps[at].restore = self.focused;
                continue;
            }
            let focus_inside = self
                .focused
                .is_none_or(|focused| self.is_within(focused, trap.node));
            if focus_inside && let Some(restore) = trap.restore {
                self.request_focus(restore);
            }
        }
    }

    /// Returns the trap that keyboard movement is confined to, when one is
    /// active.
    ///
    /// The last mounted trap wins. A trap that cannot hold the focus,
    /// because it became disabled or hidden or sits under a hidden ancestor,
    /// yields to the one mounted before it; without one, movement ranges the
    /// whole tree, and the hidden trap's own subtree is out of reach either
    /// way.
    pub(super) fn active_focus_trap(&self) -> Option<WidgetId> {
        self.focus_traps
            .iter()
            .rev()
            .map(|trap| trap.node)
            .find(|&trap| self.can_hold_focus(trap))
    }

    /// Returns whether `id` is `ancestor` or one of its descendants.
    pub(super) fn is_within(&self, id: WidgetId, ancestor: WidgetId) -> bool {
        let mut next = Some(id);
        while let Some(current) = next {
            if current == ancestor {
                return true;
            }
            next = self.nodes.get(current).and_then(|node| node.parent);
        }
        false
    }

    /// Appends the nodes of `pending` that the subtree holds, in paint order.
    ///
    /// A hidden node and its subtree are skipped, because a hidden node
    /// cannot hold the focus.
    fn collect_pending(&self, id: WidgetId, pending: &[WidgetId], order: &mut Vec<WidgetId>) {
        let Some(node) = self.nodes.get(id) else {
            return;
        };
        if node.resolved.layout.hidden {
            return;
        }
        if pending.contains(&id) {
            order.push(id);
        }
        for &child in &node.children {
            self.collect_pending(child, pending, order);
        }
    }

    /// Scrolls the nearest scrolling ancestor so the focused node is in view.
    ///
    /// Nothing scrolls yet: scroll offsets arrive with the scroll view, and
    /// focus movement calls this hook so the behavior has one home.
    fn scroll_focus_into_view(&mut self) {}
}