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::{color_to_rgba, sample_brush_rgba},
8    graph_scene::RenderDiagnostics,
9    shape_sdf,
10    software_text_raster::rasterize_annotated_text_region,
11    text_measure::SoftwareTextResources,
12};
13use cranpose_ui::text::TextMotion;
14use cranpose_ui_graphics::{
15    ArcGeometry, BlendMode, Brush, ColorFilter, CornerRadii, LineGeometry, Point, Rect, StrokeJoin,
16    Trapezoid, expand_rect,
17};
18
19use crate::{
20    pipeline,
21    scene::{ImageDraw, RasterScene, Scene, TextDraw},
22    style::point_in_resolved_rounded_rect,
23};
24
25fn is_blend_mode_supported(mode: BlendMode) -> bool {
26    matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
27}
28
29fn snapped_anchor_origin(anchor: Point) -> Point {
30    Point::new(anchor.x.round(), anchor.y.round())
31}
32
33pub(crate) fn snap_delta_for_anchor(anchor: Point) -> Point {
34    let snapped = snapped_anchor_origin(anchor);
35    Point::new(snapped.x - anchor.x, snapped.y - anchor.y)
36}
37
38/// A window of the device frame: the RGBA8 pixels `left..left + width` by
39/// `top..top + height`, row by row. The whole frame is the window at the
40/// origin; a sprite's texture is the window around the one item it holds.
41pub(crate) struct Target<'a> {
42    pub pixels: &'a mut [u8],
43    pub left: i32,
44    pub top: i32,
45    pub width: u32,
46    pub height: u32,
47}
48
49impl Target<'_> {
50    fn index(&self, x: i32, y: i32) -> usize {
51        (((y - self.top) as u32 * self.width + (x - self.left) as u32) * 4) as usize
52    }
53
54    fn pixel(&mut self, x: i32, y: i32) -> &mut [u8] {
55        let index = self.index(x, y);
56        &mut self.pixels[index..index + 4]
57    }
58}
59
60#[derive(Clone, Copy)]
61pub(crate) struct ClipBounds {
62    pub min_x: i32,
63    pub min_y: i32,
64    pub max_x: i32,
65    pub max_y: i32,
66}
67
68/// The whole pixels of `rect` cut to `clip` and to the target's window.
69fn clip_rect_to_bounds(rect: Rect, clip: Option<Rect>, target: &Target) -> Option<ClipBounds> {
70    clip_rect_to_window(
71        rect,
72        clip,
73        ClipBounds {
74            min_x: target.left,
75            min_y: target.top,
76            max_x: target.left + target.width as i32,
77            max_y: target.top + target.height as i32,
78        },
79    )
80}
81
82/// The whole pixels of `rect` cut to `clip` and to `window`.
83pub(crate) fn clip_rect_to_window(
84    rect: Rect,
85    clip: Option<Rect>,
86    window: ClipBounds,
87) -> Option<ClipBounds> {
88    let mut min_x = rect.x;
89    let mut min_y = rect.y;
90    let mut max_x = rect.x + rect.width;
91    let mut max_y = rect.y + rect.height;
92
93    if let Some(clip_rect) = clip {
94        min_x = min_x.max(clip_rect.x);
95        min_y = min_y.max(clip_rect.y);
96        max_x = max_x.min(clip_rect.x + clip_rect.width);
97        max_y = max_y.min(clip_rect.y + clip_rect.height);
98    }
99
100    min_x = min_x.max(window.min_x as f32);
101    min_y = min_y.max(window.min_y as f32);
102    max_x = max_x.min(window.max_x as f32);
103    max_y = max_y.min(window.max_y as f32);
104
105    if max_x <= min_x || max_y <= min_y {
106        return None;
107    }
108
109    let min_x = (min_x.floor() as i32).clamp(window.min_x, window.max_x);
110    let min_y = (min_y.floor() as i32).clamp(window.min_y, window.max_y);
111    let max_x = (max_x.ceil() as i32).clamp(window.min_x, window.max_x);
112    let max_y = (max_y.ceil() as i32).clamp(window.min_y, window.max_y);
113
114    if min_x >= max_x || min_y >= max_y {
115        return None;
116    }
117
118    Some(ClipBounds {
119        min_x,
120        min_y,
121        max_x,
122        max_y,
123    })
124}
125
126pub fn draw_scene(frame: &mut [u8], width: u32, height: u32, scene: &Scene) {
127    let text_resources = SoftwareTextResources::default();
128    draw_scene_with_text_resources(frame, width, height, scene, &text_resources);
129}
130
131pub fn draw_scene_with_text_resources(
132    frame: &mut [u8],
133    width: u32,
134    height: u32,
135    scene: &Scene,
136    text_resources: &SoftwareTextResources,
137) {
138    draw_scene_scaled(frame, width, height, scene, text_resources, 1.0);
139}
140
141pub(crate) fn draw_scene_scaled(
142    frame: &mut [u8],
143    width: u32,
144    height: u32,
145    scene: &Scene,
146    text_resources: &SoftwareTextResources,
147    scale: f32,
148) {
149    if let Some(graph) = scene.graph.as_ref() {
150        let raster_scene = pipeline::build_raster_scene(graph, scene.diagnostics(), scale);
151        draw_raster_scene(
152            frame,
153            width,
154            height,
155            &raster_scene,
156            scene.diagnostics(),
157            text_resources,
158        );
159    } else {
160        clear_frame(frame);
161    }
162}
163
164fn clear_frame(frame: &mut [u8]) {
165    for chunk in frame.as_chunks_mut::<4>().0 {
166        chunk.copy_from_slice(&[18, 18, 24, 255]);
167    }
168}
169
170fn draw_raster_scene(
171    frame: &mut [u8],
172    width: u32,
173    height: u32,
174    scene: &RasterScene,
175    diagnostics: &RenderDiagnostics,
176    text_resources: &SoftwareTextResources,
177) {
178    clear_frame(frame);
179    let mut target = Target {
180        pixels: frame,
181        left: 0,
182        top: 0,
183        width,
184        height,
185    };
186    for item in scene.ordered_items() {
187        draw_item(&mut target, scene, item, diagnostics, text_resources);
188    }
189}
190
191/// Draws one item of `scene` into `target`.
192pub(crate) fn draw_item(
193    target: &mut Target,
194    scene: &RasterScene,
195    item: RenderItem,
196    diagnostics: &RenderDiagnostics,
197    text_resources: &SoftwareTextResources,
198) {
199    match item {
200        RenderItem::Shape(index) => draw_shape(target, &scene.shapes[index], diagnostics),
201        RenderItem::Image(index) => draw_image(target, &scene.images[index], diagnostics),
202        RenderItem::Shadow(index) => draw_shadow(target, &scene.shadows[index], diagnostics),
203        RenderItem::Text(index) => {
204            draw_text(target, &scene.texts[index], diagnostics, text_resources);
205        }
206    }
207}
208
209impl RasterScene {
210    /// The pixels `item` can reach before its clip, for the items drawn into
211    /// a frame directly; text reaches what its rasterized image covers.
212    pub(crate) fn item_reach(&self, item: RenderItem) -> Option<Rect> {
213        match item {
214            RenderItem::Shape(index) => {
215                let placed = place_shape(&self.shapes[index])?;
216                Some(placed.coverage.reach(placed.rect))
217            }
218            RenderItem::Image(index) => {
219                let draw = &self.images[index];
220                let snap = draw
221                    .snap_anchor
222                    .map(snap_delta_for_anchor)
223                    .unwrap_or_default();
224                Some(draw.rect.translate(snap.x, snap.y))
225            }
226            RenderItem::Shadow(index) => {
227                let draw = &self.shadows[index];
228                let snap = draw
229                    .snap_anchor
230                    .map(snap_delta_for_anchor)
231                    .unwrap_or_default();
232                Some(draw.shadow.bounds.translate(snap.x, snap.y))
233            }
234            RenderItem::Text(_) => None,
235        }
236    }
237
238    /// Every item, in the order they are drawn.
239    pub(crate) fn ordered_items(&self) -> Vec<RenderItem> {
240        let mut ordered = Vec::with_capacity(
241            self.shapes.len() + self.images.len() + self.texts.len() + self.shadows.len(),
242        );
243        ordered.extend(
244            self.shapes
245                .iter()
246                .enumerate()
247                .map(|(index, shape)| (shape.z_index, RenderItem::Shape(index))),
248        );
249        ordered.extend(
250            self.images
251                .iter()
252                .enumerate()
253                .map(|(index, image)| (image.z_index, RenderItem::Image(index))),
254        );
255        ordered.extend(
256            self.texts
257                .iter()
258                .enumerate()
259                .map(|(index, text)| (text.z_index, RenderItem::Text(index))),
260        );
261        ordered.extend(
262            self.shadows
263                .iter()
264                .enumerate()
265                .map(|(index, shadow)| (shadow.z_index, RenderItem::Shadow(index))),
266        );
267        ordered.sort_unstable_by_key(|(z, _)| *z);
268        ordered.into_iter().map(|(_, item)| item).collect()
269    }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273pub(crate) enum RenderItem {
274    Shape(usize),
275    Image(usize),
276    Text(usize),
277    Shadow(usize),
278}
279
280/// Draws one pass of an elevation shadow: its color at the shadow's
281/// coverage of each pixel's centre.
282fn draw_shadow(
283    target: &mut Target,
284    draw: &crate::scene::ShadowDraw,
285    diagnostics: &RenderDiagnostics,
286) {
287    let snap_delta = draw
288        .snap_anchor
289        .map(snap_delta_for_anchor)
290        .unwrap_or_default();
291    let shadow = draw.shadow.translated(snap_delta.x, snap_delta.y);
292    let clip = draw
293        .clip
294        .map(|clip| clip.translate(snap_delta.x, snap_delta.y));
295    let Some(bounds) = clip_rect_to_bounds(shadow.bounds, clip, target) else {
296        return;
297    };
298    let color = [
299        draw.color.r(),
300        draw.color.g(),
301        draw.color.b(),
302        draw.color.a(),
303    ];
304    for py in bounds.min_y..bounds.max_y {
305        for px in bounds.min_x..bounds.max_x {
306            let coverage = shadow.coverage(px as f32 + 0.5, py as f32 + 0.5);
307            if coverage <= 0.0 {
308                continue;
309            }
310            blend_pixel(
311                target.pixel(px, py),
312                [color[0], color[1], color[2], color[3] * coverage],
313                BlendMode::SrcOver,
314                diagnostics,
315            );
316        }
317    }
318}
319
320/// A shape where its snap puts it: its rect, the coverage of each pixel and
321/// the origin its gradient's dither is measured from.
322pub(crate) struct PlacedShape {
323    pub rect: Rect,
324    pub coverage: ShapeCoverage,
325    pub dither_origin: Point,
326}
327
328/// `rect` moved to whole pixels and grown to cover at least one.
329fn on_pixel_grid(rect: Rect) -> Rect {
330    Rect {
331        x: rect.x.round(),
332        y: rect.y.round(),
333        width: if rect.width > 0.0 {
334            rect.width.ceil().max(1.0)
335        } else {
336            rect.width
337        },
338        height: if rect.height > 0.0 {
339            rect.height.ceil().max(1.0)
340        } else {
341            rect.height
342        },
343    }
344}
345
346/// `draw` where its snap puts it; `None` when it covers nothing.
347pub(crate) fn place_shape(draw: &crate::scene::DrawShape) -> Option<PlacedShape> {
348    let snap_delta = draw
349        .snap_anchor
350        .map(snap_delta_for_anchor)
351        .unwrap_or_default();
352    let rect = draw.rect.translate(snap_delta.x, snap_delta.y);
353    let dither_origin = draw
354        .snap_anchor
355        .map(snapped_anchor_origin)
356        .unwrap_or_default();
357    let rect = if draw.snap_to_pixel_grid {
358        on_pixel_grid(rect)
359    } else {
360        rect
361    };
362    let coverage = ShapeCoverage::of(draw, rect, snap_delta)?;
363    Some(PlacedShape {
364        rect,
365        coverage,
366        dither_origin,
367    })
368}
369
370fn draw_shape(
371    target: &mut Target,
372    draw: &crate::scene::DrawShape,
373    diagnostics: &RenderDiagnostics,
374) {
375    let Some(PlacedShape {
376        rect,
377        coverage,
378        dither_origin,
379    }) = place_shape(draw)
380    else {
381        return;
382    };
383    let Some(clip_bounds) = clip_rect_to_bounds(coverage.reach(rect), draw.clip, target) else {
384        return;
385    };
386
387    let opaque = opaque_solid_bytes(&draw.brush, draw.blend_mode);
388    for py in clip_bounds.min_y..clip_bounds.max_y {
389        let center_y = py as f32 + 0.5;
390        for (start, end) in coverage.row_runs(center_y, clip_bounds.min_x, clip_bounds.max_x) {
391            for px in start..end {
392                let center_x = px as f32 + 0.5;
393                let coverage = coverage.at(Point::new(center_x, center_y));
394                if coverage <= 0.0 {
395                    continue;
396                }
397                if let Some(bytes) = opaque
398                    && coverage >= 1.0
399                {
400                    target.pixel(px, py).copy_from_slice(&bytes);
401                    continue;
402                }
403
404                let mut sample =
405                    sample_brush_rgba(&draw.brush, rect, center_x, center_y, dither_origin);
406                sample[3] *= coverage;
407                let alpha = sample[3];
408                if alpha <= 0.0 {
409                    continue;
410                }
411                blend_pixel(target.pixel(px, py), sample, draw.blend_mode, diagnostics);
412            }
413        }
414    }
415}
416
417/// The bytes an opaque solid colour leaves on a pixel it covers fully,
418/// whatever was there: what `blend_pixel` works out for it.
419fn opaque_solid_bytes(brush: &Brush, blend_mode: BlendMode) -> Option<[u8; 4]> {
420    let Brush::Solid(color) = brush else {
421        return None;
422    };
423    let rgba = color_to_rgba(*color);
424    (blend_mode == BlendMode::SrcOver && rgba[3] >= 1.0).then(|| {
425        [
426            (rgba[0] * 255.0).round() as u8,
427            (rgba[1] * 255.0).round() as u8,
428            (rgba[2] * 255.0).round() as u8,
429            255,
430        ]
431    })
432}
433
434/// How far from a band's edge a pixel centre still takes ink: the
435/// antialiasing reaches half a pixel, and the rest is room for the rounding
436/// in the distance functions.
437const INK_REACH: f32 = 0.5 + 1.0 / 64.0;
438
439/// The pixels of one row worth testing, as two runs `start..end`: the band on
440/// either side of a hole. A run with `start >= end` is empty.
441type RowRuns = [(i32, i32); 2];
442
443const NO_RUN: (i32, i32) = (0, 0);
444
445/// The pixels whose centres lie within `half` of `centre`, cut to
446/// `min..max`.
447fn run_within(centre: f32, half: f32, min: i32, max: i32) -> (i32, i32) {
448    let start = ((centre - half - 0.5).ceil() as i32).max(min);
449    let end = ((centre + half - 0.5).floor() as i32 + 1).min(max);
450    (start, end)
451}
452
453/// `run` without the pixels whose centres lie in `from..=to`.
454fn runs_around(run: (i32, i32), from: f32, to: f32) -> RowRuns {
455    let first = (from - 0.5).ceil() as i32;
456    let last = (to - 0.5).floor() as i32;
457    if first > last {
458        return [run, NO_RUN];
459    }
460    [(run.0, run.1.min(first)), (run.0.max(last + 1), run.1)]
461}
462
463/// The row of an arc that can take ink. Every point of the band, caps
464/// included, lies between the inner and the outer radius, so the row stops at
465/// the outer circle and skips the inner one.
466fn arc_row_runs(arc: &ArcGeometry, y: f32, min_x: i32, max_x: i32) -> RowRuns {
467    let dy = y - arc.center.y;
468    let outer = arc.outer_radius + INK_REACH;
469    let reach = outer * outer - dy * dy;
470    if reach.is_nan() || reach <= 0.0 {
471        return [NO_RUN, NO_RUN];
472    }
473    let run = run_within(arc.center.x, reach.sqrt(), min_x, max_x);
474    let inner = arc.inner_radius - INK_REACH;
475    let hole = inner * inner - dy * dy;
476    if inner <= 0.0 || hole.is_nan() || hole <= 0.0 {
477        return [run, NO_RUN];
478    }
479    let hole = hole.sqrt();
480    runs_around(run, arc.center.x - hole, arc.center.x + hole)
481}
482
483/// The row of a stroked rounded rect that can take ink: inside the outer edge
484/// grown by `INK_REACH`, and outside the part of the hole that lies at least
485/// `INK_REACH` inside the inner edge. Both edges are the rounded boxes
486/// `shape_sdf::sdf_stroked_rounded_rect` measures, taken on each side of the
487/// centre with that side's corner radius.
488fn stroke_row_runs(
489    rect: Rect,
490    radii: Option<CornerRadii>,
491    half_width: f32,
492    join: StrokeJoin,
493    y: f32,
494    min_x: i32,
495    max_x: i32,
496) -> RowRuns {
497    let half_width = half_width.max(0.0);
498    let half_size = (rect.width * 0.5, rect.height * 0.5);
499    let geometry = (
500        (half_size.0 - half_width).max(0.0),
501        (half_size.1 - half_width).max(0.0),
502    );
503    let outer = (geometry.0 + half_width, geometry.1 + half_width);
504    let inner = (
505        (geometry.0 - half_width).max(0.0),
506        (geometry.1 - half_width).max(0.0),
507    );
508    let centre_x = rect.x + half_size.0;
509    let local_y = (y - (rect.y + half_size.1)).abs();
510    if !(local_y.is_finite() && centre_x.is_finite()) {
511        return [(min_x, max_x), NO_RUN];
512    }
513    let radii = radii.unwrap_or_default();
514    let (left, right) = if y - (rect.y + half_size.1) > 0.0 {
515        (radii.bottom_left, radii.bottom_right)
516    } else {
517        (radii.top_left, radii.top_right)
518    };
519    // How far from the centre the grown outer edge reaches on one side;
520    // `None` when the row passes outside it.
521    let ink_reach = |radius: f32| {
522        let radius = if join != StrokeJoin::Round && radius < 0.0001 {
523            0.0
524        } else {
525            radius + half_width
526        };
527        let qy = local_y - outer.1 + radius;
528        let grown = radius + INK_REACH;
529        if qy.is_nan() || qy >= grown {
530            return None;
531        }
532        Some(if qy <= 0.0 {
533            outer.0 + INK_REACH
534        } else {
535            outer.0 - radius + (grown * grown - qy * qy).max(0.0).sqrt()
536        })
537    };
538    // How far from the centre the hole is empty on one side, if it is.
539    let empty_reach = |radius: f32| {
540        let radius = (radius - half_width).max(0.0);
541        let qy = local_y - inner.1 + radius;
542        if qy.is_nan() || qy > radius - INK_REACH {
543            return None;
544        }
545        let reach = if qy <= 0.0 {
546            inner.0 - INK_REACH
547        } else {
548            let span = (radius - INK_REACH) * (radius - INK_REACH) - qy * qy;
549            inner.0 - radius + span.max(0.0).sqrt()
550        };
551        (reach >= 0.0).then_some(reach)
552    };
553    // A side the row misses still keeps the centre's own column, which
554    // belongs to the left side.
555    let run = match (ink_reach(left), ink_reach(right)) {
556        (None, None) => return [NO_RUN, NO_RUN],
557        (left, right) => {
558            let from = centre_x - left.unwrap_or(0.0);
559            let to = centre_x + right.unwrap_or(0.0);
560            let start = ((from - 0.5).ceil() as i32).max(min_x);
561            let end = ((to - 0.5).floor() as i32 + 1).min(max_x);
562            (start, end)
563        }
564    };
565    match (empty_reach(left), empty_reach(right)) {
566        (Some(left), Some(right)) => runs_around(run, centre_x - left, centre_x + right),
567        (Some(left), None) => runs_around(run, centre_x - left, centre_x),
568        // The right side starts past the centre: the centre's own column
569        // belongs to the left one.
570        (None, Some(right)) => runs_around(run, centre_x + 1.0 / 64.0, centre_x + right),
571        (None, None) => [run, NO_RUN],
572    }
573}
574
575/// How much of a pixel a shape covers, decided once per shape: a line, an
576/// arc band, a stroked outline or a fill, placed where its snap moved it.
577pub(crate) enum ShapeCoverage {
578    Line(LineGeometry),
579    Trapezoid(Trapezoid),
580    Arc(ArcGeometry),
581    Stroke {
582        rect: Rect,
583        radii: Option<CornerRadii>,
584        half_width: f32,
585        join: StrokeJoin,
586    },
587    Fill {
588        rect: Rect,
589        radii: Option<CornerRadii>,
590    },
591}
592
593impl ShapeCoverage {
594    /// The coverage of `draw` laid at `rect`, its line or arc moved by
595    /// `snap_delta` as the rect was; `None` when it covers nothing.
596    fn of(draw: &crate::scene::DrawShape, rect: Rect, snap_delta: Point) -> Option<Self> {
597        let shift = |point: Point| Point::new(point.x + snap_delta.x, point.y + snap_delta.y);
598        if let Some(line) = draw.line {
599            return (!line.is_degenerate())
600                .then(|| Self::Line(line.placed(shift(line.start), shift(line.end), 1.0)));
601        }
602        if let Some(trapezoid) = draw.trapezoid {
603            return Some(Self::Trapezoid(
604                trapezoid.translate(snap_delta.x, snap_delta.y),
605            ));
606        }
607        if let Some(arc) = draw.arc {
608            return (!arc.is_degenerate()).then(|| {
609                Self::Arc(ArcGeometry {
610                    center: shift(arc.center),
611                    ..arc
612                })
613            });
614        }
615        let outset = draw.stroke.map_or(0.0, |stroke| stroke.half_width());
616        let radii = draw.shape.map(|shape| {
617            shape.resolve(
618                (rect.width - outset * 2.0).max(0.0),
619                (rect.height - outset * 2.0).max(0.0),
620            )
621        });
622        match draw.stroke {
623            Some(stroke) if !stroke.is_visible() => None,
624            Some(stroke) => Some(Self::Stroke {
625                rect,
626                radii,
627                half_width: stroke.half_width(),
628                join: stroke.join,
629            }),
630            None => Some(Self::Fill { rect, radii }),
631        }
632    }
633
634    /// The pixels it can cover: a slice's own box, which its edges reach
635    /// half a pixel past, an arc's band, otherwise the shape's `rect`. An
636    /// arc's `rect` is the disc around its whole circle.
637    pub(crate) fn reach(&self, rect: Rect) -> Rect {
638        match self {
639            Self::Trapezoid(trapezoid) => expand_rect(trapezoid.bounds(), 0.5),
640            Self::Arc(arc) => expand_rect(arc.bounds(), INK_REACH),
641            _ => rect,
642        }
643    }
644
645    /// The pixels of the row centred at `y` that can take ink, within
646    /// `min_x..max_x`.
647    fn row_runs(&self, y: f32, min_x: i32, max_x: i32) -> RowRuns {
648        match self {
649            Self::Arc(arc) => arc_row_runs(arc, y, min_x, max_x),
650            Self::Stroke {
651                rect,
652                radii,
653                half_width,
654                join,
655            } => stroke_row_runs(*rect, *radii, *half_width, *join, y, min_x, max_x),
656            _ => [(min_x, max_x), NO_RUN],
657        }
658    }
659
660    fn at(&self, point: Point) -> f32 {
661        match self {
662            Self::Line(line) => line.coverage(point),
663            Self::Trapezoid(trapezoid) => trapezoid.coverage(point),
664            Self::Arc(arc) => shape_sdf::arc_coverage(point, arc),
665            Self::Stroke {
666                rect,
667                radii,
668                half_width,
669                join,
670            } => shape_sdf::stroked_rect_coverage(point, *rect, *radii, *half_width, *join),
671            Self::Fill { rect, radii } => match radii {
672                Some(radii) if !point_in_resolved_rounded_rect(point.x, point.y, *rect, radii) => {
673                    0.0
674                }
675                _ => 1.0,
676            },
677        }
678    }
679}
680
681fn draw_image(target: &mut Target, draw: &ImageDraw, diagnostics: &RenderDiagnostics) {
682    let snap_delta = draw
683        .snap_anchor
684        .map(snap_delta_for_anchor)
685        .unwrap_or_default();
686    let rect = draw.rect.translate(snap_delta.x, snap_delta.y);
687    let clip = draw.clip;
688
689    if draw.alpha <= 0.0 || rect.width <= 0.0 || rect.height <= 0.0 {
690        return;
691    }
692
693    let Some(clip_bounds) = clip_rect_to_bounds(rect, clip, target) else {
694        return;
695    };
696
697    let img_width = draw.image.width();
698    let img_height = draw.image.height();
699    if img_width == 0
700        || img_height == 0
701        || draw.image.format() == cranpose_ui_graphics::ImagePixelFormat::External
702    {
703        return;
704    }
705
706    let (sr_x, sr_y, sr_w, sr_h) = if let Some(sr) = draw.src_rect {
707        (sr.x, sr.y, sr.width, sr.height)
708    } else {
709        (0.0, 0.0, img_width as f32, img_height as f32)
710    };
711
712    for py in clip_bounds.min_y..clip_bounds.max_y {
713        for px in clip_bounds.min_x..clip_bounds.max_x {
714            let sample_x = px as f32 + 0.5;
715            let sample_y = py as f32 + 0.5;
716            let u = ((sample_x - rect.x) / rect.width).clamp(0.0, 1.0);
717            let v = ((sample_y - rect.y) / rect.height).clamp(0.0, 1.0);
718
719            let mut sample = match draw.sampling {
720                cranpose_ui_graphics::ImageSampling::Nearest => {
721                    let src_x = ((sr_x + u * sr_w).floor() as i32).clamp(0, img_width as i32 - 1);
722                    let src_y = ((sr_y + v * sr_h).floor() as i32).clamp(0, img_height as i32 - 1);
723                    sample_image_nearest(&draw.image, src_x as u32, src_y as u32)
724                }
725                cranpose_ui_graphics::ImageSampling::Linear => {
726                    sample_image_linear(&draw.image, sr_x + u * sr_w - 0.5, sr_y + v * sr_h - 0.5)
727                }
728            };
729
730            if let Some(filter) = draw.color_filter {
731                sample = apply_color_filter(sample, filter);
732            }
733
734            sample[3] *= draw.alpha.clamp(0.0, 1.0);
735            if sample[3] <= 0.0 {
736                continue;
737            }
738
739            blend_pixel(target.pixel(px, py), sample, draw.blend_mode, diagnostics);
740        }
741    }
742}
743
744fn sample_image_nearest(
745    image: &cranpose_ui_graphics::ImageBitmap,
746    src_x: u32,
747    src_y: u32,
748) -> [f32; 4] {
749    image
750        .rgba8_pixel(src_y as usize * image.width() as usize + src_x as usize)
751        .map(|channel| channel as f32 / 255.0)
752}
753
754fn sample_image_linear(image: &cranpose_ui_graphics::ImageBitmap, x: f32, y: f32) -> [f32; 4] {
755    let img_width = image.width();
756    let img_height = image.height();
757    let x = x.clamp(0.0, img_width.saturating_sub(1) as f32);
758    let y = y.clamp(0.0, img_height.saturating_sub(1) as f32);
759    let x0 = x.floor();
760    let y0 = y.floor();
761    let tx = x - x0;
762    let ty = y - y0;
763    let x0 = (x0 as i32).clamp(0, img_width as i32 - 1) as u32;
764    let y0 = (y0 as i32).clamp(0, img_height as i32 - 1) as u32;
765    let x1 = (x0 + 1).min(img_width - 1);
766    let y1 = (y0 + 1).min(img_height - 1);
767    let top_left = sample_image_nearest(image, x0, y0);
768    let top_right = sample_image_nearest(image, x1, y0);
769    let bottom_left = sample_image_nearest(image, x0, y1);
770    let bottom_right = sample_image_nearest(image, x1, y1);
771
772    let mut out = [0.0; 4];
773    for channel in 0..4 {
774        let top = top_left[channel] + (top_right[channel] - top_left[channel]) * tx;
775        let bottom = bottom_left[channel] + (bottom_right[channel] - bottom_left[channel]) * tx;
776        out[channel] = top + (bottom - top) * ty;
777    }
778    out
779}
780
781fn draw_text(
782    target: &mut Target,
783    draw: &TextDraw,
784    diagnostics: &RenderDiagnostics,
785    text_resources: &SoftwareTextResources,
786) {
787    if let Some((image, rect)) = rasterize_text(draw, text_resources) {
788        blit_rasterized_text_image(target, rect, draw.clip, &image, diagnostics);
789    }
790}
791
792/// `draw` rasterized into an image of its own, straight alpha, and the
793/// rect that image covers.
794pub(crate) fn rasterize_text(
795    draw: &TextDraw,
796    text_resources: &SoftwareTextResources,
797) -> Option<(cranpose_ui_graphics::ImageBitmap, Rect)> {
798    let text_scale = draw.scale.max(0.0);
799    if text_scale == 0.0 {
800        return None;
801    }
802    let static_text_motion = draw
803        .text_style
804        .paragraph_style
805        .text_motion
806        .unwrap_or(TextMotion::Static)
807        == TextMotion::Static;
808    let snap_delta = if static_text_motion {
809        draw.snap_anchor
810            .map(snap_delta_for_anchor)
811            .unwrap_or_default()
812    } else {
813        Point::default()
814    };
815    let rect = draw.rect.translate(snap_delta.x, snap_delta.y);
816    let raster_rect = if static_text_motion {
817        on_pixel_grid(rect)
818    } else {
819        rect
820    };
821    let image = rasterize_annotated_text_region(
822        draw.text.as_ref(),
823        raster_rect,
824        Point::new(raster_rect.x, raster_rect.y),
825        &draw.text_style,
826        draw.color,
827        draw.font_size,
828        text_scale,
829        text_resources.fonts(),
830        None,
831    )?;
832    let origin = if static_text_motion {
833        Point::new(raster_rect.x, raster_rect.y)
834    } else {
835        Point::new(rect.x, rect.y)
836    };
837    let rect = Rect {
838        x: origin.x,
839        y: origin.y,
840        width: image.width() as f32,
841        height: image.height() as f32,
842    };
843    Some((image, rect))
844}
845
846fn blit_rasterized_text_image(
847    target: &mut Target,
848    rect: Rect,
849    clip: Option<Rect>,
850    image: &cranpose_ui_graphics::ImageBitmap,
851    diagnostics: &RenderDiagnostics,
852) {
853    if rect.width <= 0.0 || rect.height <= 0.0 {
854        return;
855    }
856    let Some(clip_bounds) = clip_rect_to_bounds(rect, clip, target) else {
857        return;
858    };
859
860    let img_width = image.width();
861    let img_height = image.height();
862    if img_width == 0 || img_height == 0 {
863        return;
864    }
865
866    for py in clip_bounds.min_y..clip_bounds.max_y {
867        for px in clip_bounds.min_x..clip_bounds.max_x {
868            let sample_x = px as f32 + 0.5;
869            let sample_y = py as f32 + 0.5;
870            let u = ((sample_x - rect.x) / rect.width).clamp(0.0, 1.0);
871            let v = ((sample_y - rect.y) / rect.height).clamp(0.0, 1.0);
872
873            let src = sample_image_linear(
874                image,
875                u * img_width.saturating_sub(1) as f32,
876                v * img_height.saturating_sub(1) as f32,
877            );
878            if src[3] <= 0.0 {
879                continue;
880            }
881
882            blend_pixel(target.pixel(px, py), src, BlendMode::SrcOver, diagnostics);
883        }
884    }
885}
886
887fn blend_pixel(
888    dst: &mut [u8],
889    src: [f32; 4],
890    blend_mode: BlendMode,
891    diagnostics: &RenderDiagnostics,
892) {
893    let resolved_blend_mode = if is_blend_mode_supported(blend_mode) {
894        blend_mode
895    } else {
896        if diagnostics.claim_warning_once("pixels.unsupported-blend-mode") {
897            log::warn!(
898                "Pixels renderer currently supports BlendMode::SrcOver and BlendMode::DstOut; falling back to SrcOver for unsupported modes"
899            );
900        }
901        BlendMode::SrcOver
902    };
903
904    let src_alpha = src[3].clamp(0.0, 1.0);
905    if src_alpha <= 0.0 {
906        return;
907    }
908    // An opaque source over anything is the source: the same bytes the
909    // blend below works out, without reading the destination.
910    if src_alpha >= 1.0 && resolved_blend_mode != BlendMode::DstOut {
911        dst[0] = (src[0].clamp(0.0, 1.0) * 255.0).round() as u8;
912        dst[1] = (src[1].clamp(0.0, 1.0) * 255.0).round() as u8;
913        dst[2] = (src[2].clamp(0.0, 1.0) * 255.0).round() as u8;
914        dst[3] = 255;
915        return;
916    }
917    let dst_r = dst[0] as f32 / 255.0;
918    let dst_g = dst[1] as f32 / 255.0;
919    let dst_b = dst[2] as f32 / 255.0;
920    let dst_a = dst[3] as f32 / 255.0;
921
922    let (out_r, out_g, out_b, out_a) = match resolved_blend_mode {
923        BlendMode::DstOut => {
924            let keep = 1.0 - src_alpha;
925            (dst_r * keep, dst_g * keep, dst_b * keep, dst_a * keep)
926        }
927        BlendMode::SrcOver => (
928            src[0].clamp(0.0, 1.0) * src_alpha + dst_r * (1.0 - src_alpha),
929            src[1].clamp(0.0, 1.0) * src_alpha + dst_g * (1.0 - src_alpha),
930            src[2].clamp(0.0, 1.0) * src_alpha + dst_b * (1.0 - src_alpha),
931            src_alpha + dst_a * (1.0 - src_alpha),
932        ),
933        _ => (
934            src[0].clamp(0.0, 1.0) * src_alpha + dst_r * (1.0 - src_alpha),
935            src[1].clamp(0.0, 1.0) * src_alpha + dst_g * (1.0 - src_alpha),
936            src[2].clamp(0.0, 1.0) * src_alpha + dst_b * (1.0 - src_alpha),
937            src_alpha + dst_a * (1.0 - src_alpha),
938        ),
939    };
940
941    dst[0] = (out_r.clamp(0.0, 1.0) * 255.0).round() as u8;
942    dst[1] = (out_g.clamp(0.0, 1.0) * 255.0).round() as u8;
943    dst[2] = (out_b.clamp(0.0, 1.0) * 255.0).round() as u8;
944    dst[3] = (out_a.clamp(0.0, 1.0) * 255.0).round() as u8;
945}
946
947fn apply_color_filter(sample: [f32; 4], filter: ColorFilter) -> [f32; 4] {
948    filter.apply_rgba(sample)
949}
950
951#[cfg(test)]
952#[path = "tests/draw_tests.rs"]
953mod tests;