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