teksilo_scene/view/paint_node.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The two paint positions a `SceneView` cannot reach from its own `paint`.
5//!
6//! The render walker gives a widget exactly two places to draw: before its
7//! child subtree (`paint`) and after it (`post_paint`). Those are the
8//! [`Under`](crate::SceneLayer::Under) and [`Over`](crate::SceneLayer::Over)
9//! bands, and between them sits the whole heavyweight tier as one block. Two
10//! things need a position *inside* that block:
11//!
12//! - **[`SceneLayer::Interleaved`](crate::SceneLayer::Interleaved)** — a
13//! lightweight item ordered against the cards by `z`, so a stroke can be
14//! above note A and below note B. [`SceneBandProxy`] is the node that carries
15//! one, slotted into the child list by [`PaintKey`].
16//! - **A wet surface** — content being authored right now, which must repaint
17//! on its own without dragging the item bands through a pass with it.
18//! [`WetLayer`] is the handle; [`WetLayerNode`] is its node.
19//!
20//! Both nodes are **paint only**, and that is what makes them affordable:
21//!
22//! | | consequence |
23//! |---|---|
24//! | `event_pass_through` | the pointer resolves through the lightweight hit snapshot exactly as before — one picker, still |
25//! | left out of `accessibility_children` | the AT tree is bit-identical; an interleaved item keeps the same synthetic node it had in `Under` |
26//! | not focusable, no handlers | not a Tab stop, so `tab_stops_within` is unchanged |
27//!
28//! # Why there is a bridge rather than a `&SceneView`
29//!
30//! A child node cannot borrow its parent widget. Everything the per-item paint
31//! loop reads is already a cloneable handle on the view — the model, the
32//! item cache, the four camera signals, the drag cell, the transform driver —
33//! so [`ScenePaintBridge`] is those handles in a bundle, and
34//! [`ScenePaintBridge::paint_items`] is the loop, called by the view's own
35//! `paint_band` **and** by these nodes. One implementation, so an interleaved
36//! item cannot render differently from the same item in `Under`.
37//!
38//! # Layer order
39//!
40//! Stated here because it is otherwise implicit in three files:
41//!
42//! 1. `SceneView::paint` — the app background closure, then the `Under` band
43//! 2. the arena's child walk, sorted by [`PaintKey`]:
44//! [`SceneBandProxy`] nodes and heavyweight cards interleaved by `z`
45//! 3. [`WetLayerNode`] — always the last child, so wet content sits above
46//! every card and every interleaved item
47//! 4. `SceneView::post_paint` — the `Over` band, the selection marquee, the app
48//! foreground closure, magnetism feedback, the transform chrome, the debug
49//! overlay
50//!
51//! So a wet stroke is drawn over the content it is being drawn *on*, and under
52//! the view's own chrome — a marquee, a magnet ghost and the debug overlay stay
53//! visible through it. An app whose dried ink lives in the `Over` band will see
54//! the stroke rise one step at the moment it dries; `Interleaved` (or `Under`)
55//! has no such step, which is the other reason ink belongs in a band of its
56//! own.
57
58use super::*;
59use crate::item::{ItemId, SceneItemPaintContext};
60use std::cell::{Cell, RefCell};
61use std::rc::{Rc, Weak};
62use teksilo_canvas::{Canvas, Point, Rect, Transform2D};
63
64/// Everything the per-item paint loop reads, as clonable handles.
65///
66/// Built by [`SceneView::paint_bridge`] — about a dozen refcount bumps, which
67/// is nothing beside the loop it serves — and held for the node's lifetime by
68/// [`SceneBandProxy`] and [`WetLayerNode`].
69#[derive(Clone)]
70pub(crate) struct ScenePaintBridge {
71 model: SceneModel,
72 item_cache: Rc<RefCell<crate::cache::ItemCoordinateCache>>,
73 pan_x: Signal<f32>,
74 pan_y: Signal<f32>,
75 zoom: Signal<f32>,
76 rotation: Signal<f32>,
77 bounds_origin: Signal<Vec2>,
78 drag_target: Rc<Cell<Option<DragTarget>>>,
79 selection: crate::selection::SceneSelection,
80 transform: Option<super::transform::TransformDriver>,
81 /// The view's own visible scene region, republished every `place_children`
82 /// and every `SceneView::paint`.
83 ///
84 /// A child node has no way to derive it — it knows its own scene rect, not
85 /// the viewport's — and an item reads it as a hint to skip geometry it
86 /// cannot show (a tiled grid is the case). Publishing rather than deriving
87 /// is safe in both directions: a pan is a relayout, so `place_children`
88 /// refreshes it before any node paints; and when nothing moved, last
89 /// pass's value is this pass's value.
90 visible_region: Rc<Cell<Rect>>,
91}
92
93impl std::fmt::Debug for ScenePaintBridge {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("ScenePaintBridge").finish_non_exhaustive()
96 }
97}
98
99impl ScenePaintBridge {
100 /// The composed view transform, computed from the same four signals
101 /// [`SceneView::view_transform`] reads — not snapshotted, so a node painted
102 /// out of step with the view still projects correctly.
103 pub(crate) fn view_transform(&self) -> Transform2D {
104 let bo = self.bounds_origin.get();
105 super::compose_view(
106 Vec2::new(self.pan_x.get() + bo.x, self.pan_y.get() + bo.y),
107 self.zoom.get(),
108 self.rotation.get(),
109 )
110 }
111
112 /// The viewport in scene coordinates, as the view last published it.
113 pub(crate) fn visible_region(&self) -> Rect {
114 self.visible_region.get()
115 }
116
117 /// Paint `ids`, in the order given, under the active view transform.
118 ///
119 /// Callers sort. This is the whole of the lightweight tier's per-item
120 /// rendering — the drag and transform previews, `IGNORES_TRANSFORMATIONS`
121 /// pinning, effective opacity, the item-coordinate frame cache and its
122 /// raster-scale refinement — and it is the only copy of it.
123 pub(crate) fn paint_items(
124 &self,
125 ids: &[ItemId],
126 canvas: &mut Canvas,
127 region: Rect,
128 ctx: &PaintContext,
129 ) {
130 if ids.is_empty() {
131 return;
132 }
133 // Glyph-epoch gate: cached item frames bake glyph atlas UVs, and this
134 // cache lives outside the widget arena, so the framework's eviction
135 // recovery (`invalidate_all_paints`) cannot reach it. Instead, every
136 // paint pass compares the backend's eviction epoch and drops all
137 // entries when it moved — the items repaint below with fresh UVs in the
138 // same pass.
139 let glyph_epoch = canvas
140 .text_backend()
141 .map(|tb| tb.borrow().glyph_epoch())
142 .unwrap_or(0);
143 self.item_cache.borrow_mut().sync_glyph_epoch(glyph_epoch);
144
145 // Ambient text raster scale, set by the paint walker for this
146 // SceneView's content-transform scope (it already includes the view's
147 // zoom). Items whose own pushed transform carries an additional scale
148 // refine it below so their text rasterizes at the full effective
149 // density.
150 let ambient_raster_scale = canvas
151 .text_backend()
152 .map(|tb| tb.borrow().raster_scale())
153 .unwrap_or(1.0);
154
155 let view_transform = self.view_transform();
156 let mut item_ctx = SceneItemPaintContext::new(view_transform, Some(region), ctx.theme)
157 .with_text_scale(ctx.text_scale)
158 .with_window_active(ctx.window_active);
159
160 let drag_target = self.drag_target.get();
161 // Every item the in-flight drag carries — the grabbed one alone, or the
162 // whole selection when the grab landed on a selected item. Resolved
163 // once per pass rather than per item, and from the same
164 // `drag_group_of` the commit uses, so the live feedback and the
165 // committed move can never show different sets.
166 let drag_group: Vec<ItemId> = drag_target
167 .map(|t| super::drag_group_of(&self.model, &self.selection, t.item_id))
168 .unwrap_or_default();
169 // The selection transform's live preview. The heavyweight tier reads
170 // the SAME function in `place_children` — one affine, two tiers, so a
171 // mixed selection cannot half-move mid-gesture.
172 let transform_preview = self
173 .transform
174 .as_ref()
175 .filter(|d| d.is_enabled())
176 .and_then(|d| d.preview());
177
178 for &id in ids {
179 let scene = self.model.0.borrow();
180 if scene.item(id).is_none() {
181 continue;
182 }
183 if !scene.is_effectively_visible(id) {
184 continue;
185 }
186 let flags = scene.flags(id).unwrap_or_default();
187 if flags.contains(crate::flags::ItemFlags::HAS_NO_CONTENTS) {
188 continue;
189 }
190 // Per-item enabled state drives `ColorProp` disabled-role
191 // resolution. AND-combine the item's own `IS_ENABLED` flag with the
192 // widget-tree's ancestor-disabled cascade, so a SceneView inside a
193 // disabled ancestor dims its lightweight items' role colours
194 // exactly like every other widget in that subtree.
195 item_ctx.enabled =
196 ctx.effective_enabled && flags.contains(crate::flags::ItemFlags::IS_ENABLED);
197
198 // Items that are the drag target or a declared descendant paint
199 // with a visual delta in scene coords — a child follows its dragged
200 // parent until the rebuild commits the new local_pos.
201 let drag_delta = drag_target
202 .filter(|_| {
203 drag_group
204 .iter()
205 .any(|g| *g == id || scene.is_descendant_of(id, *g))
206 })
207 .map(|t| {
208 Transform2D::translate(
209 t.current_scene.x - t.anchor_scene.x,
210 t.current_scene.y - t.anchor_scene.y,
211 )
212 });
213
214 // …and the transform controller's preview, on the roots it carries
215 // and everything hanging off them.
216 let transform_delta = transform_preview.as_ref().and_then(|(roots, xform)| {
217 roots
218 .iter()
219 .any(|r| *r == id || scene.is_descendant_of(id, *r))
220 .then_some(*xform)
221 });
222
223 let mut local_to_scene = scene.scene_transform(id);
224 if let Some(t) = drag_delta {
225 local_to_scene = local_to_scene.then(&t);
226 }
227 if let Some(t) = transform_delta {
228 local_to_scene = local_to_scene.then(&t);
229 }
230 canvas.save();
231 // IGNORES_TRANSFORMATIONS items pin at their parent-relative
232 // position but render at a fixed pixel size (Qt's
233 // `ItemIgnoresTransformations`).
234 let pushed_transform =
235 if flags.contains(crate::flags::ItemFlags::IGNORES_TRANSFORMATIONS) {
236 let scene_anchor = local_to_scene.apply_point(Point::ZERO);
237 let screen_anchor = view_transform.apply_point(scene_anchor);
238 let view_inv = view_transform
239 .inverse()
240 .unwrap_or_else(Transform2D::identity);
241 Transform2D::translate(screen_anchor.x, screen_anchor.y).then(&view_inv)
242 } else {
243 local_to_scene
244 };
245 canvas.apply_transform(pushed_transform);
246 let item_raster_scale = teksilo_canvas::quantize_raster_scale(
247 ambient_raster_scale * pushed_transform.geometric_scale(),
248 );
249 let raster_scale_changed = item_raster_scale != ambient_raster_scale;
250 if raster_scale_changed && let Some(tb) = canvas.text_backend() {
251 tb.borrow_mut().set_raster_scale(item_raster_scale);
252 }
253 let alpha = scene.effective_opacity(id);
254 let opacity_pushed = alpha < 0.999;
255 if opacity_pushed {
256 canvas.set_opacity(alpha);
257 }
258 if let Some(item) = scene.item(id) {
259 match item.cache_mode() {
260 crate::cache::CacheMode::ItemCoordinate => {
261 let cached = self.item_cache.borrow().get(id, item_raster_scale).cloned();
262 if let Some(frame) = cached {
263 canvas.draw_render_frame(&frame, Point::ZERO);
264 } else {
265 let mut sub = match canvas.text_backend() {
266 Some(tb) => Canvas::with_text_backend(tb.clone()),
267 None => Canvas::new(),
268 };
269 item.paint(&mut sub, &item_ctx);
270 let frame = sub.into_render_frame();
271 canvas.draw_render_frame(&frame, Point::ZERO);
272 self.item_cache
273 .borrow_mut()
274 .insert(id, frame, item_raster_scale);
275 }
276 }
277 crate::cache::CacheMode::None => {
278 item.paint(canvas, &item_ctx);
279 }
280 }
281 }
282 if raster_scale_changed && let Some(tb) = canvas.text_backend() {
283 tb.borrow_mut().set_raster_scale(ambient_raster_scale);
284 }
285 if opacity_pushed {
286 canvas.restore_opacity();
287 }
288 canvas.restore();
289 }
290 }
291}
292
293impl SceneView {
294 /// The handles a child paint node needs. See [`ScenePaintBridge`].
295 pub(crate) fn paint_bridge(&self) -> ScenePaintBridge {
296 ScenePaintBridge {
297 model: self.model.clone(),
298 item_cache: self.item_cache.clone(),
299 pan_x: self.pan_x.clone(),
300 pan_y: self.pan_y.clone(),
301 zoom: self.zoom.clone(),
302 rotation: self.rotation.clone(),
303 bounds_origin: self.bounds_origin_signal.clone(),
304 drag_target: self.drag_target.clone(),
305 selection: self.selection.clone(),
306 transform: self.transform_driver(),
307 visible_region: self.published_visible_region.clone(),
308 }
309 }
310}
311
312// ---------------------------------------------------------------------------
313// Interleaved band
314// ---------------------------------------------------------------------------
315
316/// The node that paints one [`SceneLayer::Interleaved`](crate::SceneLayer)
317/// item, so it can sit between two cards.
318///
319/// Paint only — see the module docs for the three properties that makes true
320/// and why they are what make the band affordable.
321pub(crate) struct SceneBandProxy {
322 bridge: ScenePaintBridge,
323 item: ItemId,
324}
325
326impl std::fmt::Debug for SceneBandProxy {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 f.debug_struct("SceneBandProxy")
329 .field("item", &self.item)
330 .finish()
331 }
332}
333
334impl SceneBandProxy {
335 pub(crate) fn new(bridge: ScenePaintBridge, item: ItemId) -> Self {
336 Self { bridge, item }
337 }
338}
339
340impl Widget for SceneBandProxy {
341 /// Zero, because the view writes this node's placement outright in
342 /// `place_children` — the item's own scene rect. Returning the item's size
343 /// here as well would be a second answer for the same question, and the
344 /// one the parent overrides.
345 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
346 Size::ZERO.into()
347 }
348
349 fn paint(&self, _bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
350 // `bounds` is deliberately unused: the item is painted at its own scene
351 // coordinates through `scene_transform`, exactly as it would be in
352 // `Under`, and this node's rect exists only to place it in the child
353 // list and to tell the walker roughly where it is. Painting relative to
354 // `bounds` would apply the position twice.
355 let region = self.bridge.visible_region();
356 self.bridge.paint_items(&[self.item], canvas, region, ctx);
357 }
358
359 /// Nothing. The item's AT node is the synthetic one the scene's own walker
360 /// emits — banding must not change what assistive technology is told.
361 fn accessibility(&self, builder: &mut teksilo_core::accessibility::AccessNodeBuilder) {
362 builder.set_hidden();
363 }
364}
365
366// ---------------------------------------------------------------------------
367// Wet layer
368// ---------------------------------------------------------------------------
369
370/// A surface for content being authored **right now**, which repaints without
371/// taking the scene's item bands with it.
372///
373/// The case is ink. A stroke in flight changes on every pointer sample, and
374/// there is no way to invalidate a `SceneView`'s foreground alone: the render
375/// walker computes **one** `needs_paint` per node and gates that node's `paint`
376/// and its `post_paint` with it, so asking the view to repaint re-runs the
377/// whole `Under` band as well. A wet layer is its own node, so
378/// [`request_repaint`](Self::request_repaint) marks that node and nothing else.
379///
380/// The painter draws in **scene** coordinates — the node lives inside the
381/// view's content transform, so a wet stroke pans and zooms with the page for
382/// free, and a scene point can be drawn at its own value.
383///
384/// Where it lands in the paint order, and why: above every card and every
385/// [`Interleaved`](crate::SceneLayer::Interleaved) item, under the `Over` band
386/// and the view's own chrome (marquee, magnet feedback, transform frame, debug
387/// overlay). `docs/teksilo-scene.md` states the whole order.
388///
389/// ```no_run
390/// # use std::cell::RefCell;
391/// # use std::rc::Rc;
392/// # use teksilo_scene::{Scene, SceneView, WetLayer};
393/// # use teksilo_canvas::Point;
394/// # use teksilo_tokens::Color;
395/// let points: Rc<RefCell<Vec<Point>>> = Rc::default();
396/// let wet = {
397/// let points = points.clone();
398/// WetLayer::new(move |canvas, _ctx| {
399/// for p in points.borrow().iter() {
400/// canvas.fill_circle(*p, 2.0, Color::BLACK);
401/// }
402/// })
403/// };
404/// let view = SceneView::new(Scene::new()).wet_layer(wet.clone());
405/// // …and from a pointer handler, after pushing a point:
406/// // wet.request_repaint(ctx);
407/// ```
408///
409/// A cloneable handle: clone it into the pointer handler that feeds it and into
410/// [`SceneView::wet_layer`], the way a [`SceneModel`] is cloned into its views.
411///
412/// # More than one view
413///
414/// The comparison with [`SceneModel`] is meant literally. Mount the same layer
415/// in two views — an overview beside a detail pane — and **both** paint it, and
416/// [`request_repaint`](Self::request_repaint) repaints both. A second view does
417/// not displace the first.
418///
419/// One thing does not carry across, and it is a property of the door rather
420/// than of this type: a repaint is requested through an
421/// [`EventContext`](teksilo_core::EventContext), and a context belongs to one
422/// window's tree. So `request_repaint` repaints the mounts **in the calling
423/// context's own window** and leaves a mount in another window to that window's
424/// own frame — an id from the other tree could not reach it anyway, because
425/// ids are per-arena slot keys and two trees mint the same ones.
426///
427/// That last sentence is the reason `request_repaint` does not simply push the
428/// ids it holds: it resolves each one in the calling tree through a typed door
429/// first, so a stale id can only ever land on one of this layer's own nodes
430/// there — or trip an assertion. The rule and its one remaining edge (the same
431/// layer mounted in two **window-less** trees) are written out at
432/// [`request_repaint`](Self::request_repaint).
433#[derive(Clone)]
434pub struct WetLayer(Rc<WetLayerInner>);
435
436struct WetLayerInner {
437 painter: Box<dyn Fn(&mut Canvas, &SceneItemPaintContext<'_>)>,
438 /// One entry per view that has mounted this layer.
439 ///
440 /// `Weak`, and the strong [`WetMount`] is owned by the view — so a view
441 /// that is destroyed takes its entry out of circulation without needing a
442 /// `Drop` impl on [`SceneView`] (which would forbid the by-value builder
443 /// methods from moving fields out of `self`).
444 mounts: RefCell<Vec<Weak<WetMount>>>,
445}
446
447/// One view's mount of a [`WetLayer`]: the node it paints into, and the window
448/// whose tree that node belongs to.
449///
450/// Held strongly by the [`SceneView`] that minted it and weakly by the layer,
451/// so its lifetime is the view's.
452pub(crate) struct WetMount {
453 node: WidgetId,
454 /// `None` for a tree with no window — a headless test. Two window-less
455 /// trees wear the same `None`, so this narrows the candidates and does not
456 /// on its own decide them; what makes the marking safe anyway is the
457 /// **typed** door [`WetLayer::request_repaint`] pushes ids through. See its
458 /// own doc.
459 window: Option<teksilo_core::window::TeksiloWindowId>,
460}
461
462/// One of a [`WetLayer`]'s mounted paint nodes, as
463/// [`WetLayer::nodes`] reports it.
464///
465/// A bare [`WidgetId`] is a slot key in *some* arena and says which one it is
466/// not: two trees mint the same ids, so a caller holding several trees cannot
467/// tell whose node it has. This pairs the id with the window whose tree it
468/// belongs to, which is the identity the framework has — `None` for a headless
469/// tree, where the caller owns the question and the count is the warning.
470#[derive(Clone, Copy, Debug, PartialEq, Eq)]
471#[non_exhaustive]
472pub struct WetNode {
473 id: WidgetId,
474 window: Option<teksilo_core::window::TeksiloWindowId>,
475}
476
477impl WetNode {
478 /// A node report. The framework builds these; the constructor exists so a
479 /// consumer can build one in a test of its own.
480 pub fn new(id: WidgetId, window: Option<teksilo_core::window::TeksiloWindowId>) -> Self {
481 Self { id, window }
482 }
483
484 /// The paint node, in the arena of the window below.
485 pub fn id(&self) -> WidgetId {
486 self.id
487 }
488
489 /// The window whose tree holds [`id`](Self::id), or `None` for a tree that
490 /// has no window.
491 pub fn window(&self) -> Option<teksilo_core::window::TeksiloWindowId> {
492 self.window
493 }
494}
495
496impl std::fmt::Debug for WetLayer {
497 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498 f.debug_struct("WetLayer")
499 .field("nodes", &self.nodes())
500 .finish_non_exhaustive()
501 }
502}
503
504impl WetLayer {
505 /// A wet surface painted by `painter`, in **scene** coordinates.
506 ///
507 /// The closure is called on every repaint of a node this layer is mounted
508 /// in, and on nothing else. It owns whatever the app is authoring — a
509 /// stroke's points, a rubber-band shape, a live measurement — typically
510 /// through an `Rc<RefCell<…>>` shared with the pointer handler that feeds
511 /// it.
512 pub fn new(painter: impl Fn(&mut Canvas, &SceneItemPaintContext<'_>) + 'static) -> Self {
513 Self(Rc::new(WetLayerInner {
514 painter: Box::new(painter),
515 mounts: RefCell::new(Vec::new()),
516 }))
517 }
518
519 /// Repaint the wet surface, and only it.
520 ///
521 /// No relayout, no rebuild, no accessibility re-walk — and, crucially, no
522 /// repaint of the scene's item bands: this marks nodes, one per view that
523 /// mounted the layer in `ctx`'s own window.
524 ///
525 /// A no-op until the layer has been mounted through
526 /// [`SceneView::wet_layer`] and the view has built, which is the frame
527 /// before any handler can run.
528 ///
529 /// # Why the id goes through a typed door
530 ///
531 /// A [`WidgetId`] is a slot key in *some* arena, and every tree mints the
532 /// same ones. The window a mount was made in narrows the candidates — a
533 /// tree belongs to exactly one window — but a headless tree has no window,
534 /// so `None == None` is not a match, it is two unknowns comparing equal.
535 /// Marking on that alone is how a repaint from one test's tree lands on
536 /// whatever unrelated widget happens to hold that slot in another's.
537 ///
538 /// So the mark is not pushed as a bare
539 /// [`request_repaint`](teksilo_core::EventContext::request_repaint). It
540 /// goes through
541 /// [`with_widget_mut`](teksilo_core::EventContext::with_widget_mut), which
542 /// resolves the id **in the calling tree** and hands the node over only if
543 /// it downcasts to this crate's own `WetLayerNode` — and the closure then
544 /// checks it is a node of *this* layer. Both checks fail loudly
545 /// (`debug_assert`) rather
546 /// than quietly, so a cross-tree repaint is a test failure instead of a
547 /// stray dirty bit.
548 ///
549 /// What that buys, stated exactly:
550 ///
551 /// - An id naming a slot the calling tree does not fill, or fills with a
552 /// widget that has not opted into `Widget::as_any_mut` — which is every
553 /// ordinary widget, the default being `None` — reaches **nothing**, in
554 /// both profiles. That is the case this used to get wrong: the foreign id
555 /// was marked, and whatever held the slot repainted.
556 /// - An id naming a `WetLayerNode` of *this* layer is marked, which is
557 /// right: that node's own mount is in the same list, so it wanted
558 /// repainting anyway.
559 /// - An id naming a node that *has* opted into `as_any_mut` and is not one
560 /// of this layer's trips the assertion, so in a test or a dev build it is
561 /// a panic. In release the closure is skipped and the node still takes the
562 /// `RepaintOnly` mark, which costs one repaint of something already on
563 /// screen — never a mutation, never a relayout, rebuild or accessibility
564 /// change.
565 ///
566 /// So this does **not** make one layer in two window-less trees work; it
567 /// makes the attempt fail where it can be seen. Drive each tree's repaint
568 /// itself, from [`nodes`](Self::nodes).
569 pub fn request_repaint(&self, ctx: &mut teksilo_core::EventContext<'_>) {
570 let here = ctx.window().map(|w| w.id());
571 for mount in self.live_mounts() {
572 if mount.window != here {
573 continue;
574 }
575 let layer = self.clone();
576 ctx.with_widget_mut::<WetLayerNode>(
577 mount.node,
578 teksilo_core::binding::BindingLevel::RepaintOnly,
579 move |node| {
580 debug_assert!(
581 node.belongs_to(&layer),
582 "WetLayer::request_repaint: the node is a wet surface, but \
583 another layer's — the id came from a mount in a different \
584 tree. Drive that tree's repaint from `WetLayer::nodes`."
585 );
586 },
587 );
588 }
589 }
590
591 /// The nodes this layer paints into — one per mounted view, in mount order,
592 /// each paired with the window whose tree holds it.
593 ///
594 /// Exposed for a caller that wants to drive the repaint through some other
595 /// door than [`request_repaint`](Self::request_repaint) — a
596 /// `WidgetTree::mark_needs_paint` in a test, say. Such a caller owns the
597 /// question `request_repaint` answers from its context: **which tree** a
598 /// given node belongs to, which is why this reports
599 /// [`WetNode::window`] beside the id rather than the id alone. With one
600 /// view (the overwhelmingly common case) that is the one tree there is.
601 pub fn nodes(&self) -> Vec<WetNode> {
602 self.live_mounts()
603 .into_iter()
604 .map(|m| WetNode::new(m.node, m.window))
605 .collect()
606 }
607
608 /// Mount this layer into `node`, in the tree of `window`. Returns the
609 /// strong handle the view holds for as long as it lives.
610 pub(crate) fn attach(
611 &self,
612 node: WidgetId,
613 window: Option<teksilo_core::window::TeksiloWindowId>,
614 ) -> Rc<WetMount> {
615 let mount = Rc::new(WetMount { node, window });
616 let mut mounts = self.0.mounts.borrow_mut();
617 mounts.retain(|m| m.strong_count() > 0);
618 mounts.push(Rc::downgrade(&mount));
619 mount
620 }
621
622 /// The mounts whose view is still alive, dropping the rest on the way past.
623 fn live_mounts(&self) -> Vec<Rc<WetMount>> {
624 let mut mounts = self.0.mounts.borrow_mut();
625 mounts.retain(|m| m.strong_count() > 0);
626 mounts.iter().filter_map(Weak::upgrade).collect()
627 }
628
629 fn paint_with(&self, canvas: &mut Canvas, ctx: &SceneItemPaintContext<'_>) {
630 (self.0.painter)(canvas, ctx);
631 }
632}
633
634/// The node [`WetLayer`] paints into. Always the last child of its view.
635pub(crate) struct WetLayerNode {
636 bridge: ScenePaintBridge,
637 layer: WetLayer,
638}
639
640impl std::fmt::Debug for WetLayerNode {
641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 f.debug_struct("WetLayerNode").finish_non_exhaustive()
643 }
644}
645
646impl WetLayerNode {
647 pub(crate) fn new(bridge: ScenePaintBridge, layer: WetLayer) -> Self {
648 Self { bridge, layer }
649 }
650
651 /// Whether this node paints `layer` — the identity check
652 /// [`WetLayer::request_repaint`] runs once the arena has resolved an id in
653 /// the calling tree. Pointer equality on the layer's own `Rc`, so two
654 /// clones of one handle answer `true` and two separate layers never do.
655 fn belongs_to(&self, layer: &WetLayer) -> bool {
656 Rc::ptr_eq(&self.layer.0, &layer.0)
657 }
658}
659
660impl Widget for WetLayerNode {
661 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
662 Size::ZERO.into()
663 }
664
665 /// Required by [`WetLayer::request_repaint`]: it reaches this node through
666 /// `EventContext::with_widget_mut`, which is the only door that resolves a
667 /// [`WidgetId`] against the **calling** tree and type-checks what it finds.
668 /// Without this override the arena hands back nothing and a wet surface
669 /// silently stops repainting.
670 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
671 Some(self)
672 }
673
674 fn paint(&self, _bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
675 let region = self.bridge.visible_region();
676 let item_ctx =
677 SceneItemPaintContext::new(self.bridge.view_transform(), Some(region), ctx.theme)
678 .with_text_scale(ctx.text_scale)
679 .with_window_active(ctx.window_active)
680 .with_enabled(ctx.effective_enabled);
681 self.layer.paint_with(canvas, &item_ctx);
682 }
683
684 fn accessibility(&self, builder: &mut teksilo_core::accessibility::AccessNodeBuilder) {
685 // A gesture in flight is not an object. The finished one becomes a
686 // scene item, with whatever name the app gives it; announcing a
687 // half-drawn stroke would be noise a screen reader cannot act on.
688 builder.set_hidden();
689 }
690}