Skip to main content

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}