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}