use std::{borrow::Cow, cell::Ref, fmt, mem::size_of, ops::Deref, rc::Rc};
use cranpose_foundation::{PointerEvent, PointerEventKind};
use cranpose_ui_graphics::{
ColorFilter, GraphicsLayer, LayerShape, PointerIcon, Rect, RenderEffect, RoundedCornerShape,
Size,
};
mod snapshot;
pub(crate) use snapshot::SlicesSnapshot;
pub use snapshot::{collect_modifier_slices, collect_slices_from_modifier};
use super::{ModifierChainHandle, Point, coordinator_rect::CoordinatorRect};
use crate::{
draw::DrawCommand,
text::{TextLayoutOptions, TextStyle},
text_field_modifier_node::{TextFieldLayoutHandle, TextPanResolver},
text_modifier_node::TextPreparedLayoutHandle,
};
#[derive(Clone, Default)]
pub struct ModifierNodeSlices {
draw_commands: Vec<DrawCommand>,
layer_draw_boundary: Option<usize>,
clip_to_bounds: bool,
layer_coordinator: Option<CoordinatorRect>,
text: Option<SliceText>,
text_coordinator: Option<CoordinatorRect>,
graphics_layer: Option<Box<GraphicsLayer>>,
corner_shape: Option<RoundedCornerShape>,
rare: Option<Box<RareSlices>>,
}
fn publish_pointer_input_size(rare: &RareSlices, size: Size) {
for sink in &rare.pointer_input_sizes {
sink.set(size);
}
}
#[derive(Clone, Default)]
struct RareSlices {
pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
pointer_input_sizes: Vec<Rc<std::cell::Cell<Size>>>,
pointer_icon: Option<PointerIcon>,
motion_context_animated: bool,
translated_content_context: bool,
translated_content_context_identity: Option<usize>,
translated_content_offset_reader: Option<Rc<dyn Fn() -> Point>>,
text_window: Option<Rc<std::cell::Cell<cranpose_ui_graphics::WindowCoordinates>>>,
viewport_window_rect: Option<Rc<dyn crate::modifier_nodes::WindowRectSink>>,
graphics_layer_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
chain_guard: Option<Rc<ChainGuard>>,
z_index: f32,
}
impl RareSlices {
fn is_empty(&self) -> bool {
self.pointer_inputs.is_empty()
&& self.pointer_input_sizes.is_empty()
&& self.pointer_icon.is_none()
&& !self.motion_context_animated
&& !self.translated_content_context
&& self.translated_content_context_identity.is_none()
&& self.translated_content_offset_reader.is_none()
&& self.text_window.is_none()
&& self.viewport_window_rect.is_none()
&& self.graphics_layer_resolver.is_none()
&& self.chain_guard.is_none()
&& self.z_index == 0.0
}
fn clear(&mut self) {
self.pointer_inputs.clear();
self.pointer_input_sizes.clear();
self.pointer_icon = None;
self.motion_context_animated = false;
self.translated_content_context = false;
self.translated_content_context_identity = None;
self.translated_content_offset_reader = None;
self.text_window = None;
self.viewport_window_rect = None;
self.graphics_layer_resolver = None;
self.chain_guard = None;
self.z_index = 0.0;
}
}
struct ChainGuard {
_handle: ModifierChainHandle,
}
struct DrawSite {
coordinator: CoordinatorRect,
displaceable: bool,
modifier_index: usize,
}
#[derive(Clone)]
enum SliceText {
Text(TextPreparedLayoutHandle),
Field(Rc<FieldText>),
}
pub struct SliceTextRef<'a, T: ?Sized>(TextBorrow<'a, T>);
enum TextBorrow<'a, T: ?Sized> {
Layout(Ref<'a, T>),
Field(&'a T),
}
impl<'a, T: ?Sized> SliceTextRef<'a, T> {
fn map<U: ?Sized>(self, part: impl FnOnce(&T) -> &U) -> SliceTextRef<'a, U> {
SliceTextRef(match self.0 {
TextBorrow::Layout(text) => TextBorrow::Layout(Ref::map(text, part)),
TextBorrow::Field(text) => TextBorrow::Field(part(text)),
})
}
}
impl<T: ?Sized> Deref for SliceTextRef<'_, T> {
type Target = T;
fn deref(&self) -> &T {
match &self.0 {
TextBorrow::Layout(text) => text,
TextBorrow::Field(text) => text,
}
}
}
impl<T: ?Sized + fmt::Debug> fmt::Debug for SliceTextRef<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<T: ?Sized + fmt::Display> fmt::Display for SliceTextRef<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl PartialEq<str> for SliceTextRef<'_, str> {
fn eq(&self, other: &str) -> bool {
**self == *other
}
}
impl PartialEq<&str> for SliceTextRef<'_, str> {
fn eq(&self, other: &&str) -> bool {
**self == **other
}
}
struct FieldText {
content: Rc<crate::text::AnnotatedString>,
style: TextStyle,
layout: TextFieldLayoutHandle,
pan: Option<TextPanResolver>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ModifierNodeSlicesDebugStats {
pub draw_command_count: usize,
pub draw_command_capacity: usize,
pub pointer_input_count: usize,
pub pointer_input_capacity: usize,
pub has_text_content: bool,
pub has_text_style: bool,
pub has_text_layout_options: bool,
pub has_prepared_text_layout: bool,
pub has_graphics_layer: bool,
pub has_graphics_layer_resolver: bool,
pub heap_bytes: usize,
}
fn merge_graphics_layers(base: GraphicsLayer, overlay: GraphicsLayer) -> GraphicsLayer {
GraphicsLayer {
alpha: (base.alpha * overlay.alpha).clamp(0.0, 1.0),
scale: base.scale * overlay.scale,
scale_x: base.scale_x * overlay.scale_x,
scale_y: base.scale_y * overlay.scale_y,
rotation_x: base.rotation_x + overlay.rotation_x,
rotation_y: base.rotation_y + overlay.rotation_y,
rotation_z: base.rotation_z + overlay.rotation_z,
camera_distance: transform_frame(&base, &overlay).camera_distance,
transform_origin: transform_frame(&base, &overlay).transform_origin,
translation_x: base.translation_x + overlay.translation_x,
translation_y: base.translation_y + overlay.translation_y,
shadow_elevation: overlay.shadow_elevation,
ambient_shadow_color: overlay.ambient_shadow_color,
spot_shadow_color: overlay.spot_shadow_color,
shape: merged_layer_shape(&base, &overlay),
clip: base.clip || overlay.clip,
compositing_strategy: overlay.compositing_strategy,
blend_mode: overlay.blend_mode,
color_filter: compose_color_filters(base.color_filter, overlay.color_filter),
render_effect: compose_render_effects(base.render_effect, overlay.render_effect),
backdrop_effect: overlay.backdrop_effect.or(base.backdrop_effect),
space: overlay.space.or(base.space),
}
}
fn transform_frame<'a>(base: &'a GraphicsLayer, overlay: &'a GraphicsLayer) -> &'a GraphicsLayer {
let transforms = |layer: &GraphicsLayer| {
layer.rotation_x != 0.0
|| layer.rotation_y != 0.0
|| layer.rotation_z != 0.0
|| layer.scale != 1.0
|| layer.scale_x != 1.0
|| layer.scale_y != 1.0
};
if transforms(overlay) || !transforms(base) {
overlay
} else {
base
}
}
fn merged_layer_shape(base: &GraphicsLayer, overlay: &GraphicsLayer) -> LayerShape {
if overlay.clip || (!base.clip && overlay.backdrop_effect.is_some()) {
overlay.shape
} else if base.clip || base.backdrop_effect.is_some() {
base.shape
} else {
overlay.shape
}
}
fn compose_render_effects(
outer: Option<RenderEffect>,
inner: Option<RenderEffect>,
) -> Option<RenderEffect> {
match (outer, inner) {
(None, None) => None,
(Some(effect), None) | (None, Some(effect)) => Some(effect),
(Some(outer_effect), Some(inner_effect)) => Some(inner_effect.then(outer_effect)),
}
}
fn compose_color_filters(
base: Option<ColorFilter>,
overlay: Option<ColorFilter>,
) -> Option<ColorFilter> {
match (base, overlay) {
(None, None) => None,
(Some(filter), None) | (None, Some(filter)) => Some(filter),
(Some(filter), Some(next)) => Some(filter.compose(&next)),
}
}
impl ModifierNodeSlices {
pub fn draw_commands(&self) -> &[DrawCommand] {
&self.draw_commands
}
pub fn outer_draw_command_count(&self) -> usize {
self.layer_draw_boundary.unwrap_or(0)
}
fn insert_background_draw(
&mut self,
insert_index: usize,
precedes_layer: bool,
command: DrawCommand,
) {
let insert_index = insert_index.min(self.draw_commands.len());
self.draw_commands.insert(insert_index, command);
if let Some(boundary) = self.layer_draw_boundary.as_mut()
&& precedes_layer
{
*boundary += 1;
}
}
fn mark_layer_draw_boundary(&mut self) {
if self.layer_draw_boundary.is_none() {
self.layer_draw_boundary = Some(self.draw_commands.len());
}
}
fn rare(&self) -> Option<&RareSlices> {
self.rare.as_deref()
}
fn rare_mut(&mut self) -> &mut RareSlices {
self.rare.get_or_insert_with(Box::default)
}
pub fn pointer_inputs(&self) -> &[Rc<dyn Fn(PointerEvent)>] {
self.rare().map_or(&[], |rare| &rare.pointer_inputs)
}
pub fn dispatch_pointer_event(&self, event: PointerEvent) {
let terminal = matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel);
for handler in self.pointer_inputs() {
if event.is_consumed() && !terminal {
break;
}
handler(event.clone());
}
}
pub fn pointer_input_size_sinks(&self) -> &[Rc<std::cell::Cell<Size>>] {
self.rare().map_or(&[], |rare| &rare.pointer_input_sizes)
}
pub fn publish_pointer_input_size(&self, size: Size) {
if let Some(rare) = self.rare() {
publish_pointer_input_size(rare, size);
}
}
pub fn z_index(&self) -> f32 {
self.rare().map_or(0.0, |rare| rare.z_index)
}
pub fn pointer_icon(&self) -> Option<&PointerIcon> {
self.rare()?.pointer_icon.as_ref()
}
pub fn clip_to_bounds(&self) -> bool {
self.clip_to_bounds
}
pub fn layer_bounds(&self, node_size: Size) -> Rect {
self.layer_coordinator
.as_ref()
.map_or_else(|| Rect::from_size(node_size), |layer| layer.rect(node_size))
}
#[doc(hidden)]
pub fn publish_window_geometry(
&self,
origin: Point,
transform: cranpose_ui_graphics::ProjectiveTransform,
size: Size,
) {
let Some(rare) = self.rare() else {
return;
};
if rare.text_window.is_some() || rare.viewport_window_rect.is_some() {
let window = cranpose_ui_graphics::WindowCoordinates {
size,
local_to_window: cranpose_ui_graphics::ProjectiveTransform::translation(
origin.x, origin.y,
)
.then(transform),
};
if let Some(sink) = &rare.text_window {
sink.set(window);
}
if let Some(sink) = &rare.viewport_window_rect {
sink.set(window);
}
}
publish_pointer_input_size(rare, size);
}
pub fn motion_context_animated(&self) -> bool {
self.rare().is_some_and(|rare| rare.motion_context_animated)
}
pub fn translated_content_context(&self) -> bool {
self.rare()
.is_some_and(|rare| rare.translated_content_context)
}
pub fn translated_content_context_identity(&self) -> Option<usize> {
self.rare()?.translated_content_context_identity
}
pub fn translated_content_offset(&self) -> Option<Point> {
self.rare()?
.translated_content_offset_reader
.as_ref()
.map(|reader| reader())
}
pub fn has_text(&self) -> bool {
self.text.is_some()
}
pub fn text_content(&self) -> Option<SliceTextRef<'_, str>> {
self.annotated_text()
.map(|text| text.map(|text| text.text.as_str()))
}
pub fn annotated_text(&self) -> Option<SliceTextRef<'_, Rc<crate::text::AnnotatedString>>> {
Some(SliceTextRef(match self.text.as_ref()? {
SliceText::Text(layout) => TextBorrow::Layout(layout.annotated_text()),
SliceText::Field(field) => TextBorrow::Field(&field.content),
}))
}
pub fn text_content_rect(&self, node_size: Size) -> Rect {
self.text_coordinator.as_ref().map_or(
Rect {
x: 0.0,
y: 0.0,
width: node_size.width,
height: node_size.height,
},
|coordinator| coordinator.rect(node_size),
)
}
pub fn text_style(&self) -> Option<SliceTextRef<'_, TextStyle>> {
Some(SliceTextRef(match self.text.as_ref()? {
SliceText::Text(layout) => TextBorrow::Layout(layout.style()),
SliceText::Field(field) => TextBorrow::Field(&field.style),
}))
}
pub fn text_layout_options(&self) -> Option<TextLayoutOptions> {
match self.text.as_ref()? {
SliceText::Text(layout) => Some(layout.options()),
SliceText::Field(_) => Some(TextLayoutOptions::default()),
}
}
pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
match self.text.as_ref()? {
SliceText::Field(field) => field.pan.clone(),
SliceText::Text(_) => None,
}
}
pub fn text_window(
&self,
) -> Option<&Rc<std::cell::Cell<cranpose_ui_graphics::WindowCoordinates>>> {
self.rare()?.text_window.as_ref()
}
pub fn viewport_window_rect(&self) -> Option<&Rc<dyn crate::modifier_nodes::WindowRectSink>> {
self.rare()?.viewport_window_rect.as_ref()
}
pub fn measured_text_layout(&self) -> Option<Rc<crate::text::PreparedTextLayout>> {
match self.text.as_ref()? {
SliceText::Text(layout) => layout.measured_layout(),
SliceText::Field(field) => Some(field.layout.measured_layout(&field.style)),
}
}
#[inline]
pub fn graphics_layer(&self) -> Option<Cow<'_, GraphicsLayer>> {
if let Some(resolve) = self
.rare()
.and_then(|rare| rare.graphics_layer_resolver.as_ref())
{
Some(Cow::Owned(resolve()))
} else {
self.graphics_layer.as_deref().map(Cow::Borrowed)
}
}
pub fn has_graphics_layer(&self) -> bool {
self.graphics_layer.is_some()
|| self
.rare()
.is_some_and(|rare| rare.graphics_layer_resolver.is_some())
}
pub fn corner_shape(&self) -> Option<RoundedCornerShape> {
self.corner_shape
}
fn enter_layer(&mut self, site: &DrawSite) {
if self.layer_coordinator.is_none() {
self.layer_coordinator = Some(site.coordinator.clone());
}
}
fn push_graphics_layer(
&mut self,
layer: GraphicsLayer,
resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
) {
let existing_snapshot = self.graphics_layer.as_deref().cloned();
let next_snapshot = existing_snapshot.as_ref().map_or_else(
|| layer.clone(),
|current| merge_graphics_layers(current.clone(), layer.clone()),
);
let existing_resolver = self
.rare()
.and_then(|rare| rare.graphics_layer_resolver.clone());
match &mut self.graphics_layer {
Some(held) => **held = next_snapshot,
None => self.graphics_layer = Some(Box::new(next_snapshot)),
}
let merged_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>> =
match (existing_resolver, resolver) {
(None, None) => None,
(Some(current_resolver), None) => Some(Rc::new(move || {
merge_graphics_layers(current_resolver(), layer.clone())
})),
(None, Some(next_resolver)) => {
let base = existing_snapshot.unwrap_or_default();
Some(Rc::new(move || {
merge_graphics_layers(base.clone(), next_resolver())
}))
}
(Some(current_resolver), Some(next_resolver)) => Some(Rc::new(move || {
merge_graphics_layers(current_resolver(), next_resolver())
})),
};
if merged_resolver.is_some() {
self.rare_mut().graphics_layer_resolver = merged_resolver;
}
}
pub fn with_chain_guard(mut self, handle: ModifierChainHandle) -> Self {
self.rare_mut().chain_guard = Some(Rc::new(ChainGuard { _handle: handle }));
self
}
pub fn debug_stats(&self) -> ModifierNodeSlicesDebugStats {
let draw_command_bytes = self.draw_commands.capacity() * size_of::<DrawCommand>();
let pointer_input_capacity = self.rare().map_or(0, |rare| rare.pointer_inputs.capacity());
let pointer_input_bytes = pointer_input_capacity * size_of::<Rc<dyn Fn(PointerEvent)>>();
ModifierNodeSlicesDebugStats {
draw_command_count: self.draw_commands.len(),
draw_command_capacity: self.draw_commands.capacity(),
pointer_input_count: self.pointer_inputs().len(),
pointer_input_capacity,
has_text_content: self.text.is_some(),
has_text_style: self.text.is_some(),
has_text_layout_options: self.text.is_some(),
has_prepared_text_layout: self.text.is_some(),
has_graphics_layer: self.graphics_layer.is_some(),
has_graphics_layer_resolver: self
.rare()
.is_some_and(|rare| rare.graphics_layer_resolver.is_some()),
heap_bytes: draw_command_bytes + pointer_input_bytes,
}
}
fn is_empty(&self) -> bool {
self.draw_commands.is_empty()
&& self.layer_draw_boundary.is_none()
&& !self.clip_to_bounds
&& self.layer_coordinator.is_none()
&& self.text.is_none()
&& self.text_coordinator.is_none()
&& self.graphics_layer.is_none()
&& self.corner_shape.is_none()
&& self.rare.as_deref().is_none_or(RareSlices::is_empty)
}
pub fn clear(&mut self) {
self.draw_commands.clear();
self.layer_draw_boundary = None;
self.clip_to_bounds = false;
self.layer_coordinator = None;
self.text = None;
self.text_coordinator = None;
self.graphics_layer = None;
self.corner_shape = None;
if let Some(rare) = self.rare.as_deref_mut() {
rare.clear();
}
}
}
impl fmt::Debug for ModifierNodeSlices {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ModifierNodeSlices")
.field("draw_commands", &self.draw_commands.len())
.field("pointer_inputs", &self.pointer_inputs().len())
.field("pointer_icon", &self.pointer_icon())
.field("clip_to_bounds", &self.clip_to_bounds)
.field("motion_context_animated", &self.motion_context_animated())
.field(
"translated_content_context",
&self.translated_content_context(),
)
.field(
"translated_content_context_identity",
&self.translated_content_context_identity(),
)
.field(
"translated_content_offset",
&self.translated_content_offset(),
)
.field("text_content", &self.annotated_text())
.field("text_style", &self.text_style())
.field("text_layout_options", &self.text_layout_options())
.field("graphics_layer", &self.graphics_layer)
.field(
"graphics_layer_resolver",
&self
.rare()
.is_some_and(|rare| rare.graphics_layer_resolver.is_some()),
)
.field("corner_shape", &self.corner_shape)
.finish()
}
}
#[cfg(test)]
#[path = "tests/slices_tests.rs"]
mod tests;