zng_wgt_image/
node.rs

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