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