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 }