1use std::any::TypeId;
2use std::collections::HashMap;
3use std::hash::{Hash, Hasher};
4use std::sync::Arc;
5
6use fret_core::scene::{Paint, PaintBindingV1};
7use fret_core::{
8 AttributedText, Color, Corners, DrawOrder, EffectChain, EffectMode, EffectQuality, FontId,
9 FontWeight, Point, Px, Rect, Scene, SceneOp, SvgFit, TextConstraints, TextMetrics,
10 TextOverflow, TextSlant, TextStyle, TextWrap, Transform2D,
11};
12use fret_core::{PathCommand, PathConstraints, PathMetrics, PathStyle};
13use fret_runtime::ModelId;
14
15use crate::Theme;
16use crate::element::CanvasCachePolicy;
17use crate::widget::Invalidation;
18use crate::{SvgSource, UiHost, widget::PaintCx};
19
20pub type OnCanvasPaint = Arc<dyn for<'a> Fn(&mut CanvasPainter<'a>) + 'static>;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct CanvasKey(pub u64);
29
30impl CanvasKey {
31 pub const fn new(value: u64) -> Self {
32 Self(value)
33 }
34
35 pub fn combine(self, value: u64) -> Self {
37 Self(mix_u64(self.0, value))
38 }
39
40 pub fn combine_hash<T: Hash>(self, value: &T) -> Self {
42 self.combine(Self::from_hash(value).0)
43 }
44
45 pub fn from_hash<T: Hash>(value: &T) -> Self {
49 let mut hasher = Fnv1a64::default();
50 value.hash(&mut hasher);
51 Self(hasher.finish())
52 }
53}
54
55impl From<CanvasKey> for u64 {
56 fn from(value: CanvasKey) -> Self {
57 value.0
58 }
59}
60
61impl From<u64> for CanvasKey {
62 fn from(value: u64) -> Self {
63 Self(value)
64 }
65}
66
67#[derive(Default)]
68pub(crate) struct CanvasPaintHooks {
69 pub on_paint: Option<OnCanvasPaint>,
70}
71
72pub(crate) trait UiCanvasHost {
77 fn bounds(&self) -> Rect;
78 fn scale_factor(&self) -> f32;
79 fn text_font_stack_key(&mut self) -> u64;
80 fn inherited_foreground(&self) -> Option<Color>;
81 fn inherited_text_style(&mut self) -> Option<fret_core::TextStyleRefinement>;
82
83 fn theme(&mut self) -> &Theme;
84 fn request_redraw(&mut self);
85 fn request_animation_frame(&mut self);
86
87 fn observe_model_id(&mut self, model: ModelId, invalidation: Invalidation);
88 fn observe_global(&mut self, global: TypeId, invalidation: Invalidation);
89
90 fn scene(&mut self) -> &mut Scene;
91 fn services_and_scene(&mut self) -> (&mut dyn fret_core::UiServices, &mut Scene);
92}
93
94pub(crate) struct UiCanvasHostAdapter<'a, 'b, H: UiHost> {
95 cx: &'a mut PaintCx<'b, H>,
96}
97
98impl<'a, 'b, H: UiHost> UiCanvasHostAdapter<'a, 'b, H> {
99 pub(crate) fn new(cx: &'a mut PaintCx<'b, H>) -> Self {
100 Self { cx }
101 }
102}
103
104impl<'a, 'b, H: UiHost> UiCanvasHost for UiCanvasHostAdapter<'a, 'b, H> {
105 fn bounds(&self) -> Rect {
106 self.cx.bounds
107 }
108
109 fn scale_factor(&self) -> f32 {
110 self.cx.scale_factor
111 }
112
113 fn text_font_stack_key(&mut self) -> u64 {
114 self.cx
115 .observe_global::<fret_runtime::TextFontStackKey>(Invalidation::Layout);
116 self.cx
117 .app
118 .global::<fret_runtime::TextFontStackKey>()
119 .map(|k| k.0)
120 .unwrap_or(0)
121 }
122
123 fn inherited_foreground(&self) -> Option<Color> {
124 self.cx.inherited_foreground()
125 }
126
127 fn inherited_text_style(&mut self) -> Option<fret_core::TextStyleRefinement> {
128 let Some(window) = self.cx.window else {
129 return None;
130 };
131 crate::declarative::frame::inherited_text_style_for_node(self.cx.app, window, self.cx.node)
132 }
133
134 fn theme(&mut self) -> &Theme {
135 self.cx.theme()
136 }
137
138 fn request_redraw(&mut self) {
139 self.cx.request_redraw();
140 }
141
142 fn request_animation_frame(&mut self) {
143 self.cx.request_animation_frame();
144 }
145
146 fn observe_model_id(&mut self, model: ModelId, invalidation: Invalidation) {
147 (self.cx.observe_model)(model, invalidation);
148 }
149
150 fn observe_global(&mut self, global: TypeId, invalidation: Invalidation) {
151 (self.cx.observe_global)(global, invalidation);
152 }
153
154 fn scene(&mut self) -> &mut Scene {
155 self.cx.scene
156 }
157
158 fn services_and_scene(&mut self) -> (&mut dyn fret_core::UiServices, &mut Scene) {
159 (self.cx.services, self.cx.scene)
160 }
161}
162
163pub struct CanvasPainter<'a> {
164 host: &'a mut dyn UiCanvasHost,
165 cache: &'a mut CanvasCache,
166}
167
168impl<'a> CanvasPainter<'a> {
169 pub(crate) fn new(host: &'a mut dyn UiCanvasHost, cache: &'a mut CanvasCache) -> Self {
170 Self { host, cache }
171 }
172
173 pub fn frame_id(&self) -> u64 {
175 self.cache.frame
176 }
177
178 pub fn bounds(&self) -> Rect {
179 self.host.bounds()
180 }
181
182 pub fn scale_factor(&self) -> f32 {
183 self.host.scale_factor()
184 }
185
186 pub fn theme(&mut self) -> &Theme {
187 self.host.theme()
188 }
189
190 pub fn inherited_foreground(&self) -> Option<Color> {
191 self.host.inherited_foreground()
192 }
193
194 pub fn inherited_text_style(&mut self) -> Option<fret_core::TextStyleRefinement> {
195 self.host.inherited_text_style()
196 }
197
198 pub fn resolved_passive_text_style(&mut self, explicit: Option<TextStyle>) -> TextStyle {
199 let theme = self.host.theme().snapshot();
200 let inherited = self.host.inherited_text_style();
201 crate::text_props::resolve_text_style(theme, explicit, inherited.as_ref())
202 }
203
204 pub fn request_redraw(&mut self) {
205 self.host.request_redraw();
206 }
207
208 pub fn request_animation_frame(&mut self) {
209 self.host.request_animation_frame();
210 }
211
212 pub fn observe_model_id(&mut self, model: ModelId, invalidation: Invalidation) {
213 self.host.observe_model_id(model, invalidation);
214 }
215
216 pub fn observe_global<T: std::any::Any>(&mut self, invalidation: Invalidation) {
217 self.host.observe_global(TypeId::of::<T>(), invalidation);
218 }
219
220 pub fn key<T: Hash>(&self, value: &T) -> u64 {
222 CanvasKey::from_hash(value).0
223 }
224
225 pub fn key_scope<T: Hash>(&self, scope: &T) -> CanvasKey {
229 CanvasKey::from_hash(scope)
230 }
231
232 pub fn child_key<T: Hash>(&self, parent: CanvasKey, child: &T) -> CanvasKey {
234 parent.combine_hash(child)
235 }
236
237 pub fn scene(&mut self) -> &mut Scene {
238 self.host.scene()
239 }
240
241 pub fn services_and_scene(&mut self) -> (&mut dyn fret_core::UiServices, &mut Scene) {
246 self.host.services_and_scene()
247 }
248
249 pub fn with_clip_rect<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
250 {
251 let scene = self.host.scene();
252 scene.push(SceneOp::PushClipRect { rect });
253 }
254 let out = f(self);
255 {
256 let scene = self.host.scene();
257 scene.push(SceneOp::PopClip);
258 }
259 out
260 }
261
262 pub fn with_clip_rrect<R>(
263 &mut self,
264 rect: Rect,
265 corner_radii: Corners,
266 f: impl FnOnce(&mut Self) -> R,
267 ) -> R {
268 {
269 let scene = self.host.scene();
270 scene.push(SceneOp::PushClipRRect { rect, corner_radii });
271 }
272 let out = f(self);
273 {
274 let scene = self.host.scene();
275 scene.push(SceneOp::PopClip);
276 }
277 out
278 }
279
280 pub fn with_transform<R>(
281 &mut self,
282 transform: Transform2D,
283 f: impl FnOnce(&mut Self) -> R,
284 ) -> R {
285 let is_finite = transform.a.is_finite()
286 && transform.b.is_finite()
287 && transform.c.is_finite()
288 && transform.d.is_finite()
289 && transform.tx.is_finite()
290 && transform.ty.is_finite();
291
292 if !is_finite || transform == Transform2D::IDENTITY {
293 return f(self);
294 }
295
296 {
297 let scene = self.host.scene();
298 scene.push(SceneOp::PushTransform { transform });
299 }
300 let out = f(self);
301 {
302 let scene = self.host.scene();
303 scene.push(SceneOp::PopTransform);
304 }
305 out
306 }
307
308 pub fn with_opacity<R>(&mut self, opacity: f32, f: impl FnOnce(&mut Self) -> R) -> R {
309 let opacity = if opacity.is_finite() {
310 opacity.clamp(0.0, 1.0)
311 } else {
312 1.0
313 };
314
315 if opacity >= 1.0 {
316 return f(self);
317 }
318
319 {
320 let scene = self.host.scene();
321 scene.push(SceneOp::PushOpacity { opacity });
322 }
323 let out = f(self);
324 {
325 let scene = self.host.scene();
326 scene.push(SceneOp::PopOpacity);
327 }
328 out
329 }
330
331 pub fn with_effect<R>(
332 &mut self,
333 bounds: Rect,
334 mode: EffectMode,
335 chain: EffectChain,
336 quality: EffectQuality,
337 f: impl FnOnce(&mut Self) -> R,
338 ) -> R {
339 if chain.is_empty() {
340 return f(self);
341 }
342
343 {
344 let scene = self.host.scene();
345 scene.push(SceneOp::PushEffect {
346 bounds,
347 mode,
348 chain,
349 quality,
350 });
351 }
352 let out = f(self);
353 {
354 let scene = self.host.scene();
355 scene.push(SceneOp::PopEffect);
356 }
357 out
358 }
359
360 #[allow(clippy::too_many_arguments)]
366 pub fn text(
367 &mut self,
368 key: u64,
369 order: DrawOrder,
370 origin: Point,
371 text: impl Into<Arc<str>>,
372 style: TextStyle,
373 color: Color,
374 constraints: CanvasTextConstraints,
375 raster_scale_factor: f32,
376 ) -> TextMetrics {
377 let text = text.into();
378 let font_stack_key = self.host.text_font_stack_key();
379 let (services, scene) = self.host.services_and_scene();
380 self.cache.text(
381 services,
382 key,
383 order,
384 origin,
385 HostedTextContent::Plain(text),
386 style,
387 color,
388 constraints,
389 raster_scale_factor,
390 font_stack_key,
391 scene,
392 )
393 }
394
395 #[allow(clippy::too_many_arguments)]
401 pub fn shared_text(
402 &mut self,
403 order: DrawOrder,
404 origin: Point,
405 text: impl Into<Arc<str>>,
406 style: TextStyle,
407 color: Color,
408 constraints: CanvasTextConstraints,
409 raster_scale_factor: f32,
410 ) -> TextMetrics {
411 let text = text.into();
412 let font_stack_key = self.host.text_font_stack_key();
413 let (services, scene) = self.host.services_and_scene();
414 self.cache
415 .shared_text_draw(
416 services,
417 order,
418 origin,
419 text,
420 style,
421 color,
422 constraints,
423 raster_scale_factor,
424 font_stack_key,
425 scene,
426 )
427 .metrics
428 }
429
430 #[allow(clippy::too_many_arguments)]
435 pub fn text_with_blob(
436 &mut self,
437 key: u64,
438 order: DrawOrder,
439 origin: Point,
440 text: impl Into<Arc<str>>,
441 style: TextStyle,
442 color: Color,
443 constraints: CanvasTextConstraints,
444 raster_scale_factor: f32,
445 ) -> (fret_core::TextBlobId, TextMetrics) {
446 let text = text.into();
447 let font_stack_key = self.host.text_font_stack_key();
448 let (services, scene) = self.host.services_and_scene();
449 let draw = self.cache.text_draw(
450 services,
451 key,
452 order,
453 origin,
454 HostedTextContent::Plain(text),
455 style,
456 color,
457 constraints,
458 raster_scale_factor,
459 font_stack_key,
460 scene,
461 );
462 (draw.blob, draw.metrics)
463 }
464
465 #[allow(clippy::too_many_arguments)]
467 pub fn shared_text_with_blob(
468 &mut self,
469 order: DrawOrder,
470 origin: Point,
471 text: impl Into<Arc<str>>,
472 style: TextStyle,
473 color: Color,
474 constraints: CanvasTextConstraints,
475 raster_scale_factor: f32,
476 ) -> (fret_core::TextBlobId, TextMetrics) {
477 let text = text.into();
478 let font_stack_key = self.host.text_font_stack_key();
479 let (services, scene) = self.host.services_and_scene();
480 let draw = self.cache.shared_text_draw(
481 services,
482 order,
483 origin,
484 text,
485 style,
486 color,
487 constraints,
488 raster_scale_factor,
489 font_stack_key,
490 scene,
491 );
492 (draw.blob, draw.metrics)
493 }
494
495 #[allow(clippy::too_many_arguments)]
496 pub fn rich_text(
497 &mut self,
498 key: u64,
499 order: DrawOrder,
500 origin: Point,
501 rich: AttributedText,
502 base_style: TextStyle,
503 color: Color,
504 constraints: CanvasTextConstraints,
505 raster_scale_factor: f32,
506 ) -> TextMetrics {
507 let font_stack_key = self.host.text_font_stack_key();
508 let (services, scene) = self.host.services_and_scene();
509 self.cache.text(
510 services,
511 key,
512 order,
513 origin,
514 HostedTextContent::Rich(rich),
515 base_style,
516 color,
517 constraints,
518 raster_scale_factor,
519 font_stack_key,
520 scene,
521 )
522 }
523
524 #[allow(clippy::too_many_arguments)]
529 pub fn rich_text_with_blob(
530 &mut self,
531 key: u64,
532 order: DrawOrder,
533 origin: Point,
534 rich: AttributedText,
535 base_style: TextStyle,
536 color: Color,
537 constraints: CanvasTextConstraints,
538 raster_scale_factor: f32,
539 ) -> (fret_core::TextBlobId, TextMetrics) {
540 let font_stack_key = self.host.text_font_stack_key();
541 let (services, scene) = self.host.services_and_scene();
542 let draw = self.cache.text_draw(
543 services,
544 key,
545 order,
546 origin,
547 HostedTextContent::Rich(rich),
548 base_style,
549 color,
550 constraints,
551 raster_scale_factor,
552 font_stack_key,
553 scene,
554 );
555 (draw.blob, draw.metrics)
556 }
557
558 #[allow(clippy::too_many_arguments)]
564 pub fn path(
565 &mut self,
566 key: u64,
567 order: DrawOrder,
568 origin: Point,
569 commands: &[PathCommand],
570 style: PathStyle,
571 color: Color,
572 raster_scale_factor: f32,
573 ) -> PathMetrics {
574 let (services, scene) = self.host.services_and_scene();
575 self.cache.path(
576 services,
577 key,
578 order,
579 origin,
580 commands,
581 style,
582 color.into(),
583 raster_scale_factor,
584 scene,
585 )
586 }
587
588 #[allow(clippy::too_many_arguments)]
593 pub fn path_paint(
594 &mut self,
595 key: u64,
596 order: DrawOrder,
597 origin: Point,
598 commands: &[PathCommand],
599 style: PathStyle,
600 paint: PaintBindingV1,
601 raster_scale_factor: f32,
602 ) -> PathMetrics {
603 let (services, scene) = self.host.services_and_scene();
604 self.cache.path(
605 services,
606 key,
607 order,
608 origin,
609 commands,
610 style,
611 paint,
612 raster_scale_factor,
613 scene,
614 )
615 }
616
617 #[allow(clippy::too_many_arguments)]
618 pub fn svg_mask_icon(
619 &mut self,
620 key: u64,
621 order: DrawOrder,
622 rect: Rect,
623 svg: &SvgSource,
624 fit: SvgFit,
625 color: Color,
626 opacity: f32,
627 ) {
628 let opacity = opacity.clamp(0.0, 1.0);
629 if opacity <= 0.0 || color.a <= 0.0 {
630 return;
631 }
632
633 let (services, scene) = self.host.services_and_scene();
634 let svg_id = self.cache.svg(services, key, svg);
635 scene.push(SceneOp::SvgMaskIcon {
636 order,
637 rect,
638 svg: svg_id,
639 fit,
640 color,
641 opacity,
642 });
643 }
644
645 pub fn svg_image(
646 &mut self,
647 key: u64,
648 order: DrawOrder,
649 rect: Rect,
650 svg: &SvgSource,
651 fit: SvgFit,
652 opacity: f32,
653 ) {
654 let opacity = opacity.clamp(0.0, 1.0);
655 if opacity <= 0.0 {
656 return;
657 }
658
659 let (services, scene) = self.host.services_and_scene();
660 let svg_id = self.cache.svg(services, key, svg);
661 scene.push(SceneOp::SvgImage {
662 order,
663 rect,
664 svg: svg_id,
665 fit,
666 opacity,
667 });
668 }
669}
670
671#[derive(Debug, Clone, Copy, PartialEq)]
672pub struct CanvasTextConstraints {
673 pub max_width: Option<Px>,
674 pub wrap: TextWrap,
675 pub overflow: TextOverflow,
676}
677
678impl Default for CanvasTextConstraints {
679 fn default() -> Self {
680 Self {
681 max_width: None,
682 wrap: TextWrap::Word,
683 overflow: TextOverflow::Clip,
684 }
685 }
686}
687
688#[derive(Default)]
689pub(crate) struct CanvasCache {
690 frame: u64,
691 policy: CanvasCachePolicy,
692 text_by_key: HashMap<CanvasTextCacheKey, HostedTextEntry>,
693 shared_text_by_fingerprint: HashMap<SharedTextFingerprintKey, SharedTextEntry>,
694 path_by_key: HashMap<CanvasPathCacheKey, HostedPathEntry>,
695 svg_by_key: HashMap<CanvasSvgCacheKey, HostedSvgEntry>,
696}
697
698impl CanvasCache {
699 pub(crate) fn begin_paint(&mut self, frame: u64, policy: CanvasCachePolicy) {
700 self.frame = frame;
701 self.policy = policy;
702 }
703
704 pub(crate) fn end_paint(&mut self, services: &mut dyn fret_core::UiServices) {
705 self.evict_hosted_text(services);
706 self.evict_hosted_paths(services);
707 self.evict_hosted_svgs(services);
708
709 self.evict_shared_text(services);
710 }
711
712 pub(crate) fn cleanup_resources(&mut self, services: &mut dyn fret_core::UiServices) {
713 for (_, mut entry) in self.text_by_key.drain() {
714 if let Some(blob) = entry.blob.take() {
715 services.text().release(blob);
716 }
717 }
718 for (_, entry) in self.shared_text_by_fingerprint.drain() {
719 services.text().release(entry.blob);
720 }
721 for (_, mut entry) in self.path_by_key.drain() {
722 if let Some(path) = entry.path.take() {
723 services.path().release(path);
724 }
725 }
726 for (_, mut entry) in self.svg_by_key.drain() {
727 if let Some(svg) = entry.svg.take() {
728 let _ = services.svg().unregister_svg(svg);
729 }
730 }
731 self.frame = 0;
732 }
733
734 fn evict_shared_text(&mut self, services: &mut dyn fret_core::UiServices) {
735 let now = self.frame;
736 let keep_frames = self.policy.shared_text.keep_frames;
737 let max_entries = self.policy.shared_text.max_entries;
738
739 if self.shared_text_by_fingerprint.is_empty() {
740 return;
741 }
742
743 if max_entries == 0 {
744 for (_, entry) in self.shared_text_by_fingerprint.drain() {
745 services.text().release(entry.blob);
746 }
747 return;
748 }
749
750 let mut to_remove: Vec<SharedTextFingerprintKey> = Vec::new();
751 for (key, entry) in &self.shared_text_by_fingerprint {
752 if entry.last_used_frame == now {
753 continue;
754 }
755 if now.saturating_sub(entry.last_used_frame) > keep_frames {
756 to_remove.push(key.clone());
757 }
758 }
759
760 for key in to_remove {
761 if let Some(entry) = self.shared_text_by_fingerprint.remove(&key) {
762 services.text().release(entry.blob);
763 }
764 }
765
766 if self.shared_text_by_fingerprint.len() <= max_entries {
767 return;
768 }
769
770 let mut candidates: Vec<(u64, SharedTextFingerprintKey)> = self
771 .shared_text_by_fingerprint
772 .iter()
773 .filter_map(|(key, entry)| {
774 if entry.last_used_frame == now {
775 None
776 } else {
777 Some((entry.last_used_frame, key.clone()))
778 }
779 })
780 .collect();
781 candidates.sort_by_key(|(last_used, _)| *last_used);
782
783 let mut idx = 0usize;
784 while self.shared_text_by_fingerprint.len() > max_entries && idx < candidates.len() {
785 let key = candidates[idx].1.clone();
786 if let Some(entry) = self.shared_text_by_fingerprint.remove(&key) {
787 services.text().release(entry.blob);
788 }
789 idx += 1;
790 }
791 }
792
793 fn evict_hosted_text(&mut self, services: &mut dyn fret_core::UiServices) {
794 let now = self.frame;
795 let keep_frames = self.policy.text.keep_frames;
796 let max_entries = self.policy.text.max_entries;
797
798 self.text_by_key.retain(|_, entry| {
799 let keep = now.saturating_sub(entry.last_used_frame) <= keep_frames;
800 if !keep && let Some(blob) = entry.blob.take() {
801 services.text().release(blob);
802 }
803 keep
804 });
805
806 if max_entries == 0 {
807 for (_, mut entry) in self.text_by_key.drain() {
808 if let Some(blob) = entry.blob.take() {
809 services.text().release(blob);
810 }
811 }
812 return;
813 }
814
815 let over = self.text_by_key.len().saturating_sub(max_entries);
816 if over == 0 {
817 return;
818 }
819
820 let mut candidates: Vec<(u64, CanvasTextCacheKey)> = self
821 .text_by_key
822 .iter()
823 .map(|(k, v)| (v.last_used_frame, *k))
824 .collect();
825 candidates.sort_by_key(|(last, _)| *last);
826
827 for (_, key) in candidates.into_iter().take(over) {
828 if let Some(mut entry) = self.text_by_key.remove(&key)
829 && let Some(blob) = entry.blob.take()
830 {
831 services.text().release(blob);
832 }
833 }
834 }
835
836 fn evict_hosted_paths(&mut self, services: &mut dyn fret_core::UiServices) {
837 let now = self.frame;
838 let keep_frames = self.policy.path.keep_frames;
839 let max_entries = self.policy.path.max_entries;
840
841 self.path_by_key.retain(|_, entry| {
842 let keep = now.saturating_sub(entry.last_used_frame) <= keep_frames;
843 if !keep && let Some(path) = entry.path.take() {
844 services.path().release(path);
845 }
846 keep
847 });
848
849 if max_entries == 0 {
850 for (_, mut entry) in self.path_by_key.drain() {
851 if let Some(path) = entry.path.take() {
852 services.path().release(path);
853 }
854 }
855 return;
856 }
857
858 let over = self.path_by_key.len().saturating_sub(max_entries);
859 if over == 0 {
860 return;
861 }
862
863 let mut candidates: Vec<(u64, CanvasPathCacheKey)> = self
864 .path_by_key
865 .iter()
866 .map(|(k, v)| (v.last_used_frame, *k))
867 .collect();
868 candidates.sort_by_key(|(last, _)| *last);
869
870 for (_, key) in candidates.into_iter().take(over) {
871 if let Some(mut entry) = self.path_by_key.remove(&key)
872 && let Some(path) = entry.path.take()
873 {
874 services.path().release(path);
875 }
876 }
877 }
878
879 fn evict_hosted_svgs(&mut self, services: &mut dyn fret_core::UiServices) {
880 let now = self.frame;
881 let keep_frames = self.policy.svg.keep_frames;
882 let max_entries = self.policy.svg.max_entries;
883
884 self.svg_by_key.retain(|_, entry| {
885 let keep = now.saturating_sub(entry.last_used_frame) <= keep_frames;
886 if !keep && let Some(svg) = entry.svg.take() {
887 let _ = services.svg().unregister_svg(svg);
888 }
889 keep
890 });
891
892 if max_entries == 0 {
893 for (_, mut entry) in self.svg_by_key.drain() {
894 if let Some(svg) = entry.svg.take() {
895 let _ = services.svg().unregister_svg(svg);
896 }
897 }
898 return;
899 }
900
901 let over = self.svg_by_key.len().saturating_sub(max_entries);
902 if over == 0 {
903 return;
904 }
905
906 let mut candidates: Vec<(u64, CanvasSvgCacheKey)> = self
907 .svg_by_key
908 .iter()
909 .map(|(k, v)| (v.last_used_frame, *k))
910 .collect();
911 candidates.sort_by_key(|(last, _)| *last);
912
913 for (_, key) in candidates.into_iter().take(over) {
914 if let Some(mut entry) = self.svg_by_key.remove(&key)
915 && let Some(svg) = entry.svg.take()
916 {
917 let _ = services.svg().unregister_svg(svg);
918 }
919 }
920 }
921
922 #[allow(clippy::too_many_arguments)]
923 fn text(
924 &mut self,
925 services: &mut dyn fret_core::UiServices,
926 key: u64,
927 order: DrawOrder,
928 origin: Point,
929 content: HostedTextContent,
930 style: TextStyle,
931 color: Color,
932 constraints: CanvasTextConstraints,
933 raster_scale_factor: f32,
934 font_stack_key: u64,
935 scene: &mut Scene,
936 ) -> TextMetrics {
937 self.text_draw(
938 services,
939 key,
940 order,
941 origin,
942 content,
943 style,
944 color,
945 constraints,
946 raster_scale_factor,
947 font_stack_key,
948 scene,
949 )
950 .metrics
951 }
952
953 #[allow(clippy::too_many_arguments)]
954 fn shared_text_draw(
955 &mut self,
956 services: &mut dyn fret_core::UiServices,
957 order: DrawOrder,
958 origin: Point,
959 text: Arc<str>,
960 style: TextStyle,
961 color: Color,
962 constraints: CanvasTextConstraints,
963 raster_scale_factor: f32,
964 font_stack_key: u64,
965 scene: &mut Scene,
966 ) -> TextDraw {
967 let raster_scale_factor = normalize_scale_factor(raster_scale_factor);
968 let scale_bits = raster_scale_factor.to_bits();
969
970 if self.policy.shared_text.max_entries == 0 {
971 let text_constraints = TextConstraints {
972 max_width: constraints.max_width,
973 wrap: constraints.wrap,
974 overflow: constraints.overflow,
975 align: fret_core::TextAlign::Start,
976 scale_factor: raster_scale_factor,
977 };
978
979 let (blob, metrics) =
980 services
981 .text()
982 .prepare_str(text.as_ref(), &style, text_constraints);
983 scene.push(SceneOp::Text {
984 order,
985 origin,
986 text: blob,
987 paint: Paint::Solid(color).into(),
988 outline: None,
989 shadow: None,
990 });
991 return TextDraw { blob, metrics };
992 }
993
994 let shared_key = SharedTextFingerprintKey {
995 content: SharedTextContentKey::Plain(Arc::clone(&text)),
996 style: TextStyleCacheKey::from_style(&style),
997 constraints: CanvasTextConstraintsKey::from_constraints(constraints),
998 font_stack_key,
999 scale_bits,
1000 };
1001
1002 if let Some(entry) = self.shared_text_by_fingerprint.get_mut(&shared_key) {
1003 entry.last_used_frame = self.frame;
1004 scene.push(SceneOp::Text {
1005 order,
1006 origin,
1007 text: entry.blob,
1008 paint: Paint::Solid(color).into(),
1009 outline: None,
1010 shadow: None,
1011 });
1012 return TextDraw {
1013 blob: entry.blob,
1014 metrics: entry.metrics,
1015 };
1016 }
1017
1018 let text_constraints = TextConstraints {
1019 max_width: constraints.max_width,
1020 wrap: constraints.wrap,
1021 overflow: constraints.overflow,
1022 align: fret_core::TextAlign::Start,
1023 scale_factor: raster_scale_factor,
1024 };
1025
1026 let (blob, metrics) = services
1027 .text()
1028 .prepare_str(text.as_ref(), &style, text_constraints);
1029 self.shared_text_by_fingerprint.insert(
1030 shared_key,
1031 SharedTextEntry {
1032 blob,
1033 metrics,
1034 last_used_frame: self.frame,
1035 },
1036 );
1037
1038 scene.push(SceneOp::Text {
1039 order,
1040 origin,
1041 text: blob,
1042 paint: Paint::Solid(color).into(),
1043 outline: None,
1044 shadow: None,
1045 });
1046 TextDraw { blob, metrics }
1047 }
1048
1049 #[allow(clippy::too_many_arguments)]
1050 fn text_draw(
1051 &mut self,
1052 services: &mut dyn fret_core::UiServices,
1053 key: u64,
1054 order: DrawOrder,
1055 origin: Point,
1056 content: HostedTextContent,
1057 style: TextStyle,
1058 color: Color,
1059 constraints: CanvasTextConstraints,
1060 raster_scale_factor: f32,
1061 font_stack_key: u64,
1062 scene: &mut Scene,
1063 ) -> TextDraw {
1064 let raster_scale_factor = normalize_scale_factor(raster_scale_factor);
1065 let scale_bits = raster_scale_factor.to_bits();
1066
1067 let fingerprint_constraints = match constraints.wrap {
1068 TextWrap::None if constraints.overflow != TextOverflow::Ellipsis => {
1069 CanvasTextConstraints {
1070 max_width: None,
1071 ..constraints
1072 }
1073 }
1074 _ => constraints,
1075 };
1076
1077 let cache_key = CanvasTextCacheKey { key, scale_bits };
1078 let entry = self.text_by_key.entry(cache_key).or_default();
1079 entry.last_used_frame = self.frame;
1080
1081 let fingerprint = HostedTextFingerprint {
1082 content: content.clone(),
1083 style: style.clone(),
1084 constraints: fingerprint_constraints,
1085 font_stack_key,
1086 scale_bits,
1087 };
1088
1089 let needs_prepare =
1090 entry.blob.is_none() || entry.fingerprint.as_ref() != Some(&fingerprint);
1091 if needs_prepare {
1092 if let Some(blob) = entry.blob.take() {
1093 services.text().release(blob);
1094 }
1095
1096 let text_constraints = TextConstraints {
1097 max_width: fingerprint_constraints.max_width,
1098 wrap: fingerprint_constraints.wrap,
1099 overflow: fingerprint_constraints.overflow,
1100 align: fret_core::TextAlign::Start,
1101 scale_factor: raster_scale_factor,
1102 };
1103
1104 let (blob, metrics) = match &content {
1105 HostedTextContent::Plain(text) => {
1106 services
1107 .text()
1108 .prepare_str(text.as_ref(), &style, text_constraints)
1109 }
1110 HostedTextContent::Rich(rich) => {
1111 services.text().prepare_rich(rich, &style, text_constraints)
1112 }
1113 };
1114
1115 entry.blob = Some(blob);
1116 entry.metrics = Some(metrics);
1117 entry.fingerprint = Some(fingerprint);
1118 }
1119
1120 let Some(blob) = entry.blob else {
1121 return TextDraw {
1122 blob: fret_core::TextBlobId::default(),
1123 metrics: TextMetrics {
1124 size: fret_core::Size::new(Px(0.0), Px(0.0)),
1125 baseline: Px(0.0),
1126 },
1127 };
1128 };
1129 let metrics = entry.metrics.unwrap_or(TextMetrics {
1130 size: fret_core::Size::new(Px(0.0), Px(0.0)),
1131 baseline: Px(0.0),
1132 });
1133
1134 scene.push(SceneOp::Text {
1135 order,
1136 origin,
1137 text: blob,
1138 paint: Paint::Solid(color).into(),
1139 outline: None,
1140 shadow: None,
1141 });
1142 TextDraw { blob, metrics }
1143 }
1144
1145 #[allow(clippy::too_many_arguments)]
1146 fn path(
1147 &mut self,
1148 services: &mut dyn fret_core::UiServices,
1149 key: u64,
1150 order: DrawOrder,
1151 origin: Point,
1152 commands: &[PathCommand],
1153 style: PathStyle,
1154 paint: PaintBindingV1,
1155 raster_scale_factor: f32,
1156 scene: &mut Scene,
1157 ) -> PathMetrics {
1158 let raster_scale_factor = normalize_scale_factor(raster_scale_factor);
1159 let scale_bits = raster_scale_factor.to_bits();
1160
1161 let cache_key = CanvasPathCacheKey { key, scale_bits };
1162 let entry = self.path_by_key.entry(cache_key).or_default();
1163 entry.last_used_frame = self.frame;
1164
1165 let fingerprint = HostedPathFingerprint {
1166 commands_hash: hash_path_commands(commands),
1167 commands_len: commands.len(),
1168 style,
1169 scale_bits,
1170 };
1171
1172 let needs_prepare =
1173 entry.path.is_none() || entry.fingerprint.as_ref() != Some(&fingerprint);
1174 if needs_prepare {
1175 if let Some(path) = entry.path.take() {
1176 services.path().release(path);
1177 }
1178 let constraints = PathConstraints {
1179 scale_factor: raster_scale_factor,
1180 };
1181 let (path, metrics) = services.path().prepare(commands, style, constraints);
1182 entry.path = Some(path);
1183 entry.metrics = Some(metrics);
1184 entry.fingerprint = Some(fingerprint);
1185 }
1186
1187 let Some(path) = entry.path else {
1188 return PathMetrics::default();
1189 };
1190 let metrics = entry.metrics.unwrap_or_default();
1191
1192 scene.push(SceneOp::Path {
1193 order,
1194 origin,
1195 path,
1196 paint,
1197 });
1198 metrics
1199 }
1200
1201 fn svg(
1202 &mut self,
1203 services: &mut dyn fret_core::UiServices,
1204 key: u64,
1205 svg: &SvgSource,
1206 ) -> fret_core::SvgId {
1207 match svg {
1208 SvgSource::Id(id) => *id,
1209 SvgSource::Static(bytes) => self.svg_bytes(services, key, SvgBytesKey::Static(bytes)),
1210 SvgSource::Bytes(bytes) => {
1211 self.svg_bytes(services, key, SvgBytesKey::Bytes(bytes.clone()))
1212 }
1213 }
1214 }
1215
1216 fn svg_bytes(
1217 &mut self,
1218 services: &mut dyn fret_core::UiServices,
1219 key: u64,
1220 bytes: SvgBytesKey,
1221 ) -> fret_core::SvgId {
1222 let cache_key = CanvasSvgCacheKey { key };
1223 let entry = self.svg_by_key.entry(cache_key).or_default();
1224 entry.last_used_frame = self.frame;
1225 let fingerprint = SvgFingerprint {
1226 bytes: bytes.fingerprint(),
1227 };
1228
1229 let needs_prepare = entry.svg.is_none() || entry.fingerprint.as_ref() != Some(&fingerprint);
1230 if needs_prepare {
1231 let svg_id = match &bytes {
1232 SvgBytesKey::Static(bytes) => services.svg().register_svg(bytes),
1233 SvgBytesKey::Bytes(bytes) => services.svg().register_svg(bytes),
1234 };
1235 if let Some(old) = entry.svg.replace(svg_id) {
1236 let _ = services.svg().unregister_svg(old);
1237 }
1238 entry.fingerprint = Some(fingerprint);
1239 }
1240
1241 entry.svg.unwrap_or_default()
1242 }
1243}
1244
1245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1246struct CanvasTextCacheKey {
1247 key: u64,
1248 scale_bits: u32,
1249}
1250
1251#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1252struct TextStyleCacheKey {
1253 font: FontId,
1254 size_bits: u32,
1255 weight: FontWeight,
1256 slant: TextSlant,
1257 line_height_bits: Option<u32>,
1258 letter_spacing_em_bits: Option<u32>,
1259}
1260
1261impl TextStyleCacheKey {
1262 fn from_style(style: &TextStyle) -> Self {
1263 Self {
1264 font: style.font.clone(),
1265 size_bits: style.size.0.to_bits(),
1266 weight: style.weight,
1267 slant: style.slant,
1268 line_height_bits: style.line_height.map(|h| h.0.to_bits()),
1269 letter_spacing_em_bits: style.letter_spacing_em.map(f32::to_bits),
1270 }
1271 }
1272}
1273
1274#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1275struct CanvasTextConstraintsKey {
1276 max_width_bits: Option<u32>,
1277 wrap: TextWrap,
1278 overflow: TextOverflow,
1279}
1280
1281impl CanvasTextConstraintsKey {
1282 fn from_constraints(constraints: CanvasTextConstraints) -> Self {
1283 let max_width_bits = match constraints.wrap {
1284 TextWrap::None if constraints.overflow != TextOverflow::Ellipsis => None,
1287 _ => constraints.max_width.map(|w| w.0.to_bits()),
1288 };
1289 Self {
1290 max_width_bits,
1291 wrap: constraints.wrap,
1292 overflow: constraints.overflow,
1293 }
1294 }
1295}
1296
1297#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1298enum SharedTextContentKey {
1299 Plain(Arc<str>),
1300}
1301
1302#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1303struct SharedTextFingerprintKey {
1304 content: SharedTextContentKey,
1305 style: TextStyleCacheKey,
1306 constraints: CanvasTextConstraintsKey,
1307 font_stack_key: u64,
1308 scale_bits: u32,
1309}
1310
1311#[derive(Debug, Clone, Copy)]
1312struct SharedTextEntry {
1313 blob: fret_core::TextBlobId,
1314 metrics: TextMetrics,
1315 last_used_frame: u64,
1316}
1317
1318#[derive(Debug, Clone, Copy)]
1319struct TextDraw {
1320 blob: fret_core::TextBlobId,
1321 metrics: TextMetrics,
1322}
1323
1324#[derive(Debug, Clone)]
1325enum HostedTextContent {
1326 Plain(Arc<str>),
1327 Rich(AttributedText),
1328}
1329
1330impl PartialEq for HostedTextContent {
1331 fn eq(&self, other: &Self) -> bool {
1332 match (self, other) {
1333 (Self::Plain(a), Self::Plain(b)) => Arc::ptr_eq(a, b) || a.as_ref() == b.as_ref(),
1334 (Self::Rich(a), Self::Rich(b)) => {
1335 (Arc::ptr_eq(&a.text, &b.text) && Arc::ptr_eq(&a.spans, &b.spans)) || a == b
1336 }
1337 _ => false,
1338 }
1339 }
1340}
1341
1342#[derive(Debug, Clone, PartialEq)]
1343struct HostedTextFingerprint {
1344 content: HostedTextContent,
1345 style: TextStyle,
1346 constraints: CanvasTextConstraints,
1347 font_stack_key: u64,
1348 scale_bits: u32,
1349}
1350
1351#[derive(Default)]
1352struct HostedTextEntry {
1353 blob: Option<fret_core::TextBlobId>,
1354 metrics: Option<TextMetrics>,
1355 fingerprint: Option<HostedTextFingerprint>,
1356 last_used_frame: u64,
1357}
1358
1359#[derive(Debug, Clone, Copy, PartialEq)]
1360struct HostedPathFingerprint {
1361 commands_hash: u64,
1362 commands_len: usize,
1363 style: PathStyle,
1364 scale_bits: u32,
1365}
1366
1367#[derive(Default)]
1368struct HostedPathEntry {
1369 path: Option<fret_core::PathId>,
1370 metrics: Option<PathMetrics>,
1371 fingerprint: Option<HostedPathFingerprint>,
1372 last_used_frame: u64,
1373}
1374
1375#[derive(Default)]
1376struct HostedSvgEntry {
1377 svg: Option<fret_core::SvgId>,
1378 fingerprint: Option<SvgFingerprint>,
1379 last_used_frame: u64,
1380}
1381
1382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1383struct SvgFingerprint {
1384 bytes: SvgBytesFingerprint,
1385}
1386
1387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1388struct CanvasSvgCacheKey {
1389 key: u64,
1390}
1391
1392#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1393struct CanvasPathCacheKey {
1394 key: u64,
1395 scale_bits: u32,
1396}
1397
1398fn normalize_scale_factor(scale_factor: f32) -> f32 {
1399 if !scale_factor.is_finite() || scale_factor <= 0.0 {
1400 1.0
1401 } else {
1402 scale_factor
1403 }
1404}
1405
1406fn hash_path_commands(commands: &[PathCommand]) -> u64 {
1407 let mut state = 0u64;
1408 for cmd in commands {
1409 match *cmd {
1410 PathCommand::MoveTo(p) => {
1411 state = mix_u64(state, 1);
1412 state = mix_point(state, p);
1413 }
1414 PathCommand::LineTo(p) => {
1415 state = mix_u64(state, 2);
1416 state = mix_point(state, p);
1417 }
1418 PathCommand::QuadTo { ctrl, to } => {
1419 state = mix_u64(state, 3);
1420 state = mix_point(state, ctrl);
1421 state = mix_point(state, to);
1422 }
1423 PathCommand::CubicTo { ctrl1, ctrl2, to } => {
1424 state = mix_u64(state, 4);
1425 state = mix_point(state, ctrl1);
1426 state = mix_point(state, ctrl2);
1427 state = mix_point(state, to);
1428 }
1429 PathCommand::Close => {
1430 state = mix_u64(state, 5);
1431 }
1432 }
1433 }
1434 state
1435}
1436
1437fn mix_u64(mut state: u64, value: u64) -> u64 {
1438 state ^= value.wrapping_add(0x9E37_79B9_7F4A_7C15);
1440 state = state.rotate_left(7);
1441 state = state.wrapping_mul(0xD6E8_FEB8_6659_FD93);
1442 state
1443}
1444
1445fn mix_f32(state: u64, value: f32) -> u64 {
1446 mix_u64(state, u64::from(value.to_bits()))
1447}
1448
1449fn mix_px(state: u64, value: fret_core::Px) -> u64 {
1450 mix_f32(state, value.0)
1451}
1452
1453fn mix_point(mut state: u64, p: fret_core::Point) -> u64 {
1454 state = mix_px(state, p.x);
1455 state = mix_px(state, p.y);
1456 state
1457}
1458
1459#[derive(Clone)]
1460enum SvgBytesKey {
1461 Static(&'static [u8]),
1462 Bytes(Arc<[u8]>),
1463}
1464
1465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1466enum SvgBytesFingerprint {
1467 Static { ptr: usize, len: usize },
1468 Bytes { ptr: usize, len: usize },
1469}
1470
1471impl SvgBytesKey {
1472 fn fingerprint(&self) -> SvgBytesFingerprint {
1473 match self {
1474 SvgBytesKey::Static(bytes) => SvgBytesFingerprint::Static {
1475 ptr: bytes.as_ptr() as usize,
1476 len: bytes.len(),
1477 },
1478 SvgBytesKey::Bytes(bytes) => SvgBytesFingerprint::Bytes {
1479 ptr: bytes.as_ptr() as usize,
1480 len: bytes.len(),
1481 },
1482 }
1483 }
1484}
1485
1486#[derive(Default)]
1487struct Fnv1a64(u64);
1488
1489impl Hasher for Fnv1a64 {
1490 fn finish(&self) -> u64 {
1491 self.0
1492 }
1493
1494 fn write(&mut self, bytes: &[u8]) {
1495 let mut hash = if self.0 == 0 {
1496 0xcbf29ce484222325
1497 } else {
1498 self.0
1499 };
1500 for b in bytes {
1501 hash ^= *b as u64;
1502 hash = hash.wrapping_mul(0x100000001b3);
1503 }
1504 self.0 = hash;
1505 }
1506}