Skip to main content

yog_ui/
image.rs

1//! Image widgets: natural size (asked from the host, cached) and drawing —
2//! fitting the image into the widget per its [`Stretch`] and cutting it to
3//! the clip rect.
4
5use 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
25/// Lets image widgets size themselves from the host (texture sizes, the
26/// version's sprite sizes). Called by `UiRoot::render`; call it yourself
27/// before the first layout to get exact sizes from the very first frame.
28pub 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
34/// The size of an image the host can't tell (an item's, offline).
35const FALLBACK_SIZE: (f32, f32) = (16.0, 16.0);
36/// Most image sizes cached before the cache starts over.
37const SIZE_CACHE_CAP: usize = 4096;
38
39/// The size `source` has without a host to ask: 16×16 items, the sprite
40/// table's size, 16×16 for the rest.
41fn 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
50/// Natural size of `source` in GUI pixels.
51pub 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        // Not cached: a texture may only be loadable later (resource reload).
71        None => offline_size(source),
72    }
73}
74
75/// The image's natural size: its region's, or the whole source's.
76pub 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
83/// Item and sprite images can only be drawn whole (no cutting).
84fn whole_only(source: &str) -> bool {
85    matches!(
86        ImageSource::parse(source),
87        Some(ImageSource::Item(_) | ImageSource::Block(_) | ImageSource::Sprite(_))
88    )
89}
90
91/// Draws `image` into the content box `bx`, cut to `clip`.
92pub(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        // Can't be cut: shown whole instead.
97        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        // Drawn whole or not at all — like text lines and item icons.
107        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    // The source region shown, in image pixels.
120    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        // Texture pixels per GUI pixel.
131        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}