Skip to main content

cranpose_render_common/
primitive_emit.rs

1use std::rc::Rc;
2
3use cranpose_ui::text::{
4    AnnotatedString, TextLayoutOptions, TextOverflow, TextStyle, text_style_for_draw_style,
5};
6use cranpose_ui_graphics::{
7    ArcGeometry, BlendMode, Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, GraphicsLayer,
8    ImageBitmap, ImageSampling, Point, Rect, RoundedCornerShape, ShadowPrimitive, Stroke,
9    TextPrimitive, arc_band, inflate_rect,
10};
11
12use crate::{
13    graph::quad_bounds,
14    layer_transform::{
15        apply_layer_affine_to_point, apply_layer_affine_to_rect, apply_layer_to_quad,
16        apply_layer_to_rect, layer_uniform_scale,
17    },
18    style_shared::{
19        ResolvedBrush, apply_layer_to_color, compose_color_filters, resolve_layer_brush,
20        scale_corner_radii,
21    },
22};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum PrimitiveClipSpace {
26    Local,
27    LayerTransformed,
28}
29
30pub struct ShapeDrawParams {
31    pub rect: Rect,
32    pub local_rect: Rect,
33    pub quad: [[f32; 2]; 4],
34    /// Layer-resolved at emit time. Solid — effectively every shape of a
35    /// heavy animated frame — is an inline color; only gradients carry a
36    /// cloned [`Brush`].
37    pub brush: ResolvedBrush,
38    pub shape: Option<RoundedCornerShape>,
39    /// `Some` means "stroke the outline of `local_rect`/`shape`" instead of
40    /// filling it. The width is already in `local_rect` units (the layer scale
41    /// has been applied), and `local_rect`/`quad` have already been inflated by
42    /// half that width so the outer half of the stroke has geometry to land on.
43    pub stroke: Option<Stroke>,
44    /// `Some` replaces the rect geometry entirely with a circular band. The
45    /// center and radii are in `local_rect` units.
46    pub arc: Option<ArcGeometry>,
47    pub clip: Option<Rect>,
48    pub blend_mode: BlendMode,
49    pub motion_context_animated: bool,
50}
51
52fn stroked_draw_rect(
53    local_rect: Rect,
54    stroke: Option<Stroke>,
55    layer_bounds: Rect,
56    layer: &GraphicsLayer,
57) -> Option<(Rect, Option<Stroke>)> {
58    let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
59    let Some(stroke) = stroke else {
60        return Some((draw_rect, None));
61    };
62    if !stroke.is_visible() {
63        return None;
64    }
65    let outset = stroke.half_width();
66    Some((
67        inflate_rect(draw_rect, outset),
68        Some(stroke.scaled(layer_uniform_scale(layer))),
69    ))
70}
71
72pub struct ImageDrawParams {
73    pub rect: Rect,
74    pub local_rect: Rect,
75    pub quad: [[f32; 2]; 4],
76    pub image: ImageBitmap,
77    pub alpha: f32,
78    pub color_filter: Option<ColorFilter>,
79    pub sampling: ImageSampling,
80    pub clip: Option<Rect>,
81    pub src_rect: Option<Rect>,
82    pub blend_mode: BlendMode,
83    pub motion_context_animated: bool,
84}
85
86/// A `DrawScope` text run, lowered into the vocabulary the text pipeline
87/// already speaks.
88///
89/// The fields line up one-for-one with `CompositorScene::push_text` in both
90/// backends, so a text primitive joins the same glyph atlas, run cache and
91/// shader the `Text` composable uses instead of getting a pipeline of its own.
92pub struct TextDrawParams {
93    /// Layer-transformed block box. Glyphs are laid out from its top-left; the
94    /// draw scope already resolved alignment into it.
95    pub rect: Rect,
96    pub text: Rc<AnnotatedString>,
97    pub color: Color,
98    pub text_style: TextStyle,
99    pub font_size: f32,
100    /// Uniform layer scale — the factor glyphs are rasterized at.
101    pub scale: f32,
102    pub layout_options: TextLayoutOptions,
103    pub clip: Option<Rect>,
104}
105
106pub trait DrawPrimitiveSink {
107    fn push_shape(&mut self, params: ShapeDrawParams);
108
109    fn push_image(&mut self, params: ImageDrawParams);
110
111    /// Emits a shadow without transferring ownership of its caster and cutout.
112    fn push_shadow(
113        &mut self,
114        shadow_primitive: &ShadowPrimitive,
115        layer_bounds: Rect,
116        layer: &GraphicsLayer,
117        clip: Option<Rect>,
118    );
119
120    /// Draws a text run. The default drops it, for sinks that only collect
121    /// geometry (shadow casters, hit testing) and backends with no text
122    /// pipeline.
123    fn push_text(&mut self, params: TextDrawParams) {
124        let _ = params;
125    }
126}
127
128/// Resolves a borrowed shape with the layer transform, paint, and clipping applied.
129pub fn draw_shape_params_for_primitive(
130    primitive: &DrawPrimitive,
131    layer_bounds: Rect,
132    layer: &GraphicsLayer,
133    clip: Option<Rect>,
134    blend_mode: BlendMode,
135) -> Option<ShapeDrawParams> {
136    struct SingleShapeSink {
137        shape: Option<ShapeDrawParams>,
138    }
139
140    impl DrawPrimitiveSink for SingleShapeSink {
141        fn push_shape(&mut self, params: ShapeDrawParams) {
142            if self.shape.is_none() {
143                self.shape = Some(params);
144            }
145        }
146
147        fn push_image(&mut self, _params: ImageDrawParams) {}
148
149        fn push_shadow(
150            &mut self,
151            _shadow_primitive: &ShadowPrimitive,
152            _layer_bounds: Rect,
153            _layer: &GraphicsLayer,
154            _clip: Option<Rect>,
155        ) {
156        }
157    }
158
159    let mut sink = SingleShapeSink { shape: None };
160    emit_draw_primitive(
161        primitive,
162        layer_bounds,
163        layer,
164        clip,
165        &mut sink,
166        Some(blend_mode),
167        false,
168    );
169    sink.shape
170}
171
172/// The clip a node paints within once its own clip meets the clips above
173/// it: `None` only when nothing clips at all. Two clips that do not overlap
174/// resolve to [`Rect::EMPTY`], never to `None` -- a list that has scrolled a
175/// control past its edge has clipped the control away, not set it free.
176pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
177    match (parent_clip, requested_clip) {
178        (Some(parent), Some(current)) => Some(parent.intersect(current).unwrap_or(Rect::EMPTY)),
179        (Some(parent), None) => Some(parent),
180        (None, Some(current)) => Some(current),
181        (None, None) => None,
182    }
183}
184
185pub fn resolve_primitive_clip(
186    local_clip: Option<Rect>,
187    layer_bounds: Rect,
188    layer: &GraphicsLayer,
189    parent_clip: Option<Rect>,
190    clip_space: PrimitiveClipSpace,
191) -> Option<Rect> {
192    let Some(local_clip) = local_clip else {
193        return parent_clip;
194    };
195    let clip_rect = Rect {
196        x: layer_bounds.x + local_clip.x,
197        y: layer_bounds.y + local_clip.y,
198        width: local_clip.width,
199        height: local_clip.height,
200    };
201    let requested_clip = match clip_space {
202        PrimitiveClipSpace::Local => clip_rect,
203        PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
204    };
205    resolve_clip(parent_clip, Some(requested_clip))
206}
207
208/// The [`DrawPrimitive::Rect`] arm of [`emit_draw_primitive`] as a pure
209/// builder over borrowed fields. The parallel shape-run collect calls these
210/// directly from worker threads — a `&DrawPrimitive` cannot cross (the text
211/// variant carries `Rc`), but the shape variants' fields can.
212#[expect(clippy::too_many_arguments)]
213pub fn rect_shape_params(
214    local_rect: Rect,
215    brush: &Brush,
216    stroke: Option<Stroke>,
217    layer_bounds: Rect,
218    layer: &GraphicsLayer,
219    clip: Option<Rect>,
220    blend_mode: BlendMode,
221    motion_context_animated: bool,
222) -> Option<ShapeDrawParams> {
223    let (draw_rect, stroke) = stroked_draw_rect(local_rect, stroke, layer_bounds, layer)?;
224    let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
225    let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
226    Some(ShapeDrawParams {
227        rect: quad_bounds(quad),
228        local_rect,
229        quad,
230        brush: resolve_layer_brush(brush, layer),
231        shape: None,
232        stroke,
233        arc: None,
234        clip,
235        blend_mode,
236        motion_context_animated,
237    })
238}
239
240/// The [`DrawPrimitive::RoundRect`] arm of [`emit_draw_primitive`]; see
241/// [`rect_shape_params`].
242#[expect(clippy::too_many_arguments)]
243pub fn round_rect_shape_params(
244    local_rect: Rect,
245    brush: &Brush,
246    radii: CornerRadii,
247    stroke: Option<Stroke>,
248    layer_bounds: Rect,
249    layer: &GraphicsLayer,
250    clip: Option<Rect>,
251    blend_mode: BlendMode,
252    motion_context_animated: bool,
253) -> Option<ShapeDrawParams> {
254    let (draw_rect, stroke) = stroked_draw_rect(local_rect, stroke, layer_bounds, layer)?;
255    let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
256    let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
257    let shape =
258        RoundedCornerShape::with_radii(scale_corner_radii(radii, layer_uniform_scale(layer)));
259    Some(ShapeDrawParams {
260        rect: quad_bounds(quad),
261        local_rect,
262        quad,
263        brush: resolve_layer_brush(brush, layer),
264        shape: Some(shape),
265        stroke,
266        arc: None,
267        clip,
268        blend_mode,
269        motion_context_animated,
270    })
271}
272
273/// The [`DrawPrimitive::Arc`] arm of [`emit_draw_primitive`]; see
274/// [`rect_shape_params`].
275#[expect(clippy::too_many_arguments)]
276pub fn arc_shape_params(
277    local_rect: Rect,
278    brush: &Brush,
279    center: Point,
280    radius: f32,
281    start_angle: f32,
282    sweep_angle: f32,
283    stroke: Option<Stroke>,
284    inner_radius: f32,
285    layer_bounds: Rect,
286    layer: &GraphicsLayer,
287    clip: Option<Rect>,
288    blend_mode: BlendMode,
289    motion_context_animated: bool,
290) -> Option<ShapeDrawParams> {
291    let (band_inner, band_outer, cap) = arc_band(radius, inner_radius, stroke);
292    let arc = ArcGeometry::new(
293        center,
294        band_inner,
295        band_outer,
296        start_angle,
297        sweep_angle,
298        cap,
299    );
300    if arc.is_degenerate() {
301        return None;
302    }
303    let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
304    let out_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
305    let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
306    let scale = layer_uniform_scale(layer);
307    let arc_center = apply_layer_affine_to_point(
308        Point::new(center.x + layer_bounds.x, center.y + layer_bounds.y),
309        layer_bounds,
310        layer,
311    );
312    Some(ShapeDrawParams {
313        rect: quad_bounds(quad),
314        local_rect: out_rect,
315        quad,
316        brush: resolve_layer_brush(brush, layer),
317        shape: None,
318        stroke: None,
319        arc: Some(arc.scaled_about(arc_center, scale)),
320        clip,
321        blend_mode,
322        motion_context_animated,
323    })
324}
325
326pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
327    primitive: &DrawPrimitive,
328    layer_bounds: Rect,
329    layer: &GraphicsLayer,
330    clip: Option<Rect>,
331    sink: &mut S,
332    blend_mode: Option<BlendMode>,
333    motion_context_animated: bool,
334) {
335    match primitive {
336        DrawPrimitive::Content => {}
337        DrawPrimitive::Blend {
338            primitive,
339            blend_mode: nested,
340        } => emit_draw_primitive(
341            primitive,
342            layer_bounds,
343            layer,
344            clip,
345            sink,
346            blend_mode.or(Some(*nested)),
347            motion_context_animated,
348        ),
349        DrawPrimitive::Rect {
350            rect: local_rect,
351            brush,
352            stroke,
353        } => {
354            if let Some(params) = rect_shape_params(
355                *local_rect,
356                brush,
357                *stroke,
358                layer_bounds,
359                layer,
360                clip,
361                blend_mode.unwrap_or(BlendMode::SrcOver),
362                motion_context_animated,
363            ) {
364                sink.push_shape(params);
365            }
366        }
367        DrawPrimitive::RoundRect {
368            rect: local_rect,
369            brush,
370            radii,
371            stroke,
372        } => {
373            if let Some(params) = round_rect_shape_params(
374                *local_rect,
375                brush,
376                *radii,
377                *stroke,
378                layer_bounds,
379                layer,
380                clip,
381                blend_mode.unwrap_or(BlendMode::SrcOver),
382                motion_context_animated,
383            ) {
384                sink.push_shape(params);
385            }
386        }
387        DrawPrimitive::Arc {
388            rect: local_rect,
389            brush,
390            center,
391            radius,
392            start_angle,
393            sweep_angle,
394            stroke,
395            inner_radius,
396        } => {
397            if let Some(params) = arc_shape_params(
398                *local_rect,
399                brush,
400                *center,
401                *radius,
402                *start_angle,
403                *sweep_angle,
404                *stroke,
405                *inner_radius,
406                layer_bounds,
407                layer,
408                clip,
409                blend_mode.unwrap_or(BlendMode::SrcOver),
410                motion_context_animated,
411            ) {
412                sink.push_shape(params);
413            }
414        }
415        DrawPrimitive::Image {
416            rect: local_rect,
417            image,
418            alpha,
419            color_filter,
420            sampling,
421            src_rect,
422        } => {
423            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
424            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
425            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
426            sink.push_image(ImageDrawParams {
427                rect: quad_bounds(quad),
428                local_rect,
429                quad,
430                image: image.clone(),
431                alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
432                color_filter: compose_color_filters(*color_filter, layer.color_filter),
433                sampling: *sampling,
434                clip,
435                src_rect: *src_rect,
436                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
437                motion_context_animated,
438            });
439        }
440        DrawPrimitive::Text(text) => {
441            if let Some(params) = text_draw_params((**text).clone(), layer_bounds, layer, clip) {
442                sink.push_text(params);
443            }
444        }
445        DrawPrimitive::Shadow(shadow_primitive) => {
446            sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
447        }
448    }
449}
450
451fn text_draw_params(
452    text: TextPrimitive,
453    layer_bounds: Rect,
454    layer: &GraphicsLayer,
455    clip: Option<Rect>,
456) -> Option<TextDrawParams> {
457    if text.text.is_empty() {
458        return None;
459    }
460    let draw_rect = text.rect.translate(layer_bounds.x, layer_bounds.y);
461    let rect = apply_layer_to_rect(draw_rect, layer_bounds, layer);
462    if !(rect.width > 0.0 && rect.height > 0.0) {
463        return None;
464    }
465    let scale = layer_uniform_scale(layer);
466    if !scale.is_finite() || scale <= 0.0 {
467        return None;
468    }
469    let color = apply_layer_to_color(text.color, layer);
470    if color.3 <= 0.0 {
471        return None;
472    }
473
474    Some(TextDrawParams {
475        rect,
476        text: cranpose_ui::text::shared_plain_annotated_string(text.text.as_ref()),
477        color,
478        text_style: text_style_for_draw_style(&text.style),
479        font_size: text.style.resolved_font_size(),
480        scale,
481        layout_options: TextLayoutOptions {
482            soft_wrap: false,
483            overflow: TextOverflow::Visible,
484            ..TextLayoutOptions::default()
485        },
486        clip,
487    })
488}
489
490#[cfg(test)]
491#[path = "tests/primitive_emit_tests.rs"]
492mod tests;