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};
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(
327 draw_painter.bitmap().clone(),
328 src_rect,
329 Rect::from_size(container_size),
330 draw_alpha,
331 color_filter,
332 );
333 } else {
334 let dst_rect =
335 destination_rect(intrinsic_dp, container_size, alignment, content_scale);
336 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
337 return;
338 }
339 let container_rect = Rect::from_size(container_size);
340 let Some(clipped_dst_rect) = dst_rect.intersect(container_rect) else {
341 return;
342 };
343 let full_src_rect = Rect::from_size(intrinsic_dp);
344 let Some(clipped_src_rect) =
345 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
346 else {
347 return;
348 };
349 scope.draw_image_src(
350 draw_painter.bitmap().clone(),
351 clipped_src_rect,
352 clipped_dst_rect,
353 draw_alpha,
354 color_filter,
355 );
356 }
357 });
358
359 Layout(
360 image_modifier,
361 ImageMeasurePolicy {
362 intrinsic_size: intrinsic_dp,
363 },
364 || {},
365 )
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use crate::layout::core::Alignment;
372
373 fn sample_bitmap() -> ImageBitmap {
374 ImageBitmap::from_rgba8(4, 2, vec![255; 4 * 2 * 4]).expect("bitmap")
375 }
376
377 #[test]
378 fn painter_reports_intrinsic_size_and_bitmap() {
379 let bitmap = sample_bitmap();
380 let painter = BitmapPainter(bitmap.clone());
381 assert_eq!(painter.intrinsic_size(), Size::new(4.0, 2.0));
382 assert_eq!(painter.bitmap(), &bitmap);
383 }
384
385 #[test]
386 fn fit_keeps_aspect_ratio() {
387 let src = Size::new(200.0, 100.0);
388 let dst = Size::new(300.0, 300.0);
389 let result = ContentScale::Fit.scaled_size(src, dst);
390 assert_eq!(result, Size::new(300.0, 150.0));
391 }
392
393 #[test]
394 fn crop_fills_bounds() {
395 let src = Size::new(200.0, 100.0);
396 let dst = Size::new(300.0, 300.0);
397 let result = ContentScale::Crop.scaled_size(src, dst);
398 assert_eq!(result, Size::new(600.0, 300.0));
399 }
400
401 #[test]
402 fn destination_rect_aligns_center() {
403 let src = Size::new(200.0, 100.0);
404 let dst = Size::new(300.0, 300.0);
405 let rect = destination_rect(src, dst, Alignment::CENTER, ContentScale::Fit);
406 assert_eq!(
407 rect,
408 Rect {
409 x: 0.0,
410 y: 75.0,
411 width: 300.0,
412 height: 150.0,
413 }
414 );
415 }
416
417 #[test]
418 fn crop_source_rect_is_centered_for_wide_source() {
419 let src = Size::new(200.0, 100.0);
420 let dst = Size::new(100.0, 100.0);
421 let rect = crop_source_rect(src, dst, Alignment::CENTER);
422 assert_eq!(
423 rect,
424 Rect {
425 x: 50.0,
426 y: 0.0,
427 width: 100.0,
428 height: 100.0,
429 }
430 );
431 }
432
433 #[test]
434 fn crop_source_rect_honors_start_alignment() {
435 let src = Size::new(200.0, 100.0);
436 let dst = Size::new(100.0, 100.0);
437 let rect = crop_source_rect(src, dst, Alignment::TOP_START);
438 assert_eq!(
439 rect,
440 Rect {
441 x: 0.0,
442 y: 0.0,
443 width: 100.0,
444 height: 100.0,
445 }
446 );
447 }
448
449 fn approx_eq(left: f32, right: f32) {
450 assert!((left - right).abs() < 1e-4, "left={left}, right={right}");
451 }
452
453 #[test]
454 fn map_destination_clip_to_source_scales_proportionally() {
455 let src = Rect {
456 x: 0.0,
457 y: 0.0,
458 width: 100.0,
459 height: 100.0,
460 };
461 let dst = Rect {
462 x: -50.0,
463 y: 0.0,
464 width: 200.0,
465 height: 100.0,
466 };
467 let clipped_dst = Rect {
468 x: 0.0,
469 y: 0.0,
470 width: 100.0,
471 height: 100.0,
472 };
473 let mapped = map_destination_clip_to_source(src, dst, clipped_dst).expect("mapped");
474 approx_eq(mapped.x, 25.0);
475 approx_eq(mapped.y, 0.0);
476 approx_eq(mapped.width, 50.0);
477 approx_eq(mapped.height, 100.0);
478 }
479
480 #[test]
481 fn map_destination_clip_to_source_returns_full_source_without_clipping() {
482 let src = Rect {
483 x: 0.0,
484 y: 0.0,
485 width: 120.0,
486 height: 80.0,
487 };
488 let dst = Rect {
489 x: 10.0,
490 y: 5.0,
491 width: 60.0,
492 height: 40.0,
493 };
494 let mapped = map_destination_clip_to_source(src, dst, dst).expect("mapped");
495 approx_eq(mapped.x, src.x);
496 approx_eq(mapped.y, src.y);
497 approx_eq(mapped.width, src.width);
498 approx_eq(mapped.height, src.height);
499 }
500
501 fn measure_image(intrinsic: Size, constraints: Constraints) -> Size {
504 let policy = ImageMeasurePolicy {
505 intrinsic_size: intrinsic,
506 };
507 policy.measure(&[], constraints).size
508 }
509
510 #[test]
511 fn image_measure_unconstrained() {
512 let size = measure_image(
513 Size::new(800.0, 600.0),
514 Constraints::loose(f32::INFINITY, f32::INFINITY),
515 );
516 assert_eq!(size, Size::new(800.0, 600.0));
517 }
518
519 #[test]
520 fn image_measure_width_constrained_preserves_aspect_ratio() {
521 let size = measure_image(
523 Size::new(800.0, 600.0),
524 Constraints::loose(400.0, f32::INFINITY),
525 );
526 assert_eq!(size, Size::new(400.0, 300.0));
527 }
528
529 #[test]
530 fn image_measure_height_constrained_preserves_aspect_ratio() {
531 let size = measure_image(
533 Size::new(800.0, 600.0),
534 Constraints::loose(f32::INFINITY, 300.0),
535 );
536 assert_eq!(size, Size::new(400.0, 300.0));
537 }
538
539 #[test]
540 fn image_measure_both_constrained_uses_smaller_factor() {
541 let size = measure_image(Size::new(800.0, 600.0), Constraints::loose(200.0, 400.0));
544 assert_eq!(size, Size::new(200.0, 150.0));
545 }
546
547 #[test]
548 fn image_measure_fits_within_constraints() {
549 let size = measure_image(Size::new(200.0, 100.0), Constraints::loose(400.0, 400.0));
551 assert_eq!(size, Size::new(200.0, 100.0));
552 }
553
554 #[test]
555 fn image_measure_zero_intrinsic() {
556 let size = measure_image(Size::ZERO, Constraints::loose(400.0, 400.0));
557 assert_eq!(size, Size::new(0.0, 0.0));
558 }
559}