Skip to main content

teksilo_scene/view/
widget_trait.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `Widget` trait implementation for [`SceneView`].
5//!
6//! Each method is a thin delegation to its `*_impl` counterpart in the
7//! appropriate impl-split module (`build_impl`, `layout_impl`, `paint_impl`,
8//! `a11y_impl`). Notable flags: `clips_children` is `true` (the scene clips
9//! at the viewport boundary), `preserves_children_on_rebuild` is `true`
10//! (heavyweight widgets survive scene rebuild and are only destroyed when their
11//! item is removed), and `wants_descendant_redirects` is `true` (lightweight
12//! item AT nodes are synthetic — the a11y walker reparents them into the
13//! scene's virtual accessibility tree rather than as normal widget children).
14
15use super::*;
16
17impl Widget for SceneView {
18    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
19        self.build_impl(ctx)
20    }
21
22    fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
23        self.layout_response_impl(proposal, ctx)
24    }
25
26    fn place_children(
27        &self,
28        bounds: Rect,
29        proposal: SizeProposal,
30        children: &mut [WidgetPlacement],
31        ctx: &LayoutContext,
32    ) {
33        self.place_children_impl(bounds, proposal, children, ctx)
34    }
35
36    fn paint(&self, bounds: Rect, canvas: &mut teksilo_canvas::Canvas, ctx: &PaintContext) {
37        self.paint_impl(bounds, canvas, ctx)
38    }
39
40    fn wants_post_paint(&self) -> bool {
41        self.wants_post_paint_impl()
42    }
43
44    fn post_paint(&self, bounds: Rect, canvas: &mut teksilo_canvas::Canvas, ctx: &PaintContext) {
45        self.post_paint_impl(bounds, canvas, ctx)
46    }
47
48    fn clips_children(&self) -> bool {
49        true
50    }
51
52    /// Reject a heavyweight child for a point a press claimant **painted above
53    /// that child** covers, so the arena's verdict matches the one this view's
54    /// own dispatch gives.
55    ///
56    /// `point` is in scene coordinates: the view is a `content_transform` node,
57    /// so the arena has already mapped the pointer through the inverse view
58    /// transform before offering it to children.
59    ///
60    /// Without this, the two pickers answer independently and only the *tap*
61    /// can be papered over: press feedback, focus-on-release, the touch hold
62    /// route (hence touch tooltips and context menus), the cursor and the
63    /// view's own drag recognizer all resolve from the arena's target, so five
64    /// of the six would stay on the card while the sixth went to the item.
65    ///
66    /// **Above that child**, not "in the `Over` band": the question is asked
67    /// per child and answered by comparing whole
68    /// [`PaintKey`]s — so an
69    /// [`Interleaved`](crate::SceneLayer::Interleaved) claimant vetoes the
70    /// cards it is painted over and leaves the ones painted over *it* alone.
71    /// The `Over` band is above every card, so for it this is the rule it
72    /// always was.
73    ///
74    /// A child with no key — anything this view did not place, and the
75    /// paint-only nodes, which never reach here because they are
76    /// `event_pass_through` — is treated as sitting at the bottom of the widget
77    /// rank, which is the permissive pre-existing answer.
78    ///
79    /// The rule is [`claims_press`](crate::claims_press), not "is painted": a
80    /// decorative halo drawn over an embedded note never vetoes, so clicking it
81    /// leaves focus in the note and leaves explore-by-touch announcing the
82    /// note — which is the behaviour the crate's per-item AT tree exists to
83    /// protect.
84    ///
85    /// # The order of the two questions is the cost of the feature
86    ///
87    /// The claimant is resolved **first** and the child's key looked up only
88    /// against an actual answer. Both halves of that matter, and getting either
89    /// backwards has already cost real time here:
90    ///
91    /// * the claimant query is memoised for the whole hit walk, so the arena's
92    ///   once-per-child call costs one snapshot scan for the walk rather than
93    ///   one per child — which is only true because the memoised question is
94    ///   floor-free (see `press_claimant_above` in `view.rs`);
95    /// * `SceneView::child_paint_key` is a `RefCell` borrow
96    ///   and a `HashMap` probe. Passed as an *argument* it is evaluated before
97    ///   the claimant check can decline it, so a scene with no press claimant at
98    ///   all — the case the `over_claimants` short-circuit exists to keep free —
99    ///   still paid for one on every child it was asked about. Asked in this
100    ///   order it pays for none.
101    ///
102    /// The arena's walk stops at the first child it can descend into, so the
103    /// per-child cost only multiplies when the veto is *rejecting* — which is
104    /// exactly the scene the feature is for, and why the claimant query has to
105    /// answer from a memo rather than a scan.
106    fn accepts_child_hit(&self, child: WidgetId, point: teksilo_canvas::Point) -> bool {
107        match self.topmost_press_claimant(point) {
108            None => true,
109            // Looked up only against an actual claimant — see the second
110            // bullet above. Binding it to a local first would evaluate it for
111            // every child of every scene, including the ones with no claimant
112            // at all.
113            Some(top) => top < self.child_paint_key(child),
114        }
115    }
116
117    fn preserves_children_on_rebuild(&self) -> bool {
118        true
119    }
120
121    /// A scene holds far more than it shows, and the half it does not show
122    /// should not be *reachable*: a card 90 000 px away is a Tab stop between
123    /// two visible ones and a node assistive tech is offered. Collapsing its
124    /// size to zero answers neither — a zero-size widget is still alive.
125    ///
126    /// So `place_children` is handed every card, parked ones included, and
127    /// reads back `WidgetPlacement::dormant`. See `place_children_impl` for
128    /// the two regions that decide it, and `SceneView::retention_margin`.
129    fn culls_children(&self) -> bool {
130        true
131    }
132
133    fn wants_descendant_redirects(&self) -> bool {
134        true
135    }
136
137    /// The cards this view offers assistive technology — a subset of its arena
138    /// children, in the same order.
139    ///
140    /// Being alive and being enumerated are separate questions here (see
141    /// `place_children_impl`), and this is the hook that keeps the second one
142    /// answerable at all. The framework walker uses this list for BOTH the
143    /// child push and the recursion, so a card left out of it is neither named
144    /// by a parent nor emitted as a node: no orphan, no dangling child,
145    /// nothing downstream to clean up. Suppressing through the redirect hook
146    /// instead would leave the node emitted and unreferenced, which the
147    /// consumer rejects.
148    ///
149    /// `None` — the framework's "use the arena's children" — until the first
150    /// `place_children`, because before that nothing has been decided.
151    ///
152    /// A card in this state is still a Tab stop. That is the honest reading of
153    /// the two knobs it sits between: it is alive because
154    /// [`retention_margin`](SceneView::retention_margin) said to keep it warm,
155    /// and unlisted because
156    /// [`a11y_off_screen_mode`](SceneView::a11y_off_screen_mode) said not to
157    /// offer it. Landing focus on it pins it, and the pass that lands the focus
158    /// publishes it.
159    fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
160        self.at_children.borrow().clone()
161    }
162
163    fn a11y_redirect_descendant(
164        &self,
165        self_id: WidgetId,
166        descendant: WidgetId,
167    ) -> Option<accesskit::NodeId> {
168        self.a11y_redirect_descendant_impl(self_id, descendant)
169    }
170
171    fn accessibility(&self, builder: &mut teksilo_core::accessibility::AccessNodeBuilder) {
172        self.accessibility_impl(builder)
173    }
174
175    fn as_any(&self) -> Option<&dyn std::any::Any> {
176        Some(self)
177    }
178
179    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
180        Some(self)
181    }
182}