1#![allow(non_snake_case)]
4#![allow(clippy::too_many_arguments)] use crate::composable;
7use crate::layout::core::{Alignment, Measurable};
8use crate::modifier::{Modifier, Rect, Size};
9use crate::widgets::Layout;
10use cranpose_core::NodeId;
11use cranpose_ui_graphics::{ColorFilter, DrawScope, ImageBitmap, ImageSampling};
12use cranpose_ui_layout::{Constraints, MeasurePolicy, MeasureResult};
13
14pub const DEFAULT_ALPHA: f32 = 1.0;
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum ContentScale {
18 Fit,
19 Crop,
20 FillBounds,
21 FillWidth,
22 FillHeight,
23 Inside,
24 None,
25}
26
27impl ContentScale {
28 pub fn scaled_size(self, src_size: Size, dst_size: Size) -> Size {
29 if src_size.width <= 0.0
30 || src_size.height <= 0.0
31 || dst_size.width <= 0.0
32 || dst_size.height <= 0.0
33 {
34 return Size::ZERO;
35 }
36
37 let scale_x = dst_size.width / src_size.width;
38 let scale_y = dst_size.height / src_size.height;
39
40 let (factor_x, factor_y) = match self {
41 Self::Fit => {
42 let factor = scale_x.min(scale_y);
43 (factor, factor)
44 }
45 Self::Crop => {
46 let factor = scale_x.max(scale_y);
47 (factor, factor)
48 }
49 Self::FillBounds => (scale_x, scale_y),
50 Self::FillWidth => (scale_x, scale_x),
51 Self::FillHeight => (scale_y, scale_y),
52 Self::Inside => {
53 if src_size.width <= dst_size.width && src_size.height <= dst_size.height {
54 (1.0, 1.0)
55 } else {
56 let factor = scale_x.min(scale_y);
57 (factor, factor)
58 }
59 }
60 Self::None => (1.0, 1.0),
61 };
62
63 Size {
64 width: src_size.width * factor_x,
65 height: src_size.height * factor_y,
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Hash)]
71pub struct Painter {
72 bitmap: ImageBitmap,
73}
74
75impl Painter {
76 pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
77 Self { bitmap }
78 }
79
80 pub fn intrinsic_size(&self) -> Size {
81 self.bitmap.intrinsic_size()
82 }
83
84 pub fn bitmap(&self) -> &ImageBitmap {
85 &self.bitmap
86 }
87}
88
89impl From<ImageBitmap> for Painter {
90 fn from(value: ImageBitmap) -> Self {
91 Self::from_bitmap(value)
92 }
93}
94
95pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
96 Painter::from_bitmap(bitmap)
97}
98
99#[derive(Clone, Debug, PartialEq)]
106struct ImageMeasurePolicy {
107 intrinsic_size: Size,
108}
109
110impl MeasurePolicy for ImageMeasurePolicy {
111 fn measure(
112 &self,
113 _measurables: &[Box<dyn Measurable>],
114 constraints: Constraints,
115 ) -> MeasureResult {
116 let iw = self.intrinsic_size.width;
117 let ih = self.intrinsic_size.height;
118
119 if iw <= 0.0 || ih <= 0.0 {
120 let (w, h) = constraints.constrain(0.0, 0.0);
121 return MeasureResult::new(
122 Size {
123 width: w,
124 height: h,
125 },
126 vec![],
127 );
128 }
129
130 let cw = iw.clamp(constraints.min_width, constraints.max_width);
132 let ch = ih.clamp(constraints.min_height, constraints.max_height);
133
134 let scale_x = cw / iw;
137 let scale_y = ch / ih;
138
139 let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
140 let factor = scale_x.min(scale_y);
141 let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
142 let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
143 (w, h)
144 } else {
145 (cw, ch)
146 };
147
148 MeasureResult::new(Size { width, height }, vec![])
149 }
150
151 fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
152 self.intrinsic_size.width
153 }
154
155 fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
156 self.intrinsic_size.width
157 }
158
159 fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
160 self.intrinsic_size.height
161 }
162
163 fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
164 self.intrinsic_size.height
165 }
166}
167
168fn destination_rect(
169 src_size: Size,
170 dst_size: Size,
171 alignment: Alignment,
172 content_scale: ContentScale,
173) -> Rect {
174 let draw_size = content_scale.scaled_size(src_size, dst_size);
175 let offset_x = alignment.horizontal.align(dst_size.width, draw_size.width);
176 let offset_y = alignment.vertical.align(dst_size.height, draw_size.height);
177 Rect {
178 x: offset_x,
179 y: offset_y,
180 width: draw_size.width,
181 height: draw_size.height,
182 }
183}
184
185fn crop_source_rect(src_size: Size, dst_size: Size, alignment: Alignment) -> Rect {
186 if src_size.width <= 0.0
187 || src_size.height <= 0.0
188 || dst_size.width <= 0.0
189 || dst_size.height <= 0.0
190 {
191 return Rect::from_size(Size::ZERO);
192 }
193
194 let src_aspect = src_size.width / src_size.height;
195 let dst_aspect = dst_size.width / dst_size.height;
196
197 if (src_aspect - dst_aspect).abs() <= f32::EPSILON {
198 return Rect::from_origin_size(crate::modifier::Point::ZERO, src_size);
199 }
200
201 if src_aspect > dst_aspect {
202 let crop_width = src_size.height * dst_aspect;
204 let x = alignment
205 .horizontal
206 .align(src_size.width, crop_width)
207 .clamp(0.0, (src_size.width - crop_width).max(0.0));
208 Rect {
209 x,
210 y: 0.0,
211 width: crop_width,
212 height: src_size.height,
213 }
214 } else {
215 let crop_height = src_size.width / dst_aspect;
217 let y = alignment
218 .vertical
219 .align(src_size.height, crop_height)
220 .clamp(0.0, (src_size.height - crop_height).max(0.0));
221 Rect {
222 x: 0.0,
223 y,
224 width: src_size.width,
225 height: crop_height,
226 }
227 }
228}
229
230fn map_destination_clip_to_source(
231 src_rect: Rect,
232 dst_rect: Rect,
233 clipped_dst_rect: Rect,
234) -> Option<Rect> {
235 if src_rect.width <= 0.0
236 || src_rect.height <= 0.0
237 || dst_rect.width <= 0.0
238 || dst_rect.height <= 0.0
239 || clipped_dst_rect.width <= 0.0
240 || clipped_dst_rect.height <= 0.0
241 {
242 return None;
243 }
244
245 let scale_x = src_rect.width / dst_rect.width;
246 let scale_y = src_rect.height / dst_rect.height;
247
248 let src_min_x = src_rect.x;
249 let src_min_y = src_rect.y;
250 let src_max_x = src_rect.x + src_rect.width;
251 let src_max_y = src_rect.y + src_rect.height;
252
253 let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
254 let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
255 let raw_right =
256 src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
257 let raw_bottom =
258 src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
259
260 let left = raw_left.clamp(src_min_x, src_max_x);
261 let top = raw_top.clamp(src_min_y, src_max_y);
262 let right = raw_right.clamp(src_min_x, src_max_x);
263 let bottom = raw_bottom.clamp(src_min_y, src_max_y);
264 let width = right - left;
265 let height = bottom - top;
266
267 if width <= 0.0 || height <= 0.0 {
268 None
269 } else {
270 Some(Rect {
271 x: left,
272 y: top,
273 width,
274 height,
275 })
276 }
277}
278
279#[composable]
280pub fn Image<P>(
281 painter: P,
282 content_description: Option<String>,
283 modifier: Modifier,
284 alignment: Alignment,
285 content_scale: ContentScale,
286 alpha: f32,
287 color_filter: Option<ColorFilter>,
288) -> NodeId
289where
290 P: Into<Painter> + Clone + PartialEq + 'static,
291{
292 let painter = painter.into();
293 let intrinsic_dp = painter.intrinsic_size();
298 let draw_alpha = alpha.clamp(0.0, 1.0);
299 let draw_painter = painter.clone();
300
301 let semantics_modifier = if let Some(description) = content_description {
302 Modifier::empty().semantics(move |config| {
303 config.content_description = Some(description.clone());
304 })
305 } else {
306 Modifier::empty()
307 };
308
309 let image_modifier =
310 modifier
311 .then(semantics_modifier)
312 .draw_behind(move |scope: &mut dyn DrawScope| {
313 if draw_alpha <= 0.0 {
314 return;
315 }
316 let container_size = scope.size();
317 if container_size.width <= 0.0 || container_size.height <= 0.0 {
318 return;
319 }
320 if content_scale == ContentScale::Crop {
321 let src_rect = crop_source_rect(intrinsic_dp, container_size, alignment);
323 if src_rect.width <= 0.0 || src_rect.height <= 0.0 {
324 return;
325 }
326 scope.draw_image_src_sampled(
327 draw_painter.bitmap().clone(),
328 src_rect,
329 Rect::from_size(container_size),
330 draw_alpha,
331 color_filter,
332 ImageSampling::Linear,
333 );
334 } else {
335 let dst_rect =
336 destination_rect(intrinsic_dp, container_size, alignment, content_scale);
337 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
338 return;
339 }
340 let container_rect = Rect::from_size(container_size);
341 let Some(clipped_dst_rect) = dst_rect.intersect(container_rect) else {
342 return;
343 };
344 let full_src_rect = Rect::from_size(intrinsic_dp);
345 let Some(clipped_src_rect) =
346 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
347 else {
348 return;
349 };
350 scope.draw_image_src_sampled(
351 draw_painter.bitmap().clone(),
352 clipped_src_rect,
353 clipped_dst_rect,
354 draw_alpha,
355 color_filter,
356 ImageSampling::Linear,
357 );
358 }
359 });
360
361 Layout(
362 image_modifier,
363 ImageMeasurePolicy {
364 intrinsic_size: intrinsic_dp,
365 },
366 || {},
367 )
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::layout::core::Alignment;
374
375 fn sample_bitmap() -> ImageBitmap {
376 ImageBitmap::from_rgba8(4, 2, vec![255; 4 * 2 * 4]).expect("bitmap")
377 }
378
379 #[test]
380 fn painter_reports_intrinsic_size_and_bitmap() {
381 let bitmap = sample_bitmap();
382 let painter = BitmapPainter(bitmap.clone());
383 assert_eq!(painter.intrinsic_size(), Size::new(4.0, 2.0));
384 assert_eq!(painter.bitmap(), &bitmap);
385 }
386
387 #[test]
388 fn fit_keeps_aspect_ratio() {
389 let src = Size::new(200.0, 100.0);
390 let dst = Size::new(300.0, 300.0);
391 let result = ContentScale::Fit.scaled_size(src, dst);
392 assert_eq!(result, Size::new(300.0, 150.0));
393 }
394
395 #[test]
396 fn crop_fills_bounds() {
397 let src = Size::new(200.0, 100.0);
398 let dst = Size::new(300.0, 300.0);
399 let result = ContentScale::Crop.scaled_size(src, dst);
400 assert_eq!(result, Size::new(600.0, 300.0));
401 }
402
403 #[test]
404 fn destination_rect_aligns_center() {
405 let src = Size::new(200.0, 100.0);
406 let dst = Size::new(300.0, 300.0);
407 let rect = destination_rect(src, dst, Alignment::CENTER, ContentScale::Fit);
408 assert_eq!(
409 rect,
410 Rect {
411 x: 0.0,
412 y: 75.0,
413 width: 300.0,
414 height: 150.0,
415 }
416 );
417 }
418
419 #[test]
420 fn crop_source_rect_is_centered_for_wide_source() {
421 let src = Size::new(200.0, 100.0);
422 let dst = Size::new(100.0, 100.0);
423 let rect = crop_source_rect(src, dst, Alignment::CENTER);
424 assert_eq!(
425 rect,
426 Rect {
427 x: 50.0,
428 y: 0.0,
429 width: 100.0,
430 height: 100.0,
431 }
432 );
433 }
434
435 #[test]
436 fn crop_source_rect_honors_start_alignment() {
437 let src = Size::new(200.0, 100.0);
438 let dst = Size::new(100.0, 100.0);
439 let rect = crop_source_rect(src, dst, Alignment::TOP_START);
440 assert_eq!(
441 rect,
442 Rect {
443 x: 0.0,
444 y: 0.0,
445 width: 100.0,
446 height: 100.0,
447 }
448 );
449 }
450
451 fn approx_eq(left: f32, right: f32) {
452 assert!((left - right).abs() < 1e-4, "left={left}, right={right}");
453 }
454
455 #[test]
456 fn map_destination_clip_to_source_scales_proportionally() {
457 let src = Rect {
458 x: 0.0,
459 y: 0.0,
460 width: 100.0,
461 height: 100.0,
462 };
463 let dst = Rect {
464 x: -50.0,
465 y: 0.0,
466 width: 200.0,
467 height: 100.0,
468 };
469 let clipped_dst = Rect {
470 x: 0.0,
471 y: 0.0,
472 width: 100.0,
473 height: 100.0,
474 };
475 let mapped = map_destination_clip_to_source(src, dst, clipped_dst).expect("mapped");
476 approx_eq(mapped.x, 25.0);
477 approx_eq(mapped.y, 0.0);
478 approx_eq(mapped.width, 50.0);
479 approx_eq(mapped.height, 100.0);
480 }
481
482 #[test]
483 fn map_destination_clip_to_source_returns_full_source_without_clipping() {
484 let src = Rect {
485 x: 0.0,
486 y: 0.0,
487 width: 120.0,
488 height: 80.0,
489 };
490 let dst = Rect {
491 x: 10.0,
492 y: 5.0,
493 width: 60.0,
494 height: 40.0,
495 };
496 let mapped = map_destination_clip_to_source(src, dst, dst).expect("mapped");
497 approx_eq(mapped.x, src.x);
498 approx_eq(mapped.y, src.y);
499 approx_eq(mapped.width, src.width);
500 approx_eq(mapped.height, src.height);
501 }
502
503 fn measure_image(intrinsic: Size, constraints: Constraints) -> Size {
506 let policy = ImageMeasurePolicy {
507 intrinsic_size: intrinsic,
508 };
509 policy.measure(&[], constraints).size
510 }
511
512 #[test]
513 fn image_measure_unconstrained() {
514 let size = measure_image(
515 Size::new(800.0, 600.0),
516 Constraints::loose(f32::INFINITY, f32::INFINITY),
517 );
518 assert_eq!(size, Size::new(800.0, 600.0));
519 }
520
521 #[test]
522 fn image_measure_width_constrained_preserves_aspect_ratio() {
523 let size = measure_image(
525 Size::new(800.0, 600.0),
526 Constraints::loose(400.0, f32::INFINITY),
527 );
528 assert_eq!(size, Size::new(400.0, 300.0));
529 }
530
531 #[test]
532 fn image_measure_height_constrained_preserves_aspect_ratio() {
533 let size = measure_image(
535 Size::new(800.0, 600.0),
536 Constraints::loose(f32::INFINITY, 300.0),
537 );
538 assert_eq!(size, Size::new(400.0, 300.0));
539 }
540
541 #[test]
542 fn image_measure_both_constrained_uses_smaller_factor() {
543 let size = measure_image(Size::new(800.0, 600.0), Constraints::loose(200.0, 400.0));
546 assert_eq!(size, Size::new(200.0, 150.0));
547 }
548
549 #[test]
550 fn image_measure_fits_within_constraints() {
551 let size = measure_image(Size::new(200.0, 100.0), Constraints::loose(400.0, 400.0));
553 assert_eq!(size, Size::new(200.0, 100.0));
554 }
555
556 #[test]
557 fn image_measure_zero_intrinsic() {
558 let size = measure_image(Size::ZERO, Constraints::loose(400.0, 400.0));
559 assert_eq!(size, Size::new(0.0, 0.0));
560 }
561}