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
38pub(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
68fn 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
82pub(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
191pub(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 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 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
280fn 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
320pub(crate) struct PlacedShape {
323 pub rect: Rect,
324 pub coverage: ShapeCoverage,
325 pub dither_origin: Point,
326}
327
328fn 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
346pub(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
417fn 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
434const INK_REACH: f32 = 0.5 + 1.0 / 64.0;
438
439type RowRuns = [(i32, i32); 2];
442
443const NO_RUN: (i32, i32) = (0, 0);
444
445fn 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
453fn 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
463fn 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
483fn 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 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 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 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 (None, Some(right)) => runs_around(run, centre_x + 1.0 / 64.0, centre_x + right),
571 (None, None) => [run, NO_RUN],
572 }
573}
574
575pub(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 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 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 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
792pub(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 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;