Skip to main content

rosace_widgets/tree/
transform_layer.rs

1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::PictureRecorder;
4use rosace_state::Atom;
5use super::{Widget, LayoutCtx, PaintCtx, TransformLayerEntry};
6
7/// Captures a child widget into an independent Picture and applies a 2D scroll
8/// offset on the GPU without re-rendering the child (D080, Phase 17/19).
9///
10/// Phase 17: CPU shift in paint() — UV offset uniform wired in compositor.
11/// Phase 19: child is recorded into a separate PictureRecorder and pushed into
12/// PaintCtx.transform_entries (D087) for the platform to replay into its own
13/// SkiaCanvas and present as an extra GPU compositor layer (D088).
14pub struct TransformLayer<W: Widget + Send + Sync + 'static> {
15    pub child:      W,
16    /// Scroll offset in **logical** pixels, positive = scroll down.
17    pub scroll_y:   Atom<f32>,
18    /// Horizontal scroll offset in logical pixels.
19    pub scroll_x:   Atom<f32>,
20    /// Viewport height in logical pixels — content beyond this is clipped.
21    pub viewport_h: f32,
22}
23
24/// Physical-pixel cap for TransformLayer content (D082).
25pub const MAX_TRANSFORM_DIM: u32 = 4096;
26
27impl<W: Widget + Send + Sync + 'static> TransformLayer<W> {
28    pub fn new(child: W, viewport_h: f32, scroll_y: Atom<f32>) -> Self {
29        Self {
30            child,
31            scroll_y,
32            scroll_x: rosace_state::use_atom(0.0_f32),
33            viewport_h,
34        }
35    }
36}
37
38impl<W: Widget + Send + Sync + 'static> Widget for TransformLayer<W> {
39    fn layout(&self, ctx: &LayoutCtx) -> Size {
40        // Viewport size is what we occupy in the parent layout.
41        let unconstrained = Constraints::loose(ctx.constraints.max_width_f32(), f32::INFINITY);
42        let child_lctx = LayoutCtx::new(unconstrained, ctx.font, ctx.theme);
43        let child_size = self.child.layout(&child_lctx);
44        Size {
45            width:  child_size.width,
46            height: self.viewport_h.min(child_size.height),
47        }
48    }
49
50    fn paint(&self, ctx: &mut PaintCtx) {
51        let scroll_y = self.scroll_y.get();
52        let scroll_x = self.scroll_x.get();
53        let vp_rect  = ctx.rect;
54
55        // Measure child with unconstrained height to get its natural size.
56        let child_lctx = ctx.layout_ctx(Constraints::loose(vp_rect.size.width, f32::INFINITY));
57        let child_size = self.child.layout(&child_lctx);
58
59        // Record child into a SEPARATE PictureRecorder (D087).
60        // The child is painted at (0,0) — the platform positions it on screen.
61        let mut sub_rec = PictureRecorder::new();
62        let child_origin = Point { x: 0.0, y: 0.0 };
63        let child_rect = Rect { origin: child_origin, size: child_size };
64
65        let sub_node = ctx.tree.borrow_mut().slot(ctx.node, true);
66        let mut sub_ctx = PaintCtx {
67            recorder: &mut sub_rec,
68            rect: child_rect,
69            font: ctx.font,
70            theme: ctx.theme.clone(),
71            tree: ctx.tree.clone(),
72            node: sub_node,
73            owner: ctx.owner,
74            clip_rect: None,
75        };
76        self.child.paint(&mut sub_ctx);
77        let picture = sub_rec.finish();
78
79        // Attach the entry to this node — the platform replays it into a
80        // dedicated canvas (D088); it persists across clean frames (D091).
81        ctx.attach_transform(TransformLayerEntry {
82            picture,
83            child_size,
84            viewport_rect: vp_rect,
85            zoom: 1.0,
86            scroll_x,
87            scroll_y,
88        });
89
90        // Register wheel scrolling straight into the non-reactive offset
91        // channel, keyed by this node id (D090). A scroll tick updates the
92        // channel + requests a present-only frame — it dirties NO component,
93        // so the content texture is reused and only the compositor UV offset
94        // changes. Zero CPU paint on scroll.
95        let node_id = ctx.node as u64;
96        let max_x = (child_size.width  - vp_rect.size.width).max(0.0);
97        let max_y = (child_size.height - self.viewport_h).max(0.0);
98        ctx.register_scroll_target(
99            vp_rect,
100            super::render_tree::ScrollAxes::BOTH,
101            std::sync::Arc::new(move |dx, dy| {
102                rosace_state::scroll_offset_by(node_id, -dx, -dy, max_x, max_y);
103            }),
104        );
105
106        // Update ctx.rect to the viewport size for sibling layout correctness.
107        ctx.rect = Rect {
108            origin: vp_rect.origin,
109            size: Size {
110                width:  vp_rect.size.width,
111                height: self.viewport_h.min(vp_rect.size.height),
112            },
113        };
114    }
115}