1use std::collections::HashMap;
14use std::path::PathBuf;
15use rosace_core::types::{Point, Rect, Size};
16use rosace_render::{Color, FontCache, SkiaCanvas};
17use rosace_theme::ThemeData;
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum ImageFit {
22 Fill,
24 Contain,
26 Cover,
28 None,
30}
31
32#[derive(Debug, Clone, PartialEq)]
34pub enum ImageSource {
35 File(PathBuf),
37 Bytes(Vec<u8>),
39 Placeholder,
41}
42
43pub struct ImageWidget {
47 pub source: ImageSource,
48 pub fit: ImageFit,
49 pub width: f32,
50 pub height: f32,
51 pub placeholder_color: Color,
52 pub alt: Option<String>,
57}
58
59impl ImageWidget {
60 pub fn new() -> Self {
62 Self {
63 source: ImageSource::Placeholder,
64 fit: ImageFit::Contain,
65 width: 200.0,
66 height: 200.0,
67 placeholder_color: Color::rgb(60, 65, 90),
68 alt: None,
69 }
70 }
71
72 pub fn alt(mut self, alt: impl Into<String>) -> Self {
74 self.alt = Some(alt.into());
75 self
76 }
77
78 pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
80 self.source = ImageSource::File(path.into());
81 self
82 }
83
84 pub fn asset(self, name: impl rosace_core::asset::AssetRef) -> Self {
93 self.file(rosace_core::asset::resolve(name))
94 }
95
96 pub fn bytes(mut self, data: Vec<u8>) -> Self {
98 self.source = ImageSource::Bytes(data);
99 self
100 }
101
102 pub fn fit(mut self, f: ImageFit) -> Self {
104 self.fit = f;
105 self
106 }
107
108 pub fn width(mut self, w: f32) -> Self {
110 self.width = w;
111 self
112 }
113
114 pub fn height(mut self, h: f32) -> Self {
116 self.height = h;
117 self
118 }
119
120 pub fn placeholder_color(mut self, c: Color) -> Self {
122 self.placeholder_color = c;
123 self
124 }
125
126 pub fn render(
131 &self,
132 canvas: &mut SkiaCanvas,
133 font: &FontCache,
134 x: f32,
135 y: f32,
136 _theme: &ThemeData,
137 ) {
138 let pixmap_bytes = match &self.source {
139 ImageSource::Placeholder => None,
140 ImageSource::File(path) => std::fs::read(path).ok(),
141 ImageSource::Bytes(b) => Some(b.clone()),
142 };
143
144 let loaded = pixmap_bytes.and_then(|bytes| tiny_skia::Pixmap::decode_png(&bytes).ok());
145
146 match loaded {
147 Some(pixmap) => {
148 self.blit_pixmap(canvas, x, y, &pixmap);
149 }
150 None => {
151 canvas.fill_rect(
153 Rect {
154 origin: Point { x, y },
155 size: Size {
156 width: self.width,
157 height: self.height,
158 },
159 },
160 self.placeholder_color,
161 );
162 let cx = x + self.width / 2.0;
164 let cy = y + self.height / 2.0;
165 canvas.fill_circle(
166 Point { x: cx, y: cy - 15.0 },
167 12.0,
168 Color::rgb(100, 110, 140),
169 );
170 canvas.fill_rect(
171 Rect {
172 origin: Point {
173 x: cx - 20.0,
174 y: cy + 5.0,
175 },
176 size: Size {
177 width: 40.0,
178 height: 20.0,
179 },
180 },
181 Color::rgb(80, 90, 120),
182 );
183 if let ImageSource::File(path) = &self.source {
184 let name = path.file_name().unwrap_or_default().to_string_lossy();
185 canvas.draw_text(
186 &name,
187 Point {
188 x: x + 4.0,
189 y: y + self.height - 18.0,
190 },
191 Color::rgb(140, 145, 175),
192 font,
193 10.0,
194 );
195 }
196 }
197 }
198 }
199
200 fn blit_pixmap(&self, canvas: &mut SkiaCanvas, x: f32, y: f32, pixmap: &tiny_skia::Pixmap) {
202 let src_w = pixmap.width() as f32;
203 let src_h = pixmap.height() as f32;
204
205 let (draw_x, draw_y, draw_w, draw_h, src_crop_x, src_crop_y, src_crop_w, src_crop_h) =
206 self.compute_fit(src_w, src_h, x, y);
207
208 let dst_w = draw_w as u32;
209 let dst_h = draw_h as u32;
210 if dst_w == 0 || dst_h == 0 {
211 return;
212 }
213
214 let scale_x = src_crop_w / draw_w;
215 let scale_y = src_crop_h / draw_h;
216
217 let canvas_w = canvas.width() as i32;
218 let canvas_h = canvas.height() as i32;
219 let pixels = canvas.pixels_mut();
220
221 for dy in 0..dst_h {
222 for dx in 0..dst_w {
223 let px = draw_x as i32 + dx as i32;
224 let py = draw_y as i32 + dy as i32;
225 if px < 0 || py < 0 || px >= canvas_w || py >= canvas_h {
226 continue;
227 }
228
229 let sx = (src_crop_x + dx as f32 * scale_x) as u32;
230 let sy = (src_crop_y + dy as f32 * scale_y) as u32;
231 let sx = sx.min(pixmap.width() - 1);
232 let sy = sy.min(pixmap.height() - 1);
233
234 let src_idx = (sy * pixmap.width() + sx) as usize * 4;
235 let dst_idx = (py * canvas_w + px) as usize * 4;
236
237 if let (Some(src), Some(dst)) = (
238 pixmap.data().get(src_idx..src_idx + 4),
239 pixels.get_mut(dst_idx..dst_idx + 4),
240 ) {
241 let alpha = src[3];
243 if alpha == 255 {
244 dst[0] = src[0];
245 dst[1] = src[1];
246 dst[2] = src[2];
247 dst[3] = src[3];
248 } else if alpha > 0 {
249 let a = alpha as u32;
250 let ia = 255 - a;
251 dst[0] = ((src[0] as u32 * a + dst[0] as u32 * ia) / 255) as u8;
252 dst[1] = ((src[1] as u32 * a + dst[1] as u32 * ia) / 255) as u8;
253 dst[2] = ((src[2] as u32 * a + dst[2] as u32 * ia) / 255) as u8;
254 dst[3] = 255;
255 }
256 }
257 }
258 }
259 }
260
261 fn compute_fit(
263 &self,
264 src_w: f32,
265 src_h: f32,
266 x: f32,
267 y: f32,
268 ) -> (f32, f32, f32, f32, f32, f32, f32, f32) {
269 match self.fit {
270 ImageFit::Fill => (x, y, self.width, self.height, 0.0, 0.0, src_w, src_h),
271 ImageFit::None => (
272 x,
273 y,
274 src_w.min(self.width),
275 src_h.min(self.height),
276 0.0,
277 0.0,
278 src_w.min(self.width),
279 src_h.min(self.height),
280 ),
281 ImageFit::Contain => {
282 let scale = (self.width / src_w).min(self.height / src_h);
283 let dw = src_w * scale;
284 let dh = src_h * scale;
285 let dx = x + (self.width - dw) / 2.0;
286 let dy = y + (self.height - dh) / 2.0;
287 (dx, dy, dw, dh, 0.0, 0.0, src_w, src_h)
288 }
289 ImageFit::Cover => {
290 let scale = (self.width / src_w).max(self.height / src_h);
291 let scaled_w = src_w * scale;
292 let scaled_h = src_h * scale;
293 let crop_x = (scaled_w - self.width) / 2.0 / scale;
294 let crop_y = (scaled_h - self.height) / 2.0 / scale;
295 let crop_w = self.width / scale;
296 let crop_h = self.height / scale;
297 (x, y, self.width, self.height, crop_x, crop_y, crop_w, crop_h)
298 }
299 }
300 }
301}
302
303impl Default for ImageWidget {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309#[derive(Debug, Clone)]
318pub struct DecodedImage {
319 pub width: u32,
320 pub height: u32,
321 pub pixels: std::sync::Arc<Vec<u8>>,
322}
323
324pub struct ImageCache {
335 cache: HashMap<PathBuf, DecodedImage>,
336 by_bytes: HashMap<u64, DecodedImage>,
337}
338
339impl ImageCache {
340 pub fn new() -> Self {
342 Self {
343 cache: HashMap::new(),
344 by_bytes: HashMap::new(),
345 }
346 }
347
348 pub fn global() -> &'static std::sync::Mutex<ImageCache> {
351 use std::sync::{Mutex, OnceLock};
352 static CACHE: OnceLock<Mutex<ImageCache>> = OnceLock::new();
353 CACHE.get_or_init(|| Mutex::new(ImageCache::new()))
354 }
355
356 pub fn invalidate(&mut self, path: impl Into<PathBuf>) {
360 self.cache.remove(&path.into());
361 }
362
363 pub fn get_or_load(&mut self, path: impl Into<PathBuf>) -> Option<DecodedImage> {
366 let path = path.into();
367 if !self.cache.contains_key(&path) {
368 let bytes = std::fs::read(&path).ok()?;
369 let pixmap = tiny_skia::Pixmap::decode_png(&bytes).ok()?;
370 self.cache.insert(path.clone(), DecodedImage {
371 width: pixmap.width(),
372 height: pixmap.height(),
373 pixels: std::sync::Arc::new(pixmap.data().to_vec()),
374 });
375 }
376 self.cache.get(&path).cloned()
377 }
378
379 pub fn get_or_decode_bytes(&mut self, bytes: &[u8]) -> Option<DecodedImage> {
382 let mut h: u64 = 0xcbf29ce484222325;
383 let mut eat = |b: u8| {
384 h ^= b as u64;
385 h = h.wrapping_mul(0x100000001b3);
386 };
387 for b in (bytes.len() as u64).to_le_bytes() { eat(b); }
388 let n = bytes.len();
389 for &start in &[0usize, n / 2, n.saturating_sub(32)] {
390 for &b in &bytes[start..(start + 32).min(n)] { eat(b); }
391 }
392 if let std::collections::hash_map::Entry::Vacant(e) = self.by_bytes.entry(h) {
393 let pixmap = tiny_skia::Pixmap::decode_png(bytes).ok()?;
394 e.insert(DecodedImage {
395 width: pixmap.width(),
396 height: pixmap.height(),
397 pixels: std::sync::Arc::new(pixmap.data().to_vec()),
398 });
399 }
400 self.by_bytes.get(&h).cloned()
401 }
402
403 pub fn len(&self) -> usize {
405 self.cache.len() + self.by_bytes.len()
406 }
407
408 pub fn is_empty(&self) -> bool {
410 self.cache.is_empty() && self.by_bytes.is_empty()
411 }
412
413 pub fn clear(&mut self) {
415 self.cache.clear();
416 self.by_bytes.clear();
417 }
418
419 pub fn contains(&self, path: impl Into<PathBuf>) -> bool {
421 self.cache.contains_key(&path.into())
422 }
423}
424
425impl Default for ImageCache {
426 fn default() -> Self {
427 Self::new()
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
442 fn image_widget_new_defaults() {
443 let w = ImageWidget::new();
444 assert_eq!(w.source, ImageSource::Placeholder);
445 assert_eq!(w.fit, ImageFit::Contain);
446 assert_eq!(w.width, 200.0);
447 assert_eq!(w.height, 200.0);
448 }
449
450 #[test]
451 fn image_widget_file_source() {
452 let w = ImageWidget::new().file("/tmp/photo.png");
453 assert_eq!(w.source, ImageSource::File(PathBuf::from("/tmp/photo.png")));
454 }
455
456 #[test]
457 fn image_widget_bytes_source() {
458 let data = vec![0u8, 1, 2, 3];
459 let w = ImageWidget::new().bytes(data.clone());
460 assert_eq!(w.source, ImageSource::Bytes(data));
461 }
462
463 #[test]
464 fn image_widget_fit_setter() {
465 let w = ImageWidget::new().fit(ImageFit::Cover);
466 assert_eq!(w.fit, ImageFit::Cover);
467 }
468
469 #[test]
470 fn image_widget_size_setters() {
471 let w = ImageWidget::new().width(640.0).height(480.0);
472 assert_eq!(w.width, 640.0);
473 assert_eq!(w.height, 480.0);
474 }
475
476 #[test]
479 fn image_fit_contain() {
480 let w = ImageWidget::new().fit(ImageFit::Contain).width(200.0).height(200.0);
483 let (dx, dy, dw, dh, scx, scy, scw, sch) = w.compute_fit(100.0, 50.0, 0.0, 0.0);
484 assert!((dw - 200.0).abs() < 0.01, "dw={dw}");
485 assert!((dh - 100.0).abs() < 0.01, "dh={dh}");
486 assert!((dx - 0.0).abs() < 0.01, "dx={dx}");
487 assert!((dy - 50.0).abs() < 0.01, "dy={dy}");
488 assert!((scx).abs() < 0.01);
490 assert!((scy).abs() < 0.01);
491 assert!((scw - 100.0).abs() < 0.01);
492 assert!((sch - 50.0).abs() < 0.01);
493 }
494
495 #[test]
496 fn image_fit_fill() {
497 let w = ImageWidget::new().fit(ImageFit::Fill).width(300.0).height(150.0);
498 let (dx, dy, dw, dh, scx, scy, scw, sch) = w.compute_fit(100.0, 100.0, 5.0, 5.0);
499 assert_eq!(dx, 5.0);
500 assert_eq!(dy, 5.0);
501 assert_eq!(dw, 300.0);
502 assert_eq!(dh, 150.0);
503 assert_eq!(scx, 0.0);
504 assert_eq!(scy, 0.0);
505 assert_eq!(scw, 100.0);
506 assert_eq!(sch, 100.0);
507 }
508
509 #[test]
510 fn image_fit_cover() {
511 let w = ImageWidget::new().fit(ImageFit::Cover).width(200.0).height(100.0);
514 let (dx, dy, dw, dh, scx, scy, scw, sch) = w.compute_fit(100.0, 100.0, 0.0, 0.0);
515 assert!((dw - 200.0).abs() < 0.01, "dw={dw}");
516 assert!((dh - 100.0).abs() < 0.01, "dh={dh}");
517 assert!((dx).abs() < 0.01, "dx={dx}");
518 assert!((dy).abs() < 0.01, "dy={dy}");
519 assert!((scx).abs() < 0.01, "scx={scx}");
521 assert!((scy - 25.0).abs() < 0.01, "scy={scy}");
522 assert!((scw - 100.0).abs() < 0.01, "scw={scw}");
523 assert!((sch - 50.0).abs() < 0.01, "sch={sch}");
524 }
525
526 #[test]
529 fn image_cache_new_empty() {
530 let cache = ImageCache::new();
531 assert!(cache.is_empty());
532 assert_eq!(cache.len(), 0);
533 }
534
535 #[test]
536 fn image_cache_contains_after_miss() {
537 let cache = ImageCache::new();
538 assert!(!cache.contains("/no/such/file.png"));
540 }
541}