Skip to main content

cranpose_ui/modifier/
slices.rs

1use std::{fmt, mem::size_of, rc::Rc};
2
3use cranpose_foundation::{ModifierNodeChain, NodeCapabilities, PointerEvent, PointerEventKind};
4use cranpose_ui_graphics::{
5    ColorFilter, EdgeInsets, GraphicsLayer, LayerShape, PointerIcon, RenderEffect,
6    RoundedCornerShape,
7};
8
9use super::{ModifierChainHandle, Point};
10use crate::{
11    draw::DrawCommand,
12    modifier::{
13        Modifier,
14        scroll::{MotionContextAnimatedNode, TranslatedContentContextNode},
15    },
16    modifier_nodes::{
17        BackgroundNode, ClipToBoundsNode, CornerShapeNode, DrawCommandNode, GraphicsLayerNode,
18        PaddingNode, PointerIconNode, SelectableTextNode, WindowRectReporterNode,
19    },
20    text::{TextLayoutOptions, TextStyle},
21    text_field_modifier_node::{TextFieldLayoutHandle, TextFieldModifierNode, TextPanResolver},
22    text_modifier_node::{TextModifierNode, TextPreparedLayoutHandle},
23};
24
25/// Snapshot of modifier node slices that impact draw and pointer subsystems.
26#[derive(Default)]
27pub struct ModifierNodeSlices {
28    draw_commands: Vec<DrawCommand>,
29    layer_draw_boundary: Option<usize>,
30    pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
31    pointer_input_sizes: Vec<Rc<std::cell::Cell<cranpose_ui_graphics::Size>>>,
32    pointer_icon: Option<PointerIcon>,
33    clip_to_bounds: bool,
34    motion_context_animated: bool,
35    translated_content_context: bool,
36    translated_content_context_identity: Option<usize>,
37    translated_content_offset_reader: Option<Rc<dyn Fn() -> Point>>,
38    text_content: Option<Rc<crate::text::AnnotatedString>>,
39    text_style: Option<TextStyle>,
40    text_layout_options: Option<TextLayoutOptions>,
41    prepared_text_layout: Option<MeasuredTextLayoutSource>,
42    text_pan: Option<TextPanResolver>,
43    text_window_origin: Option<Rc<std::cell::Cell<Point>>>,
44    viewport_window_rect: Option<Rc<dyn crate::modifier_nodes::WindowRectSink>>,
45    graphics_layer: Option<GraphicsLayer>,
46    graphics_layer_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
47    corner_shape: Option<RoundedCornerShape>,
48    chain_guard: Option<Rc<ChainGuard>>,
49}
50
51struct ChainGuard {
52    _handle: ModifierChainHandle,
53}
54
55#[derive(Clone)]
56enum MeasuredTextLayoutSource {
57    Text(TextPreparedLayoutHandle),
58    TextField(TextFieldLayoutHandle),
59}
60
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
62pub struct ModifierNodeSlicesDebugStats {
63    pub draw_command_count: usize,
64    pub draw_command_capacity: usize,
65    pub pointer_input_count: usize,
66    pub pointer_input_capacity: usize,
67    pub has_text_content: bool,
68    pub has_text_style: bool,
69    pub has_text_layout_options: bool,
70    pub has_prepared_text_layout: bool,
71    pub has_graphics_layer: bool,
72    pub has_graphics_layer_resolver: bool,
73    pub heap_bytes: usize,
74}
75
76impl Clone for ModifierNodeSlices {
77    fn clone(&self) -> Self {
78        Self {
79            draw_commands: self.draw_commands.clone(),
80            layer_draw_boundary: self.layer_draw_boundary,
81            pointer_inputs: self.pointer_inputs.clone(),
82            pointer_input_sizes: self.pointer_input_sizes.clone(),
83            pointer_icon: self.pointer_icon.clone(),
84            clip_to_bounds: self.clip_to_bounds,
85            motion_context_animated: self.motion_context_animated,
86            translated_content_context: self.translated_content_context,
87            translated_content_context_identity: self.translated_content_context_identity,
88            translated_content_offset_reader: self.translated_content_offset_reader.clone(),
89            text_content: self.text_content.clone(),
90            text_style: self.text_style.clone(),
91            text_layout_options: self.text_layout_options,
92            prepared_text_layout: self.prepared_text_layout.clone(),
93            text_pan: self.text_pan.clone(),
94            text_window_origin: self.text_window_origin.clone(),
95            viewport_window_rect: self.viewport_window_rect.clone(),
96            graphics_layer: self.graphics_layer.clone(),
97            graphics_layer_resolver: self.graphics_layer_resolver.clone(),
98            corner_shape: self.corner_shape,
99            chain_guard: self.chain_guard.clone(),
100        }
101    }
102}
103
104fn merge_graphics_layers(base: GraphicsLayer, overlay: GraphicsLayer) -> GraphicsLayer {
105    GraphicsLayer {
106        alpha: (base.alpha * overlay.alpha).clamp(0.0, 1.0),
107        scale: base.scale * overlay.scale,
108        scale_x: base.scale_x * overlay.scale_x,
109        scale_y: base.scale_y * overlay.scale_y,
110        rotation_x: base.rotation_x + overlay.rotation_x,
111        rotation_y: base.rotation_y + overlay.rotation_y,
112        rotation_z: base.rotation_z + overlay.rotation_z,
113        camera_distance: transform_frame(&base, &overlay).camera_distance,
114        transform_origin: transform_frame(&base, &overlay).transform_origin,
115        translation_x: base.translation_x + overlay.translation_x,
116        translation_y: base.translation_y + overlay.translation_y,
117        shadow_elevation: overlay.shadow_elevation,
118        ambient_shadow_color: overlay.ambient_shadow_color,
119        spot_shadow_color: overlay.spot_shadow_color,
120        shape: merged_layer_shape(&base, &overlay),
121        clip: base.clip || overlay.clip,
122        compositing_strategy: overlay.compositing_strategy,
123        blend_mode: overlay.blend_mode,
124        color_filter: compose_color_filters(base.color_filter, overlay.color_filter),
125        render_effect: compose_render_effects(base.render_effect, overlay.render_effect),
126        backdrop_effect: overlay.backdrop_effect.or(base.backdrop_effect),
127    }
128}
129
130/// Which of two stacked layers merged into one frames the merged transform:
131/// the later layer when it rotates or scales itself, else the earlier one. A
132/// layer that neither rotates nor scales, such as a glass surface under a
133/// tilt, leaves its camera and pivot at their defaults, and those would
134/// replace the tilt's own.
135fn transform_frame<'a>(base: &'a GraphicsLayer, overlay: &'a GraphicsLayer) -> &'a GraphicsLayer {
136    let transforms = |layer: &GraphicsLayer| {
137        layer.rotation_x != 0.0
138            || layer.rotation_y != 0.0
139            || layer.rotation_z != 0.0
140            || layer.scale != 1.0
141            || layer.scale_x != 1.0
142            || layer.scale_y != 1.0
143    };
144    if transforms(overlay) || !transforms(base) {
145        overlay
146    } else {
147        base
148    }
149}
150
151/// The shape of two stacked layers merged into one: the layer that clips
152/// owns it, else the layer that carries the backdrop (its effect covers that
153/// shape), else the later layer, whose default resets it like every other
154/// parent-local field.
155fn merged_layer_shape(base: &GraphicsLayer, overlay: &GraphicsLayer) -> LayerShape {
156    if overlay.clip || (!base.clip && overlay.backdrop_effect.is_some()) {
157        overlay.shape
158    } else if base.clip || base.backdrop_effect.is_some() {
159        base.shape
160    } else {
161        overlay.shape
162    }
163}
164
165fn compose_render_effects(
166    outer: Option<RenderEffect>,
167    inner: Option<RenderEffect>,
168) -> Option<RenderEffect> {
169    match (outer, inner) {
170        (None, None) => None,
171        (Some(effect), None) | (None, Some(effect)) => Some(effect),
172        (Some(outer_effect), Some(inner_effect)) => Some(inner_effect.then(outer_effect)),
173    }
174}
175
176fn compose_color_filters(
177    base: Option<ColorFilter>,
178    overlay: Option<ColorFilter>,
179) -> Option<ColorFilter> {
180    match (base, overlay) {
181        (None, None) => None,
182        (Some(filter), None) | (None, Some(filter)) => Some(filter),
183        (Some(filter), Some(next)) => Some(filter.compose(next)),
184    }
185}
186
187impl ModifierNodeSlices {
188    pub fn draw_commands(&self) -> &[DrawCommand] {
189        &self.draw_commands
190    }
191
192    /// How many leading [`draw_commands`](Self::draw_commands) come from
193    /// modifiers chained before the node's graphics layer or clip. Those draw
194    /// around the layer in the parent's space, outside its clip, alpha and
195    /// transform, exactly as an outer `drawBehind` wraps a `graphicsLayer`.
196    pub fn outer_draw_command_count(&self) -> usize {
197        self.layer_draw_boundary.unwrap_or(0)
198    }
199
200    fn insert_background_draw(
201        &mut self,
202        insert_index: Option<usize>,
203        precedes_layer: bool,
204        command: DrawCommand,
205    ) {
206        let insert_index = insert_index.unwrap_or(0).min(self.draw_commands.len());
207        self.draw_commands.insert(insert_index, command);
208        if let Some(boundary) = self.layer_draw_boundary.as_mut()
209            && precedes_layer
210        {
211            *boundary += 1;
212        }
213    }
214
215    fn mark_layer_draw_boundary(&mut self) {
216        if self.layer_draw_boundary.is_none() {
217            self.layer_draw_boundary = Some(self.draw_commands.len());
218        }
219    }
220
221    pub fn pointer_inputs(&self) -> &[Rc<dyn Fn(PointerEvent)>] {
222        &self.pointer_inputs
223    }
224
225    /// Dispatches an event whose position is already local to this layout node.
226    /// Consumed events stop propagation except for release and cancellation,
227    /// which reach every handler so each can finish its active interaction.
228    pub fn dispatch_pointer_event(&self, event: PointerEvent) {
229        let terminal = matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel);
230        for handler in &self.pointer_inputs {
231            if event.is_consumed() && !terminal {
232                break;
233            }
234            handler(event.clone());
235        }
236    }
237
238    /// The write targets for this node's resolved size, one per pointer-input
239    /// node that exposes a size to its handler. See
240    /// [`ModifierNodeSlices::publish_pointer_input_size`].
241    pub fn pointer_input_size_sinks(&self) -> &[Rc<std::cell::Cell<cranpose_ui_graphics::Size>>] {
242        &self.pointer_input_sizes
243    }
244
245    /// Publishes this layout node's resolved size to every pointer-input
246    /// handler attached to it, so `PointerInputScope::size()` reports the
247    /// node's real dimensions.
248    ///
249    /// Called by every pass that resolves a node's geometry (the layout `place`
250    /// passes and the per-frame scene build), so the size is current before any
251    /// pointer event for that frame is dispatched and tracks resizes.
252    ///
253    /// `size` is the node's layout box — the same box the dispatched
254    /// [`PointerEvent`] positions are made local to — so handlers can compare
255    /// event coordinates against it directly.
256    pub fn publish_pointer_input_size(&self, size: cranpose_ui_graphics::Size) {
257        for sink in &self.pointer_input_sizes {
258            sink.set(size);
259        }
260    }
261
262    /// The pointer's appearance over this node, when a `pointer_icon`
263    /// modifier names one. The innermost declaration in the chain wins.
264    pub fn pointer_icon(&self) -> Option<&PointerIcon> {
265        self.pointer_icon.as_ref()
266    }
267
268    pub fn clip_to_bounds(&self) -> bool {
269        self.clip_to_bounds
270    }
271
272    pub fn motion_context_animated(&self) -> bool {
273        self.motion_context_animated
274    }
275
276    pub fn translated_content_context(&self) -> bool {
277        self.translated_content_context
278    }
279
280    pub fn translated_content_context_identity(&self) -> Option<usize> {
281        self.translated_content_context_identity
282    }
283
284    pub fn translated_content_offset(&self) -> Option<Point> {
285        self.translated_content_offset_reader
286            .as_ref()
287            .map(|reader| reader())
288    }
289
290    pub fn text_content(&self) -> Option<&str> {
291        self.text_content.as_ref().map(|a| a.text.as_str())
292    }
293
294    pub fn annotated_text(&self) -> Option<&Rc<crate::text::AnnotatedString>> {
295        self.text_content.as_ref()
296    }
297
298    pub fn text_style(&self) -> Option<&TextStyle> {
299        self.text_style.as_ref()
300    }
301
302    pub fn text_layout_options(&self) -> Option<TextLayoutOptions> {
303        self.text_layout_options
304    }
305
306    /// Returns the horizontal pan resolver for single-line text fields.
307    ///
308    /// The resolver takes the content viewport width (px) and returns the
309    /// horizontal scroll offset that keeps the cursor visible. Renderers
310    /// subtract this offset from the text origin so the glyphs pan together
311    /// with the cursor and selection.
312    pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
313        self.text_pan.clone()
314    }
315
316    /// The write target for the composited window origin of the text this
317    /// node shows, if it is a `BasicTextField` or a `Text` inside a
318    /// `SelectionContainer`. The layout pass writes the node's true on-screen
319    /// top-left here so selection handles and pointers find the text across
320    /// scroll.
321    pub fn text_window_origin(&self) -> Option<Rc<std::cell::Cell<Point>>> {
322        self.text_window_origin.clone()
323    }
324
325    /// The write target for a scroll container's composited window rect, if this
326    /// node carries a `report_window_rect` modifier. The layout pass writes the
327    /// node's true on-screen viewport rect here so a `BringIntoViewResponder`
328    /// can scroll a focused field's caret above the soft keyboard.
329    pub fn viewport_window_rect(&self) -> Option<Rc<dyn crate::modifier_nodes::WindowRectSink>> {
330        self.viewport_window_rect.clone()
331    }
332
333    /// Returns the text layout this node's `Text` or text field produced when
334    /// layout last measured it, laid out at the same width.
335    ///
336    /// A text field's layout is its current text wrapped at the width its
337    /// caret and selection are placed on. `None` when the node carries no text,
338    /// or carries a `Text` that has not been measured yet.
339    pub fn measured_text_layout(&self) -> Option<Rc<crate::text::PreparedTextLayout>> {
340        match self.prepared_text_layout.as_ref()? {
341            MeasuredTextLayoutSource::Text(handle) => handle.measured_layout(),
342            MeasuredTextLayoutSource::TextField(handle) => {
343                Some(handle.measured_layout(self.text_style.as_ref()?))
344            }
345        }
346    }
347
348    pub fn graphics_layer(&self) -> Option<GraphicsLayer> {
349        if let Some(resolve) = &self.graphics_layer_resolver {
350            Some(resolve())
351        } else {
352            self.graphics_layer.clone()
353        }
354    }
355
356    pub fn corner_shape(&self) -> Option<RoundedCornerShape> {
357        self.corner_shape
358    }
359
360    fn push_graphics_layer(
361        &mut self,
362        layer: GraphicsLayer,
363        resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
364    ) {
365        let existing_snapshot = self.graphics_layer.clone();
366        let next_snapshot = existing_snapshot.as_ref().map_or_else(
367            || layer.clone(),
368            |current| merge_graphics_layers(current.clone(), layer.clone()),
369        );
370        let existing_resolver = self.graphics_layer_resolver.clone();
371
372        self.graphics_layer = Some(next_snapshot);
373        self.graphics_layer_resolver = match (existing_resolver, resolver) {
374            (None, None) => None,
375            (Some(current_resolver), None) => Some(Rc::new(move || {
376                merge_graphics_layers(current_resolver(), layer.clone())
377            })),
378            (None, Some(next_resolver)) => {
379                let base = existing_snapshot.unwrap_or_default();
380                Some(Rc::new(move || {
381                    merge_graphics_layers(base.clone(), next_resolver())
382                }))
383            }
384            (Some(current_resolver), Some(next_resolver)) => Some(Rc::new(move || {
385                merge_graphics_layers(current_resolver(), next_resolver())
386            })),
387        };
388    }
389
390    pub fn with_chain_guard(mut self, handle: ModifierChainHandle) -> Self {
391        self.chain_guard = Some(Rc::new(ChainGuard { _handle: handle }));
392        self
393    }
394
395    pub fn debug_stats(&self) -> ModifierNodeSlicesDebugStats {
396        let draw_command_bytes = self.draw_commands.capacity() * size_of::<DrawCommand>();
397        let pointer_input_bytes =
398            self.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>();
399        ModifierNodeSlicesDebugStats {
400            draw_command_count: self.draw_commands.len(),
401            draw_command_capacity: self.draw_commands.capacity(),
402            pointer_input_count: self.pointer_inputs.len(),
403            pointer_input_capacity: self.pointer_inputs.capacity(),
404            has_text_content: self.text_content.is_some(),
405            has_text_style: self.text_style.is_some(),
406            has_text_layout_options: self.text_layout_options.is_some(),
407            has_prepared_text_layout: self.prepared_text_layout.is_some(),
408            has_graphics_layer: self.graphics_layer.is_some(),
409            has_graphics_layer_resolver: self.graphics_layer_resolver.is_some(),
410            heap_bytes: draw_command_bytes + pointer_input_bytes,
411        }
412    }
413
414    /// Resets the slice collection for reuse, retaining vector capacity.
415    pub fn clear(&mut self) {
416        self.draw_commands.clear();
417        self.layer_draw_boundary = None;
418        self.pointer_inputs.clear();
419        self.pointer_input_sizes.clear();
420        self.pointer_icon = None;
421        self.clip_to_bounds = false;
422        self.motion_context_animated = false;
423        self.translated_content_context = false;
424        self.translated_content_context_identity = None;
425        self.translated_content_offset_reader = None;
426        self.text_content = None;
427        self.text_style = None;
428        self.text_layout_options = None;
429        self.prepared_text_layout = None;
430        self.text_pan = None;
431        self.graphics_layer = None;
432        self.graphics_layer_resolver = None;
433        self.corner_shape = None;
434        self.chain_guard = None;
435    }
436}
437
438impl fmt::Debug for ModifierNodeSlices {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        f.debug_struct("ModifierNodeSlices")
441            .field("draw_commands", &self.draw_commands.len())
442            .field("pointer_inputs", &self.pointer_inputs.len())
443            .field("pointer_icon", &self.pointer_icon)
444            .field("clip_to_bounds", &self.clip_to_bounds)
445            .field("motion_context_animated", &self.motion_context_animated)
446            .field(
447                "translated_content_context",
448                &self.translated_content_context,
449            )
450            .field(
451                "translated_content_context_identity",
452                &self.translated_content_context_identity,
453            )
454            .field(
455                "translated_content_offset",
456                &self.translated_content_offset(),
457            )
458            .field("text_content", &self.text_content)
459            .field("text_style", &self.text_style)
460            .field("text_layout_options", &self.text_layout_options)
461            .field("prepared_text_layout", &self.prepared_text_layout.is_some())
462            .field("graphics_layer", &self.graphics_layer)
463            .field(
464                "graphics_layer_resolver",
465                &self.graphics_layer_resolver.is_some(),
466            )
467            .field("corner_shape", &self.corner_shape)
468            .finish()
469    }
470}
471
472/// Records `node`'s pointer icon when it declares one, leaving the icon already
473/// collected in place when it does not.
474///
475/// The chain is walked head to tail, so the innermost declaration is the last
476/// one written and the one that survives.
477fn collect_pointer_icon(node: &dyn std::any::Any, slices: &mut ModifierNodeSlices) {
478    if let Some(icon_node) = node.downcast_ref::<PointerIconNode>() {
479        slices.pointer_icon = Some(icon_node.icon().clone());
480    }
481}
482
483/// Collects modifier node slices directly from a reconciled [`ModifierNodeChain`].
484pub fn collect_modifier_slices(chain: &ModifierNodeChain) -> ModifierNodeSlices {
485    let mut slices = ModifierNodeSlices::default();
486    collect_modifier_slices_into(chain, &mut slices);
487    slices
488}
489
490/// Collects modifier node slices into an existing buffer to reuse allocations.
491///
492/// Single-pass: iterates the chain once instead of 4 separate capability-filtered
493/// traversals, reducing per-node `RefCell::borrow()` overhead.
494/// Collects `chain`'s slices into the node's shared snapshot. The snapshot's
495/// storage is reused when nothing else holds it; one the render graph still
496/// shares is left to the graph and replaced, not cloned only to be cleared.
497pub(crate) fn collect_modifier_slices_into_shared(
498    chain: &ModifierNodeChain,
499    slices: &mut Rc<ModifierNodeSlices>,
500) {
501    if Rc::get_mut(slices).is_none() {
502        *slices = Rc::default();
503    }
504    if let Some(slices) = Rc::get_mut(slices) {
505        collect_modifier_slices_into(chain, slices);
506    }
507}
508
509fn collect_modifier_slices_into(chain: &ModifierNodeChain, slices: &mut ModifierNodeSlices) {
510    slices.clear();
511
512    let caps = chain.capabilities();
513    let has_pointer = caps.intersects(NodeCapabilities::POINTER_INPUT);
514    let has_draw = caps.intersects(NodeCapabilities::DRAW);
515    let has_layout = caps.intersects(NodeCapabilities::LAYOUT);
516
517    if !has_pointer && !has_draw && !has_layout {
518        return;
519    }
520
521    let mut background = BackgroundSlot::default();
522    let mut padding = EdgeInsets::default();
523
524    for node_ref in chain.head_to_tail() {
525        let node_caps = node_ref.kind_set();
526
527        node_ref.with_node(|node| {
528            let any = node.as_any();
529
530            if has_pointer
531                && node_caps.intersects(NodeCapabilities::POINTER_INPUT)
532                && let Some(pointer_node) = node.as_pointer_input_node()
533            {
534                if let Some(handler) = pointer_node.pointer_input_handler() {
535                    slices.pointer_inputs.push(handler);
536                }
537                if let Some(sink) = pointer_node.layout_size_sink() {
538                    slices.pointer_input_sizes.push(sink);
539                }
540                collect_pointer_icon(any, slices);
541            }
542
543            if has_draw && node_caps.intersects(NodeCapabilities::DRAW) {
544                collect_draw_node(node, padding, slices, &mut background);
545            }
546
547            if has_layout && node_caps.intersects(NodeCapabilities::LAYOUT) {
548                if let Some(padding_node) = any.downcast_ref::<PaddingNode>() {
549                    let p = padding_node.padding();
550                    padding.left += p.left;
551                    padding.top += p.top;
552                    padding.right += p.right;
553                    padding.bottom += p.bottom;
554                }
555
556                if let Some(motion_context_node) = any.downcast_ref::<MotionContextAnimatedNode>() {
557                    slices.motion_context_animated = motion_context_node.is_active();
558                }
559
560                collect_window_geometry_sink(any, padding, slices);
561
562                if let Some(translated_content_node) =
563                    any.downcast_ref::<TranslatedContentContextNode>()
564                {
565                    slices.translated_content_context = translated_content_node.is_active();
566                    slices.translated_content_context_identity =
567                        Some(translated_content_node.identity());
568                    slices.translated_content_offset_reader =
569                        translated_content_node.content_offset_reader();
570                }
571
572                if let Some(text_node) = any.downcast_ref::<TextModifierNode>() {
573                    slices.text_content = Some(text_node.annotated_text());
574                    slices.text_style = Some(text_node.style().clone());
575                    slices.text_layout_options = Some(text_node.options());
576                    slices.prepared_text_layout = Some(MeasuredTextLayoutSource::Text(
577                        text_node.prepared_layout_handle(),
578                    ));
579                }
580
581                if let Some(text_field_node) = any.downcast_ref::<TextFieldModifierNode>() {
582                    let text = text_field_node.text();
583                    slices.text_content = Some(Rc::new(crate::text::AnnotatedString::from(text)));
584                    slices.text_style = Some(text_field_node.style().clone());
585                    slices.text_layout_options = Some(TextLayoutOptions::default());
586                    slices.prepared_text_layout = Some(MeasuredTextLayoutSource::TextField(
587                        text_field_node.layout_handle(),
588                    ));
589                    slices.text_pan = text_field_node.text_pan_resolver();
590                    slices.text_window_origin = Some(text_field_node.window_origin_sink());
591
592                    text_field_node.set_content_offset(padding.left);
593                    text_field_node.set_content_y_offset(padding.top);
594                }
595            }
596        });
597    }
598
599    background.insert_into(slices);
600}
601
602/// The chain's background as the walk finds it: the last background wins,
603/// drawn at its place in the draw order, inside the padding declared before
604/// it, in the latest corner shape.
605#[derive(Default)]
606struct BackgroundSlot {
607    color: Option<crate::modifier::Color>,
608    inset: EdgeInsets,
609    insert_index: Option<usize>,
610    precedes_layer: bool,
611    corner_shape: Option<RoundedCornerShape>,
612}
613
614impl BackgroundSlot {
615    fn insert_into(self, slices: &mut ModifierNodeSlices) {
616        slices.corner_shape = self.corner_shape;
617        let Some(color) = self.color else {
618            return;
619        };
620        let (inset, corner_shape) = (self.inset, self.corner_shape);
621        let draw_cmd = Rc::new(move |scope: &mut cranpose_ui_graphics::DrawScopeDefault| {
622            use cranpose_ui_graphics::{CornerRadii, DrawScope as _};
623
624            use crate::modifier::Brush;
625
626            let rect = inset.inset_rect(scope.size());
627            let brush = Brush::solid(color);
628            if let Some(shape) = corner_shape {
629                let radii: CornerRadii = shape.resolve(rect.width, rect.height);
630                scope.draw_round_rect_at(rect, brush, radii);
631            } else {
632                scope.draw_rect_at(rect, brush);
633            }
634        });
635        slices.insert_background_draw(
636            self.insert_index,
637            self.precedes_layer,
638            DrawCommand::Behind(draw_cmd),
639        );
640    }
641}
642
643/// Collects what a draw-capable node contributes, drawn inside `padding`, the
644/// layout padding declared before it.
645/// Where layout reports a node's window geometry: a scroll viewport's rect,
646/// or the origin of a selectable text, whose content starts after `padding`.
647fn collect_window_geometry_sink(
648    any: &dyn std::any::Any,
649    padding: EdgeInsets,
650    slices: &mut ModifierNodeSlices,
651) {
652    if let Some(reporter) = any.downcast_ref::<WindowRectReporterNode>() {
653        slices.viewport_window_rect = Some(reporter.window_rect_sink());
654    }
655    if let Some(selectable) = any.downcast_ref::<SelectableTextNode>() {
656        slices.text_window_origin = Some(selectable.geometry().node_origin_sink());
657        selectable.geometry().set_content_offset(Point {
658            x: padding.left,
659            y: padding.top,
660        });
661    }
662}
663
664fn collect_draw_node(
665    node: &dyn cranpose_foundation::ModifierNode,
666    padding: EdgeInsets,
667    slices: &mut ModifierNodeSlices,
668    background: &mut BackgroundSlot,
669) {
670    let any = node.as_any();
671    if let Some(bg_node) = any.downcast_ref::<BackgroundNode>() {
672        background.color = Some(bg_node.color());
673        background.inset = padding;
674        background.insert_index = Some(slices.draw_commands.len());
675        background.precedes_layer = slices.layer_draw_boundary.is_none();
676        if bg_node.shape().is_some() {
677            background.corner_shape = bg_node.shape();
678        }
679    }
680
681    if let Some(shape_node) = any.downcast_ref::<CornerShapeNode>() {
682        background.corner_shape = Some(shape_node.shape());
683    }
684
685    if let Some(commands) = any.downcast_ref::<DrawCommandNode>() {
686        slices.draw_commands.extend(
687            commands
688                .observed_commands()
689                .into_iter()
690                .map(|command| inset_draw_command(command, padding)),
691        );
692    }
693
694    if let Some(draw_node) = node.as_draw_node() {
695        collect_draw_closures(draw_node, padding, slices);
696    }
697
698    if let Some(layer_node) = any.downcast_ref::<GraphicsLayerNode>() {
699        slices.mark_layer_draw_boundary();
700        slices.push_graphics_layer(layer_node.layer_snapshot(), layer_node.layer_resolver());
701    }
702
703    if any.is::<ClipToBoundsNode>() {
704        slices.mark_layer_draw_boundary();
705        slices.clip_to_bounds = true;
706    }
707}
708
709/// A draw node's behind and overlay closures, or what its `draw` records
710/// when it has no overlay closure.
711fn collect_draw_closures(
712    draw_node: &dyn cranpose_foundation::DrawModifierNode,
713    padding: EdgeInsets,
714    slices: &mut ModifierNodeSlices,
715) {
716    if let Some(closure) = draw_node.create_behind_draw_closure() {
717        slices
718            .draw_commands
719            .push(inset_draw_command(DrawCommand::Behind(closure), padding));
720    }
721    if let Some(closure) = draw_node.create_draw_closure() {
722        slices
723            .draw_commands
724            .push(inset_draw_command(DrawCommand::Overlay(closure), padding));
725    }
726}
727
728/// `command` drawn where a draw modifier after `padding` draws: in the node's
729/// rect shrunk by that padding, as Compose places it.
730fn inset_draw_command(command: DrawCommand, padding: EdgeInsets) -> DrawCommand {
731    if padding.is_zero() {
732        return command;
733    }
734    let inset = |draw: crate::draw::DrawCommandFn| -> crate::draw::DrawCommandFn {
735        Rc::new(move |scope: &mut cranpose_ui_graphics::DrawScopeDefault| {
736            scope.inset(padding, |inner| draw(inner));
737        })
738    };
739    match command {
740        DrawCommand::Behind(draw) => DrawCommand::Behind(inset(draw)),
741        DrawCommand::WithContent(draw) => DrawCommand::WithContent(inset(draw)),
742        DrawCommand::Overlay(draw) => DrawCommand::Overlay(inset(draw)),
743    }
744}
745
746/// Collects modifier node slices by instantiating a temporary node chain from a [`Modifier`].
747pub fn collect_slices_from_modifier(modifier: &Modifier) -> ModifierNodeSlices {
748    let mut handle = ModifierChainHandle::new();
749    let _ = handle.update(modifier);
750    collect_modifier_slices(handle.chain()).with_chain_guard(handle)
751}
752
753#[cfg(test)]
754#[path = "tests/slices_tests.rs"]
755mod tests;