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