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, RoundedCornerShape, Size,
8};
9
10use crate::layer_transform::{layer_scale_x, layer_scale_y, layer_uniform_scale};
11
12pub struct NodeStyle {
13    pub padding: cranpose_ui_graphics::EdgeInsets,
14    pub background: Option<Color>,
15    pub click_actions: Vec<Rc<dyn Fn(Point)>>,
16    pub shape: Option<RoundedCornerShape>,
17    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
18    pub draw_commands: Vec<DrawCommand>,
19    pub graphics_layer: Option<GraphicsLayer>,
20    pub clip_to_bounds: bool,
21}
22
23impl NodeStyle {
24    pub fn from_layout_node(data: &LayoutNodeData) -> Self {
25        let resolved = data.resolved_modifiers;
26        let slices: &ModifierNodeSlices = data.modifier_slices();
27        let pointer_inputs = slices.pointer_inputs().to_vec();
28
29        Self {
30            padding: resolved.padding(),
31            background: None,
32            click_actions: slices.click_handlers().to_vec(),
33            shape: None,
34            pointer_inputs,
35            draw_commands: slices.draw_commands().to_vec(),
36            graphics_layer: slices.graphics_layer(),
37            clip_to_bounds: slices.clip_to_bounds(),
38        }
39    }
40}
41
42pub fn combine_layers(
43    current: GraphicsLayer,
44    modifier_layer: Option<GraphicsLayer>,
45) -> GraphicsLayer {
46    if let Some(layer) = modifier_layer {
47        GraphicsLayer {
48            alpha: (current.alpha * layer.alpha).clamp(0.0, 1.0),
49            scale: current.scale * layer.scale,
50            scale_x: current.scale_x * layer.scale_x,
51            scale_y: current.scale_y * layer.scale_y,
52            rotation_x: current.rotation_x + layer.rotation_x,
53            rotation_y: current.rotation_y + layer.rotation_y,
54            rotation_z: current.rotation_z + layer.rotation_z,
55            camera_distance: layer.camera_distance,
56            transform_origin: layer.transform_origin,
57            translation_x: current.translation_x + layer.translation_x,
58            translation_y: current.translation_y + layer.translation_y,
59            shadow_elevation: layer.shadow_elevation,
60            ambient_shadow_color: layer.ambient_shadow_color,
61            spot_shadow_color: layer.spot_shadow_color,
62            shape: layer.shape,
63            clip: current.clip || layer.clip,
64            color_filter: compose_color_filters(current.color_filter, layer.color_filter),
65            compositing_strategy: layer.compositing_strategy,
66            blend_mode: layer.blend_mode,
67            // render_effect is NOT inherited — it applies only to this layer's subtree
68            render_effect: layer.render_effect,
69            // backdrop_effect is NOT inherited — it applies only to this node's backdrop.
70            backdrop_effect: layer.backdrop_effect,
71        }
72    } else {
73        GraphicsLayer {
74            compositing_strategy: CompositingStrategy::Auto,
75            blend_mode: BlendMode::SrcOver,
76            render_effect: None,
77            backdrop_effect: None,
78            ..current
79        }
80    }
81}
82
83pub use crate::graph::quad_bounds;
84
85pub fn apply_layer_to_color(color: Color, layer: &GraphicsLayer) -> Color {
86    apply_color_filter_to_color(
87        Color(
88            color.0,
89            color.1,
90            color.2,
91            (color.3 * layer.alpha).clamp(0.0, 1.0),
92        ),
93        layer.color_filter,
94    )
95}
96
97fn apply_color_filter_to_color(color: Color, filter: Option<ColorFilter>) -> Color {
98    match filter {
99        Some(filter) => {
100            let [r, g, b, a] = filter.apply_rgba([color.0, color.1, color.2, color.3]);
101            Color(r, g, b, a)
102        }
103        None => color,
104    }
105}
106
107pub fn compose_color_filters(
108    base: Option<ColorFilter>,
109    overlay: Option<ColorFilter>,
110) -> Option<ColorFilter> {
111    match (base, overlay) {
112        (None, None) => None,
113        (Some(filter), None) | (None, Some(filter)) => Some(filter),
114        (Some(filter), Some(next)) => Some(filter.compose(next)),
115    }
116}
117
118pub fn apply_layer_to_brush(brush: Brush, layer: &GraphicsLayer) -> Brush {
119    let scale_x = layer_scale_x(layer);
120    let scale_y = layer_scale_y(layer);
121    let uniform_scale = layer_uniform_scale(layer);
122
123    match brush {
124        Brush::Solid(color) => Brush::solid(apply_layer_to_color(color, layer)),
125        Brush::LinearGradient {
126            colors,
127            stops,
128            mut start,
129            mut end,
130            tile_mode,
131        } => {
132            start.x *= scale_x;
133            start.y *= scale_y;
134            end.x *= scale_x;
135            end.y *= scale_y;
136            Brush::LinearGradient {
137                colors: colors
138                    .into_iter()
139                    .map(|c| apply_layer_to_color(c, layer))
140                    .collect(),
141                stops,
142                start,
143                end,
144                tile_mode,
145            }
146        }
147        Brush::RadialGradient {
148            colors,
149            stops,
150            mut center,
151            mut radius,
152            tile_mode,
153        } => {
154            center.x *= scale_x;
155            center.y *= scale_y;
156            radius *= uniform_scale;
157            Brush::RadialGradient {
158                colors: colors
159                    .into_iter()
160                    .map(|c| apply_layer_to_color(c, layer))
161                    .collect(),
162                stops,
163                center,
164                radius,
165                tile_mode,
166            }
167        }
168        Brush::SweepGradient {
169            colors,
170            stops,
171            mut center,
172        } => {
173            center.x *= scale_x;
174            center.y *= scale_y;
175            Brush::SweepGradient {
176                colors: colors
177                    .into_iter()
178                    .map(|c| apply_layer_to_color(c, layer))
179                    .collect(),
180                stops,
181                center,
182            }
183        }
184    }
185}
186
187pub fn scale_corner_radii(radii: CornerRadii, scale: f32) -> CornerRadii {
188    CornerRadii {
189        top_left: radii.top_left * scale,
190        top_right: radii.top_right * scale,
191        bottom_right: radii.bottom_right * scale,
192        bottom_left: radii.bottom_left * scale,
193    }
194}
195
196#[derive(Clone, Copy)]
197pub enum DrawPlacement {
198    Behind,
199    Overlay,
200}
201
202pub fn primitives_for_placement(
203    command: &DrawCommand,
204    placement: DrawPlacement,
205    size: Size,
206) -> Vec<DrawPrimitive> {
207    let split_with_content = |primitives: Vec<DrawPrimitive>, placement| {
208        let Some(last_content_idx) = primitives
209            .iter()
210            .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
211        else {
212            return if matches!(placement, DrawPlacement::Overlay) {
213                primitives
214                    .into_iter()
215                    .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
216                    .collect()
217            } else {
218                Vec::new()
219            };
220        };
221
222        primitives
223            .into_iter()
224            .enumerate()
225            .filter_map(|(index, primitive)| {
226                if matches!(primitive, DrawPrimitive::Content) {
227                    return None;
228                }
229                let is_before = index < last_content_idx;
230                match placement {
231                    DrawPlacement::Behind if is_before => Some(primitive),
232                    DrawPlacement::Overlay if !is_before => Some(primitive),
233                    _ => None,
234                }
235            })
236            .collect()
237    };
238
239    match (placement, command) {
240        (DrawPlacement::Behind, DrawCommand::Behind(func)) => func(size)
241            .into_iter()
242            .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
243            .collect(),
244        (DrawPlacement::Overlay, DrawCommand::Overlay(func)) => func(size)
245            .into_iter()
246            .filter(|primitive| !matches!(primitive, DrawPrimitive::Content))
247            .collect(),
248        (_, DrawCommand::WithContent(func)) => split_with_content(func(size), placement),
249        _ => Vec::new(),
250    }
251}