smudge_widgets/image.rs
1//! An image that stretches over its rectangle.
2//!
3//! The pixels travel as [`Image`], which the application builds from RGBA
4//! data it decoded off the UI thread. The widget paints no image while its
5//! source is empty, so an image that is still decoding holds a `None` until
6//! the pixels arrive. The style's background and border paint as they do on
7//! a panel, the image fills the content box inside the padding, and the
8//! style's [`corner_radius`](Element::corner_radius) rounds both.
9
10use smudge_core::element::{Element, Value};
11use smudge_core::geometry::{Corners, Size};
12use smudge_core::image::Image;
13use smudge_core::paint::PaintOp;
14use smudge_core::tree::Dirty;
15use smudge_core::widget::{MeasureInput, PaintCx, Panel, Widget};
16
17/// Creates an image that stretches its source over the rectangle its style
18/// gives it.
19///
20/// Without a fixed size it measures as the image's pixel size divided by the
21/// window's scale factor, so one image pixel is one logical pixel. With one
22/// axis fixed the other keeps the image's aspect ratio, and with both fixed
23/// the image stretches. A container that stretches its children, as a
24/// column does across its width by default, fixes that axis too, so the
25/// image scales to it; give the container any `align_items` but stretch to
26/// keep the image's pixel size. The image fills the content box, inside any
27/// padding and border, and the style's `corner_radius` rounds it.
28/// An empty source paints no image, only the style's background and border.
29///
30/// The source can be bound to a signal:
31///
32/// ```
33/// use smudge_core::reactive::{RwSignal, prelude::*};
34/// use smudge_widgets::image;
35///
36/// let source = RwSignal::new(None);
37/// let picture = image(move || source.get());
38/// ```
39pub fn image(source: impl Into<Value<Option<Image>>>) -> Element<ImageView> {
40 Element::new(ImageView { source: None }).bind(SourceKey, source.into(), set_source)
41}
42
43/// The key of an image's source binding.
44struct SourceKey;
45
46/// The widget behind [`image`]: it paints a panel's background and border,
47/// then one image stretched over its content box.
48///
49/// Build one with [`image`], which binds its source. The widget measures as
50/// the source's pixel size over the scale factor, so a source that appears,
51/// disappears or changes pixel size lays the node out again, and a swap
52/// between sources of the same size only repaints it.
53pub struct ImageView {
54 /// The pixels to draw, or `None` while there is nothing to show.
55 source: Option<Image>,
56}
57
58/// Replaces the image, and relays the node out when the replacement changes
59/// what the widget measures.
60///
61/// The widget measures as the image's pixel size, so a source that appears,
62/// disappears or changes pixel size must relay the node out: a paint-only
63/// update would leave the node's rectangle stale. A swap between images of
64/// the same size changes no layout answer, so it repaints alone.
65fn set_source(view: &mut ImageView, source: Option<Image>) -> Dirty {
66 let resized = match (&view.source, &source) {
67 (None, None) => false,
68 (Some(old), Some(new)) => old.width() != new.width() || old.height() != new.height(),
69 _ => true,
70 };
71 if view.source == source {
72 return Dirty::Clean;
73 }
74 view.source = source;
75 if resized { Dirty::Layout } else { Dirty::Paint }
76}
77
78impl Widget for ImageView {
79 /// Measures as the image's pixel size divided by the scale factor, so
80 /// one image pixel is one logical pixel.
81 ///
82 /// When one axis of the content box is fixed, by the style or by a
83 /// container that stretches the image, the other follows from the
84 /// image's aspect ratio. When both are, the image stretches to them.
85 ///
86 /// The answer depends on the source and the input alone, so it is the
87 /// same on every call with the same input, which the engine's caching
88 /// relies on.
89 fn measure(&mut self, input: MeasureInput) -> Option<Size> {
90 let image = self.source.as_ref()?;
91 let (width, height) = (image.width() as f32, image.height() as f32);
92 Some(match (input.known_width, input.known_height) {
93 (Some(known_width), Some(known_height)) => Size {
94 width: known_width,
95 height: known_height,
96 },
97 (Some(known_width), None) => Size {
98 width: known_width,
99 height: known_width * height / width,
100 },
101 (None, Some(known_height)) => Size {
102 width: known_height * width / height,
103 height: known_height,
104 },
105 (None, None) => Size {
106 width: width / input.scale,
107 height: height / input.scale,
108 },
109 })
110 }
111
112 /// Paints the style's background and border the way a panel does, then
113 /// the image stretched over the content box.
114 ///
115 /// The image's corners are the style's corner radius shrunk by the
116 /// border and padding next to them, so they stay concentric with the
117 /// outer corners.
118 fn paint(&self, cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
119 Panel.paint(cx, out);
120 let Some(image) = &self.source else {
121 return;
122 };
123 let radius = cx.style.paint.corner_radius.resolve(cx.theme);
124 let left = cx.content.x0 - cx.rect.x0;
125 let top = cx.content.y0 - cx.rect.y0;
126 let right = cx.rect.x1 - cx.content.x1;
127 let bottom = cx.rect.y1 - cx.content.y1;
128 let inner = |a: f32, b: f32| (radius - a.max(b)).max(0.0);
129 out.push(PaintOp::Image {
130 rect: cx.content,
131 radius: Corners {
132 top_left: inner(left, top),
133 top_right: inner(right, top),
134 bottom_right: inner(right, bottom),
135 bottom_left: inner(left, bottom),
136 },
137 image: image.clone(),
138 });
139 }
140}