Skip to main content

cranpose_render_common/
primitive_emit.rs

1use std::{rc::Rc, sync::Arc};
2
3use cranpose_ui::text::{AnnotatedString, TextStyle, drawn_text_style};
4use cranpose_ui_graphics::{
5    ArcGeometry, BlendMode, Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, GraphicsLayer,
6    ImageBitmap, ImageSampling, LineGeometry, Point, Rect, RoundedCornerShape, ShadowPrimitive,
7    Stroke, TextPrimitive, Trapezoid, arc_band, inflate_rect,
8};
9
10use crate::{
11    graph::quad_bounds,
12    layer_transform::{
13        apply_layer_affine_to_point, apply_layer_affine_to_rect, apply_layer_to_quad,
14        apply_layer_to_rect, layer_uniform_scale,
15    },
16    style_shared::{
17        ResolvedBrush, apply_layer_to_brush, apply_layer_to_color, compose_color_filters,
18        resolve_layer_brush, scale_corner_radii,
19    },
20};
21
22/// The space in which the clip rectangle of a primitive is given.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PrimitiveClipSpace {
25    /// The clip is in the layer's own space and the layer transform does not apply to it.
26    Local,
27    /// The clip is moved with the layer transform, like the primitive itself.
28    LayerTransformed,
29}
30
31/// A shape ready for a renderer to draw, with the layer transform, paint and clip resolved.
32pub struct ShapeDrawParams {
33    /// Axis-aligned bounds of `quad`.
34    pub rect: Rect,
35    /// The shape's rectangle after the layer's scale and translation, before any rotation.
36    pub local_rect: Rect,
37    /// The shape's rectangle after the full layer transform, as four corners.
38    pub quad: [[f32; 2]; 4],
39    /// Layer-resolved at emit time. Solid — effectively every shape of a
40    /// heavy animated frame — is an inline color; only gradients carry a
41    /// cloned [`Brush`].
42    pub brush: ResolvedBrush,
43    /// Rounded corners of the shape, or `None` for sharp corners.
44    pub shape: Option<RoundedCornerShape>,
45    /// `Some` means "stroke the outline of `local_rect`/`shape`" instead of
46    /// filling it. The width is already in `local_rect` units (the layer scale
47    /// has been applied), and `local_rect`/`quad` have already been inflated by
48    /// half that width so the outer half of the stroke has geometry to land on.
49    pub stroke: Option<Stroke>,
50    /// `Some` replaces the rect geometry entirely with a circular band. The
51    /// center and radii are in `local_rect` units.
52    pub arc: Option<ArcGeometry>,
53    /// `Some` replaces the rect geometry entirely with a stroked segment, its
54    /// ends and width in `local_rect` units.
55    pub line: Option<LineGeometry>,
56    /// `Some` replaces the rect geometry entirely with a slice of a path
57    /// fill, in `local_rect` units; `local_rect` is the rect its brush
58    /// resolves against.
59    pub trapezoid: Option<Trapezoid>,
60    /// Clip rectangle that cuts the shape, or `None` when nothing clips it.
61    pub clip: Option<Rect>,
62    /// How the shape blends onto what lies beneath it.
63    pub blend_mode: BlendMode,
64    /// Whether the shape sits in an animated motion context, where a renderer must not snap it
65    /// to the pixel grid.
66    pub motion_context_animated: bool,
67}
68
69fn stroked_draw_rect(
70    local_rect: Rect,
71    stroke: Option<Stroke>,
72    layer_bounds: Rect,
73    layer: &GraphicsLayer,
74) -> Option<(Rect, Option<Stroke>)> {
75    let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
76    let Some(stroke) = stroke else {
77        return Some((draw_rect, None));
78    };
79    if !stroke.is_visible() {
80        return None;
81    }
82    let outset = stroke.half_width();
83    Some((
84        inflate_rect(draw_rect, outset),
85        Some(stroke.scaled(layer_uniform_scale(layer))),
86    ))
87}
88
89/// An image ready for a renderer to draw, with the layer transform, opacity and clip resolved.
90pub struct ImageDrawParams {
91    /// Axis-aligned bounds of `quad`.
92    pub rect: Rect,
93    /// The image's rectangle after the layer's scale and translation, before any rotation.
94    pub local_rect: Rect,
95    /// The image's rectangle after the full layer transform, as four corners.
96    pub quad: [[f32; 2]; 4],
97    /// The bitmap to draw.
98    pub image: ImageBitmap,
99    /// Opacity of the image from 0.0 to 1.0.
100    pub alpha: f32,
101    /// Color filter applied to the image, if any.
102    pub color_filter: Option<ColorFilter>,
103    /// How the bitmap is sampled when it is scaled.
104    pub sampling: ImageSampling,
105    /// Clip rectangle that cuts the image, or `None` when nothing clips it.
106    pub clip: Option<Rect>,
107    /// Region of the bitmap to draw, in bitmap pixels, or `None` for the whole bitmap.
108    pub src_rect: Option<Rect>,
109    /// How the image blends onto what lies beneath it.
110    pub blend_mode: BlendMode,
111    /// Whether the image sits in an animated motion context, where a renderer must not snap it
112    /// to the pixel grid.
113    pub motion_context_animated: bool,
114}
115
116/// A `DrawScope` text run, lowered into the vocabulary the text pipeline
117/// already speaks.
118///
119/// The fields line up one-for-one with `CompositorScene::push_text` in both
120/// backends, so a text primitive joins the same glyph atlas, run cache and
121/// shader the `Text` composable uses instead of getting a pipeline of its own.
122pub struct TextDrawParams {
123    /// Layer-transformed block box. Glyphs are laid out from its top-left; the
124    /// draw scope already resolved alignment into it.
125    pub rect: Rect,
126    /// The text to draw.
127    pub text: Rc<AnnotatedString>,
128    /// The lines and glyphs, from `rect`'s top left corner.
129    pub layout: cranpose_text::TextLayout,
130    /// Color that fills the glyphs.
131    pub color: Color,
132    /// Style the text is drawn with.
133    pub text_style: Arc<TextStyle>,
134    /// `text_style`'s render hash.
135    pub style_hash: u64,
136    /// Font size in logical pixels, before `scale` is applied.
137    pub font_size: f32,
138    /// Uniform layer scale — the factor glyphs are rasterized at.
139    pub scale: f32,
140    /// Clip rectangle that cuts the text, or `None` when nothing clips it.
141    pub clip: Option<Rect>,
142}
143
144/// Receives the draw calls that `emit_draw_primitive` produces from a draw primitive.
145pub trait DrawPrimitiveSink {
146    /// Draws a shape.
147    fn push_shape(&mut self, params: ShapeDrawParams);
148
149    /// Draws an image.
150    fn push_image(&mut self, params: ImageDrawParams);
151
152    /// Emits a shadow without transferring ownership of its caster and cutout.
153    fn push_shadow(
154        &mut self,
155        shadow_primitive: &ShadowPrimitive,
156        layer_bounds: Rect,
157        layer: &GraphicsLayer,
158        clip: Option<Rect>,
159    );
160
161    /// Draws a text run. The default drops it, for sinks that only collect
162    /// geometry (shadow casters, hit testing) and backends with no text
163    /// pipeline.
164    fn push_text(&mut self, params: TextDrawParams) {
165        let _ = params;
166    }
167}
168
169/// Resolves a borrowed shape with the layer transform, paint, and clipping applied.
170pub fn draw_shape_params_for_primitive(
171    primitive: &DrawPrimitive,
172    layer_bounds: Rect,
173    layer: &GraphicsLayer,
174    clip: Option<Rect>,
175    blend_mode: BlendMode,
176) -> Option<ShapeDrawParams> {
177    struct SingleShapeSink {
178        shape: Option<ShapeDrawParams>,
179    }
180
181    impl DrawPrimitiveSink for SingleShapeSink {
182        fn push_shape(&mut self, params: ShapeDrawParams) {
183            if self.shape.is_none() {
184                self.shape = Some(params);
185            }
186        }
187
188        fn push_image(&mut self, _params: ImageDrawParams) {}
189
190        fn push_shadow(
191            &mut self,
192            _shadow_primitive: &ShadowPrimitive,
193            _layer_bounds: Rect,
194            _layer: &GraphicsLayer,
195            _clip: Option<Rect>,
196        ) {
197        }
198    }
199
200    let mut sink = SingleShapeSink { shape: None };
201    emit_draw_primitive(
202        primitive,
203        layer_bounds,
204        layer,
205        clip,
206        &mut sink,
207        Some(blend_mode),
208        false,
209    );
210    sink.shape
211}
212
213/// The clip a node paints within once its own clip meets the clips above
214/// it: `None` only when nothing clips at all. Two clips that do not overlap
215/// resolve to [`Rect::EMPTY`], never to `None` -- a list that has scrolled a
216/// control past its edge has clipped the control away, not set it free.
217pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
218    match (parent_clip, requested_clip) {
219        (Some(parent), Some(current)) => Some(parent.intersect(current).unwrap_or(Rect::EMPTY)),
220        (Some(parent), None) => Some(parent),
221        (None, Some(current)) => Some(current),
222        (None, None) => None,
223    }
224}
225
226/// Returns the clip for a primitive: `local_clip` moved to `layer_bounds`, moved with the layer
227/// transform for `PrimitiveClipSpace::LayerTransformed`, and intersected with `parent_clip`.
228/// Returns `parent_clip` when the primitive has no clip of its own.
229pub fn resolve_primitive_clip(
230    local_clip: Option<Rect>,
231    layer_bounds: Rect,
232    layer: &GraphicsLayer,
233    parent_clip: Option<Rect>,
234    clip_space: PrimitiveClipSpace,
235) -> Option<Rect> {
236    let Some(local_clip) = local_clip else {
237        return parent_clip;
238    };
239    let clip_rect = Rect {
240        x: layer_bounds.x + local_clip.x,
241        y: layer_bounds.y + local_clip.y,
242        width: local_clip.width,
243        height: local_clip.height,
244    };
245    let requested_clip = match clip_space {
246        PrimitiveClipSpace::Local => clip_rect,
247        PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
248    };
249    resolve_clip(parent_clip, Some(requested_clip))
250}
251
252// Where a shape lands: the layer that places it, the clip that cuts it and
253// the blend it draws with.
254struct ShapePlacement<'a> {
255    layer_bounds: Rect,
256    layer: &'a GraphicsLayer,
257    clip: Option<Rect>,
258    blend_mode: BlendMode,
259    motion_context_animated: bool,
260}
261
262impl ShapePlacement<'_> {
263    // A shape over `draw_rect` with no geometry of its own; each builder sets its geometry.
264    fn fill(&self, draw_rect: Rect, brush: &Brush) -> ShapeDrawParams {
265        let quad = apply_layer_to_quad(draw_rect, self.layer_bounds, self.layer);
266        ShapeDrawParams {
267            rect: quad_bounds(quad),
268            local_rect: apply_layer_affine_to_rect(draw_rect, self.layer_bounds, self.layer),
269            quad,
270            brush: resolve_layer_brush(brush, self.layer),
271            shape: None,
272            stroke: None,
273            arc: None,
274            line: None,
275            trapezoid: None,
276            clip: self.clip,
277            blend_mode: self.blend_mode,
278            motion_context_animated: self.motion_context_animated,
279        }
280    }
281
282    fn translate(&self, rect: Rect) -> Rect {
283        rect.translate(self.layer_bounds.x, self.layer_bounds.y)
284    }
285
286    fn place(&self, point: Point) -> Point {
287        apply_layer_affine_to_point(
288            Point::new(point.x + self.layer_bounds.x, point.y + self.layer_bounds.y),
289            self.layer_bounds,
290            self.layer,
291        )
292    }
293}
294
295fn rect_shape_params(
296    local_rect: Rect,
297    brush: &Brush,
298    stroke: Option<Stroke>,
299    placement: &ShapePlacement,
300) -> Option<ShapeDrawParams> {
301    let (draw_rect, stroke) =
302        stroked_draw_rect(local_rect, stroke, placement.layer_bounds, placement.layer)?;
303    Some(ShapeDrawParams {
304        stroke,
305        ..placement.fill(draw_rect, brush)
306    })
307}
308
309fn round_rect_shape_params(
310    local_rect: Rect,
311    brush: &Brush,
312    radii: CornerRadii,
313    stroke: Option<Stroke>,
314    placement: &ShapePlacement,
315) -> Option<ShapeDrawParams> {
316    let (draw_rect, stroke) =
317        stroked_draw_rect(local_rect, stroke, placement.layer_bounds, placement.layer)?;
318    let radii = scale_corner_radii(radii, layer_uniform_scale(placement.layer));
319    Some(ShapeDrawParams {
320        shape: Some(RoundedCornerShape::with_radii(radii)),
321        stroke,
322        ..placement.fill(draw_rect, brush)
323    })
324}
325
326// `arc` is in scope coordinates and `local_rect` is its tight bounds.
327fn arc_shape_params(
328    local_rect: Rect,
329    brush: &Brush,
330    arc: ArcGeometry,
331    placement: &ShapePlacement,
332) -> Option<ShapeDrawParams> {
333    if arc.is_degenerate() {
334        return None;
335    }
336    let scale = layer_uniform_scale(placement.layer);
337    Some(ShapeDrawParams {
338        arc: Some(arc.scaled_about(placement.place(arc.center), scale)),
339        ..placement.fill(placement.translate(local_rect), brush)
340    })
341}
342
343// The segment's ends are placed by the layer's transform and its width is
344// scaled by the layer's uniform scale.
345fn line_shape_params(
346    brush: &Brush,
347    start: Point,
348    end: Point,
349    stroke: Stroke,
350    placement: &ShapePlacement,
351) -> Option<ShapeDrawParams> {
352    let line = LineGeometry::new(start, end, stroke);
353    if line.is_degenerate() {
354        return None;
355    }
356    let scale = layer_uniform_scale(placement.layer);
357    Some(ShapeDrawParams {
358        line: Some(line.placed(placement.place(start), placement.place(end), scale)),
359        ..placement.fill(placement.translate(line.bounds()), brush)
360    })
361}
362
363// The brush rect is placed by the layer's transform as a rect is, and the
364// slice's corners are placed by it as points.
365fn trapezoid_shape_params(
366    brush_rect: Rect,
367    brush: &Brush,
368    trapezoid: Trapezoid,
369    placement: &ShapePlacement,
370) -> Option<ShapeDrawParams> {
371    if trapezoid.right <= trapezoid.left {
372        return None;
373    }
374    let place = |x: f32, y: f32| placement.place(Point::new(x, y));
375    let [top_left, top_right, bottom_left, bottom_right] = [
376        place(trapezoid.left, trapezoid.top[0]),
377        place(trapezoid.right, trapezoid.top[1]),
378        place(trapezoid.left, trapezoid.bottom[0]),
379        place(trapezoid.right, trapezoid.bottom[1]),
380    ];
381    Some(ShapeDrawParams {
382        trapezoid: Some(Trapezoid {
383            left: top_left.x,
384            right: top_right.x,
385            top: [top_left.y, top_right.y],
386            bottom: [bottom_left.y, bottom_right.y],
387            ..trapezoid
388        }),
389        ..placement.fill(placement.translate(brush_rect), brush)
390    })
391}
392
393/// Lowers `primitive` to shape, image, shadow and text draw calls on `sink`, with the transform
394/// of `layer` applied at `layer_bounds`. `blend_mode` is the mode inherited from an enclosing
395/// blend primitive, and `motion_context_animated` is passed on to every call. Content and group
396/// markers draw nothing: the scene builder turns groups into layers.
397pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
398    primitive: &DrawPrimitive,
399    layer_bounds: Rect,
400    layer: &GraphicsLayer,
401    clip: Option<Rect>,
402    sink: &mut S,
403    blend_mode: Option<BlendMode>,
404    motion_context_animated: bool,
405) {
406    let placement = ShapePlacement {
407        layer_bounds,
408        layer,
409        clip,
410        blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
411        motion_context_animated,
412    };
413    match primitive {
414        DrawPrimitive::Content | DrawPrimitive::Group(_) => {}
415        DrawPrimitive::Blend {
416            primitive,
417            blend_mode: nested,
418        } => emit_draw_primitive(
419            primitive,
420            layer_bounds,
421            layer,
422            clip,
423            sink,
424            blend_mode.or(Some(*nested)),
425            motion_context_animated,
426        ),
427        DrawPrimitive::Rect {
428            rect,
429            brush,
430            stroke,
431        } => {
432            if let Some(params) = rect_shape_params(*rect, brush, *stroke, &placement) {
433                sink.push_shape(params);
434            }
435        }
436        DrawPrimitive::RoundRect {
437            rect,
438            brush,
439            radii,
440            stroke,
441        } => {
442            if let Some(params) = round_rect_shape_params(*rect, brush, *radii, *stroke, &placement)
443            {
444                sink.push_shape(params);
445            }
446        }
447        DrawPrimitive::Arc {
448            rect,
449            brush,
450            center,
451            radius,
452            start_angle,
453            sweep_angle,
454            stroke,
455            inner_radius,
456        } => {
457            let (band_inner, band_outer, cap) = arc_band(*radius, *inner_radius, *stroke);
458            let arc = ArcGeometry::new(
459                *center,
460                band_inner,
461                band_outer,
462                *start_angle,
463                *sweep_angle,
464                cap,
465            );
466            if let Some(params) = arc_shape_params(*rect, brush, arc, &placement) {
467                sink.push_shape(params);
468            }
469        }
470        DrawPrimitive::Line {
471            brush,
472            start,
473            end,
474            stroke,
475            ..
476        } => {
477            if let Some(params) = line_shape_params(brush, *start, *end, *stroke, &placement) {
478                sink.push_shape(params);
479            }
480        }
481        DrawPrimitive::Trapezoid {
482            rect,
483            brush,
484            trapezoid,
485        } => {
486            if let Some(params) = trapezoid_shape_params(*rect, brush, *trapezoid, &placement) {
487                sink.push_shape(params);
488            }
489        }
490        DrawPrimitive::Image {
491            rect: local_rect,
492            image,
493            alpha,
494            color_filter,
495            sampling,
496            src_rect,
497        } => {
498            let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
499            let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
500            let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
501            sink.push_image(ImageDrawParams {
502                rect: quad_bounds(quad),
503                local_rect,
504                quad,
505                image: image.clone(),
506                alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
507                color_filter: compose_color_filters(
508                    color_filter.as_ref(),
509                    layer.color_filter.as_ref(),
510                ),
511                sampling: *sampling,
512                clip,
513                src_rect: *src_rect,
514                blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
515                motion_context_animated,
516            });
517        }
518        DrawPrimitive::Text(text) => {
519            if let Some(params) = text_draw_params((**text).clone(), layer_bounds, layer, clip) {
520                sink.push_text(params);
521            }
522        }
523        DrawPrimitive::Shadow(shadow_primitive) => {
524            sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
525        }
526    }
527}
528
529fn text_draw_params(
530    text: TextPrimitive,
531    layer_bounds: Rect,
532    layer: &GraphicsLayer,
533    clip: Option<Rect>,
534) -> Option<TextDrawParams> {
535    if text.text.is_empty() {
536        return None;
537    }
538    // A text no text system laid out draws nothing.
539    let layout = cranpose_text::TextLayout::clone(
540        &*text
541            .layout
542            .as_ref()?
543            .downcast::<cranpose_text::TextLayout>()?,
544    );
545    let draw_rect = text.rect.translate(layer_bounds.x, layer_bounds.y);
546    let rect = apply_layer_to_rect(draw_rect, layer_bounds, layer);
547    if !(rect.width > 0.0 && rect.height > 0.0) {
548        return None;
549    }
550    let scale = layer_uniform_scale(layer);
551    if !scale.is_finite() || scale <= 0.0 {
552        return None;
553    }
554    let (mut text_style, mut style_hash) = drawn_text_style(&text.style);
555    // A gradient fills the glyphs through the style's brush, as a `Text`
556    // composable's does, laid over the text's extent.
557    let color = match text.brush {
558        Brush::Solid(color) => apply_layer_to_color(color, layer),
559        gradient => {
560            let gradient = apply_layer_to_brush(gradient, layer);
561            let first = gradient
562                .colors()
563                .first()
564                .copied()
565                .unwrap_or(Color::TRANSPARENT);
566            let style = Arc::make_mut(&mut text_style);
567            style.span_style.brush = Some(gradient);
568            style_hash = style.render_hash();
569            first
570        }
571    };
572    if !text_style
573        .span_style
574        .brush
575        .as_ref()
576        .map_or(color.3 > 0.0, Brush::paints)
577    {
578        return None;
579    }
580
581    Some(TextDrawParams {
582        rect,
583        text: cranpose_ui::text::shared_plain_annotated_string(text.text.as_ref()),
584        layout,
585        color,
586        text_style,
587        style_hash,
588        font_size: text.style.resolved_font_size(),
589        scale,
590        clip,
591    })
592}
593
594#[cfg(test)]
595#[path = "tests/primitive_emit_tests.rs"]
596mod tests;