Skip to main content

cranpose_ui/modifier/
slices.rs

1use std::{fmt, mem::size_of, rc::Rc};
2
3use cranpose_foundation::{ModifierNodeChain, NodeCapabilities, PointerEvent};
4use cranpose_ui_graphics::{
5    ColorFilter, EdgeInsets, GraphicsLayer, LayerShape, RenderEffect, RoundedCornerShape,
6};
7
8use super::{ModifierChainHandle, Point};
9use crate::{
10    draw::DrawCommand,
11    modifier::{
12        Modifier,
13        scroll::{MotionContextAnimatedNode, TranslatedContentContextNode},
14    },
15    modifier_nodes::{
16        BackgroundNode, ClipToBoundsNode, CornerShapeNode, DrawCommandNode, GraphicsLayerNode,
17        PaddingNode, WindowRectReporterNode,
18    },
19    text::{TextLayoutOptions, TextStyle},
20    text_field_modifier_node::{TextFieldModifierNode, TextPanResolver},
21    text_modifier_node::{TextModifierNode, TextPreparedLayoutHandle},
22};
23
24/// Snapshot of modifier node slices that impact draw and pointer subsystems.
25#[derive(Default)]
26pub struct ModifierNodeSlices {
27    draw_commands: Vec<DrawCommand>,
28    layer_draw_boundary: Option<usize>,
29    pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
30    pointer_input_sizes: Vec<Rc<std::cell::Cell<cranpose_ui_graphics::Size>>>,
31    click_handlers: Vec<Rc<dyn Fn(Point)>>,
32    clip_to_bounds: bool,
33    motion_context_animated: bool,
34    translated_content_context: bool,
35    translated_content_context_identity: Option<usize>,
36    translated_content_offset_reader: Option<Rc<dyn Fn() -> Point>>,
37    text_content: Option<Rc<crate::text::AnnotatedString>>,
38    text_style: Option<TextStyle>,
39    text_layout_options: Option<TextLayoutOptions>,
40    prepared_text_layout: Option<TextPreparedLayoutHandle>,
41    text_pan: Option<TextPanResolver>,
42    text_field_window_origin: Option<Rc<std::cell::Cell<Point>>>,
43    viewport_window_rect: Option<Rc<dyn crate::modifier_nodes::WindowRectSink>>,
44    graphics_layer: Option<GraphicsLayer>,
45    graphics_layer_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
46    corner_shape: Option<RoundedCornerShape>,
47    chain_guard: Option<Rc<ChainGuard>>,
48}
49
50struct ChainGuard {
51    _handle: ModifierChainHandle,
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub struct ModifierNodeSlicesDebugStats {
56    pub draw_command_count: usize,
57    pub draw_command_capacity: usize,
58    pub pointer_input_count: usize,
59    pub pointer_input_capacity: usize,
60    pub click_handler_count: usize,
61    pub click_handler_capacity: usize,
62    pub has_text_content: bool,
63    pub has_text_style: bool,
64    pub has_text_layout_options: bool,
65    pub has_prepared_text_layout: bool,
66    pub has_graphics_layer: bool,
67    pub has_graphics_layer_resolver: bool,
68    pub heap_bytes: usize,
69}
70
71impl Clone for ModifierNodeSlices {
72    fn clone(&self) -> Self {
73        Self {
74            draw_commands: self.draw_commands.clone(),
75            layer_draw_boundary: self.layer_draw_boundary,
76            pointer_inputs: self.pointer_inputs.clone(),
77            pointer_input_sizes: self.pointer_input_sizes.clone(),
78            click_handlers: self.click_handlers.clone(),
79            clip_to_bounds: self.clip_to_bounds,
80            motion_context_animated: self.motion_context_animated,
81            translated_content_context: self.translated_content_context,
82            translated_content_context_identity: self.translated_content_context_identity,
83            translated_content_offset_reader: self.translated_content_offset_reader.clone(),
84            text_content: self.text_content.clone(),
85            text_style: self.text_style.clone(),
86            text_layout_options: self.text_layout_options,
87            prepared_text_layout: self.prepared_text_layout.clone(),
88            text_pan: self.text_pan.clone(),
89            text_field_window_origin: self.text_field_window_origin.clone(),
90            viewport_window_rect: self.viewport_window_rect.clone(),
91            graphics_layer: self.graphics_layer.clone(),
92            graphics_layer_resolver: self.graphics_layer_resolver.clone(),
93            corner_shape: self.corner_shape,
94            chain_guard: self.chain_guard.clone(),
95        }
96    }
97}
98
99fn merge_graphics_layers(base: GraphicsLayer, overlay: GraphicsLayer) -> GraphicsLayer {
100    GraphicsLayer {
101        alpha: (base.alpha * overlay.alpha).clamp(0.0, 1.0),
102        scale: base.scale * overlay.scale,
103        scale_x: base.scale_x * overlay.scale_x,
104        scale_y: base.scale_y * overlay.scale_y,
105        rotation_x: base.rotation_x + overlay.rotation_x,
106        rotation_y: base.rotation_y + overlay.rotation_y,
107        rotation_z: base.rotation_z + overlay.rotation_z,
108        camera_distance: overlay.camera_distance,
109        transform_origin: overlay.transform_origin,
110        translation_x: base.translation_x + overlay.translation_x,
111        translation_y: base.translation_y + overlay.translation_y,
112        shadow_elevation: overlay.shadow_elevation,
113        ambient_shadow_color: overlay.ambient_shadow_color,
114        spot_shadow_color: overlay.spot_shadow_color,
115        shape: merged_layer_shape(&base, &overlay),
116        clip: base.clip || overlay.clip,
117        compositing_strategy: overlay.compositing_strategy,
118        blend_mode: overlay.blend_mode,
119        color_filter: compose_color_filters(base.color_filter, overlay.color_filter),
120        render_effect: compose_render_effects(base.render_effect, overlay.render_effect),
121        backdrop_effect: overlay.backdrop_effect.or(base.backdrop_effect),
122    }
123}
124
125/// The shape of two stacked layers merged into one: the layer that clips
126/// owns it, else the layer that carries the backdrop (its effect covers that
127/// shape), else the later layer, whose default resets it like every other
128/// parent-local field.
129fn merged_layer_shape(base: &GraphicsLayer, overlay: &GraphicsLayer) -> LayerShape {
130    if overlay.clip || (!base.clip && overlay.backdrop_effect.is_some()) {
131        overlay.shape
132    } else if base.clip || base.backdrop_effect.is_some() {
133        base.shape
134    } else {
135        overlay.shape
136    }
137}
138
139fn compose_render_effects(
140    outer: Option<RenderEffect>,
141    inner: Option<RenderEffect>,
142) -> Option<RenderEffect> {
143    match (outer, inner) {
144        (None, None) => None,
145        (Some(effect), None) | (None, Some(effect)) => Some(effect),
146        (Some(outer_effect), Some(inner_effect)) => Some(inner_effect.then(outer_effect)),
147    }
148}
149
150fn compose_color_filters(
151    base: Option<ColorFilter>,
152    overlay: Option<ColorFilter>,
153) -> Option<ColorFilter> {
154    match (base, overlay) {
155        (None, None) => None,
156        (Some(filter), None) | (None, Some(filter)) => Some(filter),
157        (Some(filter), Some(next)) => Some(filter.compose(next)),
158    }
159}
160
161impl ModifierNodeSlices {
162    pub fn draw_commands(&self) -> &[DrawCommand] {
163        &self.draw_commands
164    }
165
166    /// How many leading [`draw_commands`](Self::draw_commands) come from
167    /// modifiers chained before the node's graphics layer or clip. Those draw
168    /// around the layer in the parent's space, outside its clip, alpha and
169    /// transform, exactly as an outer `drawBehind` wraps a `graphicsLayer`.
170    pub fn outer_draw_command_count(&self) -> usize {
171        self.layer_draw_boundary.unwrap_or(0)
172    }
173
174    fn insert_background_draw(
175        &mut self,
176        insert_index: Option<usize>,
177        precedes_layer: bool,
178        command: DrawCommand,
179    ) {
180        let insert_index = insert_index.unwrap_or(0).min(self.draw_commands.len());
181        self.draw_commands.insert(insert_index, command);
182        if let Some(boundary) = self.layer_draw_boundary.as_mut()
183            && precedes_layer
184        {
185            *boundary += 1;
186        }
187    }
188
189    fn mark_layer_draw_boundary(&mut self) {
190        if self.layer_draw_boundary.is_none() {
191            self.layer_draw_boundary = Some(self.draw_commands.len());
192        }
193    }
194
195    pub fn pointer_inputs(&self) -> &[Rc<dyn Fn(PointerEvent)>] {
196        &self.pointer_inputs
197    }
198
199    /// The write targets for this node's resolved size, one per pointer-input
200    /// node that exposes a size to its handler. See
201    /// [`ModifierNodeSlices::publish_pointer_input_size`].
202    pub fn pointer_input_size_sinks(&self) -> &[Rc<std::cell::Cell<cranpose_ui_graphics::Size>>] {
203        &self.pointer_input_sizes
204    }
205
206    /// Publishes this layout node's resolved size to every pointer-input
207    /// handler attached to it, so `PointerInputScope::size()` reports the
208    /// node's real dimensions.
209    ///
210    /// Called by every pass that resolves a node's geometry (the layout `place`
211    /// passes and the per-frame scene build), so the size is current before any
212    /// pointer event for that frame is dispatched and tracks resizes.
213    ///
214    /// `size` is the node's layout box — the same box the dispatched
215    /// [`PointerEvent`] positions are made local to — so handlers can compare
216    /// event coordinates against it directly.
217    pub fn publish_pointer_input_size(&self, size: cranpose_ui_graphics::Size) {
218        for sink in &self.pointer_input_sizes {
219            sink.set(size);
220        }
221    }
222
223    pub fn click_handlers(&self) -> &[Rc<dyn Fn(Point)>] {
224        &self.click_handlers
225    }
226
227    pub fn clip_to_bounds(&self) -> bool {
228        self.clip_to_bounds
229    }
230
231    pub fn motion_context_animated(&self) -> bool {
232        self.motion_context_animated
233    }
234
235    pub fn translated_content_context(&self) -> bool {
236        self.translated_content_context
237    }
238
239    pub fn translated_content_context_identity(&self) -> Option<usize> {
240        self.translated_content_context_identity
241    }
242
243    pub fn translated_content_offset(&self) -> Option<Point> {
244        self.translated_content_offset_reader
245            .as_ref()
246            .map(|reader| reader())
247    }
248
249    pub fn text_content(&self) -> Option<&str> {
250        self.text_content.as_ref().map(|a| a.text.as_str())
251    }
252
253    pub fn annotated_text(&self) -> Option<&crate::text::AnnotatedString> {
254        self.text_content.as_deref()
255    }
256
257    pub fn annotated_string(&self) -> Option<crate::text::AnnotatedString> {
258        self.annotated_text().cloned()
259    }
260
261    pub fn text_style(&self) -> Option<&TextStyle> {
262        self.text_style.as_ref()
263    }
264
265    pub fn text_layout_options(&self) -> Option<TextLayoutOptions> {
266        self.text_layout_options
267    }
268
269    /// Returns the horizontal pan resolver for single-line text fields.
270    ///
271    /// The resolver takes the content viewport width (px) and returns the
272    /// horizontal scroll offset that keeps the cursor visible. Renderers
273    /// subtract this offset from the text origin so the glyphs pan together
274    /// with the cursor and selection.
275    pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
276        self.text_pan.clone()
277    }
278
279    /// The write target for a text field's composited window origin, if this
280    /// node is a `BasicTextField`. The layout pass writes the field's true
281    /// on-screen top-left here so its finger selection handles track the field
282    /// across scroll. See [`ModifierNodeSlices::text_field_window_origin`].
283    pub fn text_field_window_origin(&self) -> Option<Rc<std::cell::Cell<Point>>> {
284        self.text_field_window_origin.clone()
285    }
286
287    /// The write target for a scroll container's composited window rect, if this
288    /// node carries a `report_window_rect` modifier. The layout pass writes the
289    /// node's true on-screen viewport rect here so a `BringIntoViewResponder`
290    /// can scroll a focused field's caret above the soft keyboard.
291    pub fn viewport_window_rect(&self) -> Option<Rc<dyn crate::modifier_nodes::WindowRectSink>> {
292        self.viewport_window_rect.clone()
293    }
294
295    pub fn prepare_text_layout(
296        &self,
297        max_width: Option<f32>,
298    ) -> Option<crate::text::PreparedTextLayout> {
299        if let Some(handle) = &self.prepared_text_layout {
300            return Some(handle.prepare(max_width));
301        }
302
303        let text = self.annotated_text()?;
304        let style = self.text_style.clone().unwrap_or_default();
305        Some(crate::text::prepare_text_layout(
306            text,
307            &style,
308            self.text_layout_options.unwrap_or_default(),
309            max_width,
310        ))
311    }
312
313    pub fn graphics_layer(&self) -> Option<GraphicsLayer> {
314        if let Some(resolve) = &self.graphics_layer_resolver {
315            Some(resolve())
316        } else {
317            self.graphics_layer.clone()
318        }
319    }
320
321    pub fn corner_shape(&self) -> Option<RoundedCornerShape> {
322        self.corner_shape
323    }
324
325    fn push_graphics_layer(
326        &mut self,
327        layer: GraphicsLayer,
328        resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
329    ) {
330        let existing_snapshot = self.graphics_layer.clone();
331        let next_snapshot = existing_snapshot
332            .as_ref()
333            .map(|current| merge_graphics_layers(current.clone(), layer.clone()))
334            .unwrap_or_else(|| layer.clone());
335        let existing_resolver = self.graphics_layer_resolver.clone();
336
337        self.graphics_layer = Some(next_snapshot);
338        self.graphics_layer_resolver = match (existing_resolver, resolver) {
339            (None, None) => None,
340            (Some(current_resolver), None) => {
341                let layer = layer.clone();
342                Some(Rc::new(move || {
343                    merge_graphics_layers(current_resolver(), layer.clone())
344                }))
345            }
346            (None, Some(next_resolver)) => {
347                let base = existing_snapshot.unwrap_or_default();
348                Some(Rc::new(move || {
349                    merge_graphics_layers(base.clone(), next_resolver())
350                }))
351            }
352            (Some(current_resolver), Some(next_resolver)) => Some(Rc::new(move || {
353                merge_graphics_layers(current_resolver(), next_resolver())
354            })),
355        };
356    }
357
358    pub fn with_chain_guard(mut self, handle: ModifierChainHandle) -> Self {
359        self.chain_guard = Some(Rc::new(ChainGuard { _handle: handle }));
360        self
361    }
362
363    pub fn debug_stats(&self) -> ModifierNodeSlicesDebugStats {
364        let draw_command_bytes = self.draw_commands.capacity() * size_of::<DrawCommand>();
365        let pointer_input_bytes =
366            self.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>();
367        let click_handler_bytes = self.click_handlers.capacity() * size_of::<Rc<dyn Fn(Point)>>();
368        ModifierNodeSlicesDebugStats {
369            draw_command_count: self.draw_commands.len(),
370            draw_command_capacity: self.draw_commands.capacity(),
371            pointer_input_count: self.pointer_inputs.len(),
372            pointer_input_capacity: self.pointer_inputs.capacity(),
373            click_handler_count: self.click_handlers.len(),
374            click_handler_capacity: self.click_handlers.capacity(),
375            has_text_content: self.text_content.is_some(),
376            has_text_style: self.text_style.is_some(),
377            has_text_layout_options: self.text_layout_options.is_some(),
378            has_prepared_text_layout: self.prepared_text_layout.is_some(),
379            has_graphics_layer: self.graphics_layer.is_some(),
380            has_graphics_layer_resolver: self.graphics_layer_resolver.is_some(),
381            heap_bytes: draw_command_bytes + pointer_input_bytes + click_handler_bytes,
382        }
383    }
384
385    /// Resets the slice collection for reuse, retaining vector capacity.
386    pub fn clear(&mut self) {
387        self.draw_commands.clear();
388        self.layer_draw_boundary = None;
389        self.pointer_inputs.clear();
390        self.pointer_input_sizes.clear();
391        self.click_handlers.clear();
392        self.clip_to_bounds = false;
393        self.motion_context_animated = false;
394        self.translated_content_context = false;
395        self.translated_content_context_identity = None;
396        self.translated_content_offset_reader = None;
397        self.text_content = None;
398        self.text_style = None;
399        self.text_layout_options = None;
400        self.prepared_text_layout = None;
401        self.text_pan = None;
402        self.graphics_layer = None;
403        self.graphics_layer_resolver = None;
404        self.corner_shape = None;
405        self.chain_guard = None;
406    }
407}
408
409impl fmt::Debug for ModifierNodeSlices {
410    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411        f.debug_struct("ModifierNodeSlices")
412            .field("draw_commands", &self.draw_commands.len())
413            .field("pointer_inputs", &self.pointer_inputs.len())
414            .field("click_handlers", &self.click_handlers.len())
415            .field("clip_to_bounds", &self.clip_to_bounds)
416            .field("motion_context_animated", &self.motion_context_animated)
417            .field(
418                "translated_content_context",
419                &self.translated_content_context,
420            )
421            .field(
422                "translated_content_context_identity",
423                &self.translated_content_context_identity,
424            )
425            .field(
426                "translated_content_offset",
427                &self.translated_content_offset(),
428            )
429            .field("text_content", &self.text_content)
430            .field("text_style", &self.text_style)
431            .field("text_layout_options", &self.text_layout_options)
432            .field("prepared_text_layout", &self.prepared_text_layout.is_some())
433            .field("graphics_layer", &self.graphics_layer)
434            .field(
435                "graphics_layer_resolver",
436                &self.graphics_layer_resolver.is_some(),
437            )
438            .field("corner_shape", &self.corner_shape)
439            .finish()
440    }
441}
442
443/// Collects modifier node slices directly from a reconciled [`ModifierNodeChain`].
444pub fn collect_modifier_slices(chain: &ModifierNodeChain) -> ModifierNodeSlices {
445    let mut slices = ModifierNodeSlices::default();
446    collect_modifier_slices_into(chain, &mut slices);
447    slices
448}
449
450/// Collects modifier node slices into an existing buffer to reuse allocations.
451///
452/// Single-pass: iterates the chain once instead of 4 separate capability-filtered
453/// traversals, reducing per-node `RefCell::borrow()` overhead.
454pub fn collect_modifier_slices_into(chain: &ModifierNodeChain, slices: &mut ModifierNodeSlices) {
455    slices.clear();
456
457    let caps = chain.capabilities();
458    let has_pointer = caps.intersects(NodeCapabilities::POINTER_INPUT);
459    let has_draw = caps.intersects(NodeCapabilities::DRAW);
460    let has_layout = caps.intersects(NodeCapabilities::LAYOUT);
461
462    if !has_pointer && !has_draw && !has_layout {
463        return;
464    }
465
466    let mut background_color = None;
467    let mut background_insert_index = None::<usize>;
468    let mut background_precedes_layer = false;
469    let mut corner_shape = None;
470    let mut padding = EdgeInsets::default();
471
472    for node_ref in chain.head_to_tail() {
473        let node_caps = node_ref.kind_set();
474
475        node_ref.with_node(|node| {
476            let any = node.as_any();
477
478            if has_pointer
479                && node_caps.intersects(NodeCapabilities::POINTER_INPUT)
480                && let Some(pointer_node) = node.as_pointer_input_node()
481            {
482                if let Some(handler) = pointer_node.pointer_input_handler() {
483                    slices.pointer_inputs.push(handler);
484                }
485                if let Some(sink) = pointer_node.layout_size_sink() {
486                    slices.pointer_input_sizes.push(sink);
487                }
488            }
489
490            if has_draw && node_caps.intersects(NodeCapabilities::DRAW) {
491                if let Some(bg_node) = any.downcast_ref::<BackgroundNode>() {
492                    background_color = Some(bg_node.color());
493                    background_insert_index = Some(slices.draw_commands.len());
494                    background_precedes_layer = slices.layer_draw_boundary.is_none();
495                    if bg_node.shape().is_some() {
496                        corner_shape = bg_node.shape();
497                    }
498                }
499
500                if let Some(shape_node) = any.downcast_ref::<CornerShapeNode>() {
501                    corner_shape = Some(shape_node.shape());
502                }
503
504                if let Some(commands) = any.downcast_ref::<DrawCommandNode>() {
505                    slices.draw_commands.extend(commands.observed_commands());
506                }
507
508                if let Some(draw_node) = node.as_draw_node() {
509                    if let Some(closure) = draw_node.create_behind_draw_closure() {
510                        slices.draw_commands.push(DrawCommand::Behind(closure));
511                    }
512                    if let Some(closure) = draw_node.create_draw_closure() {
513                        slices.draw_commands.push(DrawCommand::Overlay(closure));
514                    } else {
515                        use cranpose_ui_graphics::{DrawScope as _, DrawScopeDefault};
516                        let mut scope = DrawScopeDefault::with_text_measurer(
517                            crate::modifier::Size {
518                                width: 0.0,
519                                height: 0.0,
520                            },
521                            crate::text::AppContextTextMeasurer::shared(),
522                        );
523                        draw_node.draw(&mut scope);
524                        let primitives = scope.into_primitives();
525                        if !primitives.is_empty() {
526                            let draw_cmd = Rc::new(
527                                move |scope: &mut cranpose_ui_graphics::DrawScopeDefault| {
528                                    scope.push_recorded(primitives.clone())
529                                },
530                            );
531                            slices.draw_commands.push(DrawCommand::Overlay(draw_cmd));
532                        }
533                    }
534                }
535
536                if let Some(layer_node) = any.downcast_ref::<GraphicsLayerNode>() {
537                    slices.mark_layer_draw_boundary();
538                    slices.push_graphics_layer(
539                        layer_node.layer_snapshot(),
540                        layer_node.layer_resolver(),
541                    );
542                }
543
544                if any.is::<ClipToBoundsNode>() {
545                    slices.mark_layer_draw_boundary();
546                    slices.clip_to_bounds = true;
547                }
548            }
549
550            if has_layout && node_caps.intersects(NodeCapabilities::LAYOUT) {
551                if let Some(padding_node) = any.downcast_ref::<PaddingNode>() {
552                    let p = padding_node.padding();
553                    padding.left += p.left;
554                    padding.top += p.top;
555                    padding.right += p.right;
556                    padding.bottom += p.bottom;
557                }
558
559                if let Some(motion_context_node) = any.downcast_ref::<MotionContextAnimatedNode>() {
560                    slices.motion_context_animated = motion_context_node.is_active();
561                }
562
563                if let Some(reporter) = any.downcast_ref::<WindowRectReporterNode>() {
564                    slices.viewport_window_rect = Some(reporter.window_rect_sink());
565                }
566
567                if let Some(translated_content_node) =
568                    any.downcast_ref::<TranslatedContentContextNode>()
569                {
570                    slices.translated_content_context = translated_content_node.is_active();
571                    slices.translated_content_context_identity =
572                        Some(translated_content_node.identity());
573                    slices.translated_content_offset_reader =
574                        translated_content_node.content_offset_reader();
575                }
576
577                if let Some(text_node) = any.downcast_ref::<TextModifierNode>() {
578                    slices.text_content = Some(text_node.annotated_text());
579                    slices.text_style = Some(text_node.style().clone());
580                    slices.text_layout_options = Some(text_node.options());
581                    slices.prepared_text_layout = Some(text_node.prepared_layout_handle());
582                }
583
584                if let Some(text_field_node) = any.downcast_ref::<TextFieldModifierNode>() {
585                    let text = text_field_node.text();
586                    slices.text_content = Some(Rc::new(crate::text::AnnotatedString::from(text)));
587                    slices.text_style = Some(text_field_node.style().clone());
588                    slices.text_layout_options = Some(TextLayoutOptions::default());
589                    slices.prepared_text_layout = None;
590                    slices.text_pan = text_field_node.text_pan_resolver();
591                    slices.text_field_window_origin = Some(text_field_node.window_origin_sink());
592
593                    text_field_node.set_content_offset(padding.left);
594                    text_field_node.set_content_y_offset(padding.top);
595                }
596            }
597        });
598    }
599
600    slices.corner_shape = corner_shape;
601
602    if let Some(color) = background_color {
603        let draw_cmd = Rc::new(move |scope: &mut cranpose_ui_graphics::DrawScopeDefault| {
604            use cranpose_ui_graphics::{CornerRadii, DrawScope as _};
605
606            use crate::modifier::Brush;
607
608            let size = scope.size();
609            let brush = Brush::solid(color);
610            if let Some(shape) = corner_shape {
611                let radii: CornerRadii = shape.resolve(size.width, size.height);
612                scope.draw_round_rect(brush, radii);
613            } else {
614                scope.draw_rect(brush);
615            }
616        });
617
618        slices.insert_background_draw(
619            background_insert_index,
620            background_precedes_layer,
621            DrawCommand::Behind(draw_cmd),
622        );
623    }
624}
625
626/// Collects modifier node slices by instantiating a temporary node chain from a [`Modifier`].
627pub fn collect_slices_from_modifier(modifier: &Modifier) -> ModifierNodeSlices {
628    let mut handle = ModifierChainHandle::new();
629    let _ = handle.update(modifier);
630    collect_modifier_slices(handle.chain()).with_chain_guard(handle)
631}