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};
11
12use crate::layout::Rect;
13use crate::widget::Image;
14
15struct Host {
16 size: Option<ImageSizeFn>,
17 cache: Option<HashMap<String, (f32, f32)>>,
18}
19
20static HOST: Mutex<Host> = Mutex::new(Host {
21 size: None,
22 cache: None,
23});
24
25pub fn use_host_images(ctx: &GfxContext) {
29 if let Ok(mut h) = HOST.lock() {
30 h.size = Some(ctx.image_size_fn());
31 }
32}
33
34const FALLBACK_SIZE: (f32, f32) = (16.0, 16.0);
36const SIZE_CACHE_CAP: usize = 4096;
38
39fn offline_size(source: &str) -> (f32, f32) {
42 match ImageSource::parse(source) {
43 Some(ImageSource::Sprite(name)) => {
44 img::sprite(&name).map_or(FALLBACK_SIZE, |s| s.size(true))
45 }
46 _ => FALLBACK_SIZE,
47 }
48}
49
50pub fn source_size(source: &str) -> (f32, f32) {
52 let Ok(mut h) = HOST.lock() else {
53 return offline_size(source);
54 };
55 let Some(size_fn) = h.size else {
56 return offline_size(source);
57 };
58 let cache = h.cache.get_or_insert_with(HashMap::new);
59 if let Some(size) = cache.get(source) {
60 return *size;
61 }
62 match draw2d::image_size_with(size_fn, source) {
63 Some(size) => {
64 if cache.len() > SIZE_CACHE_CAP {
65 cache.clear();
66 }
67 cache.insert(source.to_owned(), size);
68 size
69 }
70 None => offline_size(source),
72 }
73}
74
75pub fn natural_size(image: &Image) -> (f32, f32) {
77 match image.region {
78 Some([_, _, w, h]) if w > 0.0 && h > 0.0 => (w, h),
79 _ => source_size(&image.source),
80 }
81}
82
83fn whole_only(source: &str) -> bool {
85 matches!(
86 ImageSource::parse(source),
87 Some(ImageSource::Item(_) | ImageSource::Block(_) | ImageSource::Sprite(_))
88 )
89}
90
91pub(crate) fn render(d2d: &Draw2D, image: &Image, bx: Rect, clip: Option<Rect>) {
93 let (nw, nh) = natural_size(image);
94 let whole = whole_only(&image.source);
95 let stretch = match image.stretch {
96 Stretch::UniformToFill if whole => Stretch::Uniform,
98 s => s,
99 };
100 let ([x, y, w, h], [u0, v0, u1, v1]) = fit(stretch, nw, nh, bx.x, bx.y, bx.w, bx.h);
101 if w <= 0.0 || h <= 0.0 {
102 return;
103 }
104 let dest = Rect { x, y, w, h };
105 if whole {
106 let slack = crate::render::CLIP_SLACK;
108 let inside = clip.is_none_or(|c| {
109 x >= c.x - slack
110 && y >= c.y - slack
111 && x + w <= c.x + c.w + slack
112 && y + h <= c.y + c.h + slack
113 });
114 if inside {
115 d2d.image_region(&image.source, x, y, w, h, [0.0; 4], image.tint);
116 }
117 return;
118 }
119 let [ru, rv, rw, rh] = match image.region {
121 Some(r) if r[2] > 0.0 && r[3] > 0.0 => r,
122 _ => [0.0, 0.0, nw, nh],
123 };
124 let mut src = [ru + u0 * rw, rv + v0 * rh, (u1 - u0) * rw, (v1 - v0) * rh];
125 let mut dest = dest;
126 if let Some(c) = clip {
127 let Some(cut) = dest.intersect(&c) else {
128 return;
129 };
130 let (sx, sy) = (src[2] / dest.w, src[3] / dest.h);
132 src = [
133 src[0] + (cut.x - dest.x) * sx,
134 src[1] + (cut.y - dest.y) * sy,
135 cut.w * sx,
136 cut.h * sy,
137 ];
138 dest = cut;
139 }
140 d2d.image_region(
141 &image.source,
142 dest.x,
143 dest.y,
144 dest.w,
145 dest.h,
146 src,
147 image.tint,
148 );
149}