1#![allow(non_snake_case)]
4#![allow(clippy::too_many_arguments)] use crate::composable;
7use crate::layout::core::Alignment;
8use crate::layout::policies::LeafMeasurePolicy;
9use crate::modifier::{Modifier, Rect, Size};
10use crate::render_state::current_density;
11use crate::widgets::Layout;
12use cranpose_core::NodeId;
13use cranpose_ui_graphics::{ColorFilter, DrawScope, ImageBitmap};
14
15pub const DEFAULT_ALPHA: f32 = 1.0;
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum ContentScale {
19 Fit,
20 Crop,
21 FillBounds,
22 FillWidth,
23 FillHeight,
24 Inside,
25 None,
26}
27
28impl ContentScale {
29 pub fn scaled_size(self, src_size: Size, dst_size: Size) -> Size {
30 if src_size.width <= 0.0
31 || src_size.height <= 0.0
32 || dst_size.width <= 0.0
33 || dst_size.height <= 0.0
34 {
35 return Size::ZERO;
36 }
37
38 let scale_x = dst_size.width / src_size.width;
39 let scale_y = dst_size.height / src_size.height;
40
41 let (factor_x, factor_y) = match self {
42 Self::Fit => {
43 let factor = scale_x.min(scale_y);
44 (factor, factor)
45 }
46 Self::Crop => {
47 let factor = scale_x.max(scale_y);
48 (factor, factor)
49 }
50 Self::FillBounds => (scale_x, scale_y),
51 Self::FillWidth => (scale_x, scale_x),
52 Self::FillHeight => (scale_y, scale_y),
53 Self::Inside => {
54 if src_size.width <= dst_size.width && src_size.height <= dst_size.height {
55 (1.0, 1.0)
56 } else {
57 let factor = scale_x.min(scale_y);
58 (factor, factor)
59 }
60 }
61 Self::None => (1.0, 1.0),
62 };
63
64 Size {
65 width: src_size.width * factor_x,
66 height: src_size.height * factor_y,
67 }
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Hash)]
72pub struct Painter {
73 bitmap: ImageBitmap,
74}
75
76impl Painter {
77 pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
78 Self { bitmap }
79 }
80
81 pub fn intrinsic_size(&self) -> Size {
82 self.bitmap.intrinsic_size()
83 }
84
85 pub fn bitmap(&self) -> &ImageBitmap {
86 &self.bitmap
87 }
88}
89
90impl From<ImageBitmap> for Painter {
91 fn from(value: ImageBitmap) -> Self {
92 Self::from_bitmap(value)
93 }
94}
95
96pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
97 Painter::from_bitmap(bitmap)
98}
99
100fn destination_rect(
101 src_size: Size,
102 dst_size: Size,
103 alignment: Alignment,
104 content_scale: ContentScale,
105) -> Rect {
106 let draw_size = content_scale.scaled_size(src_size, dst_size);
107 let offset_x = alignment.horizontal.align(dst_size.width, draw_size.width);
108 let offset_y = alignment.vertical.align(dst_size.height, draw_size.height);
109 Rect {
110 x: offset_x,
111 y: offset_y,
112 width: draw_size.width,
113 height: draw_size.height,
114 }
115}
116
117#[composable]
118pub fn Image<P>(
119 painter: P,
120 content_description: Option<String>,
121 modifier: Modifier,
122 alignment: Alignment,
123 content_scale: ContentScale,
124 alpha: f32,
125 color_filter: Option<ColorFilter>,
126) -> NodeId
127where
128 P: Into<Painter> + Clone + PartialEq + 'static,
129{
130 let painter = painter.into();
131 let density = current_density().max(1.0);
132 let pixel_size = painter.intrinsic_size();
133 let intrinsic_dp = Size {
137 width: pixel_size.width / density,
138 height: pixel_size.height / density,
139 };
140 let draw_alpha = alpha.clamp(0.0, 1.0);
141 let draw_painter = painter.clone();
142
143 let semantics_modifier = if let Some(description) = content_description {
144 Modifier::empty().semantics(move |config| {
145 config.content_description = Some(description.clone());
146 })
147 } else {
148 Modifier::empty()
149 };
150
151 let image_modifier = modifier
152 .then(semantics_modifier)
153 .clip_to_bounds()
154 .draw_behind(move |scope: &mut dyn DrawScope| {
155 if draw_alpha <= 0.0 {
156 return;
157 }
158 let container_size = scope.size();
159 let rect = destination_rect(intrinsic_dp, container_size, alignment, content_scale);
160 if rect.width <= 0.0 || rect.height <= 0.0 {
161 return;
162 }
163 scope.draw_image_at(
164 rect,
165 draw_painter.bitmap().clone(),
166 draw_alpha,
167 color_filter,
168 );
169 });
170
171 Layout(image_modifier, LeafMeasurePolicy::new(intrinsic_dp), || {})
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::layout::core::Alignment;
178
179 fn sample_bitmap() -> ImageBitmap {
180 ImageBitmap::from_rgba8(4, 2, vec![255; 4 * 2 * 4]).expect("bitmap")
181 }
182
183 #[test]
184 fn painter_reports_intrinsic_size_and_bitmap() {
185 let bitmap = sample_bitmap();
186 let painter = BitmapPainter(bitmap.clone());
187 assert_eq!(painter.intrinsic_size(), Size::new(4.0, 2.0));
188 assert_eq!(painter.bitmap(), &bitmap);
189 }
190
191 #[test]
192 fn fit_keeps_aspect_ratio() {
193 let src = Size::new(200.0, 100.0);
194 let dst = Size::new(300.0, 300.0);
195 let result = ContentScale::Fit.scaled_size(src, dst);
196 assert_eq!(result, Size::new(300.0, 150.0));
197 }
198
199 #[test]
200 fn crop_fills_bounds() {
201 let src = Size::new(200.0, 100.0);
202 let dst = Size::new(300.0, 300.0);
203 let result = ContentScale::Crop.scaled_size(src, dst);
204 assert_eq!(result, Size::new(600.0, 300.0));
205 }
206
207 #[test]
208 fn destination_rect_aligns_center() {
209 let src = Size::new(200.0, 100.0);
210 let dst = Size::new(300.0, 300.0);
211 let rect = destination_rect(src, dst, Alignment::CENTER, ContentScale::Fit);
212 assert_eq!(
213 rect,
214 Rect {
215 x: 0.0,
216 y: 75.0,
217 width: 300.0,
218 height: 150.0,
219 }
220 );
221 }
222}