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#[derive(Clone, Default)]
24pub struct ModifierNodeSlices {
25 draw_commands: Vec<DrawCommand>,
26 layer_draw_boundary: Option<usize>,
27 clip_to_bounds: bool,
28 layer_coordinator: Option<CoordinatorRect>,
31 text: Option<SliceText>,
32 text_coordinator: Option<CoordinatorRect>,
33 graphics_layer: Option<Box<GraphicsLayer>>,
36 corner_shape: Option<RoundedCornerShape>,
37 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#[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
103struct DrawSite {
106 coordinator: CoordinatorRect,
107 displaceable: bool,
108 modifier_index: usize,
109}
110
111#[derive(Clone)]
115enum SliceText {
116 Text(TextPreparedLayoutHandle),
117 Field(Rc<FieldText>),
118}
119
120pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
183pub struct ModifierNodeSlicesDebugStats {
184 pub draw_command_count: usize,
186 pub draw_command_capacity: usize,
188 pub pointer_input_count: usize,
190 pub pointer_input_capacity: usize,
192 pub has_text_content: bool,
194 pub has_text_style: bool,
196 pub has_text_layout_options: bool,
198 pub has_prepared_text_layout: bool,
200 pub has_graphics_layer: bool,
202 pub has_graphics_layer_resolver: bool,
204 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
235fn 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
256fn 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 pub fn draw_commands(&self) -> &[DrawCommand] {
295 &self.draw_commands
296 }
297
298 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 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 pub fn pointer_inputs(&self) -> &[Rc<dyn Fn(PointerEvent)>] {
338 self.rare().map_or(&[], |rare| &rare.pointer_inputs)
339 }
340
341 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 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 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 pub fn z_index(&self) -> f32 {
381 self.rare().map_or(0.0, |rare| rare.z_index)
382 }
383
384 pub fn pointer_icon(&self) -> Option<&PointerIcon> {
387 self.rare()?.pointer_icon.as_ref()
388 }
389
390 pub fn clip_to_bounds(&self) -> bool {
392 self.clip_to_bounds
393 }
394
395 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 #[doc(hidden)]
409 pub fn publish_window_geometry(
410 &self,
411 origin: Point,
412 transform: cranpose_ui_graphics::ProjectiveTransform,
413 size: Size,
414 ) {
415 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 pub fn motion_context_animated(&self) -> bool {
440 self.rare().is_some_and(|rare| rare.motion_context_animated)
441 }
442
443 pub fn translated_content_context(&self) -> bool {
446 self.rare()
447 .is_some_and(|rare| rare.translated_content_context)
448 }
449
450 pub fn translated_content_context_identity(&self) -> Option<usize> {
453 self.rare()?.translated_content_context_identity
454 }
455
456 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 pub fn has_text(&self) -> bool {
467 self.text.is_some()
468 }
469
470 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 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 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 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 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 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 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 pub fn viewport_window_rect(&self) -> Option<&Rc<dyn crate::modifier_nodes::WindowRectSink>> {
542 self.rare()?.viewport_window_rect.as_ref()
543 }
544
545 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 #[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 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 pub fn corner_shape(&self) -> Option<RoundedCornerShape> {
584 self.corner_shape
585 }
586
587 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 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 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 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 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;