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(self, radius: f32) -> Self {
49 self.with_border_radius(BorderRadius::all(radius))
50 }
51
52 pub fn with_border_radius(mut self, radius: BorderRadius) -> Self {
54 self.radius = BorderRadius {
57 top_left: radius.top_left.max(0.0),
58 top_right: radius.top_right.max(0.0),
59 bottom_right: radius.bottom_right.max(0.0),
60 bottom_left: radius.bottom_left.max(0.0),
61 };
62 self
63 }
64}
65
66impl Component for Image {
67 fn view(&self) -> RenderNode {
68 let r = self.leaf.rect.get();
69 let r_local = Rect {
70 x: 0.0,
71 y: 0.0,
72 width: r.width,
73 height: r.height,
74 };
75 let data = (self.data)();
76 let (content, clip) = geometry_core::fit_rect(
77 (data.width as f32, data.height as f32),
78 r_local,
79 (self.fit)(),
80 );
81 let image = RenderNode::Primitive(DrawCommand::Image {
82 data,
83 rect: content,
84 filter: (self.filter)(),
85 });
86 let node = if clip || !self.radius.is_zero() {
88 RenderNode::Clip {
89 rect: r_local,
90 radius: self.radius,
91 children: NodeVec::collect([image]),
92 }
93 } else {
94 image
95 };
96 self.leaf.at_layout_position(node)
97 }
98
99 fn on_event(&mut self, _event: &Event) -> EventResult {
100 EventResult::Ignored
101 }
102
103 fn debug_name(&self) -> &'static str {
104 "Image"
105 }
106}
107
108impl_leaf_widget!(Image);
109
110#[cfg(test)]
111mod tests {
112 use crate::context::reset_layout_runtime;
113 use layout_core::AvailableSpace;
114
115 use super::*;
116 use crate::context::{compute_layout, new_container};
117 use crate::layout_item::LayoutItem;
118
119 #[test]
120 fn image_without_size_uses_intrinsic_size() {
121 reset_layout_runtime();
122 let data = Arc::new(ImageData::new(vec![0u8; 40 * 20 * 4], 40, 20));
123 let image = Image::new(
124 LayoutStyle::new(),
125 move || Arc::clone(&data),
126 || ImageFilter::Linear,
127 || ObjectFit::Contain,
128 )
129 .unwrap();
130 let root = new_container(
131 LayoutStyle::new().flex_column().width(200.0).height(200.0),
132 &[image.layout_node()],
133 )
134 .unwrap();
135 compute_layout(
136 root,
137 AvailableSpace::Definite(200.0),
138 AvailableSpace::Definite(200.0),
139 )
140 .unwrap();
141
142 let rect = image.leaf.rect.get();
143 assert_eq!(rect.width, 40.0);
144 assert_eq!(rect.height, 20.0);
145 }
146
147 #[test]
150 fn a_radius_clips_the_picture_whatever_the_fit() {
151 reset_layout_runtime();
152 let data = Arc::new(ImageData::new(vec![0u8; 40 * 20 * 4], 40, 20));
153 let image = Image::new(
154 LayoutStyle::new().width(40.0).height(20.0),
155 move || Arc::clone(&data),
156 || ImageFilter::Linear,
157 || ObjectFit::Contain,
158 )
159 .unwrap()
160 .with_radius(6.0);
161
162 let radius_of = |node: &RenderNode| -> Option<BorderRadius> {
163 let mut current = node;
164 loop {
165 match current {
166 RenderNode::Clip { radius, .. } => return Some(*radius),
167 RenderNode::Transform { children, .. } | RenderNode::Layer { children, .. } => {
168 current = children.first()?;
169 }
170 _ => return None,
171 }
172 }
173 };
174 assert_eq!(
175 radius_of(&image.view()).map(|r| r.top_left),
176 Some(6.0),
177 "the clip carries the radius the caller asked for"
178 );
179 }
180
181 #[test]
182 fn image_single_side_derives_aspect() {
183 reset_layout_runtime();
184 let data = Arc::new(ImageData::new(vec![0u8; 40 * 20 * 4], 40, 20));
185 let image = Image::new(
187 LayoutStyle::new().width(100.0),
188 move || Arc::clone(&data),
189 || ImageFilter::Linear,
190 || ObjectFit::Contain,
191 )
192 .unwrap();
193 let root = new_container(
194 LayoutStyle::new().flex_column().width(300.0).height(300.0),
195 &[image.layout_node()],
196 )
197 .unwrap();
198 compute_layout(
199 root,
200 AvailableSpace::Definite(300.0),
201 AvailableSpace::Definite(300.0),
202 )
203 .unwrap();
204
205 let rect = image.leaf.rect.get();
206 assert_eq!(rect.width, 100.0);
207 assert_eq!(rect.height, 50.0);
208 }
209}