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
264enum 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 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;