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