Skip to main content

inset_rendering/
image.rs

1//! Flutter counterpart: `rendering/image.dart` (`RenderImage`).
2//!
3//! Sizing is the interesting part: an image with no width or height given takes its own size,
4//! and one given a single dimension keeps its proportions. That is what lets a picture sit in a
5//! layout without the author having to know how big the file is.
6//!
7//! The colour blend, the nine-patch `centerSlice`, colour inversion and the filter-quality
8//! choice Dart carries are not here; see `PORTING.md`.
9
10use inset_embedder::{Offset, Size, TextDirection};
11use inset_foundation::App;
12use inset_painting::{Alignment, BoxFit, ImageInfo, ImagePaintStyle, ImageRepeat, paint_image};
13
14use crate::box_::{BoxConstraints, RenderBox, RenderBoxData};
15use crate::object::{AnyRenderObject, RenderHandle, RenderObject, RenderObjectData};
16use crate::painting_context::PaintingContext;
17use inset_painting::ClipContext;
18
19/// Draws an image into the box it is given. Flutter `RenderImage`.
20pub struct RenderImage {
21    render_object: RenderObjectData,
22    render_box: RenderBoxData,
23    image: Option<ImageInfo>,
24    width: Option<f64>,
25    height: Option<f64>,
26    opacity: f64,
27    fit: Option<BoxFit>,
28    alignment: Alignment,
29    repeat: ImageRepeat,
30    match_text_direction: bool,
31    text_direction: Option<TextDirection>,
32}
33
34impl RenderImage {
35    /// Creates an image render object with no image yet, which lays out to nothing.
36    pub fn new(app: &mut App) -> RenderHandle<RenderImage> {
37        RenderHandle::new_box(
38            app,
39            RenderImage {
40                render_object: RenderObjectData::new(),
41                render_box: RenderBoxData::new(),
42                image: None,
43                width: None,
44                height: None,
45                opacity: 1.0,
46                fit: None,
47                alignment: Alignment::CENTER,
48                repeat: ImageRepeat::NoRepeat,
49                match_text_direction: false,
50                text_direction: None,
51            },
52        )
53    }
54
55    /// The image being drawn, if one has arrived.
56    pub fn image(self: RenderHandle<Self>, app: &App) -> Option<ImageInfo> {
57        self.get(app).image.clone()
58    }
59
60    /// Sets [`image`](Self::image).
61    ///
62    /// Laying out again is only needed while the size follows the image, which is whenever a
63    /// width or a height was left unset.
64    pub fn set_image(self: RenderHandle<Self>, app: &mut App, value: Option<ImageInfo>) {
65        if self.get(app).image == value {
66            return;
67        }
68        let follows_the_image = self.get(app).width.is_none() || self.get(app).height.is_none();
69        self.get_mut(app).image = value;
70        self.mark_needs_paint(app);
71        if follows_the_image {
72            self.mark_needs_layout(app);
73        }
74    }
75
76    /// The width to take, or `None` to follow the image.
77    pub fn width(self: RenderHandle<Self>, app: &App) -> Option<f64> {
78        self.get(app).width
79    }
80
81    /// Sets [`width`](Self::width).
82    pub fn set_width(self: RenderHandle<Self>, app: &mut App, value: Option<f64>) {
83        if self.get(app).width == value {
84            return;
85        }
86        self.get_mut(app).width = value;
87        self.mark_needs_layout(app);
88    }
89
90    /// The height to take, or `None` to follow the image.
91    pub fn height(self: RenderHandle<Self>, app: &App) -> Option<f64> {
92        self.get(app).height
93    }
94
95    /// Sets [`height`](Self::height).
96    pub fn set_height(self: RenderHandle<Self>, app: &mut App, value: Option<f64>) {
97        if self.get(app).height == value {
98            return;
99        }
100        self.get_mut(app).height = value;
101        self.mark_needs_layout(app);
102    }
103
104    /// How opaque the image is drawn, from zero to one.
105    pub fn opacity(self: RenderHandle<Self>, app: &App) -> f64 {
106        self.get(app).opacity
107    }
108
109    /// Sets [`opacity`](Self::opacity).
110    pub fn set_opacity(self: RenderHandle<Self>, app: &mut App, value: f64) {
111        if self.get(app).opacity == value {
112            return;
113        }
114        self.get_mut(app).opacity = value;
115        self.mark_needs_paint(app);
116    }
117
118    /// How the image is fitted to the box.
119    pub fn fit(self: RenderHandle<Self>, app: &App) -> Option<BoxFit> {
120        self.get(app).fit
121    }
122
123    /// Sets [`fit`](Self::fit).
124    pub fn set_fit(self: RenderHandle<Self>, app: &mut App, value: Option<BoxFit>) {
125        if self.get(app).fit == value {
126            return;
127        }
128        self.get_mut(app).fit = value;
129        self.mark_needs_paint(app);
130    }
131
132    /// Where the image sits when it does not fill the box.
133    pub fn alignment(self: RenderHandle<Self>, app: &App) -> Alignment {
134        self.get(app).alignment
135    }
136
137    /// Sets [`alignment`](Self::alignment).
138    pub fn set_alignment(self: RenderHandle<Self>, app: &mut App, value: Alignment) {
139        if self.get(app).alignment == value {
140            return;
141        }
142        self.get_mut(app).alignment = value;
143        self.mark_needs_paint(app);
144    }
145
146    /// What fills the space the image does not cover.
147    pub fn repeat(self: RenderHandle<Self>, app: &App) -> ImageRepeat {
148        self.get(app).repeat
149    }
150
151    /// Sets [`repeat`](Self::repeat).
152    pub fn set_repeat(self: RenderHandle<Self>, app: &mut App, value: ImageRepeat) {
153        if self.get(app).repeat == value {
154            return;
155        }
156        self.get_mut(app).repeat = value;
157        self.mark_needs_paint(app);
158    }
159
160    /// Whether the image is mirrored in a right-to-left layout, as a directional arrow is.
161    pub fn match_text_direction(self: RenderHandle<Self>, app: &App) -> bool {
162        self.get(app).match_text_direction
163    }
164
165    /// Sets [`match_text_direction`](Self::match_text_direction).
166    pub fn set_match_text_direction(self: RenderHandle<Self>, app: &mut App, value: bool) {
167        if self.get(app).match_text_direction == value {
168            return;
169        }
170        self.get_mut(app).match_text_direction = value;
171        self.mark_needs_paint(app);
172    }
173
174    /// The reading direction, which only matters with
175    /// [`match_text_direction`](Self::match_text_direction).
176    pub fn text_direction(self: RenderHandle<Self>, app: &App) -> Option<TextDirection> {
177        self.get(app).text_direction
178    }
179
180    /// Sets [`text_direction`](Self::text_direction).
181    pub fn set_text_direction(
182        self: RenderHandle<Self>,
183        app: &mut App,
184        value: Option<TextDirection>,
185    ) {
186        if self.get(app).text_direction == value {
187            return;
188        }
189        self.get_mut(app).text_direction = value;
190        self.mark_needs_paint(app);
191    }
192
193    /// The size this box takes under `constraints`. Dart's `_sizeForConstraints`.
194    ///
195    /// A width and a height both given make the size exact. Otherwise the image's own size is
196    /// constrained while its proportions are kept, which is what makes one given dimension
197    /// derive the other.
198    fn size_for_constraints(
199        self: RenderHandle<Self>,
200        app: &App,
201        constraints: BoxConstraints,
202    ) -> Size {
203        let held = self.get(app);
204        let constraints = BoxConstraints::tight_for(held.width, held.height).enforce(constraints);
205        let Some(image) = &held.image else {
206            return constraints.smallest();
207        };
208        constraints.constrain_size_and_attempt_to_preserve_aspect_ratio(image.size())
209    }
210
211    /// The style `paint_image` is called with, assembled from this object's fields.
212    fn paint_style(self: RenderHandle<Self>, app: &App) -> ImagePaintStyle {
213        let held = self.get(app);
214        let scale = held.image.as_ref().map_or(1.0, |image| image.scale);
215        ImagePaintStyle {
216            scale,
217            opacity: held.opacity,
218            fit: held.fit,
219            alignment: held.alignment,
220            repeat: held.repeat,
221            flip_horizontally: held.match_text_direction
222                && held.text_direction == Some(TextDirection::Rtl),
223        }
224    }
225}
226
227impl RenderObject for RenderImage {
228    crate::render_object_accessors!();
229
230    fn visit_children(
231        self: RenderHandle<Self>,
232        _app: &App,
233        _visitor: &mut dyn FnMut(AnyRenderObject),
234    ) {
235        // An image has no children.
236    }
237
238    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
239        let constraints = self.constraints(app);
240        let size = self.size_for_constraints(app, constraints);
241        self.set_size(app, size);
242    }
243
244    fn paint(
245        self: RenderHandle<Self>,
246        app: &mut App,
247        context: &mut PaintingContext,
248        offset: Offset,
249    ) {
250        let Some(image) = self.get(app).image.clone() else {
251            return;
252        };
253        let size = self.size(app);
254        let style = self.paint_style(app);
255        paint_image(context.canvas(), offset & size, &image.image, &style);
256    }
257}
258
259impl RenderBox for RenderImage {
260    crate::render_box_accessors!();
261
262    /// Dart returns zero for every intrinsic when a dimension is unset, because an image with no
263    /// constraint has no preferred size of its own to report.
264    fn compute_min_intrinsic_width(self: RenderHandle<Self>, app: &mut App, height: f64) -> f64 {
265        self.compute_max_intrinsic_width(app, height)
266    }
267
268    fn compute_max_intrinsic_width(self: RenderHandle<Self>, app: &mut App, height: f64) -> f64 {
269        debug_assert!(height >= 0.0);
270        if self.get(app).width.is_none() && self.get(app).height.is_none() {
271            return 0.0;
272        }
273        self.size_for_constraints(app, BoxConstraints::tight_for(None, Some(height)))
274            .width()
275    }
276
277    fn compute_min_intrinsic_height(self: RenderHandle<Self>, app: &mut App, width: f64) -> f64 {
278        self.compute_max_intrinsic_height(app, width)
279    }
280
281    fn compute_max_intrinsic_height(self: RenderHandle<Self>, app: &mut App, width: f64) -> f64 {
282        debug_assert!(width >= 0.0);
283        if self.get(app).width.is_none() && self.get(app).height.is_none() {
284            return 0.0;
285        }
286        self.size_for_constraints(app, BoxConstraints::tight_for(Some(width), None))
287            .height()
288    }
289
290    fn compute_dry_layout(
291        self: RenderHandle<Self>,
292        app: &mut App,
293        constraints: BoxConstraints,
294    ) -> Size {
295        self.size_for_constraints(app, constraints)
296    }
297
298    /// An image absorbs a hit anywhere in its box, as every painted box does.
299    fn hit_test_self(self: RenderHandle<Self>, _app: &App, _position: Offset) -> bool {
300        true
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use inset_embedder::test_support::solid_image;
307    use inset_foundation::AppCell;
308
309    use super::*;
310
311    /// A forty-by-twenty image, so a change of proportion is unmistakable.
312    fn wide_image(scale: f64) -> ImageInfo {
313        ImageInfo::new(solid_image([40, 20], [255, 0, 0, 255])).scale(scale)
314    }
315
316    fn loose(width: f64, height: f64) -> BoxConstraints {
317        BoxConstraints::loose(Size::new(width, height))
318    }
319
320    #[test]
321    fn an_image_that_has_not_arrived_takes_no_room() {
322        let cell = AppCell::new();
323        let mut app = cell.borrow_mut();
324        let render = RenderImage::new(&mut app);
325        assert_eq!(
326            render.compute_dry_layout(&mut app, loose(100.0, 100.0)),
327            Size::new(0.0, 0.0)
328        );
329    }
330
331    #[test]
332    fn an_image_with_no_size_given_takes_its_own() {
333        let cell = AppCell::new();
334        let mut app = cell.borrow_mut();
335        let render = RenderImage::new(&mut app);
336        render.set_image(&mut app, Some(wide_image(1.0)));
337        assert_eq!(
338            render.compute_dry_layout(&mut app, loose(1000.0, 1000.0)),
339            Size::new(40.0, 20.0)
340        );
341    }
342
343    #[test]
344    fn a_scaled_image_takes_fewer_points_than_it_has_pixels() {
345        let cell = AppCell::new();
346        let mut app = cell.borrow_mut();
347        let render = RenderImage::new(&mut app);
348        // A two-times asset: the same pixels over half the points.
349        render.set_image(&mut app, Some(wide_image(2.0)));
350        assert_eq!(
351            render.compute_dry_layout(&mut app, loose(1000.0, 1000.0)),
352            Size::new(20.0, 10.0)
353        );
354    }
355
356    #[test]
357    fn one_given_dimension_derives_the_other_from_the_proportions() {
358        let cell = AppCell::new();
359        let mut app = cell.borrow_mut();
360        let render = RenderImage::new(&mut app);
361        render.set_image(&mut app, Some(wide_image(1.0)));
362
363        render.set_width(&mut app, Some(80.0));
364        assert_eq!(
365            render.compute_dry_layout(&mut app, loose(1000.0, 1000.0)),
366            Size::new(80.0, 40.0)
367        );
368
369        render.set_width(&mut app, None);
370        render.set_height(&mut app, Some(5.0));
371        assert_eq!(
372            render.compute_dry_layout(&mut app, loose(1000.0, 1000.0)),
373            Size::new(10.0, 5.0)
374        );
375    }
376
377    #[test]
378    fn both_dimensions_given_make_the_size_exact() {
379        let cell = AppCell::new();
380        let mut app = cell.borrow_mut();
381        let render = RenderImage::new(&mut app);
382        render.set_image(&mut app, Some(wide_image(1.0)));
383        render.set_width(&mut app, Some(7.0));
384        render.set_height(&mut app, Some(9.0));
385        assert_eq!(
386            render.compute_dry_layout(&mut app, loose(1000.0, 1000.0)),
387            Size::new(7.0, 9.0)
388        );
389    }
390
391    #[test]
392    fn a_box_too_small_for_the_image_constrains_it() {
393        let cell = AppCell::new();
394        let mut app = cell.borrow_mut();
395        let render = RenderImage::new(&mut app);
396        render.set_image(&mut app, Some(wide_image(1.0)));
397        // Half the width available, so the proportions carry the height down with it.
398        assert_eq!(
399            render.compute_dry_layout(&mut app, loose(20.0, 1000.0)),
400            Size::new(20.0, 10.0)
401        );
402    }
403
404    #[test]
405    fn an_image_with_no_dimension_given_reports_no_intrinsic_size() {
406        let cell = AppCell::new();
407        let mut app = cell.borrow_mut();
408        let render = RenderImage::new(&mut app);
409        render.set_image(&mut app, Some(wide_image(1.0)));
410        // Dart's rule: without a width or a height there is no preferred size to report.
411        assert_eq!(render.compute_max_intrinsic_width(&mut app, 100.0), 0.0);
412        assert_eq!(render.compute_max_intrinsic_height(&mut app, 100.0), 0.0);
413
414        render.set_width(&mut app, Some(80.0));
415        assert_eq!(render.compute_max_intrinsic_height(&mut app, 80.0), 40.0);
416    }
417}