1use std::cell::RefCell;
2use std::fmt;
3use std::rc::Rc;
4
5use ravenrs_core::{ImageData, ImageQuad, Rect, Role};
6use ravenrs_layout::{Align, LayoutStyle};
7
8use crate::View;
9use crate::context::{BuildCx, NodeId, Paint, Semantics};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13#[non_exhaustive]
14#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
15pub enum ContentMode {
16 #[default]
18 Cover,
19 Fit,
21 Fill,
23}
24
25#[derive(Debug)]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
28pub struct DecodeError(String);
29
30impl fmt::Display for DecodeError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "the image could not be decoded: {}", self.0)
33 }
34}
35
36impl std::error::Error for DecodeError {}
37
38#[cfg(feature = "decode")]
40pub fn decode_image(bytes: &[u8]) -> Result<ImageData, DecodeError> {
41 let decoded = image::load_from_memory(bytes).map_err(|e| DecodeError(e.to_string()))?.into_rgba8();
42 let (w, h) = decoded.dimensions();
43 ImageData::from_rgba8(w, h, decoded.into_raw()).ok_or_else(|| DecodeError("empty image".into()))
44}
45
46#[derive(Clone, Debug)]
60#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
61pub struct Image {
62 data: ImageData,
63 mode: ContentMode,
64 radius: f32,
65 width: Option<f32>,
66 height: Option<f32>,
67 stretch: bool,
68 opacity: f32,
69 alt: Option<String>,
70 crossfade: Option<f64>,
71}
72
73struct Fade {
75 current: ImageData,
76 previous: Option<ImageData>,
77 started: f64,
78}
79
80#[cfg_attr(feature = "live", ravenrs_live::expose)]
81impl Image {
82 pub fn new(data: ImageData) -> Self {
84 Self {
85 data,
86 mode: ContentMode::Cover,
87 radius: 0.0,
88 width: None,
89 height: None,
90 stretch: false,
91 opacity: 1.0,
92 alt: None,
93 crossfade: None,
94 }
95 }
96
97 #[cfg(feature = "decode")]
99 pub fn from_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
100 decode_image(bytes).map(Self::new)
101 }
102
103 pub fn content_mode(mut self, mode: ContentMode) -> Self {
105 self.mode = mode;
106 self
107 }
108
109 pub fn corner_radius(mut self, radius: f32) -> Self {
111 self.radius = radius;
112 self
113 }
114
115 pub fn width(mut self, width: f32) -> Self {
117 self.width = Some(width);
118 self
119 }
120
121 pub fn height(mut self, height: f32) -> Self {
123 self.height = Some(height);
124 self
125 }
126
127 pub fn size(self, width: f32, height: f32) -> Self {
129 self.width(width).height(height)
130 }
131
132 pub fn stretch(mut self) -> Self {
134 self.stretch = true;
135 self
136 }
137
138 pub fn alt(mut self, text: impl Into<String>) -> Self {
141 self.alt = Some(text.into());
142 self
143 }
144
145 pub fn opacity(mut self, opacity: f32) -> Self {
147 self.opacity = opacity;
148 self
149 }
150
151 pub fn crossfade(mut self, seconds: f64) -> Self {
156 self.crossfade = Some(seconds);
157 self
158 }
159
160 fn fading_out(&self, cx: &mut BuildCx<'_>, seconds: f64) -> Option<(ImageData, f64)> {
163 let (data, now) = (self.data.clone(), cx.now());
164 let fade = cx
165 .use_state(|| Rc::new(RefCell::new(Fade { current: data, previous: None, started: 0.0 })))
166 .get();
167 let mut fade = fade.borrow_mut();
168 if fade.current != self.data {
169 let old = std::mem::replace(&mut fade.current, self.data.clone());
170 fade.previous = (!cx.reduced_motion() && seconds > 0.0).then_some(old);
171 fade.started = now;
172 }
173 if now >= fade.started + seconds {
174 fade.previous = None;
175 }
176 fade.previous.clone().map(|old| (old, fade.started))
177 }
178}
179
180fn push_image(
182 scene: &mut ravenrs_core::Scene,
183 data: &ImageData,
184 frame: Rect,
185 (mode, radius, opacity): (ContentMode, f32, f32),
186) {
187 if frame.is_empty() {
188 return;
189 }
190 let (rect, uv) = placement(mode, frame, data.aspect_ratio());
191 let mut quad = ImageQuad::new(rect, data.clone()).radius(radius);
192 quad.uv = uv;
193 quad.opacity = opacity;
194 scene.push_image(quad);
195}
196
197pub(crate) fn placement(mode: ContentMode, frame: Rect, aspect: f32) -> (Rect, [f32; 4]) {
199 let frame_aspect = frame.width() / frame.height().max(f32::EPSILON);
200 match mode {
201 ContentMode::Fill => (frame, [0.0, 0.0, 1.0, 1.0]),
202 ContentMode::Cover => {
203 if frame_aspect > aspect {
204 let visible = aspect / frame_aspect;
205 let v0 = (1.0 - visible) / 2.0;
206 (frame, [0.0, v0, 1.0, v0 + visible])
207 } else {
208 let visible = frame_aspect / aspect;
209 let u0 = (1.0 - visible) / 2.0;
210 (frame, [u0, 0.0, u0 + visible, 1.0])
211 }
212 }
213 ContentMode::Fit => {
214 let (w, h) = if frame_aspect > aspect {
215 (frame.height() * aspect, frame.height())
216 } else {
217 (frame.width(), frame.width() / aspect)
218 };
219 let x = frame.x() + (frame.width() - w) / 2.0;
220 let y = frame.y() + (frame.height() - h) / 2.0;
221 (Rect::new(x, y, w, h), [0.0, 0.0, 1.0, 1.0])
222 }
223 }
224}
225
226impl View for Image {
227 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
228 let aspect = self.data.aspect_ratio();
229 let (width, height) = match (self.width, self.height, self.stretch) {
230 (None, None, false) => (Some(self.data.width() as f32), Some(self.data.height() as f32)),
231 (w, h, _) => (w, h),
232 };
233 let style = LayoutStyle {
234 width,
235 height,
236 aspect_ratio: (width.is_none() || height.is_none()).then_some(aspect),
237 align_self: self.stretch.then_some(Align::Stretch),
238 ..LayoutStyle::leaf()
239 };
240 let (data, look) = (self.data.clone(), (self.mode, self.radius, self.opacity));
241 let fading = self.crossfade.and_then(|seconds| Some((self.fading_out(cx, seconds)?, seconds)));
242 let paint = if let Some(((old, started), seconds)) = fading {
243 cx.wake_at(started + seconds);
245 Paint::Live(Rc::new(move |frame: Rect, time, scene| {
246 let t = (((time - started) / seconds) as f32).clamp(0.0, 1.0);
247 push_image(scene, &old, frame, (look.0, look.1, look.2 * (1.0 - t)));
248 push_image(scene, &data, frame, (look.0, look.1, look.2 * t));
249 }))
250 } else {
251 Paint::Custom(Rc::new(move |frame: Rect, scene| push_image(scene, &data, frame, look)))
252 };
253 let node = cx.container(style, &[], paint);
254 if let Some(alt) = &self.alt {
255 cx.semantics(node, Semantics::new(Role::Image).name(alt.clone()));
256 }
257 node
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn cover_crops_and_fit_letterboxes() {
267 let frame = Rect::new(0.0, 0.0, 200.0, 100.0);
268 let (r, uv) = placement(ContentMode::Cover, frame, 1.0);
270 assert_eq!(r, frame);
271 assert_eq!(uv, [0.0, 0.25, 1.0, 0.75]);
272 let (r, uv) = placement(ContentMode::Fit, frame, 1.0);
273 assert_eq!(r, Rect::new(50.0, 0.0, 100.0, 100.0));
274 assert_eq!(uv, [0.0, 0.0, 1.0, 1.0]);
275 }
276
277 #[cfg(feature = "decode")]
278 #[test]
279 fn decodes_png() {
280 let png: &[u8] = &[
282 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
283 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
284 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
285 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
286 0x44, 0xae, 0x42, 0x60, 0x82,
287 ];
288 let image = decode_image(png).expect("valid png");
289 assert_eq!((image.width(), image.height()), (1, 1));
290 assert_eq!(image.pixels(), &[255, 0, 0, 255]);
291 assert!(decode_image(b"not an image").is_err());
292 }
293}