1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult},
5 Canvas, Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub enum ImageFit {
13 Cover,
15 #[default]
17 Contain,
18 Fill,
20 None,
22 ScaleDown,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Image {
29 source: String,
31 alt: String,
33 fit: ImageFit,
35 width: Option<f32>,
37 height: Option<f32>,
39 #[serde(skip)]
41 loading: bool,
42 #[serde(skip)]
44 error: bool,
45 accessible_name_value: Option<String>,
47 test_id_value: Option<String>,
49 #[serde(skip)]
51 bounds: Rect,
52}
53
54impl Default for Image {
55 fn default() -> Self {
56 Self {
57 source: String::new(),
58 alt: String::new(),
59 fit: ImageFit::Contain,
60 width: None,
61 height: None,
62 loading: false,
63 error: false,
64 accessible_name_value: None,
65 test_id_value: None,
66 bounds: Rect::default(),
67 }
68 }
69}
70
71impl Image {
72 #[must_use]
74 pub fn new(source: impl Into<String>) -> Self {
75 Self {
76 source: source.into(),
77 ..Self::default()
78 }
79 }
80
81 #[must_use]
83 pub fn source(mut self, source: impl Into<String>) -> Self {
84 self.source = source.into();
85 self
86 }
87
88 #[must_use]
90 pub fn alt(mut self, alt: impl Into<String>) -> Self {
91 self.alt = alt.into();
92 self
93 }
94
95 #[must_use]
97 pub const fn fit(mut self, fit: ImageFit) -> Self {
98 self.fit = fit;
99 self
100 }
101
102 #[must_use]
104 pub fn width(mut self, width: f32) -> Self {
105 self.width = Some(width.max(0.0));
106 self
107 }
108
109 #[must_use]
111 pub fn height(mut self, height: f32) -> Self {
112 self.height = Some(height.max(0.0));
113 self
114 }
115
116 #[must_use]
118 pub fn size(self, width: f32, height: f32) -> Self {
119 self.width(width).height(height)
120 }
121
122 #[must_use]
124 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
125 self.accessible_name_value = Some(name.into());
126 self
127 }
128
129 #[must_use]
131 pub fn test_id(mut self, id: impl Into<String>) -> Self {
132 self.test_id_value = Some(id.into());
133 self
134 }
135
136 #[must_use]
138 pub fn get_source(&self) -> &str {
139 &self.source
140 }
141
142 #[must_use]
144 pub fn get_alt(&self) -> &str {
145 &self.alt
146 }
147
148 #[must_use]
150 pub const fn get_fit(&self) -> ImageFit {
151 self.fit
152 }
153
154 #[must_use]
156 pub const fn get_width(&self) -> Option<f32> {
157 self.width
158 }
159
160 #[must_use]
162 pub const fn get_height(&self) -> Option<f32> {
163 self.height
164 }
165
166 #[must_use]
168 pub const fn is_loading(&self) -> bool {
169 self.loading
170 }
171
172 #[must_use]
174 pub const fn has_error(&self) -> bool {
175 self.error
176 }
177
178 pub fn set_loading(&mut self, loading: bool) {
180 self.loading = loading;
181 }
182
183 pub fn set_error(&mut self, error: bool) {
185 self.error = error;
186 }
187
188 #[must_use]
190 pub fn aspect_ratio(&self) -> Option<f32> {
191 match (self.width, self.height) {
192 (Some(w), Some(h)) if h > 0.0 => Some(w / h),
193 _ => None,
194 }
195 }
196
197 fn calculate_display_size(&self, container: Size) -> Size {
199 let intrinsic = Size::new(
200 self.width.unwrap_or(container.width),
201 self.height.unwrap_or(container.height),
202 );
203
204 match self.fit {
205 ImageFit::Fill => container,
206 ImageFit::None => intrinsic,
207 ImageFit::Contain => {
208 let scale =
209 (container.width / intrinsic.width).min(container.height / intrinsic.height);
210 Size::new(intrinsic.width * scale, intrinsic.height * scale)
211 }
212 ImageFit::Cover => {
213 let scale =
214 (container.width / intrinsic.width).max(container.height / intrinsic.height);
215 Size::new(intrinsic.width * scale, intrinsic.height * scale)
216 }
217 ImageFit::ScaleDown => {
218 if intrinsic.width <= container.width && intrinsic.height <= container.height {
219 intrinsic
220 } else {
221 let scale = (container.width / intrinsic.width)
222 .min(container.height / intrinsic.height);
223 Size::new(intrinsic.width * scale, intrinsic.height * scale)
224 }
225 }
226 }
227 }
228}
229
230impl Widget for Image {
231 fn type_id(&self) -> TypeId {
232 TypeId::of::<Self>()
233 }
234
235 fn measure(&self, constraints: Constraints) -> Size {
236 let preferred = Size::new(self.width.unwrap_or(100.0), self.height.unwrap_or(100.0));
237 constraints.constrain(preferred)
238 }
239
240 fn layout(&mut self, bounds: Rect) -> LayoutResult {
241 self.bounds = bounds;
242 LayoutResult {
243 size: bounds.size(),
244 }
245 }
246
247 fn paint(&self, canvas: &mut dyn Canvas) {
248 let display_size = self.calculate_display_size(self.bounds.size());
252
253 let x_offset = (self.bounds.width - display_size.width) / 2.0;
255 let y_offset = (self.bounds.height - display_size.height) / 2.0;
256
257 let image_rect = Rect::new(
258 self.bounds.x + x_offset,
259 self.bounds.y + y_offset,
260 display_size.width,
261 display_size.height,
262 );
263
264 let color = if self.error {
266 presentar_core::Color::new(0.9, 0.7, 0.7, 1.0)
267 } else if self.loading {
268 presentar_core::Color::new(0.9, 0.9, 0.9, 1.0)
269 } else {
270 presentar_core::Color::new(0.8, 0.8, 0.8, 1.0)
271 };
272
273 canvas.fill_rect(image_rect, color);
274 }
275
276 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
277 None
278 }
279
280 fn children(&self) -> &[Box<dyn Widget>] {
281 &[]
282 }
283
284 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
285 &mut []
286 }
287
288 fn is_interactive(&self) -> bool {
289 false
290 }
291
292 fn is_focusable(&self) -> bool {
293 false
294 }
295
296 fn accessible_name(&self) -> Option<&str> {
297 self.accessible_name_value
298 .as_deref()
299 .or(if self.alt.is_empty() {
300 None
301 } else {
302 Some(&self.alt)
303 })
304 }
305
306 fn accessible_role(&self) -> AccessibleRole {
307 AccessibleRole::Image
308 }
309
310 fn test_id(&self) -> Option<&str> {
311 self.test_id_value.as_deref()
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
322 fn test_image_fit_default() {
323 assert_eq!(ImageFit::default(), ImageFit::Contain);
324 }
325
326 #[test]
327 fn test_image_fit_equality() {
328 assert_eq!(ImageFit::Cover, ImageFit::Cover);
329 assert_ne!(ImageFit::Cover, ImageFit::Contain);
330 }
331
332 #[test]
335 fn test_image_new() {
336 let img = Image::new("https://example.com/image.png");
337 assert_eq!(img.get_source(), "https://example.com/image.png");
338 assert!(img.get_alt().is_empty());
339 }
340
341 #[test]
342 fn test_image_default() {
343 let img = Image::default();
344 assert!(img.get_source().is_empty());
345 assert!(img.get_alt().is_empty());
346 assert_eq!(img.get_fit(), ImageFit::Contain);
347 assert!(img.get_width().is_none());
348 assert!(img.get_height().is_none());
349 }
350
351 #[test]
352 fn test_image_builder() {
353 let img = Image::new("photo.jpg")
354 .alt("A beautiful sunset")
355 .fit(ImageFit::Cover)
356 .width(800.0)
357 .height(600.0)
358 .accessible_name("Sunset photo")
359 .test_id("hero-image");
360
361 assert_eq!(img.get_source(), "photo.jpg");
362 assert_eq!(img.get_alt(), "A beautiful sunset");
363 assert_eq!(img.get_fit(), ImageFit::Cover);
364 assert_eq!(img.get_width(), Some(800.0));
365 assert_eq!(img.get_height(), Some(600.0));
366 assert_eq!(Widget::accessible_name(&img), Some("Sunset photo"));
367 assert_eq!(Widget::test_id(&img), Some("hero-image"));
368 }
369
370 #[test]
371 fn test_image_source() {
372 let img = Image::default().source("new-source.png");
373 assert_eq!(img.get_source(), "new-source.png");
374 }
375
376 #[test]
377 fn test_image_size() {
378 let img = Image::default().size(1920.0, 1080.0);
379 assert_eq!(img.get_width(), Some(1920.0));
380 assert_eq!(img.get_height(), Some(1080.0));
381 }
382
383 #[test]
384 fn test_image_width_min() {
385 let img = Image::default().width(-100.0);
386 assert_eq!(img.get_width(), Some(0.0));
387 }
388
389 #[test]
390 fn test_image_height_min() {
391 let img = Image::default().height(-50.0);
392 assert_eq!(img.get_height(), Some(0.0));
393 }
394
395 #[test]
398 fn test_image_loading_state() {
399 let mut img = Image::new("image.png");
400 assert!(!img.is_loading());
401 img.set_loading(true);
402 assert!(img.is_loading());
403 }
404
405 #[test]
406 fn test_image_error_state() {
407 let mut img = Image::new("broken.png");
408 assert!(!img.has_error());
409 img.set_error(true);
410 assert!(img.has_error());
411 }
412
413 #[test]
416 fn test_image_aspect_ratio() {
417 let img = Image::default().size(1600.0, 900.0);
418 let ratio = img.aspect_ratio().unwrap();
419 assert!((ratio - 16.0 / 9.0).abs() < 0.001);
420 }
421
422 #[test]
423 fn test_image_aspect_ratio_square() {
424 let img = Image::default().size(100.0, 100.0);
425 assert_eq!(img.aspect_ratio(), Some(1.0));
426 }
427
428 #[test]
429 fn test_image_aspect_ratio_no_dimensions() {
430 let img = Image::default();
431 assert!(img.aspect_ratio().is_none());
432 }
433
434 #[test]
435 fn test_image_aspect_ratio_zero_height() {
436 let img = Image::default().width(100.0).height(0.0);
437 assert!(img.aspect_ratio().is_none());
438 }
439
440 #[test]
443 fn test_display_size_fill() {
444 let img = Image::default().size(100.0, 100.0).fit(ImageFit::Fill);
445 let display = img.calculate_display_size(Size::new(200.0, 150.0));
446 assert_eq!(display, Size::new(200.0, 150.0));
447 }
448
449 #[test]
450 fn test_display_size_none() {
451 let img = Image::default().size(100.0, 100.0).fit(ImageFit::None);
452 let display = img.calculate_display_size(Size::new(200.0, 150.0));
453 assert_eq!(display, Size::new(100.0, 100.0));
454 }
455
456 #[test]
457 fn test_display_size_contain() {
458 let img = Image::default().size(200.0, 100.0).fit(ImageFit::Contain);
459 let display = img.calculate_display_size(Size::new(100.0, 100.0));
460 assert_eq!(display, Size::new(100.0, 50.0));
462 }
463
464 #[test]
465 fn test_display_size_cover() {
466 let img = Image::default().size(200.0, 100.0).fit(ImageFit::Cover);
467 let display = img.calculate_display_size(Size::new(100.0, 100.0));
468 assert_eq!(display, Size::new(200.0, 100.0));
470 }
471
472 #[test]
473 fn test_display_size_scale_down_smaller() {
474 let img = Image::default().size(50.0, 50.0).fit(ImageFit::ScaleDown);
475 let display = img.calculate_display_size(Size::new(100.0, 100.0));
476 assert_eq!(display, Size::new(50.0, 50.0));
478 }
479
480 #[test]
481 fn test_display_size_scale_down_larger() {
482 let img = Image::default().size(200.0, 200.0).fit(ImageFit::ScaleDown);
483 let display = img.calculate_display_size(Size::new(100.0, 100.0));
484 assert_eq!(display, Size::new(100.0, 100.0));
486 }
487
488 #[test]
491 fn test_image_type_id() {
492 let img = Image::new("test.png");
493 assert_eq!(Widget::type_id(&img), TypeId::of::<Image>());
494 }
495
496 #[test]
497 fn test_image_measure_with_size() {
498 let img = Image::default().size(200.0, 150.0);
499 let size = img.measure(Constraints::loose(Size::new(500.0, 500.0)));
500 assert_eq!(size, Size::new(200.0, 150.0));
501 }
502
503 #[test]
504 fn test_image_measure_default_size() {
505 let img = Image::default();
506 let size = img.measure(Constraints::loose(Size::new(500.0, 500.0)));
507 assert_eq!(size, Size::new(100.0, 100.0)); }
509
510 #[test]
511 fn test_image_layout() {
512 let mut img = Image::new("test.png");
513 let bounds = Rect::new(10.0, 20.0, 200.0, 150.0);
514 let result = img.layout(bounds);
515 assert_eq!(result.size, Size::new(200.0, 150.0));
516 assert_eq!(img.bounds, bounds);
517 }
518
519 #[test]
520 fn test_image_children() {
521 let img = Image::new("test.png");
522 assert!(img.children().is_empty());
523 }
524
525 #[test]
526 fn test_image_is_interactive() {
527 let img = Image::new("test.png");
528 assert!(!img.is_interactive());
529 }
530
531 #[test]
532 fn test_image_is_focusable() {
533 let img = Image::new("test.png");
534 assert!(!img.is_focusable());
535 }
536
537 #[test]
538 fn test_image_accessible_role() {
539 let img = Image::new("test.png");
540 assert_eq!(img.accessible_role(), AccessibleRole::Image);
541 }
542
543 #[test]
544 fn test_image_accessible_name_from_alt() {
545 let img = Image::new("photo.jpg").alt("Mountain landscape");
546 assert_eq!(Widget::accessible_name(&img), Some("Mountain landscape"));
547 }
548
549 #[test]
550 fn test_image_accessible_name_override() {
551 let img = Image::new("photo.jpg")
552 .alt("Photo")
553 .accessible_name("Beautiful mountain landscape at sunset");
554 assert_eq!(
555 Widget::accessible_name(&img),
556 Some("Beautiful mountain landscape at sunset")
557 );
558 }
559
560 #[test]
561 fn test_image_accessible_name_none() {
562 let img = Image::new("decorative.png");
563 assert_eq!(Widget::accessible_name(&img), None);
564 }
565
566 #[test]
567 fn test_image_test_id() {
568 let img = Image::new("test.png").test_id("profile-avatar");
569 assert_eq!(Widget::test_id(&img), Some("profile-avatar"));
570 }
571}