1use std::collections::HashMap;
6use std::sync::Mutex;
7
8use yog_gfx::draw2d::{self, Draw2D};
9use yog_gfx::image::{self as img, ImageSource, Stretch, fit};
10use yog_gfx::{GfxContext, ImageSizeFn, Rect, Vec2};
11
12use crate::widget::Image;
13
14struct Host {
15 size: Option<ImageSizeFn>,
16 cache: Option<HashMap<String, Vec2>>,
17}
18
19static HOST: Mutex<Host> = Mutex::new(Host {
20 size: None,
21 cache: None,
22});
23
24pub fn use_host_images(ctx: &GfxContext) {
28 if let Ok(mut h) = HOST.lock() {
29 h.size = Some(ctx.image_size_fn());
30 }
31}
32
33const FALLBACK_SIZE: Vec2 = Vec2::new(16.0, 16.0);
35const SIZE_CACHE_CAP: usize = 4096;
37
38fn offline_size(source: &str) -> Vec2 {
41 match ImageSource::parse(source) {
42 Some(ImageSource::Sprite(name)) => {
43 img::sprite(&name).map_or(FALLBACK_SIZE, |s| s.size(true))
44 }
45 _ => FALLBACK_SIZE,
46 }
47}
48
49pub fn source_size(source: &str) -> Vec2 {
51 let Ok(mut h) = HOST.lock() else {
52 return offline_size(source);
53 };
54 let Some(size_fn) = h.size else {
55 return offline_size(source);
56 };
57 let cache = h.cache.get_or_insert_with(HashMap::new);
58 if let Some(size) = cache.get(source) {
59 return *size;
60 }
61 match draw2d::image_size_with(size_fn, source) {
62 Some(size) => {
63 if cache.len() > SIZE_CACHE_CAP {
64 cache.clear();
65 }
66 cache.insert(source.to_owned(), size);
67 size
68 }
69 None => offline_size(source),
71 }
72}
73
74pub fn natural_size(image: &Image) -> Vec2 {
76 match image.region {
77 Some(r) if r.w > 0.0 && r.h > 0.0 => Vec2::new(r.w, r.h),
78 _ => source_size(&image.source),
79 }
80}
81
82fn whole_only(source: &str) -> bool {
84 matches!(
85 ImageSource::parse(source),
86 Some(ImageSource::Item(_) | ImageSource::Block(_) | ImageSource::Sprite(_))
87 )
88}
89
90pub(crate) fn render(d2d: &Draw2D, image: &Image, bx: Rect, clip: Option<Rect>) {
92 let natural = natural_size(image);
93 let whole = whole_only(&image.source);
94 let stretch = match image.stretch {
95 Stretch::UniformToFill if whole => Stretch::Uniform,
97 s => s,
98 };
99 let (dest, shown) = fit(stretch, natural, bx);
100 if dest.w <= 0.0 || dest.h <= 0.0 {
101 return;
102 }
103 if whole {
104 let inside = clip.is_none_or(|c| {
106 let c = c.outset(crate::render::CLIP_SLACK);
107 dest.x >= c.x && dest.y >= c.y && dest.right() <= c.right() && dest.bottom() <= c.bottom()
108 });
109 if inside {
110 d2d.image_region(&image.source, dest, None, image.tint);
111 }
112 return;
113 }
114 let region = match image.region {
116 Some(r) if r.w > 0.0 && r.h > 0.0 => r,
117 _ => Rect::new(0.0, 0.0, natural.x, natural.y),
118 };
119 let mut src = Rect::new(region.x + shown.x * region.w, region.y + shown.y * region.h, shown.w * region.w, shown.h * region.h);
120 let mut dest = dest;
121 if let Some(c) = clip {
122 let Some(cut) = dest.intersect(&c) else {
123 return;
124 };
125 let (sx, sy) = (src.w / dest.w, src.h / dest.h);
127 src = Rect::new(src.x + (cut.x - dest.x) * sx, src.y + (cut.y - dest.y) * sy, cut.w * sx, cut.h * sy);
128 dest = cut;
129 }
130 d2d.image_region(&image.source, dest, Some(src), image.tint);
131}