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