Skip to main content

cranpose_ui/modifier/
slices.rs

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