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