Skip to main content

cranpose_render_common/
style_shared.rs

1use std::rc::Rc;
2
3use cranpose_foundation::PointerEvent;
4use cranpose_ui::{Brush, DrawCommand, LayoutNodeData, ModifierNodeSlices};
5use cranpose_ui_graphics::{
6    BlendMode, Color, ColorFilter, CompositingStrategy, CornerRadii, DrawPrimitive, GraphicsLayer,
7    Point, Rect, RoundedCornerShape, Size,
8};
9
10pub struct NodeStyle {
11    pub padding: cranpose_ui_graphics::EdgeInsets,
12    pub background: Option<Color>,
13    pub click_actions: Vec<Rc<dyn Fn(Point)>>,
14    pub shape: Option<RoundedCornerShape>,
15    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
16    pub draw_commands: Vec<DrawCommand>,
17    pub graphics_layer: Option<GraphicsLayer>,
18    pub clip_to_bounds: bool,
19}
20
21impl NodeStyle {
22    pub fn from_layout_node(data: &LayoutNodeData) -> Self {
23        let resolved = data.resolved_modifiers;
24        let slices: &ModifierNodeSlices = data.modifier_slices();
25        let pointer_inputs = slices.pointer_inputs().to_vec();
26
27        Self {
28            padding: resolved.padding(),
29            background: None,
30            click_actions: slices.click_handlers().to_vec(),
31            shape: None,
32            pointer_inputs,
33            draw_commands: slices.draw_commands().to_vec(),
34            graphics_layer: slices.graphics_layer(),
35            clip_to_bounds: slices.clip_to_bounds(),
36        }
37    }
38}
39
40pub fn combine_layers(
41    current: GraphicsLayer,
42    modifier_layer: Option<GraphicsLayer>,
43) -> GraphicsLayer {
44    if let Some(layer) = modifier_layer {
45        GraphicsLayer {
46            alpha: (current.alpha * layer.alpha).clamp(0.0, 1.0),
47            scale: current.scale * layer.scale,
48            scale_x: current.scale_x * layer.scale_x,
49            scale_y: current.scale_y * layer.scale_y,
50            rotation_x: current.rotation_x + layer.rotation_x,
51            rotation_y: current.rotation_y + layer.rotation_y,
52            rotation_z: current.rotation_z + layer.rotation_z,
53            camera_distance: layer.camera_distance,
54            transform_origin: layer.transform_origin,
55            translation_x: current.translation_x + layer.translation_x,
56            translation_y: current.translation_y + layer.translation_y,
57            shadow_elevation: layer.shadow_elevation,
58            ambient_shadow_color: layer.ambient_shadow_color,
59            spot_shadow_color: layer.spot_shadow_color,
60            shape: layer.shape,
61            clip: current.clip || layer.clip,
62            color_filter: compose_color_filters(current.color_filter, layer.color_filter),
63            compositing_strategy: layer.compositing_strategy,
64            blend_mode: layer.blend_mode,
65            // render_effect is NOT inherited — it applies only to this layer's subtree
66            render_effect: layer.render_effect,
67            // backdrop_effect is NOT inherited — it applies only to this node's backdrop.
68            backdrop_effect: layer.backdrop_effect,
69        }
70    } else {
71        GraphicsLayer {
72            compositing_strategy: CompositingStrategy::Auto,
73            blend_mode: BlendMode::SrcOver,
74            render_effect: None,
75            backdrop_effect: None,
76            ..current
77        }
78    }
79}
80
81fn layer_scale_x(layer: &GraphicsLayer) -> f32 {
82    layer.scale * layer.scale_x
83}
84
85fn layer_scale_y(layer: &GraphicsLayer) -> f32 {
86    layer.scale * layer.scale_y
87}
88
89pub fn layer_uniform_scale(layer: &GraphicsLayer) -> f32 {
90    layer_scale_x(layer).min(layer_scale_y(layer))
91}
92
93pub fn apply_layer_affine_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
94    let offset_x = rect.x - layer_bounds.x;
95    let offset_y = rect.y - layer_bounds.y;
96    let scale_x = layer_scale_x(layer);
97    let scale_y = layer_scale_y(layer);
98    Rect {
99        x: layer_bounds.x + offset_x * scale_x + layer.translation_x,
100        y: layer_bounds.y + offset_y * scale_y + layer.translation_y,
101        width: rect.width * scale_x,
102        height: rect.height * scale_y,
103    }
104}
105
106fn layer_rotation_pivot(layer_bounds: Rect, layer: &GraphicsLayer) -> (f32, f32) {
107    (
108        layer_bounds.x + layer_bounds.width * layer.transform_origin.pivot_fraction_x,
109        layer_bounds.y + layer_bounds.height * layer.transform_origin.pivot_fraction_y,
110    )
111}
112
113fn layer_has_rotation(layer: &GraphicsLayer) -> bool {
114    layer.rotation_x.abs() > f32::EPSILON
115        || layer.rotation_y.abs() > f32::EPSILON
116        || layer.rotation_z.abs() > f32::EPSILON
117}
118
119fn apply_rotation_and_perspective(
120    point: [f32; 2],
121    pivot: (f32, f32),
122    layer: &GraphicsLayer,
123) -> [f32; 2] {
124    if !layer_has_rotation(layer) {
125        return point;
126    }
127
128    let mut x = point[0] - pivot.0;
129    let mut y = point[1] - pivot.1;
130    let mut z = 0.0f32;
131
132    let (sin_x, cos_x) = layer.rotation_x.to_radians().sin_cos();
133    let (sin_y, cos_y) = layer.rotation_y.to_radians().sin_cos();
134    let (sin_z, cos_z) = layer.rotation_z.to_radians().sin_cos();
135
136    let y_rot_x = y * cos_x - z * sin_x;
137    let z_rot_x = y * sin_x + z * cos_x;
138    y = y_rot_x;
139    z = z_rot_x;
140
141    let x_rot_y = x * cos_y + z * sin_y;
142    let z_rot_y = -x * sin_y + z * cos_y;
143    x = x_rot_y;
144    z = z_rot_y;
145
146    let x_rot_z = x * cos_z - y * sin_z;
147    let y_rot_z = x * sin_z + y * cos_z;
148    x = x_rot_z;
149    y = y_rot_z;
150
151    // Compose cameraDistance is effectively scaled by display density when mapped to
152    // backend transforms; raw values like 8.0 are not literal "near camera plane"
153    // distances in local layer units.
154    const CAMERA_DISTANCE_SCALE: f32 = 72.0;
155    let camera_distance = (layer.camera_distance * CAMERA_DISTANCE_SCALE).max(1.0);
156    let denom = (camera_distance - z).max(1.0);
157    let perspective = camera_distance / denom;
158
159    [pivot.0 + x * perspective, pivot.1 + y * perspective]
160}
161
162pub fn apply_layer_to_quad(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> [[f32; 2]; 4] {
163    let affine_rect = apply_layer_affine_to_rect(rect, layer_bounds, layer);
164    let affine_layer_bounds = apply_layer_affine_to_rect(layer_bounds, layer_bounds, layer);
165    let pivot = layer_rotation_pivot(affine_layer_bounds, layer);
166    let quad = [
167        [affine_rect.x, affine_rect.y],
168        [affine_rect.x + affine_rect.width, affine_rect.y],
169        [affine_rect.x, affine_rect.y + affine_rect.height],
170        [
171            affine_rect.x + affine_rect.width,
172            affine_rect.y + affine_rect.height,
173        ],
174    ];
175
176    quad.map(|point| apply_rotation_and_perspective(point, pivot, layer))
177}
178
179pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
180    let mut min_x = f32::INFINITY;
181    let mut min_y = f32::INFINITY;
182    let mut max_x = f32::NEG_INFINITY;
183    let mut max_y = f32::NEG_INFINITY;
184
185    for [x, y] in quad {
186        min_x = min_x.min(x);
187        min_y = min_y.min(y);
188        max_x = max_x.max(x);
189        max_y = max_y.max(y);
190    }
191
192    Rect {
193        x: min_x,
194        y: min_y,
195        width: (max_x - min_x).max(0.0),
196        height: (max_y - min_y).max(0.0),
197    }
198}
199
200pub fn apply_layer_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
201    quad_bounds(apply_layer_to_quad(rect, layer_bounds, layer))
202}
203
204pub fn apply_layer_to_color(color: Color, layer: &GraphicsLayer) -> Color {
205    apply_color_filter_to_color(
206        Color(
207            color.0,
208            color.1,
209            color.2,
210            (color.3 * layer.alpha).clamp(0.0, 1.0),
211        ),
212        layer.color_filter,
213    )
214}
215
216fn apply_color_filter_to_color(color: Color, filter: Option<ColorFilter>) -> Color {
217    match filter {
218        Some(filter) => {
219            let [r, g, b, a] = filter.apply_rgba([color.0, color.1, color.2, color.3]);
220            Color(r, g, b, a)
221        }
222        None => color,
223    }
224}
225
226pub fn compose_color_filters(
227    base: Option<ColorFilter>,
228    overlay: Option<ColorFilter>,
229) -> Option<ColorFilter> {
230    match (base, overlay) {
231        (None, None) => None,
232        (Some(filter), None) | (None, Some(filter)) => Some(filter),
233        (Some(filter), Some(next)) => Some(filter.compose(next)),
234    }
235}
236
237pub fn apply_layer_to_brush(brush: Brush, layer: &GraphicsLayer) -> Brush {
238    let scale_x = layer_scale_x(layer);
239    let scale_y = layer_scale_y(layer);
240    let uniform_scale = layer_uniform_scale(layer);
241
242    match brush {
243        Brush::Solid(color) => Brush::solid(apply_layer_to_color(color, layer)),
244        Brush::LinearGradient {
245            colors,
246            stops,
247            mut start,
248            mut end,
249            tile_mode,
250        } => {
251            start.x *= scale_x;
252            start.y *= scale_y;
253            end.x *= scale_x;
254            end.y *= scale_y;
255            Brush::LinearGradient {
256                colors: colors
257                    .into_iter()
258                    .map(|c| apply_layer_to_color(c, layer))
259                    .collect(),
260                stops,
261                start,
262                end,
263                tile_mode,
264            }
265        }
266        Brush::RadialGradient {
267            colors,
268            stops,
269            mut center,
270            mut radius,
271            tile_mode,
272        } => {
273            center.x *= scale_x;
274            center.y *= scale_y;
275            radius *= uniform_scale;
276            Brush::RadialGradient {
277                colors: colors
278                    .into_iter()
279                    .map(|c| apply_layer_to_color(c, layer))
280                    .collect(),
281                stops,
282                center,
283                radius,
284                tile_mode,
285            }
286        }
287        Brush::SweepGradient {
288            colors,
289            stops,
290            mut center,
291        } => {
292            center.x *= scale_x;
293            center.y *= scale_y;
294            Brush::SweepGradient {
295                colors: colors
296                    .into_iter()
297                    .map(|c| apply_layer_to_color(c, layer))
298                    .collect(),
299                stops,
300                center,
301            }
302        }
303    }
304}
305
306pub fn scale_corner_radii(radii: CornerRadii, scale: f32) -> CornerRadii {
307    CornerRadii {
308        top_left: radii.top_left * scale,
309        top_right: radii.top_right * scale,
310        bottom_right: radii.bottom_right * scale,
311        bottom_left: radii.bottom_left * scale,
312    }
313}
314
315#[derive(Clone, Copy)]
316pub enum DrawPlacement {
317    Behind,
318    Overlay,
319}
320
321pub fn primitives_for_placement(
322    command: &DrawCommand,
323    placement: DrawPlacement,
324    size: Size,
325) -> Vec<DrawPrimitive> {
326    let split_with_content = |primitives: Vec<DrawPrimitive>, placement| {
327        let Some(last_content_idx) = primitives
328            .iter()
329            .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
330        else {
331            return if matches!(placement, DrawPlacement::Overlay) {
332                primitives
333                    .into_iter()
334                    .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
335                    .collect()
336            } else {
337                Vec::new()
338            };
339        };
340
341        primitives
342            .into_iter()
343            .enumerate()
344            .filter_map(|(index, primitive)| {
345                if matches!(primitive, DrawPrimitive::Content) {
346                    return None;
347                }
348                let is_before = index < last_content_idx;
349                match placement {
350                    DrawPlacement::Behind if is_before => Some(primitive),
351                    DrawPlacement::Overlay if !is_before => Some(primitive),
352                    _ => None,
353                }
354            })
355            .collect()
356    };
357
358    match (placement, command) {
359        (DrawPlacement::Behind, DrawCommand::Behind(func)) => func(size)
360            .into_iter()
361            .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
362            .collect(),
363        (DrawPlacement::Overlay, DrawCommand::Overlay(func)) => func(size)
364            .into_iter()
365            .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
366            .collect(),
367        (_, DrawCommand::WithContent(func)) => split_with_content(func(size), placement),
368        _ => Vec::new(),
369    }
370}