git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/display/image_view.rs (5.1K)

  1 //! `ImageView` — a GPU-textured image fitted into the widget's rect via
  2 //! [`fit_rect`]. The view BORROWS its image id: ids come from
  3 //! [`crate::vk::upload_rgba`] and stay owned by the app, which frees them with
  4 //! [`crate::vk::free_image`] when done — the widget never uploads or frees GPU
  5 //! resources, so one id can back several views and a dropped view leaks
  6 //! nothing. `image: None` paints only the optional letterbox floor.
  7 //!
  8 //! **Borrowing it means the owner has to replace it when the renderer is
  9 //! rebuilt.** An image id names an entry in one renderer's image table, and a
 10 //! renderer does not outlive its session: `window_runner` builds a new one
 11 //! around the same `Application` when it repairs a lost Wayland transport. A
 12 //! draw for an id the new table does not hold is skipped rather than
 13 //! reported, so a view left holding a pre-reconnect id goes blank and stays
 14 //! blank, with nothing logged. Set the image again from
 15 //! [`Application::renderer_init`] on every renderer after the first — the
 16 //! app is the only party that can produce those pixels a second time.
 17 //! ([`crate::upload_icon`] is the one exception, for bundled glyphs: it
 18 //! re-resolves itself, and `Button::with_icon_name` is how a widget opts into
 19 //! that.)
 20 //!
 21 //! [`Application::renderer_init`]: crate::engine::Application::renderer_init
 22 
 23 use crate::scene::layout::{fit_rect, FitMode, Rect, Size};
 24 use crate::scene::paint::PaintCtx;
 25 use crate::widget::{Adapted, Input, Layout, Paint};
 26 
 27 #[derive(Debug, Clone)]
 28 pub struct ImageView {
 29     /// (image id, native width, native height).
 30     pub image: Option<(u32, u32, u32)>,
 31     pub fit: FitMode,
 32     pub alpha: f32,
 33     /// Floor painted across the whole rect behind the fitted image (the
 34     /// letterbox bars); `None` paints no floor.
 35     pub bg: Option<[f32; 4]>,
 36 }
 37 
 38 impl ImageView {
 39     pub fn new() -> Adapted<ImageView> {
 40         Adapted::new(ImageView {
 41             image: None,
 42             fit: FitMode::Contain { max_upscale: 4.0 },
 43             alpha: 1.0,
 44             bg: None,
 45         })
 46     }
 47 
 48     pub fn set_image(&mut self, image: Option<(u32, u32, u32)>) {
 49         self.image = image;
 50     }
 51 }
 52 
 53 /// By-value builders can't flow through `Deref`, so they live on the wrapped
 54 /// type (the `UsageBar::with_colors` idiom).
 55 impl Adapted<ImageView> {
 56     pub fn with_image(mut self, id: u32, width: u32, height: u32) -> Self {
 57         self.image = Some((id, width, height));
 58         self
 59     }
 60 
 61     pub fn with_fit(mut self, fit: FitMode) -> Self {
 62         self.fit = fit;
 63         self
 64     }
 65 
 66     pub fn with_alpha(mut self, alpha: f32) -> Self {
 67         self.alpha = alpha;
 68         self
 69     }
 70 
 71     pub fn with_bg(mut self, bg: [f32; 4]) -> Self {
 72         self.bg = Some(bg);
 73         self
 74     }
 75 }
 76 
 77 impl Layout for ImageView {
 78     /// Native pixel dimensions; the container may still assign any rect —
 79     /// `fit` decides how the image maps into it.
 80     fn intrinsic_size(&self) -> Option<Size> {
 81         self.image.map(|(_, w, h)| Size::new(w as f32, h as f32))
 82     }
 83 }
 84 
 85 impl Paint for ImageView {
 86     fn color(&self) -> [f32; 4] {
 87         [0.0, 0.0, 0.0, 0.0]
 88     }
 89 
 90     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 91         if let Some(bg) = self.bg {
 92             ctx.quad(rect, bg);
 93         }
 94         if let Some((id, w, h)) = self.image {
 95             let fitted = fit_rect(w, h, rect, self.fit);
 96             if fitted.width > 0.0 && fitted.height > 0.0 {
 97                 ctx.image(id, fitted, self.alpha);
 98             }
 99         }
100     }
101 }
102 
103 impl Input for ImageView {}
104 
105 #[cfg(test)]
106 mod tests {
107     use super::*;
108     use crate::scene::paint::Prim;
109     use crate::widget::{UiContext, WidgetHost};
110 
111     fn image_prims(view: &Adapted<ImageView>, ctx: &UiContext) -> Vec<(u32, Rect, f32)> {
112         let mut pc = PaintCtx::new();
113         crate::scene::painter::paint_root_into(ctx, view, &mut pc);
114         pc.finish()
115             .items
116             .into_iter()
117             .filter_map(|item| match item.prim {
118                 Prim::Image { image, rect, alpha } => Some((image, rect, alpha)),
119                 _ => None,
120             })
121             .collect()
122     }
123 
124     #[test]
125     fn paints_fitted_image_prim() {
126         let ctx = UiContext::new();
127         let mut view = ImageView::new().with_image(7, 50, 50);
128         WidgetHost::set_rect(&mut view, 0.0, 0.0, 400.0, 400.0);
129         let prims = image_prims(&view, &ctx);
130         assert_eq!(prims.len(), 1);
131         let (id, rect, alpha) = prims[0];
132         assert_eq!(id, 7);
133         assert_eq!(alpha, 1.0);
134         // Contain caps at 4x: 200x200 centered in 400x400.
135         assert_eq!((rect.x, rect.y, rect.width, rect.height), (100.0, 100.0, 200.0, 200.0));
136     }
137 
138     #[test]
139     fn empty_view_paints_nothing_but_bg() {
140         let ctx = UiContext::new();
141         let mut view = ImageView::new().with_bg([0.1, 0.1, 0.1, 1.0]);
142         WidgetHost::set_rect(&mut view, 0.0, 0.0, 100.0, 100.0);
143         assert!(image_prims(&view, &ctx).is_empty());
144     }
145 
146     #[test]
147     fn intrinsic_size_is_native_dims() {
148         let view = ImageView::new().with_image(1, 64, 40);
149         let s = Layout::intrinsic_size(&*view).unwrap();
150         assert_eq!((s.width, s.height), (64.0, 40.0));
151     }
152 }