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