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