1use std::sync::Arc;
2
3use jag_draw::{
4 Brush, ColorLinPremul, FontStyle, Painter, Path, RasterizedGlyph, Rect, RoundedRadii,
5 RoundedRect, Stroke, TextProvider, TextRun, Transform2D, Viewport, snap_to_device,
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ImageFitMode {
11 Fill,
13 Contain,
15 Cover,
17}
18
19impl Default for ImageFitMode {
20 fn default() -> Self {
21 Self::Contain
22 }
23}
24
25pub struct Canvas {
27 pub(crate) viewport: Viewport,
28 pub(crate) painter: Painter,
29 pub(crate) clear_color: Option<ColorLinPremul>,
30 pub(crate) text_provider: Option<Arc<dyn TextProvider + Send + Sync>>, pub(crate) glyph_draws: Vec<([f32; 2], RasterizedGlyph, ColorLinPremul, i32)>, pub(crate) svg_draws: Vec<(
33 std::path::PathBuf,
34 [f32; 2],
35 [f32; 2],
36 Option<jag_draw::SvgStyle>,
37 i32,
38 Transform2D,
39 Option<Rect>,
40 )>, pub(crate) image_draws: Vec<(
42 std::path::PathBuf,
43 [f32; 2],
44 [f32; 2],
45 ImageFitMode,
46 i32,
47 Transform2D,
48 Option<Rect>,
49 )>, pub(crate) raw_image_draws: Vec<RawImageDraw>,
52 pub(crate) dpi_scale: f32, pub(crate) clip_stack: Vec<Option<Rect>>,
56 pub(crate) overlay_draws: Vec<(Rect, ColorLinPremul)>,
59 pub(crate) scrim_draws: Vec<ScrimDraw>,
62}
63
64#[derive(Clone, Copy)]
66pub enum ScrimDraw {
67 Rect(Rect, ColorLinPremul),
68 Cutout {
69 hole: RoundedRect,
70 color: ColorLinPremul,
71 },
72}
73
74#[derive(Clone)]
76pub struct RawImageDraw {
77 pub pixels: Vec<u8>,
79 pub src_width: u32,
81 pub src_height: u32,
83 pub origin: [f32; 2],
85 pub dst_size: [f32; 2],
87 pub z: i32,
89 pub transform: Transform2D,
91 pub dirty_rects: Vec<(u32, u32, u32, u32)>,
93 pub clip: Option<Rect>,
95}
96
97impl Canvas {
98 pub fn viewport(&self) -> Viewport {
99 self.viewport
100 }
101
102 pub fn current_transform(&self) -> Transform2D {
104 self.painter.current_transform()
105 }
106
107 pub fn clear(&mut self, color: ColorLinPremul) {
109 self.clear_color = Some(color);
110 }
111
112 pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, brush: Brush, z: i32) {
114 let rect = Rect { x, y, w, h };
115 if let Some(clip) = self.clip_rect_local() {
116 if let Some(clipped) = intersect_rect(rect, clip) {
117 self.painter.rect(clipped, brush, z);
118 }
119 } else {
120 self.painter.rect(rect, brush, z);
121 }
122 }
123
124 pub fn external_texture(
129 &mut self,
130 rect: Rect,
131 texture_id: jag_draw::ExternalTextureId,
132 z: i32,
133 ) {
134 if let Some(clip) = self.clip_rect_local() {
136 if intersect_rect(rect, clip).is_none() {
137 return;
138 }
139 }
140 self.painter.external_texture(rect, texture_id, z);
141 }
142
143 pub fn fill_overlay_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: ColorLinPremul) {
150 let t = self.painter.current_transform();
153 let [a, b, c, d, e, f] = t.m;
154
155 let p0 = [a * x + c * y + e, b * x + d * y + f];
157 let p1 = [a * (x + w) + c * y + e, b * (x + w) + d * y + f];
158 let p2 = [a * (x + w) + c * (y + h) + e, b * (x + w) + d * (y + h) + f];
159 let p3 = [a * x + c * (y + h) + e, b * x + d * (y + h) + f];
160
161 let min_x = p0[0].min(p1[0]).min(p2[0]).min(p3[0]);
164 let max_x = p0[0].max(p1[0]).max(p2[0]).max(p3[0]);
165 let min_y = p0[1].min(p1[1]).min(p2[1]).min(p3[1]);
166 let max_y = p0[1].max(p1[1]).max(p2[1]).max(p3[1]);
167
168 self.overlay_draws.push((
169 Rect {
170 x: min_x,
171 y: min_y,
172 w: max_x - min_x,
173 h: max_y - min_y,
174 },
175 color,
176 ));
177 }
178
179 pub fn fill_scrim_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: ColorLinPremul) {
189 let t = self.painter.current_transform();
191 let [a, b, c, d, e, f] = t.m;
192
193 let p0 = [a * x + c * y + e, b * x + d * y + f];
195 let p1 = [a * (x + w) + c * y + e, b * (x + w) + d * y + f];
196 let p2 = [a * (x + w) + c * (y + h) + e, b * (x + w) + d * (y + h) + f];
197 let p3 = [a * x + c * (y + h) + e, b * x + d * (y + h) + f];
198
199 let min_x = p0[0].min(p1[0]).min(p2[0]).min(p3[0]);
201 let max_x = p0[0].max(p1[0]).max(p2[0]).max(p3[0]);
202 let min_y = p0[1].min(p1[1]).min(p2[1]).min(p3[1]);
203 let max_y = p0[1].max(p1[1]).max(p2[1]).max(p3[1]);
204
205 self.scrim_draws.push(ScrimDraw::Rect(
206 Rect {
207 x: min_x,
208 y: min_y,
209 w: max_x - min_x,
210 h: max_y - min_y,
211 },
212 color,
213 ));
214 }
215
216 pub fn fill_scrim_with_cutout(&mut self, hole: RoundedRect, color: ColorLinPremul) {
218 let t = self.painter.current_transform();
221 let [a, b, c, d, e, f] = t.m;
222
223 let rect = hole.rect;
224 let corners = [
225 [rect.x, rect.y],
226 [rect.x + rect.w, rect.y],
227 [rect.x + rect.w, rect.y + rect.h],
228 [rect.x, rect.y + rect.h],
229 ];
230
231 let mut min_x = f32::INFINITY;
232 let mut max_x = f32::NEG_INFINITY;
233 let mut min_y = f32::INFINITY;
234 let mut max_y = f32::NEG_INFINITY;
235
236 for p in corners {
237 let tx = a * p[0] + c * p[1] + e;
238 let ty = b * p[0] + d * p[1] + f;
239 min_x = min_x.min(tx);
240 max_x = max_x.max(tx);
241 min_y = min_y.min(ty);
242 max_y = max_y.max(ty);
243 }
244
245 let sx = (a * a + b * b).sqrt();
247 let sy = (c * c + d * d).sqrt();
248 let scale = if sx.is_finite() && sy.is_finite() && sx > 0.0 && sy > 0.0 {
249 (sx + sy) * 0.5
250 } else {
251 1.0
252 };
253
254 let transformed = RoundedRect {
255 rect: Rect {
256 x: min_x,
257 y: min_y,
258 w: (max_x - min_x).max(0.0),
259 h: (max_y - min_y).max(0.0),
260 },
261 radii: RoundedRadii {
262 tl: hole.radii.tl * scale,
263 tr: hole.radii.tr * scale,
264 br: hole.radii.br * scale,
265 bl: hole.radii.bl * scale,
266 },
267 };
268
269 self.scrim_draws.push(ScrimDraw::Cutout {
270 hole: transformed,
271 color,
272 });
273 }
274
275 pub fn stroke_path(&mut self, path: Path, width: f32, color: ColorLinPremul, z: i32) {
280 if let Some(clip) = self.clip_rect_local() {
281 if let Some(bounds) = path_bounds(&path) {
282 let expanded = Rect {
283 x: bounds.x - width,
284 y: bounds.y - width,
285 w: bounds.w + width * 2.0,
286 h: bounds.h + width * 2.0,
287 };
288 if intersect_rect(expanded, clip).is_none() {
293 return;
294 }
295 }
296 }
297 self.painter.stroke_path(path, Stroke { width }, color, z);
298 }
299
300 pub fn fill_path(&mut self, path: Path, color: ColorLinPremul, z: i32) {
304 if let Some(clip) = self.clip_rect_local() {
305 if let Some(bounds) = path_bounds(&path) {
306 if intersect_rect(bounds, clip).is_none() {
307 return;
308 }
309 }
310 }
311 self.painter.fill_path(path, color, z);
312 }
313
314 pub fn ellipse(&mut self, center: [f32; 2], radii: [f32; 2], brush: Brush, z: i32) {
316 if let Some(clip) = self.clip_rect_local() {
317 let bounds = Rect {
318 x: center[0] - radii[0],
319 y: center[1] - radii[1],
320 w: radii[0] * 2.0,
321 h: radii[1] * 2.0,
322 };
323 if let Some(clipped) = intersect_rect(bounds, clip) {
326 let fully_inside = (clipped.x - bounds.x).abs() < 0.5
327 && (clipped.y - bounds.y).abs() < 0.5
328 && (clipped.w - bounds.w).abs() < 0.5
329 && (clipped.h - bounds.h).abs() < 0.5;
330 if !fully_inside {
331 return;
332 }
333 } else {
334 return;
335 }
336 }
337 self.painter.ellipse(center, radii, brush, z);
338 }
339
340 pub fn circle(&mut self, center: [f32; 2], radius: f32, brush: Brush, z: i32) {
342 if let Some(clip) = self.clip_rect_local() {
343 let bounds = Rect {
344 x: center[0] - radius,
345 y: center[1] - radius,
346 w: radius * 2.0,
347 h: radius * 2.0,
348 };
349 if let Some(clipped) = intersect_rect(bounds, clip) {
352 let fully_inside = (clipped.x - bounds.x).abs() < 0.5
353 && (clipped.y - bounds.y).abs() < 0.5
354 && (clipped.w - bounds.w).abs() < 0.5
355 && (clipped.h - bounds.h).abs() < 0.5;
356 if !fully_inside {
357 return;
358 }
359 } else {
360 return;
361 }
362 }
363 self.painter.circle(center, radius, brush, z);
364 }
365
366 pub fn rounded_rect(&mut self, rrect: RoundedRect, brush: Brush, z: i32) {
368 if let Some(clip) = self.clip_rect_local() {
369 if let Some(clipped) = intersect_rect(rrect.rect, clip) {
370 let fully_inside = (clipped.x - rrect.rect.x).abs() < 0.5
371 && (clipped.y - rrect.rect.y).abs() < 0.5
372 && (clipped.w - rrect.rect.w).abs() < 0.5
373 && (clipped.h - rrect.rect.h).abs() < 0.5;
374 if fully_inside {
375 self.painter.rounded_rect(rrect, brush, z);
376 } else {
377 let mut radii = rrect.radii;
379 if clipped.x > rrect.rect.x + 0.5 {
380 radii.tl = 0.0;
381 radii.bl = 0.0;
382 }
383 if clipped.x + clipped.w < rrect.rect.x + rrect.rect.w - 0.5 {
384 radii.tr = 0.0;
385 radii.br = 0.0;
386 }
387 if clipped.y > rrect.rect.y + 0.5 {
388 radii.tl = 0.0;
389 radii.tr = 0.0;
390 }
391 if clipped.y + clipped.h < rrect.rect.y + rrect.rect.h - 0.5 {
392 radii.bl = 0.0;
393 radii.br = 0.0;
394 }
395 self.painter.rounded_rect(
396 RoundedRect {
397 rect: clipped,
398 radii,
399 },
400 brush,
401 z,
402 );
403 }
404 }
405 } else {
406 self.painter.rounded_rect(rrect, brush, z);
407 }
408 }
409
410 pub fn stroke_rounded_rect(&mut self, rrect: RoundedRect, width: f32, brush: Brush, z: i32) {
412 if let Some(clip) = self.clip_rect_local() {
413 let expanded = Rect {
415 x: rrect.rect.x - width,
416 y: rrect.rect.y - width,
417 w: rrect.rect.w + width * 2.0,
418 h: rrect.rect.h + width * 2.0,
419 };
420 if let Some(clipped_expanded) = intersect_rect(expanded, clip) {
421 let fully_inside = (clipped_expanded.x - expanded.x).abs() < 0.5
422 && (clipped_expanded.y - expanded.y).abs() < 0.5
423 && (clipped_expanded.w - expanded.w).abs() < 0.5
424 && (clipped_expanded.h - expanded.h).abs() < 0.5;
425 if fully_inside {
426 self.painter
427 .stroke_rounded_rect(rrect, Stroke { width }, brush, z);
428 } else {
429 if let Some(clipped_inner) = intersect_rect(rrect.rect, clip) {
431 let mut radii = rrect.radii;
432 if clipped_inner.x > rrect.rect.x + 0.5 {
433 radii.tl = 0.0;
434 radii.bl = 0.0;
435 }
436 if clipped_inner.x + clipped_inner.w < rrect.rect.x + rrect.rect.w - 0.5 {
437 radii.tr = 0.0;
438 radii.br = 0.0;
439 }
440 if clipped_inner.y > rrect.rect.y + 0.5 {
441 radii.tl = 0.0;
442 radii.tr = 0.0;
443 }
444 if clipped_inner.y + clipped_inner.h < rrect.rect.y + rrect.rect.h - 0.5 {
445 radii.bl = 0.0;
446 radii.br = 0.0;
447 }
448 self.painter.stroke_rounded_rect(
449 RoundedRect {
450 rect: clipped_inner,
451 radii,
452 },
453 Stroke { width },
454 brush,
455 z,
456 );
457 }
458 }
459 }
460 } else {
461 self.painter
462 .stroke_rounded_rect(rrect, Stroke { width }, brush, z);
463 }
464 }
465
466 pub fn draw_text_run(
498 &mut self,
499 origin: [f32; 2],
500 text: String,
501 size_px: f32,
502 color: ColorLinPremul,
503 z: i32,
504 ) {
505 self.draw_text_run_weighted(origin, text, size_px, 400.0, color, z);
507 }
508
509 pub fn draw_text_run_weighted(
513 &mut self,
514 origin: [f32; 2],
515 text: String,
516 size_px: f32,
517 weight: f32,
518 color: ColorLinPremul,
519 z: i32,
520 ) {
521 self.draw_text_run_styled(
522 origin,
523 text,
524 size_px,
525 weight,
526 FontStyle::Normal,
527 None,
528 color,
529 z,
530 );
531 }
532
533 pub fn draw_text_run_styled(
539 &mut self,
540 origin: [f32; 2],
541 text: String,
542 size_px: f32,
543 weight: f32,
544 style: FontStyle,
545 family: Option<String>,
546 color: ColorLinPremul,
547 z: i32,
548 ) {
549 if let Some(ref provider) = self.text_provider
554 && !self.painter.has_active_opacity()
555 {
556 let transform = self.painter.current_transform();
558 let [a, b, c, d, e, f] = transform.m;
559 let transformed_origin = [
560 a * origin[0] + c * origin[1] + e,
561 b * origin[0] + d * origin[1] + f,
562 ];
563
564 let sf = if self.dpi_scale.is_finite() && self.dpi_scale > 0.0 {
566 self.dpi_scale
567 } else {
568 1.0
569 };
570
571 let run = TextRun {
577 text,
578 pos: [0.0, 0.0],
579 size: (size_px * sf).max(1.0),
580 color,
581 weight,
582 style,
583 family,
584 };
585
586 let glyphs = jag_draw::rasterize_run_cached(provider.as_ref(), &run);
589 let current_clip = self.clip_stack.last().cloned().unwrap_or(None);
591
592 for g in glyphs.iter() {
593 let mut glyph_origin_logical = [
597 transformed_origin[0] + g.offset[0] / sf,
598 transformed_origin[1] + g.offset[1] / sf,
599 ];
600
601 if size_px <= 15.0 {
603 glyph_origin_logical[0] = (glyph_origin_logical[0] * sf).round() / sf;
604 glyph_origin_logical[1] = (glyph_origin_logical[1] * sf).round() / sf;
605 }
606
607 let glyph_origin_device =
609 [glyph_origin_logical[0] * sf, glyph_origin_logical[1] * sf];
610
611 if let Some(clip) = current_clip {
612 if let Some((clipped_mask, clipped_origin_device)) =
614 clip_glyph_to_rect(&g.mask, glyph_origin_device, clip)
615 {
616 let clipped = RasterizedGlyph {
617 offset: [0.0, 0.0],
618 mask: clipped_mask,
619 };
620 let mut clipped_origin_logical =
622 [clipped_origin_device[0] / sf, clipped_origin_device[1] / sf];
623 if size_px <= 15.0 {
624 clipped_origin_logical[0] =
625 (clipped_origin_logical[0] * sf).round() / sf;
626 clipped_origin_logical[1] =
627 (clipped_origin_logical[1] * sf).round() / sf;
628 }
629 self.glyph_draws
630 .push((clipped_origin_logical, clipped, color, z));
631 }
632 } else {
633 self.glyph_draws
634 .push((glyph_origin_logical, g.clone(), color, z));
635 }
636 }
637 } else {
638 self.painter.text(
640 TextRun {
641 text,
642 pos: origin,
643 size: size_px,
644 color,
645 weight,
646 style,
647 family,
648 },
649 z,
650 );
651 }
652 }
653
654 #[allow(clippy::too_many_arguments)]
661 pub fn draw_text_run_gradient(
662 &mut self,
663 origin: [f32; 2],
664 text: String,
665 size_px: f32,
666 weight: f32,
667 style: FontStyle,
668 family: Option<String>,
669 brush: &Brush,
670 text_width: f32,
671 z: i32,
672 ) {
673 if let Some(ref provider) = self.text_provider
674 && !self.painter.has_active_opacity()
675 {
676 let transform = self.painter.current_transform();
677 let [a, b, c, d, e, f] = transform.m;
678 let transformed_origin = [
679 a * origin[0] + c * origin[1] + e,
680 b * origin[0] + d * origin[1] + f,
681 ];
682
683 let sf = if self.dpi_scale.is_finite() && self.dpi_scale > 0.0 {
684 self.dpi_scale
685 } else {
686 1.0
687 };
688
689 let solid_fallback = match brush {
691 Brush::Solid(c) => *c,
692 Brush::LinearGradient { stops, .. } => stops.first().map_or(
693 ColorLinPremul {
694 r: 1.0,
695 g: 1.0,
696 b: 1.0,
697 a: 1.0,
698 },
699 |s| s.1,
700 ),
701 Brush::RadialGradient { stops, .. } => stops.first().map_or(
702 ColorLinPremul {
703 r: 1.0,
704 g: 1.0,
705 b: 1.0,
706 a: 1.0,
707 },
708 |s| s.1,
709 ),
710 };
711
712 let run = TextRun {
713 text,
714 pos: [0.0, 0.0],
715 size: (size_px * sf).max(1.0),
716 color: solid_fallback,
717 weight,
718 style,
719 family,
720 };
721
722 let glyphs = jag_draw::rasterize_run_cached(provider.as_ref(), &run);
723 let current_clip = self.clip_stack.last().cloned().unwrap_or(None);
724 let tw = text_width.max(1.0);
725
726 let grad_stops: Vec<(f32, [f32; 4])> = match brush {
728 Brush::LinearGradient { stops, .. } | Brush::RadialGradient { stops, .. } => stops
729 .iter()
730 .map(|(t, c)| (*t, [c.r, c.g, c.b, c.a]))
731 .collect(),
732 _ => Vec::new(),
733 };
734
735 for g in glyphs.iter() {
736 let mut glyph_origin_logical = [
737 transformed_origin[0] + g.offset[0] / sf,
738 transformed_origin[1] + g.offset[1] / sf,
739 ];
740 if size_px <= 15.0 {
741 glyph_origin_logical[0] = (glyph_origin_logical[0] * sf).round() / sf;
742 glyph_origin_logical[1] = (glyph_origin_logical[1] * sf).round() / sf;
743 }
744
745 let glyph_color = if grad_stops.is_empty() {
747 solid_fallback
748 } else {
749 let glyph_x = g.offset[0] / sf;
750 let t = (glyph_x / tw).clamp(0.0, 1.0);
751 let [r, g, b, a] = jag_draw::sample_gradient_stops(&grad_stops, t);
752 ColorLinPremul { r, g, b, a }
753 };
754
755 let glyph_origin_device =
756 [glyph_origin_logical[0] * sf, glyph_origin_logical[1] * sf];
757
758 if let Some(clip) = current_clip {
759 if let Some((clipped_mask, clipped_origin_device)) =
760 clip_glyph_to_rect(&g.mask, glyph_origin_device, clip)
761 {
762 let clipped = RasterizedGlyph {
763 offset: [0.0, 0.0],
764 mask: clipped_mask,
765 };
766 let mut clipped_origin_logical =
767 [clipped_origin_device[0] / sf, clipped_origin_device[1] / sf];
768 if size_px <= 15.0 {
769 clipped_origin_logical[0] =
770 (clipped_origin_logical[0] * sf).round() / sf;
771 clipped_origin_logical[1] =
772 (clipped_origin_logical[1] * sf).round() / sf;
773 }
774 self.glyph_draws
775 .push((clipped_origin_logical, clipped, glyph_color, z));
776 }
777 } else {
778 self.glyph_draws
779 .push((glyph_origin_logical, g.clone(), glyph_color, z));
780 }
781 }
782 } else {
783 let fallback_color = match brush {
785 Brush::Solid(c) => *c,
786 Brush::LinearGradient { stops, .. } | Brush::RadialGradient { stops, .. } => {
787 stops.first().map_or(
788 ColorLinPremul {
789 r: 1.0,
790 g: 1.0,
791 b: 1.0,
792 a: 1.0,
793 },
794 |s| s.1,
795 )
796 }
797 };
798 self.painter.text(
799 TextRun {
800 text,
801 pos: origin,
802 size: size_px,
803 color: fallback_color,
804 weight,
805 style,
806 family,
807 },
808 z,
809 );
810 }
811 }
812
813 pub fn draw_text_direct(
816 &mut self,
817 origin: [f32; 2],
818 text: &str,
819 size_px: f32,
820 color: ColorLinPremul,
821 provider: &dyn TextProvider,
822 z: i32,
823 ) {
824 let transform = self.painter.current_transform();
826 let [a, b, c, d, e, f] = transform.m;
827 let transformed_origin = [
828 a * origin[0] + c * origin[1] + e,
829 b * origin[0] + d * origin[1] + f,
830 ];
831
832 let current_clip = self.clip_stack.last().cloned().unwrap_or(None);
834
835 let sf = if self.dpi_scale.is_finite() && self.dpi_scale > 0.0 {
837 self.dpi_scale
838 } else {
839 1.0
840 };
841
842 let run = TextRun {
844 text: text.to_string(),
845 pos: [0.0, 0.0],
846 size: (size_px * sf).max(1.0),
847 color,
848 weight: 400.0,
849 style: FontStyle::Normal,
850 family: None,
851 };
852
853 let glyphs = jag_draw::rasterize_run_cached(provider, &run);
856
857 for g in glyphs.iter() {
858 let mut glyph_origin_logical = [
862 transformed_origin[0] + g.offset[0] / sf,
863 transformed_origin[1] + g.offset[1] / sf,
864 ];
865
866 if size_px <= 15.0 {
867 glyph_origin_logical[0] = (glyph_origin_logical[0] * sf).round() / sf;
868 glyph_origin_logical[1] = (glyph_origin_logical[1] * sf).round() / sf;
869 }
870
871 let glyph_origin_device = [glyph_origin_logical[0] * sf, glyph_origin_logical[1] * sf];
873
874 if let Some(clip) = current_clip {
875 if let Some((clipped_mask, clipped_origin_device)) =
876 clip_glyph_to_rect(&g.mask, glyph_origin_device, clip)
877 {
878 let clipped = RasterizedGlyph {
879 offset: [0.0, 0.0],
880 mask: clipped_mask,
881 };
882 let mut clipped_origin_logical =
884 [clipped_origin_device[0] / sf, clipped_origin_device[1] / sf];
885 if size_px <= 15.0 {
886 clipped_origin_logical[0] = (clipped_origin_logical[0] * sf).round() / sf;
887 clipped_origin_logical[1] = (clipped_origin_logical[1] * sf).round() / sf;
888 }
889 self.glyph_draws
890 .push((clipped_origin_logical, clipped, color, z));
891 }
892 } else {
893 self.glyph_draws
894 .push((glyph_origin_logical, g.clone(), color, z));
895 }
896 }
897 }
898
899 pub fn set_text_provider(&mut self, provider: Arc<dyn TextProvider + Send + Sync>) {
901 self.text_provider = Some(provider);
902 }
903
904 pub fn text_provider(&self) -> Option<&Arc<dyn TextProvider + Send + Sync>> {
905 self.text_provider.as_ref()
906 }
907
908 pub fn measure_text_width(&self, text: &str, size_px: f32) -> f32 {
914 if let Some(provider) = self.text_provider() {
915 if let Some(shaped) = provider.shape_paragraph(text, size_px) {
916 let total: f32 = shaped.glyphs.iter().map(|g| g.x_advance).sum();
917 return total.max(0.0);
919 }
920 }
921 text.chars().count() as f32 * size_px * 0.55
923 }
924
925 pub fn measure_text_width_styled(
930 &self,
931 text: &str,
932 size_px: f32,
933 weight: f32,
934 style: FontStyle,
935 family: Option<&str>,
936 ) -> f32 {
937 if let Some(provider) = self.text_provider() {
938 let run = TextRun {
939 text: text.to_string(),
940 pos: [0.0, 0.0],
941 size: size_px,
942 color: ColorLinPremul::from_srgba_u8([0, 0, 0, 0]),
943 weight,
944 style,
945 family: family.map(String::from),
946 };
947 provider.measure_run(&run)
948 } else {
949 text.chars().count() as f32 * size_px * 0.55
950 }
951 }
952
953 pub fn draw_text_glyphs(
955 &mut self,
956 origin: [f32; 2],
957 glyphs: &[RasterizedGlyph],
958 color: ColorLinPremul,
959 z: i32,
960 ) {
961 for g in glyphs.iter().cloned() {
962 self.glyph_draws.push((origin, g, color, z));
963 }
964 }
965
966 pub fn draw_hyperlink(&mut self, hyperlink: jag_draw::Hyperlink, z: i32) {
989 if self.text_provider.is_some() && !self.painter.has_active_opacity() {
995 self.draw_text_run_styled(
997 hyperlink.pos,
998 hyperlink.text.clone(),
999 hyperlink.size,
1000 hyperlink.weight,
1001 hyperlink.style,
1002 hyperlink.family.clone(),
1003 hyperlink.color,
1004 z,
1005 );
1006
1007 if hyperlink.underline {
1009 let underline_color = hyperlink.underline_color.unwrap_or(hyperlink.color);
1010 let (underline_x, text_width) =
1011 if let Some(w) = hyperlink.measured_width.map(|v| v.max(0.0)) {
1012 (hyperlink.pos[0], w)
1013 } else {
1014 let trimmed = hyperlink.text.trim_end();
1015 let char_count = trimmed.chars().count() as f32;
1016 let weight_boost = ((hyperlink.weight - 400.0).max(0.0) / 500.0) * 0.08;
1017 let char_width = hyperlink.size * (0.50 + weight_boost);
1018 let mut width = char_count * char_width;
1019 let inset = hyperlink.size * 0.10;
1020 if width > inset * 2.0 {
1021 width -= inset * 2.0;
1022 }
1023 (hyperlink.pos[0] + inset, width)
1024 };
1025
1026 let underline_thickness = (hyperlink.size * 0.08).max(1.0);
1027 let underline_offset = hyperlink.size * 0.10;
1028 self.fill_rect(
1029 underline_x,
1030 hyperlink.pos[1] + underline_offset,
1031 text_width,
1032 underline_thickness,
1033 Brush::Solid(underline_color),
1034 z,
1035 );
1036 }
1037
1038 let mut hit_only = hyperlink;
1040 if hit_only.measured_width.is_none() {
1043 hit_only.measured_width = Some(self.measure_text_width_styled(
1044 &hit_only.text,
1045 hit_only.size,
1046 hit_only.weight,
1047 hit_only.style,
1048 hit_only.family.as_deref(),
1049 ));
1050 }
1051 hit_only.text = String::new();
1052 hit_only.underline = false;
1053 self.painter.hyperlink(hit_only, z);
1054 } else {
1055 self.painter.hyperlink(hyperlink, z);
1058 }
1059 }
1060
1061 pub fn draw_svg<P: Into<std::path::PathBuf>>(
1065 &mut self,
1066 path: P,
1067 origin: [f32; 2],
1068 max_size: [f32; 2],
1069 z: i32,
1070 ) {
1071 if let Some(clip) = self.clip_rect_local() {
1073 let bounds = Rect {
1074 x: origin[0],
1075 y: origin[1],
1076 w: max_size[0],
1077 h: max_size[1],
1078 };
1079 if intersect_rect(bounds, clip).is_none() {
1080 return;
1081 }
1082 }
1083 let device_clip = self.clip_stack.last().copied().flatten();
1084 let transform = self.painter.current_transform();
1085 self.svg_draws.push((
1086 path.into(),
1087 origin,
1088 max_size,
1089 None,
1090 z,
1091 transform,
1092 device_clip,
1093 ));
1094 }
1095
1096 pub fn draw_svg_styled<P: Into<std::path::PathBuf>>(
1098 &mut self,
1099 path: P,
1100 origin: [f32; 2],
1101 max_size: [f32; 2],
1102 style: jag_draw::SvgStyle,
1103 z: i32,
1104 ) {
1105 if let Some(clip) = self.clip_rect_local() {
1107 let bounds = Rect {
1108 x: origin[0],
1109 y: origin[1],
1110 w: max_size[0],
1111 h: max_size[1],
1112 };
1113 if intersect_rect(bounds, clip).is_none() {
1114 return;
1115 }
1116 }
1117 let device_clip = self.clip_stack.last().copied().flatten();
1118 let path_buf = path.into();
1119 let transform = self.painter.current_transform();
1120 self.svg_draws.push((
1121 path_buf,
1122 origin,
1123 max_size,
1124 Some(style),
1125 z,
1126 transform,
1127 device_clip,
1128 ));
1129 }
1130
1131 pub fn draw_image<P: Into<std::path::PathBuf>>(
1135 &mut self,
1136 path: P,
1137 origin: [f32; 2],
1138 size: [f32; 2],
1139 fit: ImageFitMode,
1140 z: i32,
1141 ) {
1142 if let Some(clip) = self.clip_rect_local() {
1144 let bounds = Rect {
1145 x: origin[0],
1146 y: origin[1],
1147 w: size[0],
1148 h: size[1],
1149 };
1150 if intersect_rect(bounds, clip).is_none() {
1151 return;
1152 }
1153 }
1154 let device_clip = self.clip_stack.last().copied().flatten();
1155 let transform = self.painter.current_transform();
1156 self.image_draws
1157 .push((path.into(), origin, size, fit, z, transform, device_clip));
1158 }
1159
1160 pub fn draw_raw_image(
1164 &mut self,
1165 pixels: Vec<u8>,
1166 src_width: u32,
1167 src_height: u32,
1168 origin: [f32; 2],
1169 dst_size: [f32; 2],
1170 z: i32,
1171 ) {
1172 if let Some(clip) = self.clip_rect_local() {
1174 let bounds = Rect {
1175 x: origin[0],
1176 y: origin[1],
1177 w: dst_size[0],
1178 h: dst_size[1],
1179 };
1180 if intersect_rect(bounds, clip).is_none() {
1181 return;
1182 }
1183 }
1184 let device_clip = self.clip_stack.last().copied().flatten();
1185 let transform = self.painter.current_transform();
1186 self.raw_image_draws.push(RawImageDraw {
1187 pixels,
1188 src_width,
1189 src_height,
1190 origin,
1191 dst_size,
1192 z,
1193 transform,
1194 dirty_rects: Vec::new(), clip: device_clip,
1196 });
1197 }
1198
1199 pub fn draw_raw_image_with_dirty_rects(
1203 &mut self,
1204 pixels: Vec<u8>,
1205 src_width: u32,
1206 src_height: u32,
1207 origin: [f32; 2],
1208 dst_size: [f32; 2],
1209 z: i32,
1210 dirty_rects: Vec<(u32, u32, u32, u32)>,
1211 ) {
1212 if let Some(clip) = self.clip_rect_local() {
1214 let bounds = Rect {
1215 x: origin[0],
1216 y: origin[1],
1217 w: dst_size[0],
1218 h: dst_size[1],
1219 };
1220 if intersect_rect(bounds, clip).is_none() {
1221 return;
1222 }
1223 }
1224 let device_clip = self.clip_stack.last().copied().flatten();
1225 let transform = self.painter.current_transform();
1226 self.raw_image_draws.push(RawImageDraw {
1227 pixels,
1228 src_width,
1229 src_height,
1230 origin,
1231 dst_size,
1232 z,
1233 transform,
1234 dirty_rects,
1235 clip: device_clip,
1236 });
1237 }
1238
1239 pub fn push_clip_rect(&mut self, rect: Rect) {
1241 self.painter.push_clip_rect(rect);
1243
1244 let t = self.painter.current_transform();
1246 let [a, b, c, d, e, f] = t.m;
1247
1248 let x0 = rect.x;
1249 let y0 = rect.y;
1250 let x1 = rect.x + rect.w;
1251 let y1 = rect.y + rect.h;
1252
1253 let p0 = [a * x0 + c * y0 + e, b * x0 + d * y0 + f];
1254 let p1 = [a * x1 + c * y0 + e, b * x1 + d * y0 + f];
1255 let p2 = [a * x0 + c * y1 + e, b * x0 + d * y1 + f];
1256 let p3 = [a * x1 + c * y1 + e, b * x1 + d * y1 + f];
1257
1258 let min_x = p0[0].min(p1[0]).min(p2[0]).min(p3[0]) * self.dpi_scale;
1259 let max_x = p0[0].max(p1[0]).max(p2[0]).max(p3[0]) * self.dpi_scale;
1260 let min_y = p0[1].min(p1[1]).min(p2[1]).min(p3[1]) * self.dpi_scale;
1261 let max_y = p0[1].max(p1[1]).max(p2[1]).max(p3[1]) * self.dpi_scale;
1262
1263 let new_clip = Rect {
1264 x: min_x,
1265 y: min_y,
1266 w: (max_x - min_x).max(0.0),
1267 h: (max_y - min_y).max(0.0),
1268 };
1269
1270 let merged = match self.clip_stack.last().cloned().unwrap_or(None) {
1271 None => Some(new_clip),
1272 Some(prev) => {
1273 Some(intersect_rect(prev, new_clip).unwrap_or(Rect {
1278 x: prev.x,
1279 y: prev.y,
1280 w: 0.0,
1281 h: 0.0,
1282 }))
1283 }
1284 };
1285 self.clip_stack.push(merged);
1286 }
1287
1288 pub fn pop_clip(&mut self) {
1289 self.painter.pop_clip();
1290 if self.clip_stack.len() > 1 {
1291 self.clip_stack.pop();
1292 }
1293 }
1294 pub fn push_transform(&mut self, t: Transform2D) {
1295 self.painter.push_transform(t);
1296 }
1297 pub fn pop_transform(&mut self) {
1298 self.painter.pop_transform();
1299 }
1300
1301 pub fn push_opacity(&mut self, opacity: f32) {
1302 self.painter.push_opacity(opacity);
1303 }
1304
1305 pub fn pop_opacity(&mut self) {
1306 self.painter.pop_opacity();
1307 }
1308
1309 pub fn hit_region_rect(&mut self, id: u32, rect: Rect, z: i32) {
1311 self.painter.hit_region_rect(id, rect, z);
1312 }
1313
1314 pub fn command_count(&self) -> usize {
1316 self.painter.command_count()
1317 }
1318
1319 pub fn display_list(&self) -> &jag_draw::DisplayList {
1321 self.painter.display_list()
1322 }
1323
1324 pub fn snap_rect_logical_to_device(&self, rect: Rect) -> Rect {
1330 let sf = if self.dpi_scale.is_finite() && self.dpi_scale > 0.0 {
1331 self.dpi_scale
1332 } else {
1333 1.0
1334 };
1335 let t = self.painter.current_transform();
1336 let [a, b, c, d, e, f] = t.m;
1337
1338 let is_simple = (b.abs() < 1e-4)
1341 && (c.abs() < 1e-4)
1342 && ((a - 1.0).abs() < 1e-4)
1343 && ((d - 1.0).abs() < 1e-4);
1344 if !is_simple {
1345 return rect;
1346 }
1347
1348 let tx = e;
1349 let ty = f;
1350
1351 let x0_device = snap_to_device(rect.x + tx, sf);
1354 let y0_device = snap_to_device(rect.y + ty, sf);
1355 let x1_device = snap_to_device(rect.x + rect.w + tx, sf);
1356 let y1_device = snap_to_device(rect.y + rect.h + ty, sf);
1357
1358 let x0 = x0_device - tx;
1359 let y0 = y0_device - ty;
1360 let x1 = x1_device - tx;
1361 let y1 = y1_device - ty;
1362
1363 Rect {
1364 x: x0,
1365 y: y0,
1366 w: (x1 - x0).max(0.0),
1367 h: (y1 - y0).max(0.0),
1368 }
1369 }
1370
1371 fn clip_rect_local(&self) -> Option<Rect> {
1378 let clip_device = match self.clip_stack.last() {
1379 Some(Some(r)) => *r,
1380 _ => return None,
1381 };
1382 let t = self.painter.current_transform();
1383 let [a, b, c, d, e, f] = t.m;
1384
1385 if b.abs() > 1e-4 || c.abs() > 1e-4 {
1387 return None;
1388 }
1389
1390 let sf = if self.dpi_scale.is_finite() && self.dpi_scale > 0.0 {
1391 self.dpi_scale
1392 } else {
1393 1.0
1394 };
1395
1396 let sx = a * sf;
1397 let sy = d * sf;
1398 if sx.abs() < 1e-6 || sy.abs() < 1e-6 {
1399 return None;
1400 }
1401
1402 let local_x0 = clip_device.x / sx - e / a;
1405 let local_y0 = clip_device.y / sy - f / d;
1406 let local_x1 = (clip_device.x + clip_device.w) / sx - e / a;
1407 let local_y1 = (clip_device.y + clip_device.h) / sy - f / d;
1408
1409 let (lx0, lx1) = if local_x0 < local_x1 {
1411 (local_x0, local_x1)
1412 } else {
1413 (local_x1, local_x0)
1414 };
1415 let (ly0, ly1) = if local_y0 < local_y1 {
1416 (local_y0, local_y1)
1417 } else {
1418 (local_y1, local_y0)
1419 };
1420
1421 Some(Rect {
1422 x: lx0,
1423 y: ly0,
1424 w: lx1 - lx0,
1425 h: ly1 - ly0,
1426 })
1427 }
1428}
1429
1430fn path_bounds(path: &jag_draw::Path) -> Option<Rect> {
1433 use jag_draw::PathCmd;
1434 let mut min_x = f32::INFINITY;
1435 let mut min_y = f32::INFINITY;
1436 let mut max_x = f32::NEG_INFINITY;
1437 let mut max_y = f32::NEG_INFINITY;
1438 let mut has_points = false;
1439
1440 let mut extend = |p: &[f32; 2]| {
1441 min_x = min_x.min(p[0]);
1442 min_y = min_y.min(p[1]);
1443 max_x = max_x.max(p[0]);
1444 max_y = max_y.max(p[1]);
1445 has_points = true;
1446 };
1447
1448 for cmd in &path.cmds {
1449 match cmd {
1450 PathCmd::MoveTo(p) | PathCmd::LineTo(p) => extend(p),
1451 PathCmd::QuadTo(a, b) => {
1452 extend(a);
1453 extend(b);
1454 }
1455 PathCmd::CubicTo(a, b, c) => {
1456 extend(a);
1457 extend(b);
1458 extend(c);
1459 }
1460 PathCmd::Close => {}
1461 }
1462 }
1463
1464 if has_points {
1465 Some(Rect {
1466 x: min_x,
1467 y: min_y,
1468 w: max_x - min_x,
1469 h: max_y - min_y,
1470 })
1471 } else {
1472 None
1473 }
1474}
1475
1476fn intersect_rect(a: Rect, b: Rect) -> Option<Rect> {
1477 let ax1 = a.x + a.w;
1478 let ay1 = a.y + a.h;
1479 let bx1 = b.x + b.w;
1480 let by1 = b.y + b.h;
1481
1482 let x0 = a.x.max(b.x);
1483 let y0 = a.y.max(b.y);
1484 let x1 = ax1.min(bx1);
1485 let y1 = ay1.min(by1);
1486
1487 if x1 <= x0 || y1 <= y0 {
1488 None
1489 } else {
1490 Some(Rect {
1491 x: x0,
1492 y: y0,
1493 w: x1 - x0,
1494 h: y1 - y0,
1495 })
1496 }
1497}
1498
1499fn clip_glyph_to_rect(
1501 mask: &jag_draw::GlyphMask,
1502 origin: [f32; 2],
1503 clip: Rect,
1504) -> Option<(jag_draw::GlyphMask, [f32; 2])> {
1505 use jag_draw::{ColorMask, GlyphMask, SubpixelMask};
1506
1507 let glyph_x0 = origin[0];
1508 let glyph_y0 = origin[1];
1509 let (width, height, data, bpp) = match mask {
1510 GlyphMask::Subpixel(m) => (m.width, m.height, &m.data, m.bytes_per_pixel()),
1511 GlyphMask::Color(m) => (m.width, m.height, &m.data, m.bytes_per_pixel()),
1512 };
1513
1514 let glyph_x1 = glyph_x0 + width as f32;
1515 let glyph_y1 = glyph_y0 + height as f32;
1516
1517 let clip_x0 = clip.x;
1518 let clip_y0 = clip.y;
1519 let clip_x1 = clip.x + clip.w;
1520 let clip_y1 = clip.y + clip.h;
1521
1522 let ix0 = glyph_x0.max(clip_x0);
1523 let iy0 = glyph_y0.max(clip_y0);
1524 let ix1 = glyph_x1.min(clip_x1);
1525 let iy1 = glyph_y1.min(clip_y1);
1526
1527 if ix0 >= ix1 || iy0 >= iy1 {
1528 return None;
1529 }
1530
1531 let start_x = ((ix0 - glyph_x0).floor().max(0.0)) as u32;
1533 let start_y = ((iy0 - glyph_y0).floor().max(0.0)) as u32;
1534 let end_x = ((ix1 - glyph_x0).ceil().min(width as f32)) as u32;
1535 let end_y = ((iy1 - glyph_y0).ceil().min(height as f32)) as u32;
1536
1537 if end_x <= start_x || end_y <= start_y {
1538 return None;
1539 }
1540
1541 let new_w = end_x - start_x;
1542 let new_h = end_y - start_y;
1543
1544 let src_stride = width * bpp as u32;
1545 let dst_stride = new_w * bpp as u32;
1546 let mut clipped_data = vec![0u8; (new_w * new_h * bpp as u32) as usize];
1547
1548 for row in 0..new_h {
1549 let src_y = start_y + row;
1550 let src_offset = (src_y * src_stride + start_x * bpp as u32) as usize;
1551 let dst_offset = (row * dst_stride) as usize;
1552 clipped_data[dst_offset..dst_offset + dst_stride as usize]
1553 .copy_from_slice(&data[src_offset..src_offset + dst_stride as usize]);
1554 }
1555
1556 let clipped = match mask {
1557 GlyphMask::Subpixel(m) => GlyphMask::Subpixel(SubpixelMask {
1558 width: new_w,
1559 height: new_h,
1560 format: m.format,
1561 data: clipped_data,
1562 }),
1563 GlyphMask::Color(_) => GlyphMask::Color(ColorMask {
1564 width: new_w,
1565 height: new_h,
1566 data: clipped_data,
1567 }),
1568 };
1569
1570 let new_origin = [glyph_x0 + start_x as f32, glyph_y0 + start_y as f32];
1571 Some((clipped, new_origin))
1572}