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