Skip to main content

cranpose_ui/modifier/
slices.rs

1use std::{borrow::Cow, cell::Ref, fmt, mem::size_of, ops::Deref, rc::Rc};
2
3use cranpose_foundation::{PointerEvent, PointerEventKind};
4use cranpose_ui_graphics::{
5    ColorFilter, GraphicsLayer, LayerShape, PointerIcon, Rect, RenderEffect, RoundedCornerShape,
6    Size,
7};
8
9mod snapshot;
10
11pub(crate) use snapshot::SlicesSnapshot;
12pub use snapshot::{collect_modifier_slices, collect_slices_from_modifier};
13
14use super::{ModifierChainHandle, Point, coordinator_rect::CoordinatorRect};
15use crate::{
16    draw::DrawCommand,
17    text::{TextLayoutOptions, TextStyle},
18    text_field_modifier_node::{TextFieldLayoutHandle, TextPanResolver},
19    text_modifier_node::TextPreparedLayoutHandle,
20};
21
22/// Snapshot of modifier node slices that impact draw and pointer subsystems.
23#[derive(Clone, Default)]
24pub struct ModifierNodeSlices {
25    draw_commands: Vec<DrawCommand>,
26    layer_draw_boundary: Option<usize>,
27    clip_to_bounds: bool,
28    /// Where the outermost clip or graphics layer of the chain sits: its
29    /// coordinator, which a later offset moves the content inside of.
30    layer_coordinator: Option<CoordinatorRect>,
31    text: Option<SliceText>,
32    text_coordinator: Option<CoordinatorRect>,
33    /// Boxed: few nodes carry a layer, and inline it took 240 bytes of
34    /// every node's slices.
35    graphics_layer: Option<Box<GraphicsLayer>>,
36    corner_shape: Option<RoundedCornerShape>,
37    /// Boxed for the same reason: inline they took 150 bytes of every
38    /// node's slices, and on the gauntlet's 5,600 nodes none sets them but the
39    /// 44 animated layers' resolvers.
40    rare: Option<Box<RareSlices>>,
41}
42
43fn publish_pointer_input_size(rare: &RareSlices, size: Size) {
44    for sink in &rare.pointer_input_sizes {
45        sink.set(size);
46    }
47}
48
49/// What few nodes' chains contribute: pointer input, window geometry sinks,
50/// translated content and live layer resolvers.
51#[derive(Clone, Default)]
52struct RareSlices {
53    pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
54    pointer_input_sizes: Vec<Rc<std::cell::Cell<Size>>>,
55    pointer_icon: Option<PointerIcon>,
56    motion_context_animated: bool,
57    translated_content_context: bool,
58    translated_content_context_identity: Option<usize>,
59    translated_content_offset_reader: Option<Rc<dyn Fn() -> Point>>,
60    text_window: Option<Rc<std::cell::Cell<cranpose_ui_graphics::WindowCoordinates>>>,
61    viewport_window_rect: Option<Rc<dyn crate::modifier_nodes::WindowRectSink>>,
62    graphics_layer_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
63    chain_guard: Option<Rc<ChainGuard>>,
64    z_index: f32,
65}
66
67impl RareSlices {
68    fn is_empty(&self) -> bool {
69        self.pointer_inputs.is_empty()
70            && self.pointer_input_sizes.is_empty()
71            && self.pointer_icon.is_none()
72            && !self.motion_context_animated
73            && !self.translated_content_context
74            && self.translated_content_context_identity.is_none()
75            && self.translated_content_offset_reader.is_none()
76            && self.text_window.is_none()
77            && self.viewport_window_rect.is_none()
78            && self.graphics_layer_resolver.is_none()
79            && self.chain_guard.is_none()
80            && self.z_index == 0.0
81    }
82
83    fn clear(&mut self) {
84        self.pointer_inputs.clear();
85        self.pointer_input_sizes.clear();
86        self.pointer_icon = None;
87        self.motion_context_animated = false;
88        self.translated_content_context = false;
89        self.translated_content_context_identity = None;
90        self.translated_content_offset_reader = None;
91        self.text_window = None;
92        self.viewport_window_rect = None;
93        self.graphics_layer_resolver = None;
94        self.chain_guard = None;
95        self.z_index = 0.0;
96    }
97}
98
99struct ChainGuard {
100    _handle: ModifierChainHandle,
101}
102
103/// Where a draw modifier draws: in its coordinator's rect, which only moves
104/// off the node's own rect when the chain has layout modifiers.
105struct DrawSite {
106    coordinator: CoordinatorRect,
107    displaceable: bool,
108    modifier_index: usize,
109}
110
111/// The text a node shows. A `Text`'s string, style and options are its
112/// prepared layout's, read from there rather than copied into every
113/// rebuild of the slices; a text field's are held here.
114#[derive(Clone)]
115enum SliceText {
116    Text(TextPreparedLayoutHandle),
117    Field(Rc<FieldText>),
118}
119
120/// The text, or a part of it, that a node's slices lend: a `Text`'s is
121/// borrowed from its layout, which an update changes in place; a text
122/// field's is held by the slices.
123pub struct SliceTextRef<'a, T: ?Sized>(TextBorrow<'a, T>);
124
125enum TextBorrow<'a, T: ?Sized> {
126    Layout(Ref<'a, T>),
127    Field(&'a T),
128}
129
130impl<'a, T: ?Sized> SliceTextRef<'a, T> {
131    fn map<U: ?Sized>(self, part: impl FnOnce(&T) -> &U) -> SliceTextRef<'a, U> {
132        SliceTextRef(match self.0 {
133            TextBorrow::Layout(text) => TextBorrow::Layout(Ref::map(text, part)),
134            TextBorrow::Field(text) => TextBorrow::Field(part(text)),
135        })
136    }
137}
138
139impl<T: ?Sized> Deref for SliceTextRef<'_, T> {
140    type Target = T;
141
142    fn deref(&self) -> &T {
143        match &self.0 {
144            TextBorrow::Layout(text) => text,
145            TextBorrow::Field(text) => text,
146        }
147    }
148}
149
150impl<T: ?Sized + fmt::Debug> fmt::Debug for SliceTextRef<'_, T> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        (**self).fmt(f)
153    }
154}
155
156impl<T: ?Sized + fmt::Display> fmt::Display for SliceTextRef<'_, T> {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        (**self).fmt(f)
159    }
160}
161
162impl PartialEq<str> for SliceTextRef<'_, str> {
163    fn eq(&self, other: &str) -> bool {
164        **self == *other
165    }
166}
167
168impl PartialEq<&str> for SliceTextRef<'_, str> {
169    fn eq(&self, other: &&str) -> bool {
170        **self == **other
171    }
172}
173
174struct FieldText {
175    content: Rc<crate::text::AnnotatedString>,
176    style: TextStyle,
177    layout: TextFieldLayoutHandle,
178    pan: Option<TextPanResolver>,
179}
180
181/// Counts and capacities of the data that modifier node slices hold, for memory diagnostics.
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
183pub struct ModifierNodeSlicesDebugStats {
184    /// Number of draw commands.
185    pub draw_command_count: usize,
186    /// Allocated capacity of the draw command list, in commands.
187    pub draw_command_capacity: usize,
188    /// Number of pointer input handlers.
189    pub pointer_input_count: usize,
190    /// Allocated capacity of the pointer input handler list, in handlers.
191    pub pointer_input_capacity: usize,
192    /// Whether the node shows text.
193    pub has_text_content: bool,
194    /// Whether the node carries a text style.
195    pub has_text_style: bool,
196    /// Whether the node carries text layout options.
197    pub has_text_layout_options: bool,
198    /// Whether the node carries a prepared text layout.
199    pub has_prepared_text_layout: bool,
200    /// Whether the node carries a graphics layer.
201    pub has_graphics_layer: bool,
202    /// Whether the graphics layer is read through a resolver that runs when the layer is used.
203    pub has_graphics_layer_resolver: bool,
204    /// Heap bytes of the draw command and pointer input handler lists, counted by capacity.
205    pub heap_bytes: usize,
206}
207
208fn merge_graphics_layers(base: GraphicsLayer, overlay: GraphicsLayer) -> GraphicsLayer {
209    GraphicsLayer {
210        alpha: (base.alpha * overlay.alpha).clamp(0.0, 1.0),
211        scale: base.scale * overlay.scale,
212        scale_x: base.scale_x * overlay.scale_x,
213        scale_y: base.scale_y * overlay.scale_y,
214        rotation_x: base.rotation_x + overlay.rotation_x,
215        rotation_y: base.rotation_y + overlay.rotation_y,
216        rotation_z: base.rotation_z + overlay.rotation_z,
217        camera_distance: transform_frame(&base, &overlay).camera_distance,
218        transform_origin: transform_frame(&base, &overlay).transform_origin,
219        translation_x: base.translation_x + overlay.translation_x,
220        translation_y: base.translation_y + overlay.translation_y,
221        shadow_elevation: overlay.shadow_elevation,
222        ambient_shadow_color: overlay.ambient_shadow_color,
223        spot_shadow_color: overlay.spot_shadow_color,
224        shape: merged_layer_shape(&base, &overlay),
225        clip: base.clip || overlay.clip,
226        compositing_strategy: overlay.compositing_strategy,
227        blend_mode: overlay.blend_mode,
228        color_filter: compose_color_filters(base.color_filter, overlay.color_filter),
229        render_effect: compose_render_effects(base.render_effect, overlay.render_effect),
230        backdrop_effect: overlay.backdrop_effect.or(base.backdrop_effect),
231        space: overlay.space.or(base.space),
232    }
233}
234
235/// Which of two stacked layers merged into one frames the merged transform:
236/// the later layer when it rotates or scales itself, else the earlier one. A
237/// layer that neither rotates nor scales, such as a glass surface under a
238/// tilt, leaves its camera and pivot at their defaults, and those would
239/// replace the tilt's own.
240fn transform_frame<'a>(base: &'a GraphicsLayer, overlay: &'a GraphicsLayer) -> &'a GraphicsLayer {
241    let transforms = |layer: &GraphicsLayer| {
242        layer.rotation_x != 0.0
243            || layer.rotation_y != 0.0
244            || layer.rotation_z != 0.0
245            || layer.scale != 1.0
246            || layer.scale_x != 1.0
247            || layer.scale_y != 1.0
248    };
249    if transforms(overlay) || !transforms(base) {
250        overlay
251    } else {
252        base
253    }
254}
255
256/// The shape of two stacked layers merged into one: the layer that clips
257/// owns it, else the layer that carries the backdrop (its effect covers that
258/// shape), else the later layer, whose default resets it like every other
259/// parent-local field.
260fn merged_layer_shape(base: &GraphicsLayer, overlay: &GraphicsLayer) -> LayerShape {
261    if overlay.clip || (!base.clip && overlay.backdrop_effect.is_some()) {
262        overlay.shape
263    } else if base.clip || base.backdrop_effect.is_some() {
264        base.shape
265    } else {
266        overlay.shape
267    }
268}
269
270fn compose_render_effects(
271    outer: Option<RenderEffect>,
272    inner: Option<RenderEffect>,
273) -> Option<RenderEffect> {
274    match (outer, inner) {
275        (None, None) => None,
276        (Some(effect), None) | (None, Some(effect)) => Some(effect),
277        (Some(outer_effect), Some(inner_effect)) => Some(inner_effect.then(outer_effect)),
278    }
279}
280
281fn compose_color_filters(
282    base: Option<ColorFilter>,
283    overlay: Option<ColorFilter>,
284) -> Option<ColorFilter> {
285    match (base, overlay) {
286        (None, None) => None,
287        (Some(filter), None) | (None, Some(filter)) => Some(filter),
288        (Some(filter), Some(next)) => Some(filter.compose(&next)),
289    }
290}
291
292impl ModifierNodeSlices {
293    /// Returns the node's draw commands in the order they draw.
294    pub fn draw_commands(&self) -> &[DrawCommand] {
295        &self.draw_commands
296    }
297
298    /// How many leading [`draw_commands`](Self::draw_commands) come from
299    /// modifiers chained before the node's graphics layer or clip. Those draw
300    /// around the layer in the parent's space, outside its clip, alpha and
301    /// transform, exactly as an outer `drawBehind` wraps a `graphicsLayer`.
302    pub fn outer_draw_command_count(&self) -> usize {
303        self.layer_draw_boundary.unwrap_or(0)
304    }
305
306    fn insert_background_draw(
307        &mut self,
308        insert_index: usize,
309        precedes_layer: bool,
310        command: DrawCommand,
311    ) {
312        let insert_index = insert_index.min(self.draw_commands.len());
313        self.draw_commands.insert(insert_index, command);
314        if let Some(boundary) = self.layer_draw_boundary.as_mut()
315            && precedes_layer
316        {
317            *boundary += 1;
318        }
319    }
320
321    fn mark_layer_draw_boundary(&mut self) {
322        if self.layer_draw_boundary.is_none() {
323            self.layer_draw_boundary = Some(self.draw_commands.len());
324        }
325    }
326
327    /// The boxed fields when the chain set any; see [`RareSlices`].
328    fn rare(&self) -> Option<&RareSlices> {
329        self.rare.as_deref()
330    }
331
332    fn rare_mut(&mut self) -> &mut RareSlices {
333        self.rare.get_or_insert_with(Box::default)
334    }
335
336    /// Returns the node's pointer input handlers in chain order.
337    pub fn pointer_inputs(&self) -> &[Rc<dyn Fn(PointerEvent)>] {
338        self.rare().map_or(&[], |rare| &rare.pointer_inputs)
339    }
340
341    /// Dispatches an event whose position is already local to this layout node.
342    /// Consumed events stop propagation except for release and cancellation,
343    /// which reach every handler so each can finish its active interaction.
344    pub fn dispatch_pointer_event(&self, event: PointerEvent) {
345        let terminal = matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel);
346        for handler in self.pointer_inputs() {
347            if event.is_consumed() && !terminal {
348                break;
349            }
350            handler(event.clone());
351        }
352    }
353
354    /// The write targets for this node's resolved size, one per pointer-input
355    /// node that exposes a size to its handler. See
356    /// [`ModifierNodeSlices::publish_pointer_input_size`].
357    pub fn pointer_input_size_sinks(&self) -> &[Rc<std::cell::Cell<Size>>] {
358        self.rare().map_or(&[], |rare| &rare.pointer_input_sizes)
359    }
360
361    /// Publishes this layout node's resolved size to every pointer-input
362    /// handler attached to it, so `PointerInputScope::size()` reports the
363    /// node's real dimensions.
364    ///
365    /// Called by every pass that resolves a node's geometry (the layout `place`
366    /// passes and the per-frame scene build), so the size is current before any
367    /// pointer event for that frame is dispatched and tracks resizes.
368    ///
369    /// `size` is the node's layout box — the same box the dispatched
370    /// [`PointerEvent`] positions are made local to — so handlers can compare
371    /// event coordinates against it directly.
372    pub fn publish_pointer_input_size(&self, size: Size) {
373        if let Some(rare) = self.rare() {
374            publish_pointer_input_size(rare, size);
375        }
376    }
377
378    /// Where the node draws among its siblings: the sum of its `z_index`
379    /// modifiers.
380    pub fn z_index(&self) -> f32 {
381        self.rare().map_or(0.0, |rare| rare.z_index)
382    }
383
384    /// The pointer's appearance over this node, when a `pointer_icon`
385    /// modifier names one. The innermost declaration in the chain wins.
386    pub fn pointer_icon(&self) -> Option<&PointerIcon> {
387        self.rare()?.pointer_icon.as_ref()
388    }
389
390    /// Returns whether the node clips its content to its bounds.
391    pub fn clip_to_bounds(&self) -> bool {
392        self.clip_to_bounds
393    }
394
395    /// The bounds of the node's layer in a node of `node_size`, where its
396    /// clip cuts and its transforms pivot: as in Compose, the rect of the
397    /// coordinator its outermost clip or graphics layer wraps, so an offset
398    /// declared after it moves the content inside the layer rather than the
399    /// layer. The node's own rect when it has neither.
400    pub fn layer_bounds(&self, node_size: Size) -> Rect {
401        self.layer_coordinator
402            .as_ref()
403            .map_or_else(|| Rect::from_size(node_size), |layer| layer.rect(node_size))
404    }
405
406    /// Publishes geometry in window coordinates through the same transform as
407    /// rendering. Pointer-input sizes stay in the node's local coordinates.
408    #[doc(hidden)]
409    pub fn publish_window_geometry(
410        &self,
411        origin: Point,
412        transform: cranpose_ui_graphics::ProjectiveTransform,
413        size: Size,
414    ) {
415        // Every sink is a rare slice: most nodes have none to publish to.
416        let Some(rare) = self.rare() else {
417            return;
418        };
419        if rare.text_window.is_some() || rare.viewport_window_rect.is_some() {
420            let window = cranpose_ui_graphics::WindowCoordinates {
421                size,
422                local_to_window: cranpose_ui_graphics::ProjectiveTransform::translation(
423                    origin.x, origin.y,
424                )
425                .then(transform),
426            };
427            if let Some(sink) = &rare.text_window {
428                sink.set(window);
429            }
430            if let Some(sink) = &rare.viewport_window_rect {
431                sink.set(window);
432            }
433        }
434        publish_pointer_input_size(rare, size);
435    }
436
437    /// Returns whether a scroll container on this node is moving its content, for example in a
438    /// fling or a settle.
439    pub fn motion_context_animated(&self) -> bool {
440        self.rare().is_some_and(|rare| rare.motion_context_animated)
441    }
442
443    /// Returns whether this node is a scroll container that draws its content translated by
444    /// [`Self::translated_content_offset`].
445    pub fn translated_content_context(&self) -> bool {
446        self.rare()
447            .is_some_and(|rare| rare.translated_content_context)
448    }
449
450    /// Returns an identifier of that scroll container that stays the same while it lives, or `None`
451    /// when the node is not one.
452    pub fn translated_content_context_identity(&self) -> Option<usize> {
453        self.rare()?.translated_content_context_identity
454    }
455
456    /// Returns the offset in logical pixels by which the scroll container translates its content
457    /// now, or `None` when the node has no such offset.
458    pub fn translated_content_offset(&self) -> Option<Point> {
459        self.rare()?
460            .translated_content_offset_reader
461            .as_ref()
462            .map(|reader| reader())
463    }
464
465    /// Whether the node shows a `Text`'s or a text field's text.
466    pub fn has_text(&self) -> bool {
467        self.text.is_some()
468    }
469
470    /// Returns the plain text the node shows, or `None` when the node shows no text.
471    pub fn text_content(&self) -> Option<SliceTextRef<'_, str>> {
472        self.annotated_text()
473            .map(|text| text.map(|text| text.text.as_str()))
474    }
475
476    /// Returns the text the node shows with its span styles, or `None` when the node shows no text.
477    pub fn annotated_text(&self) -> Option<SliceTextRef<'_, Rc<crate::text::AnnotatedString>>> {
478        Some(SliceTextRef(match self.text.as_ref()? {
479            SliceText::Text(layout) => TextBorrow::Layout(layout.annotated_text()),
480            SliceText::Field(field) => TextBorrow::Field(&field.content),
481        }))
482    }
483
484    /// Where the text draws in a node of `node_size`: the rect its layout put
485    /// the text in, after every layout modifier before it.
486    pub fn text_content_rect(&self, node_size: Size) -> Rect {
487        self.text_coordinator.as_ref().map_or(
488            Rect {
489                x: 0.0,
490                y: 0.0,
491                width: node_size.width,
492                height: node_size.height,
493            },
494            |coordinator| coordinator.rect(node_size),
495        )
496    }
497
498    /// Returns the style of the text the node shows, or `None` when the node shows no text.
499    pub fn text_style(&self) -> Option<SliceTextRef<'_, TextStyle>> {
500        Some(SliceTextRef(match self.text.as_ref()? {
501            SliceText::Text(layout) => TextBorrow::Layout(layout.style()),
502            SliceText::Field(field) => TextBorrow::Field(&field.style),
503        }))
504    }
505
506    /// Returns the layout options of the text the node shows, or `None` when the node shows no
507    /// text. A text field reports the default options.
508    pub fn text_layout_options(&self) -> Option<TextLayoutOptions> {
509        match self.text.as_ref()? {
510            SliceText::Text(layout) => Some(layout.options()),
511            SliceText::Field(_) => Some(TextLayoutOptions::default()),
512        }
513    }
514
515    /// Returns the horizontal pan resolver for single-line text fields.
516    ///
517    /// The resolver takes the content viewport width (px) and returns the
518    /// horizontal scroll offset that keeps the cursor visible. Renderers
519    /// subtract this offset from the text origin so the glyphs pan together
520    /// with the cursor and selection.
521    pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
522        match self.text.as_ref()? {
523            SliceText::Field(field) => field.pan.clone(),
524            SliceText::Text(_) => None,
525        }
526    }
527
528    /// The write target for a text node's size and local-to-window
529    /// transform, used by editable and selectable text for caret, selection
530    /// and pointer geometry and a focused field's window bounds.
531    pub fn text_window(
532        &self,
533    ) -> Option<&Rc<std::cell::Cell<cranpose_ui_graphics::WindowCoordinates>>> {
534        self.rare()?.text_window.as_ref()
535    }
536
537    /// The write target for a scroll container's composited window rect, if this
538    /// node carries a `report_window_rect` modifier. The layout pass writes the
539    /// node's true on-screen viewport rect here so a `BringIntoViewResponder`
540    /// can scroll a focused field's caret above the soft keyboard.
541    pub fn viewport_window_rect(&self) -> Option<&Rc<dyn crate::modifier_nodes::WindowRectSink>> {
542        self.rare()?.viewport_window_rect.as_ref()
543    }
544
545    /// Returns the text layout this node's `Text` or text field produced when
546    /// layout last measured it, laid out at the same width.
547    ///
548    /// A text field's layout is its current text wrapped at the width its
549    /// caret and selection are placed on. `None` when the node carries no text,
550    /// or carries a `Text` that has not been measured yet.
551    pub fn measured_text_layout(&self) -> Option<Rc<crate::text::PreparedTextLayout>> {
552        match self.text.as_ref()? {
553            SliceText::Text(layout) => layout.measured_layout(),
554            SliceText::Field(field) => Some(field.layout.measured_layout(&field.style)),
555        }
556    }
557
558    /// Returns the graphics layer that the layer modifiers of the chain combine into, or `None`
559    /// when the chain has none: lent when the chain holds it, built when a layer block computes
560    /// it.
561    #[inline]
562    pub fn graphics_layer(&self) -> Option<Cow<'_, GraphicsLayer>> {
563        if let Some(resolve) = self
564            .rare()
565            .and_then(|rare| rare.graphics_layer_resolver.as_ref())
566        {
567            Some(Cow::Owned(resolve()))
568        } else {
569            self.graphics_layer.as_deref().map(Cow::Borrowed)
570        }
571    }
572
573    /// Whether the chain sets a graphics layer, live or fixed, without
574    /// resolving a live one as [`Self::graphics_layer`] does.
575    pub fn has_graphics_layer(&self) -> bool {
576        self.graphics_layer.is_some()
577            || self
578                .rare()
579                .is_some_and(|rare| rare.graphics_layer_resolver.is_some())
580    }
581
582    /// Returns the rounded corner shape the chain sets, or `None` when it sets none.
583    pub fn corner_shape(&self) -> Option<RoundedCornerShape> {
584        self.corner_shape
585    }
586
587    /// Records the layer's coordinator when this is the chain's outermost
588    /// clip or graphics layer.
589    fn enter_layer(&mut self, site: &DrawSite) {
590        if self.layer_coordinator.is_none() {
591            self.layer_coordinator = Some(site.coordinator.clone());
592        }
593    }
594
595    fn push_graphics_layer(
596        &mut self,
597        layer: GraphicsLayer,
598        resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
599    ) {
600        let existing_snapshot = self.graphics_layer.as_deref().cloned();
601        let next_snapshot = existing_snapshot.as_ref().map_or_else(
602            || layer.clone(),
603            |current| merge_graphics_layers(current.clone(), layer.clone()),
604        );
605        let existing_resolver = self
606            .rare()
607            .and_then(|rare| rare.graphics_layer_resolver.clone());
608
609        match &mut self.graphics_layer {
610            Some(held) => **held = next_snapshot,
611            None => self.graphics_layer = Some(Box::new(next_snapshot)),
612        }
613        let merged_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>> =
614            match (existing_resolver, resolver) {
615                (None, None) => None,
616                (Some(current_resolver), None) => Some(Rc::new(move || {
617                    merge_graphics_layers(current_resolver(), layer.clone())
618                })),
619                (None, Some(next_resolver)) => {
620                    let base = existing_snapshot.unwrap_or_default();
621                    Some(Rc::new(move || {
622                        merge_graphics_layers(base.clone(), next_resolver())
623                    }))
624                }
625                (Some(current_resolver), Some(next_resolver)) => Some(Rc::new(move || {
626                    merge_graphics_layers(current_resolver(), next_resolver())
627                })),
628            };
629        if merged_resolver.is_some() {
630            self.rare_mut().graphics_layer_resolver = merged_resolver;
631        }
632    }
633
634    /// Returns these slices holding `handle`, which keeps the node chain they come from alive.
635    pub fn with_chain_guard(mut self, handle: ModifierChainHandle) -> Self {
636        self.rare_mut().chain_guard = Some(Rc::new(ChainGuard { _handle: handle }));
637        self
638    }
639
640    /// Returns counts and capacities of the slices' data, for memory diagnostics.
641    pub fn debug_stats(&self) -> ModifierNodeSlicesDebugStats {
642        let draw_command_bytes = self.draw_commands.capacity() * size_of::<DrawCommand>();
643        let pointer_input_capacity = self.rare().map_or(0, |rare| rare.pointer_inputs.capacity());
644        let pointer_input_bytes = pointer_input_capacity * size_of::<Rc<dyn Fn(PointerEvent)>>();
645        ModifierNodeSlicesDebugStats {
646            draw_command_count: self.draw_commands.len(),
647            draw_command_capacity: self.draw_commands.capacity(),
648            pointer_input_count: self.pointer_inputs().len(),
649            pointer_input_capacity,
650            has_text_content: self.text.is_some(),
651            has_text_style: self.text.is_some(),
652            has_text_layout_options: self.text.is_some(),
653            has_prepared_text_layout: self.text.is_some(),
654            has_graphics_layer: self.graphics_layer.is_some(),
655            has_graphics_layer_resolver: self
656                .rare()
657                .is_some_and(|rare| rare.graphics_layer_resolver.is_some()),
658            heap_bytes: draw_command_bytes + pointer_input_bytes,
659        }
660    }
661
662    // Whether the chain gave nothing: no draws, text, layer, clip, shape or
663    // rare part.
664    fn is_empty(&self) -> bool {
665        self.draw_commands.is_empty()
666            && self.layer_draw_boundary.is_none()
667            && !self.clip_to_bounds
668            && self.layer_coordinator.is_none()
669            && self.text.is_none()
670            && self.text_coordinator.is_none()
671            && self.graphics_layer.is_none()
672            && self.corner_shape.is_none()
673            && self.rare.as_deref().is_none_or(RareSlices::is_empty)
674    }
675
676    /// Resets the slice collection for reuse, retaining vector capacity.
677    pub fn clear(&mut self) {
678        self.draw_commands.clear();
679        self.layer_draw_boundary = None;
680        self.clip_to_bounds = false;
681        self.layer_coordinator = None;
682        self.text = None;
683        self.text_coordinator = None;
684        self.graphics_layer = None;
685        self.corner_shape = None;
686        if let Some(rare) = self.rare.as_deref_mut() {
687            rare.clear();
688        }
689    }
690}
691
692impl fmt::Debug for ModifierNodeSlices {
693    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694        f.debug_struct("ModifierNodeSlices")
695            .field("draw_commands", &self.draw_commands.len())
696            .field("pointer_inputs", &self.pointer_inputs().len())
697            .field("pointer_icon", &self.pointer_icon())
698            .field("clip_to_bounds", &self.clip_to_bounds)
699            .field("motion_context_animated", &self.motion_context_animated())
700            .field(
701                "translated_content_context",
702                &self.translated_content_context(),
703            )
704            .field(
705                "translated_content_context_identity",
706                &self.translated_content_context_identity(),
707            )
708            .field(
709                "translated_content_offset",
710                &self.translated_content_offset(),
711            )
712            .field("text_content", &self.annotated_text())
713            .field("text_style", &self.text_style())
714            .field("text_layout_options", &self.text_layout_options())
715            .field("graphics_layer", &self.graphics_layer)
716            .field(
717                "graphics_layer_resolver",
718                &self
719                    .rare()
720                    .is_some_and(|rare| rare.graphics_layer_resolver.is_some()),
721            )
722            .field("corner_shape", &self.corner_shape)
723            .finish()
724    }
725}
726
727#[cfg(test)]
728#[path = "tests/slices_tests.rs"]
729mod tests;