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