Skip to main content

cranpose_render_pixels/
draw.rs

1#[cfg(test)]
2use cranpose_render_common::text_measure::{
3    fallback_char_width, fallback_cursor_x_for_byte_offset, fallback_line_height,
4    fallback_text_metrics,
5};
6use cranpose_render_common::{
7    brush_sampling::sample_brush_rgba, graph_scene::RenderDiagnostics, shape_sdf,
8    software_text_raster::rasterize_annotated_text_region, text_measure::SoftwareTextResources,
9};
10use cranpose_ui::text::TextMotion;
11use cranpose_ui_graphics::{
12    ArcGeometry, BlendMode, ColorFilter, CornerRadii, LineGeometry, Point, Rect, StrokeJoin,
13    Trapezoid, expand_rect,
14};
15
16use crate::{
17    pipeline,
18    scene::{ImageDraw, RasterScene, Scene, TextDraw},
19    style::point_in_resolved_rounded_rect,
20};
21
22fn is_blend_mode_supported(mode: BlendMode) -> bool {
23    matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
24}
25
26fn snapped_anchor_origin(anchor: Point) -> Point {
27    Point::new(anchor.x.round(), anchor.y.round())
28}
29
30fn snap_delta_for_anchor(anchor: Point) -> Point {
31    let snapped = snapped_anchor_origin(anchor);
32    Point::new(snapped.x - anchor.x, snapped.y - anchor.y)
33}
34
35#[derive(Clone, Copy)]
36struct ClipBounds {
37    min_x: i32,
38    min_y: i32,
39    max_x: i32,
40    max_y: i32,
41}
42
43fn clip_rect_to_bounds(
44    rect: Rect,
45    clip: Option<Rect>,
46    width: u32,
47    height: u32,
48) -> Option<ClipBounds> {
49    let mut min_x = rect.x;
50    let mut min_y = rect.y;
51    let mut max_x = rect.x + rect.width;
52    let mut max_y = rect.y + rect.height;
53
54    if let Some(clip_rect) = clip {
55        min_x = min_x.max(clip_rect.x);
56        min_y = min_y.max(clip_rect.y);
57        max_x = max_x.min(clip_rect.x + clip_rect.width);
58        max_y = max_y.min(clip_rect.y + clip_rect.height);
59    }
60
61    min_x = min_x.max(0.0);
62    min_y = min_y.max(0.0);
63    max_x = max_x.min(width as f32);
64    max_y = max_y.min(height as f32);
65
66    if max_x <= min_x || max_y <= min_y {
67        return None;
68    }
69
70    let min_x = min_x.floor() as i32;
71    let min_y = min_y.floor() as i32;
72    let max_x = max_x.ceil() as i32;
73    let max_y = max_y.ceil() as i32;
74
75    let min_x = min_x.clamp(0, width as i32);
76    let min_y = min_y.clamp(0, height as i32);
77    let max_x = max_x.clamp(0, width as i32);
78    let max_y = max_y.clamp(0, height as i32);
79
80    if min_x >= max_x || min_y >= max_y {
81        return None;
82    }
83
84    Some(ClipBounds {
85        min_x,
86        min_y,
87        max_x,
88        max_y,
89    })
90}
91
92pub fn draw_scene(frame: &mut [u8], width: u32, height: u32, scene: &Scene) {
93    let text_resources = SoftwareTextResources::default();
94    draw_scene_with_text_resources(frame, width, height, scene, &text_resources);
95}
96
97pub fn draw_scene_with_text_resources(
98    frame: &mut [u8],
99    width: u32,
100    height: u32,
101    scene: &Scene,
102    text_resources: &SoftwareTextResources,
103) {
104    draw_scene_scaled(frame, width, height, scene, text_resources, 1.0);
105}
106
107pub(crate) fn draw_scene_scaled(
108    frame: &mut [u8],
109    width: u32,
110    height: u32,
111    scene: &Scene,
112    text_resources: &SoftwareTextResources,
113    scale: f32,
114) {
115    if let Some(graph) = scene.graph.as_ref() {
116        let raster_scene = pipeline::build_raster_scene(graph, scene.diagnostics(), scale);
117        draw_raster_scene(
118            frame,
119            width,
120            height,
121            &raster_scene,
122            scene.diagnostics(),
123            text_resources,
124        );
125    } else {
126        clear_frame(frame);
127    }
128}
129
130fn clear_frame(frame: &mut [u8]) {
131    for chunk in frame.as_chunks_mut::<4>().0 {
132        chunk.copy_from_slice(&[18, 18, 24, 255]);
133    }
134}
135
136fn draw_raster_scene(
137    frame: &mut [u8],
138    width: u32,
139    height: u32,
140    scene: &RasterScene,
141    diagnostics: &RenderDiagnostics,
142    text_resources: &SoftwareTextResources,
143) {
144    clear_frame(frame);
145    let mut ordered_items =
146        Vec::with_capacity(scene.shapes.len() + scene.images.len() + scene.texts.len());
147    for (index, shape) in scene.shapes.iter().enumerate() {
148        ordered_items.push((shape.z_index, RenderItem::Shape(index)));
149    }
150    for (index, image) in scene.images.iter().enumerate() {
151        ordered_items.push((image.z_index, RenderItem::Image(index)));
152    }
153    for (index, text) in scene.texts.iter().enumerate() {
154        ordered_items.push((text.z_index, RenderItem::Text(index)));
155    }
156    for (index, shadow) in scene.shadows.iter().enumerate() {
157        ordered_items.push((shadow.z_index, RenderItem::Shadow(index)));
158    }
159    ordered_items.sort_unstable_by_key(|(z, _)| *z);
160
161    for (_, item) in ordered_items {
162        match item {
163            RenderItem::Shape(index) => {
164                draw_shape(frame, width, height, &scene.shapes[index], diagnostics);
165            }
166            RenderItem::Image(index) => {
167                draw_image(frame, width, height, &scene.images[index], diagnostics);
168            }
169            RenderItem::Shadow(index) => {
170                draw_shadow(frame, width, height, &scene.shadows[index], diagnostics);
171            }
172            RenderItem::Text(index) => {
173                draw_text(
174                    frame,
175                    width,
176                    height,
177                    &scene.texts[index],
178                    diagnostics,
179                    text_resources,
180                );
181            }
182        }
183    }
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187enum RenderItem {
188    Shape(usize),
189    Image(usize),
190    Text(usize),
191    Shadow(usize),
192}
193
194/// Draws one pass of an elevation shadow: its color at the shadow's
195/// coverage of each pixel's centre.
196fn draw_shadow(
197    frame: &mut [u8],
198    width: u32,
199    height: u32,
200    draw: &crate::scene::ShadowDraw,
201    diagnostics: &RenderDiagnostics,
202) {
203    let snap_delta = draw
204        .snap_anchor
205        .map(snap_delta_for_anchor)
206        .unwrap_or_default();
207    let shadow = draw.shadow.translated(snap_delta.x, snap_delta.y);
208    let clip = draw
209        .clip
210        .map(|clip| clip.translate(snap_delta.x, snap_delta.y));
211    let Some(bounds) = clip_rect_to_bounds(shadow.bounds, clip, width, height) else {
212        return;
213    };
214    let color = [
215        draw.color.r(),
216        draw.color.g(),
217        draw.color.b(),
218        draw.color.a(),
219    ];
220    for py in bounds.min_y.max(0)..bounds.max_y.min(height as i32) {
221        for px in bounds.min_x.max(0)..bounds.max_x.min(width as i32) {
222            let coverage = shadow.coverage(px as f32 + 0.5, py as f32 + 0.5);
223            if coverage <= 0.0 {
224                continue;
225            }
226            let idx = ((py as u32 * width + px as u32) * 4) as usize;
227            blend_pixel(
228                &mut frame[idx..idx + 4],
229                [color[0], color[1], color[2], color[3] * coverage],
230                BlendMode::SrcOver,
231                diagnostics,
232            );
233        }
234    }
235}
236
237fn draw_shape(
238    frame: &mut [u8],
239    width: u32,
240    height: u32,
241    draw: &crate::scene::DrawShape,
242    diagnostics: &RenderDiagnostics,
243) {
244    let snap_delta = draw
245        .snap_anchor
246        .map(snap_delta_for_anchor)
247        .unwrap_or_default();
248    let rect = draw.rect.translate(snap_delta.x, snap_delta.y);
249    let clip = draw.clip;
250    let dither_origin = draw
251        .snap_anchor
252        .map(snapped_anchor_origin)
253        .unwrap_or_default();
254    let rect = if draw.snap_to_pixel_grid {
255        Rect {
256            x: rect.x.round(),
257            y: rect.y.round(),
258            width: if rect.width > 0.0 {
259                rect.width.ceil().max(1.0)
260            } else {
261                rect.width
262            },
263            height: if rect.height > 0.0 {
264                rect.height.ceil().max(1.0)
265            } else {
266                rect.height
267            },
268        }
269    } else {
270        rect
271    };
272    let Some(coverage) = ShapeCoverage::of(draw, rect, snap_delta) else {
273        return;
274    };
275    let Some(clip_bounds) = clip_rect_to_bounds(coverage.reach(rect), clip, width, height) else {
276        return;
277    };
278
279    for py in clip_bounds.min_y..clip_bounds.max_y {
280        if py < 0 || py >= height as i32 {
281            continue;
282        }
283        for px in clip_bounds.min_x..clip_bounds.max_x {
284            if px < 0 || px >= width as i32 {
285                continue;
286            }
287            let center_x = px as f32 + 0.5;
288            let center_y = py as f32 + 0.5;
289            let coverage = coverage.at(Point::new(center_x, center_y));
290            if coverage <= 0.0 {
291                continue;
292            }
293
294            let mut sample =
295                sample_brush_rgba(&draw.brush, rect, center_x, center_y, dither_origin);
296            sample[3] *= coverage;
297            let alpha = sample[3];
298            if alpha <= 0.0 {
299                continue;
300            }
301            let idx = ((py as u32 * width + px as u32) * 4) as usize;
302            blend_pixel(
303                &mut frame[idx..idx + 4],
304                sample,
305                draw.blend_mode,
306                diagnostics,
307            );
308        }
309    }
310}
311
312/// How much of a pixel a shape covers, decided once per shape: a line, an
313/// arc band, a stroked outline or a fill, placed where its snap moved it.
314enum ShapeCoverage {
315    Line(LineGeometry),
316    Trapezoid(Trapezoid),
317    Arc(ArcGeometry),
318    Stroke {
319        rect: Rect,
320        radii: Option<CornerRadii>,
321        half_width: f32,
322        join: StrokeJoin,
323    },
324    Fill {
325        rect: Rect,
326        radii: Option<CornerRadii>,
327    },
328}
329
330impl ShapeCoverage {
331    /// The coverage of `draw` laid at `rect`, its line or arc moved by
332    /// `snap_delta` as the rect was; `None` when it covers nothing.
333    fn of(draw: &crate::scene::DrawShape, rect: Rect, snap_delta: Point) -> Option<Self> {
334        let shift = |point: Point| Point::new(point.x + snap_delta.x, point.y + snap_delta.y);
335        if let Some(line) = draw.line {
336            return (!line.is_degenerate())
337                .then(|| Self::Line(line.placed(shift(line.start), shift(line.end), 1.0)));
338        }
339        if let Some(trapezoid) = draw.trapezoid {
340            return Some(Self::Trapezoid(
341                trapezoid.translate(snap_delta.x, snap_delta.y),
342            ));
343        }
344        if let Some(arc) = draw.arc {
345            return (!arc.is_degenerate()).then(|| {
346                Self::Arc(ArcGeometry {
347                    center: shift(arc.center),
348                    ..arc
349                })
350            });
351        }
352        let outset = draw.stroke.map_or(0.0, |stroke| stroke.half_width());
353        let radii = draw.shape.map(|shape| {
354            shape.resolve(
355                (rect.width - outset * 2.0).max(0.0),
356                (rect.height - outset * 2.0).max(0.0),
357            )
358        });
359        match draw.stroke {
360            Some(stroke) if !stroke.is_visible() => None,
361            Some(stroke) => Some(Self::Stroke {
362                rect,
363                radii,
364                half_width: stroke.half_width(),
365                join: stroke.join,
366            }),
367            None => Some(Self::Fill { rect, radii }),
368        }
369    }
370
371    /// The pixels it can cover: a slice's own box, which its edges reach
372    /// half a pixel past, otherwise the shape's `rect`.
373    fn reach(&self, rect: Rect) -> Rect {
374        match self {
375            Self::Trapezoid(trapezoid) => expand_rect(trapezoid.bounds(), 0.5),
376            _ => rect,
377        }
378    }
379
380    fn at(&self, point: Point) -> f32 {
381        match self {
382            Self::Line(line) => line.coverage(point),
383            Self::Trapezoid(trapezoid) => trapezoid.coverage(point),
384            Self::Arc(arc) => shape_sdf::arc_coverage(point, arc),
385            Self::Stroke {
386                rect,
387                radii,
388                half_width,
389                join,
390            } => shape_sdf::stroked_rect_coverage(point, *rect, *radii, *half_width, *join),
391            Self::Fill { rect, radii } => match radii {
392                Some(radii) if !point_in_resolved_rounded_rect(point.x, point.y, *rect, radii) => {
393                    0.0
394                }
395                _ => 1.0,
396            },
397        }
398    }
399}
400
401fn draw_image(
402    frame: &mut [u8],
403    width: u32,
404    height: u32,
405    draw: &ImageDraw,
406    diagnostics: &RenderDiagnostics,
407) {
408    let snap_delta = draw
409        .snap_anchor
410        .map(snap_delta_for_anchor)
411        .unwrap_or_default();
412    let rect = draw.rect.translate(snap_delta.x, snap_delta.y);
413    let clip = draw.clip;
414
415    if draw.alpha <= 0.0 || rect.width <= 0.0 || rect.height <= 0.0 {
416        return;
417    }
418
419    let Some(clip_bounds) = clip_rect_to_bounds(rect, clip, width, height) else {
420        return;
421    };
422
423    let img_width = draw.image.width();
424    let img_height = draw.image.height();
425    if img_width == 0 || img_height == 0 {
426        return;
427    }
428
429    let (sr_x, sr_y, sr_w, sr_h) = if let Some(sr) = draw.src_rect {
430        (sr.x, sr.y, sr.width, sr.height)
431    } else {
432        (0.0, 0.0, img_width as f32, img_height as f32)
433    };
434
435    for py in clip_bounds.min_y..clip_bounds.max_y {
436        for px in clip_bounds.min_x..clip_bounds.max_x {
437            let sample_x = px as f32 + 0.5;
438            let sample_y = py as f32 + 0.5;
439            let u = ((sample_x - rect.x) / rect.width).clamp(0.0, 1.0);
440            let v = ((sample_y - rect.y) / rect.height).clamp(0.0, 1.0);
441
442            let mut sample = match draw.sampling {
443                cranpose_ui_graphics::ImageSampling::Nearest => {
444                    let src_x = ((sr_x + u * sr_w).floor() as i32).clamp(0, img_width as i32 - 1);
445                    let src_y = ((sr_y + v * sr_h).floor() as i32).clamp(0, img_height as i32 - 1);
446                    sample_image_nearest(&draw.image, src_x as u32, src_y as u32)
447                }
448                cranpose_ui_graphics::ImageSampling::Linear => {
449                    sample_image_linear(&draw.image, sr_x + u * sr_w - 0.5, sr_y + v * sr_h - 0.5)
450                }
451            };
452
453            if let Some(filter) = draw.color_filter {
454                sample = apply_color_filter(sample, filter);
455            }
456
457            sample[3] *= draw.alpha.clamp(0.0, 1.0);
458            if sample[3] <= 0.0 {
459                continue;
460            }
461
462            let dst_idx = ((py as u32 * width + px as u32) * 4) as usize;
463            blend_pixel(
464                &mut frame[dst_idx..dst_idx + 4],
465                sample,
466                draw.blend_mode,
467                diagnostics,
468            );
469        }
470    }
471}
472
473fn sample_image_nearest(
474    image: &cranpose_ui_graphics::ImageBitmap,
475    src_x: u32,
476    src_y: u32,
477) -> [f32; 4] {
478    image
479        .rgba8_pixel(src_y as usize * image.width() as usize + src_x as usize)
480        .map(|channel| channel as f32 / 255.0)
481}
482
483fn sample_image_linear(image: &cranpose_ui_graphics::ImageBitmap, x: f32, y: f32) -> [f32; 4] {
484    let img_width = image.width();
485    let img_height = image.height();
486    let x = x.clamp(0.0, img_width.saturating_sub(1) as f32);
487    let y = y.clamp(0.0, img_height.saturating_sub(1) as f32);
488    let x0 = x.floor();
489    let y0 = y.floor();
490    let tx = x - x0;
491    let ty = y - y0;
492    let x0 = (x0 as i32).clamp(0, img_width as i32 - 1) as u32;
493    let y0 = (y0 as i32).clamp(0, img_height as i32 - 1) as u32;
494    let x1 = (x0 + 1).min(img_width - 1);
495    let y1 = (y0 + 1).min(img_height - 1);
496    let top_left = sample_image_nearest(image, x0, y0);
497    let top_right = sample_image_nearest(image, x1, y0);
498    let bottom_left = sample_image_nearest(image, x0, y1);
499    let bottom_right = sample_image_nearest(image, x1, y1);
500
501    let mut out = [0.0; 4];
502    for channel in 0..4 {
503        let top = top_left[channel] + (top_right[channel] - top_left[channel]) * tx;
504        let bottom = bottom_left[channel] + (bottom_right[channel] - bottom_left[channel]) * tx;
505        out[channel] = top + (bottom - top) * ty;
506    }
507    out
508}
509
510fn draw_text(
511    frame: &mut [u8],
512    width: u32,
513    height: u32,
514    draw: &TextDraw,
515    diagnostics: &RenderDiagnostics,
516    text_resources: &SoftwareTextResources,
517) {
518    let text_scale = draw.scale.max(0.0);
519    if text_scale == 0.0 {
520        return;
521    }
522    let static_text_motion = draw
523        .text_style
524        .paragraph_style
525        .text_motion
526        .unwrap_or(TextMotion::Static)
527        == TextMotion::Static;
528    let snap_delta = if static_text_motion {
529        draw.snap_anchor
530            .map(snap_delta_for_anchor)
531            .unwrap_or_default()
532    } else {
533        Point::default()
534    };
535    let rect = draw.rect.translate(snap_delta.x, snap_delta.y);
536    let raster_rect = if static_text_motion {
537        Rect {
538            x: rect.x.round(),
539            y: rect.y.round(),
540            width: if rect.width > 0.0 {
541                rect.width.ceil().max(1.0)
542            } else {
543                rect.width
544            },
545            height: if rect.height > 0.0 {
546                rect.height.ceil().max(1.0)
547            } else {
548                rect.height
549            },
550        }
551    } else {
552        rect
553    };
554    let image = rasterize_annotated_text_region(
555        draw.text.as_ref(),
556        raster_rect,
557        Point::new(raster_rect.x, raster_rect.y),
558        &draw.text_style,
559        draw.color,
560        draw.font_size,
561        text_scale,
562        text_resources.fonts(),
563        None,
564    );
565    let Some(image) = image else {
566        return;
567    };
568    let blit_origin = if static_text_motion {
569        Point::new(raster_rect.x, raster_rect.y)
570    } else {
571        Point::new(rect.x, rect.y)
572    };
573    let blit_rect = Rect {
574        x: blit_origin.x,
575        y: blit_origin.y,
576        width: image.width() as f32,
577        height: image.height() as f32,
578    };
579    blit_rasterized_text_image(
580        frame,
581        width,
582        height,
583        blit_rect,
584        draw.clip,
585        &image,
586        diagnostics,
587    );
588}
589
590fn blit_rasterized_text_image(
591    frame: &mut [u8],
592    width: u32,
593    height: u32,
594    rect: Rect,
595    clip: Option<Rect>,
596    image: &cranpose_ui_graphics::ImageBitmap,
597    diagnostics: &RenderDiagnostics,
598) {
599    if rect.width <= 0.0 || rect.height <= 0.0 {
600        return;
601    }
602    let Some(clip_bounds) = clip_rect_to_bounds(rect, clip, width, height) else {
603        return;
604    };
605
606    let img_width = image.width();
607    let img_height = image.height();
608    if img_width == 0 || img_height == 0 {
609        return;
610    }
611
612    for py in clip_bounds.min_y..clip_bounds.max_y {
613        for px in clip_bounds.min_x..clip_bounds.max_x {
614            let sample_x = px as f32 + 0.5;
615            let sample_y = py as f32 + 0.5;
616            let u = ((sample_x - rect.x) / rect.width).clamp(0.0, 1.0);
617            let v = ((sample_y - rect.y) / rect.height).clamp(0.0, 1.0);
618
619            let src = sample_image_linear(
620                image,
621                u * img_width.saturating_sub(1) as f32,
622                v * img_height.saturating_sub(1) as f32,
623            );
624            if src[3] <= 0.0 {
625                continue;
626            }
627
628            let dst_idx = ((py as u32 * width + px as u32) * 4) as usize;
629            blend_pixel(
630                &mut frame[dst_idx..dst_idx + 4],
631                src,
632                BlendMode::SrcOver,
633                diagnostics,
634            );
635        }
636    }
637}
638
639fn blend_pixel(
640    dst: &mut [u8],
641    src: [f32; 4],
642    blend_mode: BlendMode,
643    diagnostics: &RenderDiagnostics,
644) {
645    let resolved_blend_mode = if is_blend_mode_supported(blend_mode) {
646        blend_mode
647    } else {
648        if diagnostics.claim_warning_once("pixels.unsupported-blend-mode") {
649            log::warn!(
650                "Pixels renderer currently supports BlendMode::SrcOver and BlendMode::DstOut; falling back to SrcOver for unsupported modes"
651            );
652        }
653        BlendMode::SrcOver
654    };
655
656    let src_alpha = src[3].clamp(0.0, 1.0);
657    if src_alpha <= 0.0 {
658        return;
659    }
660    let dst_r = dst[0] as f32 / 255.0;
661    let dst_g = dst[1] as f32 / 255.0;
662    let dst_b = dst[2] as f32 / 255.0;
663    let dst_a = dst[3] as f32 / 255.0;
664
665    let (out_r, out_g, out_b, out_a) = match resolved_blend_mode {
666        BlendMode::DstOut => {
667            let keep = 1.0 - src_alpha;
668            (dst_r * keep, dst_g * keep, dst_b * keep, dst_a * keep)
669        }
670        BlendMode::SrcOver => (
671            src[0].clamp(0.0, 1.0) * src_alpha + dst_r * (1.0 - src_alpha),
672            src[1].clamp(0.0, 1.0) * src_alpha + dst_g * (1.0 - src_alpha),
673            src[2].clamp(0.0, 1.0) * src_alpha + dst_b * (1.0 - src_alpha),
674            src_alpha + dst_a * (1.0 - src_alpha),
675        ),
676        _ => (
677            src[0].clamp(0.0, 1.0) * src_alpha + dst_r * (1.0 - src_alpha),
678            src[1].clamp(0.0, 1.0) * src_alpha + dst_g * (1.0 - src_alpha),
679            src[2].clamp(0.0, 1.0) * src_alpha + dst_b * (1.0 - src_alpha),
680            src_alpha + dst_a * (1.0 - src_alpha),
681        ),
682    };
683
684    dst[0] = (out_r.clamp(0.0, 1.0) * 255.0).round() as u8;
685    dst[1] = (out_g.clamp(0.0, 1.0) * 255.0).round() as u8;
686    dst[2] = (out_b.clamp(0.0, 1.0) * 255.0).round() as u8;
687    dst[3] = (out_a.clamp(0.0, 1.0) * 255.0).round() as u8;
688}
689
690fn apply_color_filter(sample: [f32; 4], filter: ColorFilter) -> [f32; 4] {
691    filter.apply_rgba(sample)
692}
693
694#[cfg(test)]
695#[path = "tests/draw_tests.rs"]
696mod tests;