Skip to main content

cranpose_render_common/
style_shared.rs

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