1use std::sync::Arc;
2
3use geometry_core::{ObjectFit, Rect};
4use layout_core::{LayoutError, LayoutStyle};
5use platform_core::Event;
6use renderer_core::{BorderRadius, DrawCommand, ImageData, ImageFilter};
7use ui_tree::{Component, EventResult, NodeVec, RenderNode};
8
9use crate::impl_leaf_widget;
10use crate::layout_leaf::LayoutLeaf;
11
12pub struct Image {
13 data: Box<dyn Fn() -> Arc<ImageData>>,
14 leaf: LayoutLeaf,
15 filter: Box<dyn Fn() -> ImageFilter>,
16 fit: Box<dyn Fn() -> ObjectFit>,
17 radius: BorderRadius,
18}
19
20impl Image {
21 pub fn new(
22 layout_style: LayoutStyle,
23 data_fn: impl Fn() -> Arc<ImageData> + 'static,
24 filter_fn: impl Fn() -> ImageFilter + 'static,
25 fit_fn: impl Fn() -> ObjectFit + 'static,
26 ) -> Result<Self, LayoutError> {
27 let layout_style = crate::layout_leaf::resolve_intrinsic_size(layout_style, || {
29 let d = data_fn();
30 (d.width as f32, d.height as f32)
31 });
32
33 let leaf = LayoutLeaf::register(layout_style)?;
34 Ok(Self {
35 data: Box::new(data_fn),
36 leaf,
37 filter: Box::new(filter_fn),
38 fit: Box::new(fit_fn),
39 radius: BorderRadius::zero(),
40 })
41 }
42
43 pub fn with_radius(mut self, radius: f32) -> Self {
49 self.radius = BorderRadius::all(radius.max(0.0));
50 self
51 }
52
53 pub fn with_border_radius(mut self, radius: BorderRadius) -> Self {
55 self.radius = radius;
56 self
57 }
58}
59
60impl Component for Image {
61 fn view(&self) -> RenderNode {
62 let r = self.leaf.rect.get();
63 let r_local = Rect {
64 x: 0.0,
65 y: 0.0,
66 width: r.width,
67 height: r.height,
68 };
69 let data = (self.data)();
70 let (content, clip) = geometry_core::fit_rect(
71 (data.width as f32, data.height as f32),
72 r_local,
73 (self.fit)(),
74 );
75 let image = RenderNode::Primitive(DrawCommand::Image {
76 data,
77 rect: content,
78 filter: (self.filter)(),
79 });
80 let node = if clip || !self.radius.is_zero() {
82 RenderNode::Clip {
83 rect: r_local,
84 radius: self.radius,
85 children: NodeVec::collect([image]),
86 }
87 } else {
88 image
89 };
90 self.leaf.at_layout_position(node)
91 }
92
93 fn on_event(&mut self, _event: &Event) -> EventResult {
94 EventResult::Ignored
95 }
96
97 fn debug_name(&self) -> &'static str {
98 "Image"
99 }
100}
101
102impl_leaf_widget!(Image);
103
104#[cfg(test)]
105mod tests {
106 use crate::context::reset_layout_runtime;
107 use layout_core::AvailableSpace;
108
109 use super::*;
110 use crate::context::{compute_layout, new_container};
111 use crate::layout_item::LayoutItem;
112
113 #[test]
114 fn image_without_size_uses_intrinsic_size() {
115 reset_layout_runtime();
116 let data = Arc::new(ImageData::new(vec![0u8; 40 * 20 * 4], 40, 20));
117 let image = Image::new(
118 LayoutStyle::new(),
119 move || Arc::clone(&data),
120 || ImageFilter::Linear,
121 || ObjectFit::Contain,
122 )
123 .unwrap();
124 let root = new_container(
125 LayoutStyle::new().flex_column().width(200.0).height(200.0),
126 &[image.layout_node()],
127 )
128 .unwrap();
129 compute_layout(
130 root,
131 AvailableSpace::Definite(200.0),
132 AvailableSpace::Definite(200.0),
133 )
134 .unwrap();
135
136 let rect = image.leaf.rect.get();
137 assert_eq!(rect.width, 40.0);
138 assert_eq!(rect.height, 20.0);
139 }
140
141 #[test]
144 fn a_radius_clips_the_picture_whatever_the_fit() {
145 reset_layout_runtime();
146 let data = Arc::new(ImageData::new(vec![0u8; 40 * 20 * 4], 40, 20));
147 let image = Image::new(
148 LayoutStyle::new().width(40.0).height(20.0),
149 move || Arc::clone(&data),
150 || ImageFilter::Linear,
151 || ObjectFit::Contain,
152 )
153 .unwrap()
154 .with_radius(6.0);
155
156 let radius_of = |node: &RenderNode| -> Option<BorderRadius> {
157 let mut current = node;
158 loop {
159 match current {
160 RenderNode::Clip { radius, .. } => return Some(*radius),
161 RenderNode::Transform { children, .. } | RenderNode::Layer { children, .. } => {
162 current = children.first()?;
163 }
164 _ => return None,
165 }
166 }
167 };
168 assert_eq!(
169 radius_of(&image.view()).map(|r| r.top_left),
170 Some(6.0),
171 "the clip carries the radius the caller asked for"
172 );
173 }
174
175 #[test]
176 fn image_single_side_derives_aspect() {
177 reset_layout_runtime();
178 let data = Arc::new(ImageData::new(vec![0u8; 40 * 20 * 4], 40, 20));
179 let image = Image::new(
181 LayoutStyle::new().width(100.0),
182 move || Arc::clone(&data),
183 || ImageFilter::Linear,
184 || ObjectFit::Contain,
185 )
186 .unwrap();
187 let root = new_container(
188 LayoutStyle::new().flex_column().width(300.0).height(300.0),
189 &[image.layout_node()],
190 )
191 .unwrap();
192 compute_layout(
193 root,
194 AvailableSpace::Definite(300.0),
195 AvailableSpace::Definite(300.0),
196 )
197 .unwrap();
198
199 let rect = image.leaf.rect.get();
200 assert_eq!(rect.width, 100.0);
201 assert_eq!(rect.height, 50.0);
202 }
203}