Skip to main content

jag_ui/elements/
image_element.rs

1//! Image display element.
2
3use jag_draw::{Brush, ColorLinPremul, Rect};
4use jag_surface::{Canvas, ImageFitMode};
5
6use crate::event::{
7    EventHandler, EventResult, KeyboardEvent, MouseClickEvent, MouseMoveEvent, ScrollEvent,
8};
9use crate::focus::FocusId;
10
11use super::Element;
12
13/// How the image should fit within its bounding rectangle.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ImageFit {
16    /// Stretch to fill (may distort aspect ratio).
17    Fill,
18    /// Fit inside the rect maintaining aspect ratio (letterbox).
19    Contain,
20    /// Fill the rect maintaining aspect ratio (may crop).
21    Cover,
22}
23
24impl Default for ImageFit {
25    fn default() -> Self {
26        Self::Contain
27    }
28}
29
30impl ImageFit {
31    /// Convert to `jag_surface::ImageFitMode`.
32    fn to_fit_mode(self) -> ImageFitMode {
33        match self {
34            Self::Fill => ImageFitMode::Fill,
35            Self::Contain => ImageFitMode::Contain,
36            Self::Cover => ImageFitMode::Cover,
37        }
38    }
39}
40
41/// An element that displays an image from a file path, with a
42/// configurable fit mode and a fallback tint color.
43pub struct ImageElement {
44    pub rect: Rect,
45    /// Path to the image file.
46    pub source: Option<String>,
47    /// Fallback color shown when no source is set or loading fails.
48    pub fallback_color: ColorLinPremul,
49    /// How the image fits within the rect.
50    pub fit: ImageFit,
51}
52
53impl ImageElement {
54    /// Create an image element with the given source path.
55    pub fn new(source: impl Into<String>) -> Self {
56        Self {
57            rect: Rect {
58                x: 0.0,
59                y: 0.0,
60                w: 200.0,
61                h: 150.0,
62            },
63            source: Some(source.into()),
64            fallback_color: ColorLinPremul::from_srgba_u8([200, 200, 200, 255]),
65            fit: ImageFit::default(),
66        }
67    }
68
69    /// Create an image element with a fallback color only (no image).
70    pub fn placeholder(color: ColorLinPremul) -> Self {
71        Self {
72            rect: Rect {
73                x: 0.0,
74                y: 0.0,
75                w: 200.0,
76                h: 150.0,
77            },
78            source: None,
79            fallback_color: color,
80            fit: ImageFit::default(),
81        }
82    }
83
84    /// Set the fit mode.
85    pub fn with_fit(mut self, fit: ImageFit) -> Self {
86        self.fit = fit;
87        self
88    }
89
90    /// Hit-test.
91    pub fn hit_test(&self, x: f32, y: f32) -> bool {
92        x >= self.rect.x
93            && x <= self.rect.x + self.rect.w
94            && y >= self.rect.y
95            && y <= self.rect.y + self.rect.h
96    }
97}
98
99// ---------------------------------------------------------------------------
100// Element trait
101// ---------------------------------------------------------------------------
102
103impl Element for ImageElement {
104    fn rect(&self) -> Rect {
105        self.rect
106    }
107
108    fn set_rect(&mut self, rect: Rect) {
109        self.rect = rect;
110    }
111
112    fn render(&self, canvas: &mut Canvas, z: i32) {
113        if let Some(ref path) = self.source {
114            canvas.draw_image(
115                path.clone(),
116                [self.rect.x, self.rect.y],
117                [self.rect.w, self.rect.h],
118                self.fit.to_fit_mode(),
119                z,
120            );
121        } else {
122            canvas.fill_rect(
123                self.rect.x,
124                self.rect.y,
125                self.rect.w,
126                self.rect.h,
127                Brush::Solid(self.fallback_color),
128                z,
129            );
130        }
131    }
132
133    fn focus_id(&self) -> Option<FocusId> {
134        None
135    }
136}
137
138// ---------------------------------------------------------------------------
139// EventHandler trait
140// ---------------------------------------------------------------------------
141
142impl EventHandler for ImageElement {
143    fn handle_mouse_click(&mut self, _event: &MouseClickEvent) -> EventResult {
144        EventResult::Ignored
145    }
146
147    fn handle_keyboard(&mut self, _event: &KeyboardEvent) -> EventResult {
148        EventResult::Ignored
149    }
150
151    fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
152        EventResult::Ignored
153    }
154
155    fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
156        EventResult::Ignored
157    }
158
159    fn is_focused(&self) -> bool {
160        false
161    }
162
163    fn set_focused(&mut self, _focused: bool) {}
164
165    fn contains_point(&self, x: f32, y: f32) -> bool {
166        self.hit_test(x, y)
167    }
168}
169
170// ---------------------------------------------------------------------------
171// Tests
172// ---------------------------------------------------------------------------
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn image_element_new() {
180        let img = ImageElement::new("/path/to/image.png");
181        assert_eq!(img.source.as_deref(), Some("/path/to/image.png"));
182        assert_eq!(img.fit, ImageFit::Contain);
183    }
184
185    #[test]
186    fn image_element_placeholder() {
187        let color = ColorLinPremul::from_srgba_u8([128, 128, 128, 255]);
188        let img = ImageElement::placeholder(color);
189        assert!(img.source.is_none());
190        assert_eq!(img.fallback_color, color);
191    }
192
193    #[test]
194    fn image_element_with_fit() {
195        let img = ImageElement::new("test.jpg").with_fit(ImageFit::Cover);
196        assert_eq!(img.fit, ImageFit::Cover);
197    }
198
199    #[test]
200    fn image_element_hit_test() {
201        let mut img = ImageElement::new("test.png");
202        img.rect = Rect {
203            x: 10.0,
204            y: 10.0,
205            w: 100.0,
206            h: 80.0,
207        };
208        assert!(img.hit_test(50.0, 50.0));
209        assert!(!img.hit_test(0.0, 0.0));
210    }
211
212    #[test]
213    fn image_element_not_focusable() {
214        let img = ImageElement::new("test.png");
215        assert!(img.focus_id().is_none());
216    }
217}