zng_wgt_image/
node.rs

1//! UI nodes used for building the image widget.
2
3use std::mem;
4
5use zng_ext_image::{IMAGES, ImageCacheMode, ImagePpi, ImageRenderArgs};
6use zng_wgt_stack::stack_nodes_layout_by;
7
8use super::image_properties::{
9    IMAGE_ALIGN_VAR, IMAGE_CACHE_VAR, IMAGE_CROP_VAR, IMAGE_DOWNSCALE_VAR, IMAGE_ERROR_FN_VAR, IMAGE_FIT_VAR, IMAGE_LIMITS_VAR,
10    IMAGE_LOADING_FN_VAR, IMAGE_OFFSET_VAR, IMAGE_RENDERING_VAR, IMAGE_SCALE_FACTOR_VAR, IMAGE_SCALE_PPI_VAR, IMAGE_SCALE_VAR, ImageFit,
11    ImgErrorArgs, ImgLoadingArgs,
12};
13
14use super::*;
15
16context_var! {
17    /// Image acquired by [`image_source`], or `"no image source in context"` error by default.
18    ///
19    /// [`image_source`]: fn@image_source
20    pub static CONTEXT_IMAGE_VAR: Img = no_context_image();
21}
22fn no_context_image() -> Img {
23    Img::dummy(Some(Txt::from_static("no image source in context")))
24}
25
26/// Requests an image from [`IMAGES`] and sets [`CONTEXT_IMAGE_VAR`].
27///
28/// Caches the image if [`img_cache`] is `true` in the context.
29///
30/// The image is not rendered by this property, the [`image_presenter`] renders the image in [`CONTEXT_IMAGE_VAR`].
31///
32/// In a widget this should be placed inside context properties and before event properties.
33///
34/// [`img_cache`]: fn@crate::img_cache
35/// [`IMAGES`]: zng_ext_image::IMAGES
36pub fn image_source(child: impl UiNode, source: impl IntoVar<ImageSource>) -> impl UiNode {
37    let source = source.into_var();
38    let ctx_img = var(Img::dummy(None));
39    let child = with_context_var(child, CONTEXT_IMAGE_VAR, ctx_img.read_only());
40    let mut img = var(Img::dummy(None)).read_only();
41    let mut _ctx_binding = None;
42
43    match_node(child, move |child, op| match op {
44        UiNodeOp::Init => {
45            WIDGET.sub_var(&source).sub_var(&IMAGE_CACHE_VAR).sub_var(&IMAGE_DOWNSCALE_VAR);
46
47            let mode = if IMAGE_CACHE_VAR.get() {
48                ImageCacheMode::Cache
49            } else {
50                ImageCacheMode::Ignore
51            };
52            let limits = IMAGE_LIMITS_VAR.get();
53            let downscale = IMAGE_DOWNSCALE_VAR.get();
54
55            let mut source = source.get();
56            if let ImageSource::Render(_, args) = &mut source {
57                *args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
58            }
59            img = IMAGES.image(source, mode, limits, downscale, None);
60
61            ctx_img.set_from(&img);
62            _ctx_binding = Some(img.bind(&ctx_img));
63        }
64        UiNodeOp::Deinit => {
65            child.deinit();
66
67            ctx_img.set(no_context_image());
68            img = var(no_context_image()).read_only();
69            _ctx_binding = None;
70        }
71        UiNodeOp::Update { .. } => {
72            if source.is_new() || IMAGE_DOWNSCALE_VAR.is_new() {
73                // source update:
74
75                let mut source = source.get();
76
77                if let ImageSource::Render(_, args) = &mut source {
78                    *args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
79                }
80
81                let mode = if IMAGE_CACHE_VAR.get() {
82                    ImageCacheMode::Cache
83                } else {
84                    ImageCacheMode::Ignore
85                };
86                let limits = IMAGE_LIMITS_VAR.get();
87                let downscale = IMAGE_DOWNSCALE_VAR.get();
88
89                img = IMAGES.image(source, mode, limits, downscale, None);
90
91                ctx_img.set_from(&img);
92                _ctx_binding = Some(img.bind(&ctx_img));
93            } else if let Some(enabled) = IMAGE_CACHE_VAR.get_new() {
94                // cache-mode update:
95                let is_cached = ctx_img.with(|img| IMAGES.is_cached(img));
96                if enabled != is_cached {
97                    img = if is_cached {
98                        // must not cache, but is cached, detach from cache.
99
100                        let img = mem::replace(&mut img, var(Img::dummy(None)).read_only());
101                        IMAGES.detach(img)
102                    } else {
103                        // must cache, but image is not cached, get source again.
104
105                        let source = source.get();
106                        let limits = IMAGE_LIMITS_VAR.get();
107                        let downscale = IMAGE_DOWNSCALE_VAR.get();
108                        IMAGES.image(source, ImageCacheMode::Cache, limits, downscale, None)
109                    };
110
111                    ctx_img.set_from(&img);
112                    _ctx_binding = Some(img.bind(&ctx_img));
113                }
114            }
115        }
116        _ => {}
117    })
118}
119
120context_local! {
121    /// Used to avoid recursion in [`image_error_presenter`].
122    static IN_ERROR_VIEW: bool = false;
123    /// Used to avoid recursion in [`image_loading_presenter`].
124    static IN_LOADING_VIEW: bool = false;
125}
126
127/// Presents the contextual [`IMAGE_ERROR_FN_VAR`] if the [`CONTEXT_IMAGE_VAR`] is an error.
128///
129/// The error view is rendered under the `child`.
130///
131/// The image widget adds this node around the [`image_presenter`] node.
132pub fn image_error_presenter(child: impl UiNode) -> impl UiNode {
133    let view = presenter_opt(
134        CONTEXT_IMAGE_VAR.map(|i| i.error().map(|e| ImgErrorArgs { error: e })),
135        IMAGE_ERROR_FN_VAR.map(|f| {
136            wgt_fn!(f, |e| {
137                if IN_ERROR_VIEW.get_clone() {
138                    NilUiNode.boxed()
139                } else {
140                    with_context_local(f(e), &IN_ERROR_VIEW, true).boxed()
141                }
142            })
143        }),
144    );
145
146    stack_nodes_layout_by(ui_vec![view, child], 1, |constraints, _, img_size| {
147        if img_size == PxSize::zero() {
148            constraints
149        } else {
150            PxConstraints2d::new_fill_size(img_size)
151        }
152    })
153}
154
155/// Presents the contextual [`IMAGE_LOADING_FN_VAR`] if the [`CONTEXT_IMAGE_VAR`] is loading.
156///
157/// The loading view is rendered under the `child`.
158///
159/// The image widget adds this node around the [`image_error_presenter`] node.
160pub fn image_loading_presenter(child: impl UiNode) -> impl UiNode {
161    let view = presenter_opt(
162        CONTEXT_IMAGE_VAR.map(|i| if i.is_loading() { Some(ImgLoadingArgs {}) } else { None }),
163        IMAGE_LOADING_FN_VAR.map(|f| {
164            wgt_fn!(f, |a| {
165                if IN_LOADING_VIEW.get_clone() {
166                    NilUiNode.boxed()
167                } else {
168                    with_context_local(f(a), &IN_LOADING_VIEW, true).boxed()
169                }
170            })
171        }),
172    );
173
174    stack_nodes_layout_by(ui_vec![view, child], 1, |constraints, _, img_size| {
175        if img_size == PxSize::zero() {
176            constraints
177        } else {
178            PxConstraints2d::new_fill_size(img_size)
179        }
180    })
181}
182
183/// Renders the [`CONTEXT_IMAGE_VAR`] if set.
184///
185/// This is the inner-most node of an image widget, it is fully configured by context variables:
186///
187/// * [`CONTEXT_IMAGE_VAR`]: Defines the image to render.
188/// * [`IMAGE_CROP_VAR`]: Clip the image before layout.
189/// * [`IMAGE_SCALE_PPI_VAR`]: If the image desired size is scaled by PPI.
190/// * [`IMAGE_SCALE_FACTOR_VAR`]: If the image desired size is scaled by the screen scale factor.
191/// * [`IMAGE_SCALE_VAR`]: Custom scale applied to the desired size.
192/// * [`IMAGE_FIT_VAR`]: Defines the image final size.
193/// * [`IMAGE_ALIGN_VAR`]: Defines the image alignment in the presenter final size.
194/// * [`IMAGE_RENDERING_VAR`]: Defines the image resize algorithm used in the GPU.
195/// * [`IMAGE_OFFSET_VAR`]: Defines an offset applied to the image after all measure and arrange.
196pub fn image_presenter() -> impl UiNode {
197    let mut img_size = PxSize::zero();
198    let mut render_clip = PxRect::zero();
199    let mut render_img_size = PxSize::zero();
200    let mut render_tile_size = PxSize::zero();
201    let mut render_tile_spacing = PxSize::zero();
202    let mut render_offset = PxVector::zero();
203    let spatial_id = SpatialFrameId::new_unique();
204
205    match_node_leaf(move |op| match op {
206        UiNodeOp::Init => {
207            WIDGET
208                .sub_var(&CONTEXT_IMAGE_VAR)
209                .sub_var_layout(&IMAGE_CROP_VAR)
210                .sub_var_layout(&IMAGE_SCALE_PPI_VAR)
211                .sub_var_layout(&IMAGE_SCALE_FACTOR_VAR)
212                .sub_var_layout(&IMAGE_SCALE_VAR)
213                .sub_var_layout(&IMAGE_FIT_VAR)
214                .sub_var_layout(&IMAGE_ALIGN_VAR)
215                .sub_var_layout(&IMAGE_OFFSET_VAR)
216                .sub_var_layout(&IMAGE_REPEAT_VAR)
217                .sub_var_layout(&IMAGE_REPEAT_SPACING_VAR)
218                .sub_var_render(&IMAGE_RENDERING_VAR);
219
220            img_size = CONTEXT_IMAGE_VAR.with(Img::size);
221        }
222        UiNodeOp::Update { .. } => {
223            if let Some(img) = CONTEXT_IMAGE_VAR.get_new() {
224                let ig_size = img.size();
225                if img_size != ig_size {
226                    img_size = ig_size;
227                    WIDGET.layout();
228                } else if img.is_loaded() {
229                    WIDGET.render();
230                }
231            }
232        }
233        UiNodeOp::Measure { desired_size, .. } => {
234            // Similar to `layout` Part 1.
235
236            let metrics = LAYOUT.metrics();
237
238            let mut scale = IMAGE_SCALE_VAR.get();
239            if IMAGE_SCALE_PPI_VAR.get() {
240                let sppi = metrics.screen_ppi();
241                let ippi = CONTEXT_IMAGE_VAR.with(Img::ppi).unwrap_or(ImagePpi::splat(sppi.0));
242                scale *= Factor2d::new(sppi.0 / ippi.x, sppi.0 / ippi.y);
243            }
244            if IMAGE_SCALE_FACTOR_VAR.get() {
245                scale *= metrics.scale_factor();
246            }
247
248            let img_rect = PxRect::from_size(img_size);
249            let crop = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(img_size), || {
250                let mut r = IMAGE_CROP_VAR.get();
251                r.replace_default(&img_rect.into());
252                r.layout()
253            });
254            let render_clip = img_rect.intersection(&crop).unwrap_or_default() * scale;
255
256            let min_size = metrics.constraints().clamp_size(render_clip.size);
257            let wgt_ratio = metrics.constraints().with_min_size(min_size).fill_ratio(render_clip.size);
258
259            *desired_size = metrics.constraints().fill_size_or(wgt_ratio);
260        }
261        UiNodeOp::Layout { final_size, .. } => {
262            // Part 1 - Scale & Crop
263            // - Starting from the image pixel size, apply scaling then crop.
264
265            let metrics = LAYOUT.metrics();
266
267            let mut scale = IMAGE_SCALE_VAR.get();
268            if IMAGE_SCALE_PPI_VAR.get() {
269                let sppi = metrics.screen_ppi();
270                let ippi = CONTEXT_IMAGE_VAR.with(Img::ppi).unwrap_or(ImagePpi::splat(sppi.0));
271                scale *= Factor2d::new(sppi.0 / ippi.x, sppi.0 / ippi.y);
272            }
273            if IMAGE_SCALE_FACTOR_VAR.get() {
274                scale *= metrics.scale_factor();
275            }
276
277            // webrender needs the full image size, we offset and clip it to render the final image.
278            let mut r_img_size = img_size * scale;
279
280            // crop is relative to the unscaled pixel size, then applied scaled as the clip.
281            let img_rect = PxRect::from_size(img_size);
282            let crop = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(img_size), || {
283                let mut r = IMAGE_CROP_VAR.get();
284                r.replace_default(&img_rect.into());
285                r.layout()
286            });
287            let mut r_clip = img_rect.intersection(&crop).unwrap_or_default() * scale;
288            let mut r_offset = -r_clip.origin.to_vector();
289
290            // Part 2 - Fit, Align & Clip
291            // - Fit the cropped and scaled image to the constraints, add a bounds clip to the crop clip.
292
293            let mut align = IMAGE_ALIGN_VAR.get();
294
295            let min_size = metrics.constraints().clamp_size(r_clip.size);
296            let wgt_ratio = metrics.constraints().with_min_size(min_size).fill_ratio(r_clip.size);
297            let wgt_size = metrics.constraints().fill_size_or(wgt_ratio);
298
299            let mut fit = IMAGE_FIT_VAR.get();
300            if let ImageFit::ScaleDown = fit {
301                if r_clip.size.width < wgt_size.width && r_clip.size.height < wgt_size.height {
302                    fit = ImageFit::None;
303                } else {
304                    fit = ImageFit::Contain;
305                }
306            }
307            match fit {
308                ImageFit::Fill => {
309                    align = Align::FILL;
310                }
311                ImageFit::Contain => {
312                    let container = wgt_size.to_f32();
313                    let content = r_clip.size.to_f32();
314                    let scale = (container.width / content.width).min(container.height / content.height).fct();
315                    r_clip *= scale;
316                    r_img_size *= scale;
317                    r_offset *= scale;
318                }
319                ImageFit::Cover => {
320                    let container = wgt_size.to_f32();
321                    let content = r_clip.size.to_f32();
322                    let scale = (container.width / content.width).max(container.height / content.height).fct();
323                    r_clip *= scale;
324                    r_img_size *= scale;
325                    r_offset *= scale;
326                }
327                ImageFit::None => {}
328                ImageFit::ScaleDown => unreachable!(),
329            }
330
331            if align.is_fill_x() {
332                let factor = wgt_size.width.0 as f32 / r_clip.size.width.0 as f32;
333                r_clip.size.width = wgt_size.width;
334                r_clip.origin.x *= factor;
335                r_img_size.width *= factor;
336                r_offset.x = -r_clip.origin.x;
337            } else {
338                let diff = wgt_size.width - r_clip.size.width;
339                let offset = diff * align.x(metrics.direction());
340                r_offset.x += offset;
341                if diff < Px(0) {
342                    r_clip.origin.x -= offset;
343                    r_clip.size.width += diff;
344                }
345            }
346            if align.is_fill_y() {
347                let factor = wgt_size.height.0 as f32 / r_clip.size.height.0 as f32;
348                r_clip.size.height = wgt_size.height;
349                r_clip.origin.y *= factor;
350                r_img_size.height *= factor;
351                r_offset.y = -r_clip.origin.y;
352            } else {
353                let diff = wgt_size.height - r_clip.size.height;
354                let offset = diff * align.y();
355                r_offset.y += offset;
356                if diff < Px(0) {
357                    r_clip.origin.y -= offset;
358                    r_clip.size.height += diff;
359                }
360            }
361
362            // Part 3 - Custom Offset and Update
363            let offset = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(wgt_size), || IMAGE_OFFSET_VAR.layout());
364            if offset != PxVector::zero() {
365                r_offset += offset;
366
367                let screen_clip = PxRect::new(-r_offset.to_point(), wgt_size);
368                r_clip.origin -= offset;
369                r_clip = r_clip.intersection(&screen_clip).unwrap_or_default();
370            }
371
372            // Part 4 - Repeat
373            let mut r_tile_size = r_img_size;
374            let mut r_tile_spacing = PxSize::zero();
375            if matches!(IMAGE_REPEAT_VAR.get(), ImageRepeat::Repeat) {
376                r_clip = PxRect::from_size(wgt_size);
377                r_tile_size = r_img_size;
378                r_img_size = wgt_size;
379                r_offset = PxVector::zero();
380
381                let leftover = tile_leftover(r_tile_size, wgt_size);
382                r_tile_spacing = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(r_tile_size), || {
383                    LAYOUT.with_leftover(Some(leftover.width), Some(leftover.height), || IMAGE_REPEAT_SPACING_VAR.layout())
384                });
385            }
386
387            if render_clip != r_clip
388                || render_img_size != r_img_size
389                || render_offset != r_offset
390                || render_tile_size != r_tile_size
391                || render_tile_spacing != r_tile_spacing
392            {
393                render_clip = r_clip;
394                render_img_size = r_img_size;
395                render_offset = r_offset;
396                render_tile_size = r_tile_size;
397                render_tile_spacing = r_tile_spacing;
398                WIDGET.render();
399            }
400
401            *final_size = wgt_size;
402        }
403        UiNodeOp::Render { frame } => {
404            CONTEXT_IMAGE_VAR.with(|img| {
405                if img.is_loaded() && !img_size.is_empty() && !render_clip.is_empty() {
406                    if render_offset != PxVector::zero() {
407                        let transform = PxTransform::from(render_offset);
408                        frame.push_reference_frame(spatial_id.into(), FrameValue::Value(transform), true, false, |frame| {
409                            frame.push_image(
410                                render_clip,
411                                render_img_size,
412                                render_tile_size,
413                                render_tile_spacing,
414                                img,
415                                IMAGE_RENDERING_VAR.get(),
416                            )
417                        });
418                    } else {
419                        frame.push_image(
420                            render_clip,
421                            render_img_size,
422                            render_tile_size,
423                            render_tile_spacing,
424                            img,
425                            IMAGE_RENDERING_VAR.get(),
426                        );
427                    }
428                }
429            });
430        }
431        _ => {}
432    })
433}
434
435fn tile_leftover(tile_size: PxSize, wgt_size: PxSize) -> PxSize {
436    if tile_size.is_empty() || wgt_size.is_empty() {
437        return PxSize::zero();
438    }
439
440    let full_leftover_x = wgt_size.width % tile_size.width;
441    let full_leftover_y = wgt_size.height % tile_size.height;
442    let full_tiles_x = wgt_size.width / tile_size.width;
443    let full_tiles_y = wgt_size.height / tile_size.height;
444    let spaces_x = full_tiles_x - Px(1);
445    let spaces_y = full_tiles_y - Px(1);
446    PxSize::new(
447        if spaces_x > Px(0) { full_leftover_x / spaces_x } else { Px(0) },
448        if spaces_y > Px(0) { full_leftover_y / spaces_y } else { Px(0) },
449    )
450}