Skip to main content

rlvgl_widgets/
image.rs

1//! Simple pixel-buffer image widget.
2use rlvgl_core::draw::draw_widget_bg;
3use rlvgl_core::event::Event;
4use rlvgl_core::image::{BlitOpts, ImageDescriptor};
5use rlvgl_core::renderer::Renderer;
6use rlvgl_core::style::Style;
7use rlvgl_core::widget::{Color, Rect, Widget};
8
9/// Display a raw pixel buffer.
10pub struct Image<'a> {
11    bounds: Rect,
12    /// Styling for the image background.
13    pub style: Style,
14    /// Source image width and height in pixels.
15    width: i32,
16    height: i32,
17    pixels: &'a [Color],
18    blit_opts: BlitOpts,
19    /// When `true`, [`draw`](Image::draw) is a no-op (the image is hidden).
20    hidden: bool,
21}
22
23impl<'a> Image<'a> {
24    /// Create an image widget backed by a slice of pixels.
25    pub fn new(bounds: Rect, width: i32, height: i32, pixels: &'a [Color]) -> Self {
26        Self {
27            bounds,
28            style: Style::default(),
29            width,
30            height,
31            pixels,
32            blit_opts: BlitOpts::default(),
33            hidden: false,
34        }
35    }
36
37    /// Configure low-level blit options for this image instance.
38    pub fn with_blit_opts(mut self, blit_opts: BlitOpts) -> Self {
39        self.blit_opts = blit_opts;
40        self
41    }
42
43    /// Replace the displayed pixel buffer and its dimensions at runtime.
44    ///
45    /// The widget's pixel buffer was previously construction-only. This
46    /// setter exists so generated reactive bindings (QT-05g
47    /// `PredicateBinding`) can swap artwork as a state machine's
48    /// predicates change, without rebuilding the widget tree. The blit
49    /// options (e.g. a stretch scale) are left unchanged — callers that
50    /// swap to a differently-sized buffer and need a different scale must
51    /// also call [`set_blit_opts`](Self::set_blit_opts).
52    pub fn set_pixels(&mut self, width: i32, height: i32, pixels: &'a [Color]) {
53        self.width = width;
54        self.height = height;
55        self.pixels = pixels;
56    }
57
58    /// Replace the low-level blit options at runtime (companion to
59    /// [`set_pixels`](Self::set_pixels) when the new buffer differs in size).
60    pub fn set_blit_opts(&mut self, blit_opts: BlitOpts) {
61        self.blit_opts = blit_opts;
62    }
63
64    /// Hide or show the image at runtime. A hidden image's
65    /// [`draw`](Image::draw) is a no-op — it paints nothing (not even its
66    /// background), leaving whatever is behind it visible. Generated reactive
67    /// bindings (QT-05h `VisibilityBinding`) use this to drive a widget's
68    /// visibility from a state-machine predicate without removing it from the
69    /// widget tree.
70    pub fn set_hidden(&mut self, hidden: bool) {
71        self.hidden = hidden;
72    }
73
74    /// Whether the image is currently hidden.
75    pub fn is_hidden(&self) -> bool {
76        self.hidden
77    }
78}
79
80impl<'a> Widget for Image<'a> {
81    fn bounds(&self) -> Rect {
82        self.bounds
83    }
84
85    fn draw(&self, renderer: &mut dyn Renderer) {
86        // QT-05h: a hidden image paints nothing — not even its background — so
87        // a `visible:`-bound widget reveals whatever is behind it when off.
88        if self.hidden {
89            return;
90        }
91        draw_widget_bg(renderer, self.bounds, &self.style);
92        let Some(width) = u16::try_from(self.width).ok() else {
93            return;
94        };
95        let Some(height) = u16::try_from(self.height).ok() else {
96            return;
97        };
98        let descriptor = ImageDescriptor::from_color_slice(self.pixels, width, height);
99        renderer.blit_image(self.bounds, &descriptor, &self.blit_opts);
100    }
101
102    /// Images are purely visual and do not handle events.
103    fn handle_event(&mut self, _event: &Event) -> bool {
104        false
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn set_pixels_swaps_buffer_and_dims() {
114        let off = [Color(1, 2, 3, 255)];
115        let on = [Color(4, 5, 6, 255), Color(7, 8, 9, 255)];
116        let bounds = Rect {
117            x: 0,
118            y: 0,
119            width: 48,
120            height: 48,
121        };
122        let mut img = Image::new(bounds, 1, 1, &off);
123        assert_eq!(img.width, 1);
124        assert_eq!(img.height, 1);
125        assert_eq!(img.pixels.len(), 1);
126
127        // Swap to the "on" artwork (QT-05g PredicateBinding refresh path).
128        img.set_pixels(2, 1, &on);
129        assert_eq!(img.width, 2);
130        assert_eq!(img.height, 1);
131        assert_eq!(img.pixels.len(), 2);
132        assert_eq!(img.pixels[0].0, 4);
133
134        // Swap back.
135        img.set_pixels(1, 1, &off);
136        assert_eq!(img.pixels.len(), 1);
137        assert_eq!(img.pixels[0].0, 1);
138    }
139
140    /// Counts blit/fill calls so we can assert a hidden image draws nothing.
141    struct CountingRenderer {
142        fills: u32,
143        blits: u32,
144    }
145    impl rlvgl_core::renderer::Renderer for CountingRenderer {
146        fn fill_rect(&mut self, _rect: Rect, _color: Color) {
147            self.fills += 1;
148        }
149        fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
150        fn blit_image(
151            &mut self,
152            _dest: Rect,
153            _desc: &rlvgl_core::image::ImageDescriptor<'_>,
154            _opts: &BlitOpts,
155        ) {
156            self.blits += 1;
157        }
158    }
159
160    #[test]
161    fn set_hidden_makes_draw_a_noop() {
162        let px = [Color(10, 20, 30, 255)];
163        let bounds = Rect {
164            x: 0,
165            y: 0,
166            width: 8,
167            height: 8,
168        };
169        let mut img = Image::new(bounds, 1, 1, &px);
170        assert!(!img.is_hidden());
171
172        // Visible: draw paints (background fill and/or image blit).
173        let mut r1 = CountingRenderer { fills: 0, blits: 0 };
174        img.draw(&mut r1);
175        assert!(r1.fills + r1.blits > 0, "visible image must paint");
176
177        // Hidden: draw is a no-op.
178        img.set_hidden(true);
179        assert!(img.is_hidden());
180        let mut r2 = CountingRenderer { fills: 0, blits: 0 };
181        img.draw(&mut r2);
182        assert_eq!(r2.fills + r2.blits, 0, "hidden image must paint nothing");
183
184        // Shown again: paints once more.
185        img.set_hidden(false);
186        let mut r3 = CountingRenderer { fills: 0, blits: 0 };
187        img.draw(&mut r3);
188        assert!(r3.fills + r3.blits > 0, "shown image must paint again");
189    }
190}