Skip to main content

cranpose_render_common/
style_shared.rs

1use std::ops::Range;
2
3use cranpose_ui::{Brush, DrawCommand, DrawCommandFn};
4use cranpose_ui_graphics::{
5    BlendMode, Color, ColorFilter, CommandRecording, CompositingStrategy, CornerRadii,
6    DrawPrimitive, GraphicsLayer, Size,
7};
8
9use crate::layer_transform::{layer_scale_x, layer_scale_y, layer_uniform_scale};
10
11pub fn combine_layers(
12    current: GraphicsLayer,
13    modifier_layer: Option<GraphicsLayer>,
14) -> GraphicsLayer {
15    if let Some(layer) = modifier_layer {
16        GraphicsLayer {
17            alpha: (current.alpha * layer.alpha).clamp(0.0, 1.0),
18            scale: current.scale * layer.scale,
19            scale_x: current.scale_x * layer.scale_x,
20            scale_y: current.scale_y * layer.scale_y,
21            rotation_x: current.rotation_x + layer.rotation_x,
22            rotation_y: current.rotation_y + layer.rotation_y,
23            rotation_z: current.rotation_z + layer.rotation_z,
24            camera_distance: layer.camera_distance,
25            transform_origin: layer.transform_origin,
26            translation_x: current.translation_x + layer.translation_x,
27            translation_y: current.translation_y + layer.translation_y,
28            shadow_elevation: layer.shadow_elevation,
29            ambient_shadow_color: layer.ambient_shadow_color,
30            spot_shadow_color: layer.spot_shadow_color,
31            shape: layer.shape,
32            clip: current.clip || layer.clip,
33            color_filter: compose_color_filters(current.color_filter, layer.color_filter),
34            compositing_strategy: layer.compositing_strategy,
35            blend_mode: layer.blend_mode,
36            render_effect: layer.render_effect,
37            backdrop_effect: layer.backdrop_effect,
38        }
39    } else {
40        GraphicsLayer {
41            compositing_strategy: CompositingStrategy::Auto,
42            blend_mode: BlendMode::SrcOver,
43            render_effect: None,
44            backdrop_effect: None,
45            ..current
46        }
47    }
48}
49
50pub use crate::graph::quad_bounds;
51
52/// The colour a primitive is actually painted in, once this layer has had its
53/// say.
54///
55/// The colour is snapped to eight bits *first*, because that is where the
56/// platform's own colour type already is by the time anything paints with it
57/// (see [`Color::srgb_8bit`]). Only then does the layer's alpha multiply it.
58/// The order is the whole point: an isolated layer's contents land in an 8-bit
59/// buffer and the alpha multiplies whole channel values, so
60/// `round(round(c * 255) * a)` and not `round(c * 255 * a)`.
61pub fn apply_layer_to_color(color: Color, layer: &GraphicsLayer) -> Color {
62    let color = color.srgb_8bit();
63    apply_color_filter_to_color(
64        Color(
65            color.0,
66            color.1,
67            color.2,
68            (color.3 * layer.alpha).clamp(0.0, 1.0),
69        ),
70        layer.color_filter,
71    )
72}
73
74fn apply_color_filter_to_color(color: Color, filter: Option<ColorFilter>) -> Color {
75    match filter {
76        Some(filter) => {
77            let [r, g, b, a] = filter.apply_rgba([color.0, color.1, color.2, color.3]);
78            Color(r, g, b, a)
79        }
80        None => color,
81    }
82}
83
84pub fn compose_color_filters(
85    base: Option<ColorFilter>,
86    overlay: Option<ColorFilter>,
87) -> Option<ColorFilter> {
88    match (base, overlay) {
89        (None, None) => None,
90        (Some(filter), None) | (None, Some(filter)) => Some(filter),
91        (Some(filter), Some(next)) => Some(filter.compose(next)),
92    }
93}
94
95pub fn apply_layer_to_brush(brush: Brush, layer: &GraphicsLayer) -> Brush {
96    if layer.alpha == 1.0
97        && layer.color_filter.is_none()
98        && layer_scale_x(layer) == 1.0
99        && layer_scale_y(layer) == 1.0
100    {
101        return map_brush_colors(brush, Color::srgb_8bit);
102    }
103    map_brush_colors(scale_brush_geometry(brush, layer), |color| {
104        apply_layer_to_color(color, layer)
105    })
106}
107
108fn map_brush_colors(brush: Brush, paint: impl Fn(Color) -> Color) -> Brush {
109    match brush {
110        Brush::Solid(color) => Brush::solid(paint(color)),
111        Brush::LinearGradient {
112            colors,
113            stops,
114            start,
115            end,
116            tile_mode,
117        } => Brush::LinearGradient {
118            colors: colors.into_iter().map(paint).collect(),
119            stops,
120            start,
121            end,
122            tile_mode,
123        },
124        Brush::RadialGradient {
125            colors,
126            stops,
127            center,
128            radius,
129            tile_mode,
130        } => Brush::RadialGradient {
131            colors: colors.into_iter().map(paint).collect(),
132            stops,
133            center,
134            radius,
135            tile_mode,
136        },
137        Brush::SweepGradient {
138            colors,
139            stops,
140            center,
141        } => Brush::SweepGradient {
142            colors: colors.into_iter().map(paint).collect(),
143            stops,
144            center,
145        },
146    }
147}
148
149fn scale_brush_geometry(brush: Brush, layer: &GraphicsLayer) -> Brush {
150    let scale_x = layer_scale_x(layer);
151    let scale_y = layer_scale_y(layer);
152    let uniform_scale = layer_uniform_scale(layer);
153
154    match brush {
155        Brush::Solid(color) => Brush::Solid(color),
156        Brush::LinearGradient {
157            colors,
158            stops,
159            mut start,
160            mut end,
161            tile_mode,
162        } => {
163            start.x *= scale_x;
164            start.y *= scale_y;
165            end.x *= scale_x;
166            end.y *= scale_y;
167            Brush::LinearGradient {
168                colors,
169                stops,
170                start,
171                end,
172                tile_mode,
173            }
174        }
175        Brush::RadialGradient {
176            colors,
177            stops,
178            mut center,
179            mut radius,
180            tile_mode,
181        } => {
182            center.x *= scale_x;
183            center.y *= scale_y;
184            radius *= uniform_scale;
185            Brush::RadialGradient {
186                colors,
187                stops,
188                center,
189                radius,
190                tile_mode,
191            }
192        }
193        Brush::SweepGradient {
194            colors,
195            stops,
196            mut center,
197        } => {
198            center.x *= scale_x;
199            center.y *= scale_y;
200            Brush::SweepGradient {
201                colors,
202                stops,
203                center,
204            }
205        }
206    }
207}
208
209/// A layer-resolved brush, split at the solid/gradient boundary.
210///
211/// The per-frame shape emit produces one of these for every draw: the solid
212/// case — effectively all of a heavy animated scene — carries its color
213/// inline, so emitting a shape neither clones a `Brush` nor leaves an enum
214/// with heap-carrying variants for frame teardown to walk. Only the rare
215/// gradient still travels as a cloned [`Brush`].
216#[derive(Clone, Debug, PartialEq)]
217pub enum ResolvedBrush {
218    Solid(Color),
219    /// A non-solid brush (gradients), already layer-resolved.
220    Other(Brush),
221}
222
223impl ResolvedBrush {
224    pub fn from_brush(brush: Brush) -> Self {
225        match brush {
226            Brush::Solid(color) => Self::Solid(color),
227            other => Self::Other(other),
228        }
229    }
230
231    /// The plain `Brush` this resolved form stands for — same values,
232    /// reassembled for consumers that keep speaking `Brush`.
233    pub fn into_brush(self) -> Brush {
234        match self {
235            Self::Solid(color) => Brush::Solid(color),
236            Self::Other(brush) => brush,
237        }
238    }
239}
240
241/// [`apply_layer_to_brush`] without the solid-brush clone: the borrowed
242/// brush's color is copied (or layer-adjusted) inline, and only gradients
243/// are cloned. Produces exactly the values `apply_layer_to_brush` would —
244/// both branches below mirror its fast path and its `Solid` arm verbatim.
245pub fn resolve_layer_brush(brush: &Brush, layer: &GraphicsLayer) -> ResolvedBrush {
246    match brush {
247        Brush::Solid(color) => {
248            if layer.alpha == 1.0
249                && layer.color_filter.is_none()
250                && layer_scale_x(layer) == 1.0
251                && layer_scale_y(layer) == 1.0
252            {
253                ResolvedBrush::Solid(*color)
254            } else {
255                ResolvedBrush::Solid(apply_layer_to_color(*color, layer))
256            }
257        }
258        other => ResolvedBrush::Other(apply_layer_to_brush(other.clone(), layer)),
259    }
260}
261
262pub fn scale_corner_radii(radii: CornerRadii, scale: f32) -> CornerRadii {
263    CornerRadii {
264        top_left: radii.top_left * scale,
265        top_right: radii.top_right * scale,
266        bottom_right: radii.bottom_right * scale,
267        bottom_left: radii.bottom_left * scale,
268    }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
272pub enum DrawPlacement {
273    Behind,
274    Overlay,
275}
276
277pub fn primitives_for_placement(
278    command: &DrawCommand,
279    placement: DrawPlacement,
280    size: Size,
281) -> Vec<DrawPrimitive> {
282    recording_for_placement_reusing(command, placement, size, CommandRecording::default)
283        .map(|(recording, segments)| recording.primitives(segments).collect())
284        .unwrap_or_default()
285}
286
287/// Records `command` into `storage`, a recording the caller kept from an
288/// earlier frame so its buffers keep the capacity they earned, and names
289/// the segments `placement` draws: everything for a behind or overlay
290/// command, the part before or after the last content marker for a
291/// with-content command. `None` when the command has no `placement` half,
292/// in which case storage is not acquired and nothing is recorded.
293pub fn recording_for_placement_reusing(
294    command: &DrawCommand,
295    placement: DrawPlacement,
296    size: Size,
297    storage: impl FnOnce() -> CommandRecording,
298) -> Option<(CommandRecording, Range<u32>)> {
299    let record = move |func: &DrawCommandFn| {
300        let mut scope = cranpose_ui::command_draw_scope_reusing(size, storage());
301        func(&mut scope);
302        scope.finish()
303    };
304    match (placement, command) {
305        (DrawPlacement::Behind, DrawCommand::Behind(func))
306        | (DrawPlacement::Overlay, DrawCommand::Overlay(func)) => {
307            let recording = record(func);
308            let segments = recording.all_segments();
309            Some((recording, segments))
310        }
311        (_, DrawCommand::WithContent(func)) => {
312            let recording = record(func);
313            let segments = recording.content_split(placement == DrawPlacement::Behind);
314            Some((recording, segments))
315        }
316        _ => None,
317    }
318}
319
320#[cfg(test)]
321#[path = "tests/style_shared_tests.rs"]
322mod tests;