zng_wgt_image/
image_properties.rs

1use super::*;
2use std::fmt;
3
4use node::CONTEXT_IMAGE_VAR;
5use zng_app::render::ImageRendering;
6use zng_ext_image::{ImageDownscaleMode, ImageEntriesMode, ImageLimits};
7use zng_ext_window::WINDOW_Ext as _;
8use zng_wgt_window::BlockWindowLoad;
9
10/// Image layout mode.
11///
12/// This layout mode can be set to all images inside a widget using [`img_fit`], the [`image_presenter`] uses this value
13/// to calculate the image final size.
14///
15/// The image desired size is its original size, either in pixels or DIPs after cropping and scaling.
16///
17/// [`img_fit`]: fn@img_fit
18/// [`image_presenter`]: crate::node::image_presenter
19#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub enum ImageFit {
21    /// The image original size is preserved, the image is clipped if larger then the final size.
22    None,
23    /// The image is resized to fill the final size, the aspect-ratio is not preserved.
24    Fill,
25    /// The image is resized to fit the final size, preserving the aspect-ratio.
26    Contain,
27    /// The image is resized to fill the final size while preserving the aspect-ratio.
28    /// If the aspect ratio of the final size differs from the image, it is clipped.
29    Cover,
30    /// If the image is smaller then the final size applies the [`None`] layout, if its larger applies the [`Contain`] layout.
31    ///
32    /// [`None`]: ImageFit::None
33    /// [`Contain`]: ImageFit::Contain
34    ScaleDown,
35}
36impl fmt::Debug for ImageFit {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        if f.alternate() {
39            write!(f, "ImageFit::")?
40        }
41        match self {
42            Self::None => write!(f, "None"),
43            Self::Fill => write!(f, "Fill"),
44            Self::Contain => write!(f, "Contain"),
45            Self::Cover => write!(f, "Cover"),
46            Self::ScaleDown => write!(f, "ScaleDown"),
47        }
48    }
49}
50
51/// Image repeat mode.
52///
53/// After the image is fit, aligned, offset and clipped the final image can be repeated
54/// to fill any blank space by enabling [`img_repeat`] with one of these options.
55///
56/// [`img_repeat`]: fn@img_repeat
57#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58pub enum ImageRepeat {
59    /// The image is only rendered once.
60    None,
61    /// The image is repeated to fill empty space, border copies are clipped.
62    Repeat,
63}
64impl fmt::Debug for ImageRepeat {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        if f.alternate() {
67            write!(f, "ImageRepeat::")?
68        }
69        match self {
70            Self::None => write!(f, "None"),
71            Self::Repeat => write!(f, "Repeat"),
72        }
73    }
74}
75impl_from_and_into_var! {
76    fn from(repeat: bool) -> ImageRepeat {
77        if repeat { ImageRepeat::Repeat } else { ImageRepeat::None }
78    }
79}
80
81/// Image auto scale mode.
82///
83/// Images can be auto scaled to display a a size normalized across screens.
84#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85pub enum ImageAutoScale {
86    /// Don't auto scale, image is sized by pixel size.
87    ///
88    /// Note that most widgets are scaled by the screen factor (DIP units), so the image will appear to have
89    /// different sizes depending on the screen.
90    Pixel,
91    /// Image is scaled by the screen scale factor, similar to DIP unit.
92    ///
93    /// This is the default, it makes the image appear to have the same proportional size as other widgets independent
94    /// of the screen that is displaying it.
95    #[default]
96    Factor,
97    /// Image is scaled by the physical size metadata of the image and the screen with the intent of displaying
98    /// at the *print size*.
99    Density,
100}
101impl fmt::Debug for ImageAutoScale {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        if f.alternate() {
104            write!(f, "ImageAutoScale::")?
105        }
106        match self {
107            Self::Pixel => write!(f, "Pixel"),
108            Self::Factor => write!(f, "Factor"),
109            Self::Density => write!(f, "Density"),
110        }
111    }
112}
113impl_from_and_into_var! {
114    fn from(factor: bool) -> ImageAutoScale {
115        if factor { ImageAutoScale::Factor } else { ImageAutoScale::Pixel }
116    }
117}
118
119context_var! {
120    /// The Image scaling algorithm in the renderer.
121    ///
122    /// Is `ImageRendering::Auto` by default.
123    pub static IMAGE_RENDERING_VAR: ImageRendering = ImageRendering::Auto;
124
125    /// If the image is cached.
126    ///
127    /// Is `true` by default.
128    pub static IMAGE_CACHE_VAR: bool = true;
129
130    /// Widget function for the content shown when the image does not load.
131    pub static IMAGE_ERROR_FN_VAR: WidgetFn<ImgErrorArgs> = WidgetFn::nil();
132
133    /// Widget function for the content shown when the image is still loading.
134    pub static IMAGE_LOADING_FN_VAR: WidgetFn<ImgLoadingArgs> = WidgetFn::nil();
135
136    /// Custom image load and decode limits.
137    ///
138    /// Set to `None` to use the [`IMAGES.limits`].
139    ///
140    /// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
141    pub static IMAGE_LIMITS_VAR: Option<ImageLimits> = None;
142
143    /// Custom resize applied during image decode.
144    ///
145    /// Is `None` by default.
146    pub static IMAGE_DOWNSCALE_VAR: Option<ImageDownscaleMode> = None;
147
148    /// Defines what images are decoded from multi image containers.
149    pub static IMAGE_ENTRIES_MODE_VAR: ImageEntriesMode = ImageEntriesMode::PRIMARY;
150
151    /// The image layout mode.
152    ///
153    /// Is [`ImageFit::Contain`] by default.
154    pub static IMAGE_FIT_VAR: ImageFit = ImageFit::Contain;
155
156    /// Auto scaling applied to the image.
157    ///
158    /// Scales by factor by default.
159    pub static IMAGE_AUTO_SCALE_VAR: ImageAutoScale = ImageAutoScale::Factor;
160
161    /// Scaling applied to the image desired size.
162    ///
163    /// Does not scale by default, `1.0`.
164    pub static IMAGE_SCALE_VAR: Factor2d = Factor2d::identity();
165
166    /// Align of the image in relation to the image widget final size.
167    ///
168    /// Is `Align::CENTER` by default.
169    pub static IMAGE_ALIGN_VAR: Align = Align::CENTER;
170
171    /// Offset applied to the image after all measure and arrange.
172    pub static IMAGE_OFFSET_VAR: Vector = Vector::default();
173
174    /// Simple clip applied to the image before layout.
175    ///
176    /// No cropping is done by default.
177    pub static IMAGE_CROP_VAR: Rect = Rect::default();
178
179    /// Pattern repeat applied on the final image.
180    ///
181    /// Is `ImageRepeat::None` by default.
182    pub static IMAGE_REPEAT_VAR: ImageRepeat = ImageRepeat::None;
183
184    /// Spacing between repeated image copies.
185    ///
186    /// is `Size::zero` by default.
187    pub static IMAGE_REPEAT_SPACING_VAR: Size = Size::zero();
188}
189
190/// Sets the [`ImageFit`] of all inner images.
191///
192/// This property sets the [`IMAGE_FIT_VAR`].
193#[property(CONTEXT, default(IMAGE_FIT_VAR), widget_impl(Image))]
194pub fn img_fit(child: impl IntoUiNode, fit: impl IntoVar<ImageFit>) -> UiNode {
195    with_context_var(child, IMAGE_FIT_VAR, fit)
196}
197
198/// Sets the auto scale mode applied to all inner images.
199///
200/// By default scales by the screen factor so that the image layouts with the same proportional dimensions
201/// as other widgets independent of what screen is displaying it.
202///
203/// Set to `false` to display the original pixel size.
204///
205/// Set to [`ImageAutoScale::Density`] to display the *print size* in a calibrated screen.
206///
207/// This property sets the [`IMAGE_AUTO_SCALE_VAR`].
208#[property(CONTEXT, default(IMAGE_AUTO_SCALE_VAR), widget_impl(Image))]
209pub fn img_auto_scale(child: impl IntoUiNode, scale: impl IntoVar<ImageAutoScale>) -> UiNode {
210    with_context_var(child, IMAGE_AUTO_SCALE_VAR, scale)
211}
212
213/// Custom scale applied to all inner images.
214///
215/// By default only [`img_auto_scale`] is done. If this is set it multiplies the auto scale.
216///
217/// This property sets the [`IMAGE_SCALE_VAR`].
218///
219/// [`img_auto_scale`]: fn@img_auto_scale
220#[property(CONTEXT, default(IMAGE_SCALE_VAR), widget_impl(Image))]
221pub fn img_scale(child: impl IntoUiNode, scale: impl IntoVar<Factor2d>) -> UiNode {
222    with_context_var(child, IMAGE_SCALE_VAR, scale)
223}
224
225/// Sets the [`Align`] of all inner images within each image widget area.
226///
227/// If the image is smaller then the widget area it is aligned like normal, if it is larger the "viewport" it is aligned to clip,
228/// for example, alignment [`BOTTOM_RIGHT`] makes a smaller image sit at the bottom-right of the widget and makes
229/// a larger image bottom-right fill the widget, clipping the rest.
230///
231/// By default the alignment is [`CENTER`]. The [`BASELINE`] alignment is treaded the same as [`BOTTOM`].
232///
233/// This property sets the [`IMAGE_ALIGN_VAR`].
234///
235/// [`BOTTOM_RIGHT`]: zng_wgt::prelude::Align::BOTTOM_RIGHT
236/// [`CENTER`]: zng_wgt::prelude::Align::CENTER
237/// [`BASELINE`]: zng_wgt::prelude::Align::BASELINE
238/// [`BOTTOM`]: zng_wgt::prelude::Align::BOTTOM
239/// [`Align`]: zng_wgt::prelude::Align
240/// [`img_align`]: fn@crate::img_align
241#[property(CONTEXT, default(IMAGE_ALIGN_VAR), widget_impl(Image))]
242pub fn img_align(child: impl IntoUiNode, align: impl IntoVar<Align>) -> UiNode {
243    with_context_var(child, IMAGE_ALIGN_VAR, align)
244}
245
246/// Sets a [`Point`] that is an offset applied to all inner images within each image widget area.
247///
248/// Relative values are calculated from the widget final size. Note that this is different the applying the
249/// `offset` property on the widget itself, the widget is not moved just the image within the widget area.
250///
251/// This property sets the [`IMAGE_OFFSET_VAR`]. By default no offset is applied.
252///
253/// [`img_offset`]: fn@crate::img_offset
254/// [`Point`]: zng_wgt::prelude::Point
255#[property(CONTEXT, default(IMAGE_OFFSET_VAR), widget_impl(Image))]
256pub fn img_offset(child: impl IntoUiNode, offset: impl IntoVar<Vector>) -> UiNode {
257    with_context_var(child, IMAGE_OFFSET_VAR, offset)
258}
259
260/// Sets a [`Rect`] that is a clip applied to all inner images before their layout.
261///
262/// Relative values are calculated from the image pixel size, the [`img_scale_density`] is only considered after.
263/// Note that more complex clipping can be applied after to the full widget, this property exists primarily to
264/// render selections of a [texture atlas].
265///
266/// By default no cropping is done.
267///
268/// This property sets the [`IMAGE_CROP_VAR`].
269///
270/// [`img_scale_density`]: #fn@img_scale_density
271/// [texture atlas]: https://en.wikipedia.org/wiki/Texture_atlas
272/// [`Rect`]: zng_wgt::prelude::Rect
273#[property(CONTEXT, default(IMAGE_CROP_VAR), widget_impl(Image))]
274pub fn img_crop(child: impl IntoUiNode, crop: impl IntoVar<Rect>) -> UiNode {
275    with_context_var(child, IMAGE_CROP_VAR, crop)
276}
277
278/// Sets the [`ImageRepeat`] of all inner images.
279///
280/// Note that `repeat` converts from `bool` so you can set this property to `img_repeat = true;` to
281/// enable repeat in all inner images.
282///
283/// See also [`img_repeat_spacing`] to control the space between repeated tiles.
284///
285/// This property sets the [`IMAGE_REPEAT_VAR`].
286///
287/// [`img_repeat_spacing`]: fn@img_repeat_spacing
288#[property(CONTEXT, default(IMAGE_REPEAT_VAR), widget_impl(Image))]
289pub fn img_repeat(child: impl IntoUiNode, repeat: impl IntoVar<ImageRepeat>) -> UiNode {
290    with_context_var(child, IMAGE_REPEAT_VAR, repeat)
291}
292
293/// Sets the spacing between copies of the image if it is repeated.
294///
295/// Relative lengths are computed on the size of a single repeated tile image, so `100.pct()` is *skips*
296/// an entire image of space. The leftover size is set to the space taken by tile images that do not fully
297/// fit inside the clip area, `1.lft()` will insert space to cause only fully visible tiles to remain on screen.
298///
299/// This property sets the [`IMAGE_REPEAT_SPACING_VAR`].
300#[property(CONTEXT, default(IMAGE_REPEAT_SPACING_VAR), widget_impl(Image))]
301pub fn img_repeat_spacing(child: impl IntoUiNode, spacing: impl IntoVar<Size>) -> UiNode {
302    with_context_var(child, IMAGE_REPEAT_SPACING_VAR, spacing)
303}
304
305/// Sets the [`ImageRendering`] of all inner images.
306///
307/// If the image layout size is not the same as the `source` pixel size the image must be re-scaled
308/// during rendering, this property selects what algorithm is used to do this re-scaling.
309///
310/// Note that the algorithms used in the renderer value performance over quality and do a good
311/// enough job for small or temporary changes in scale only. If the image stays at a very different scale
312/// after a short time a CPU re-scale task is automatically started to generate a better quality re-scaling.
313///
314/// If the image is an app resource known during build time you should consider pre-scaling it to match the screen
315/// size at different DPIs using mipmaps.
316///
317/// This is [`ImageRendering::Auto`] by default.
318///
319/// This property sets the [`IMAGE_RENDERING_VAR`].
320///
321/// [`ImageRendering`]: zng_app::render::ImageRendering
322/// [`ImageRendering::Auto`]: zng_app::render::ImageRendering::Auto
323#[property(CONTEXT, default(IMAGE_RENDERING_VAR), widget_impl(Image))]
324pub fn img_rendering(child: impl IntoUiNode, rendering: impl IntoVar<ImageRendering>) -> UiNode {
325    with_context_var(child, IMAGE_RENDERING_VAR, rendering)
326}
327
328/// Sets the cache mode of all inner images.
329///
330/// Sets if the [`source`] is cached.
331///
332/// By default this is `true`, meaning the image is loaded from cache and if not present it is inserted into
333/// the cache, the cache lives for the app in the [`IMAGES`] service, the image can be manually removed from cache.
334///
335/// If set to `false` the image is always loaded and decoded on init or when [`source`] updates and is dropped when
336/// the widget is deinited or dropped.
337///
338/// This property sets the [`IMAGE_CACHE_VAR`].
339///
340/// [`source`]: fn@crate::source
341/// [`IMAGES`]: zng_ext_image::IMAGES
342#[property(CONTEXT, default(IMAGE_CACHE_VAR), widget_impl(Image))]
343pub fn img_cache(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
344    with_context_var(child, IMAGE_CACHE_VAR, enabled)
345}
346
347/// Sets custom image load and decode limits.
348///
349/// If not set or set to `None` the [`IMAGES.limits`] is used.
350///
351/// See also [`img_downscale`] for a way to still display unexpected large images.
352///
353/// This property sets the [`IMAGE_LIMITS_VAR`].
354///
355/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
356/// [`img_downscale`]: fn@img_downscale
357#[property(CONTEXT, default(IMAGE_LIMITS_VAR), widget_impl(Image))]
358pub fn img_limits(child: impl IntoUiNode, limits: impl IntoVar<Option<ImageLimits>>) -> UiNode {
359    with_context_var(child, IMAGE_LIMITS_VAR, limits)
360}
361
362/// Custom pixel resize applied during image load/decode.
363///
364/// Note that this resize affects the image actual pixel size directly when it is loading, it can also generate multiple image entries.
365///
366/// If the image is smaller than the requested size it is not upscaled. If multiple downscale samples are requested they are generated as
367/// synthetic [`ImageEntryKind::Reduced`].
368///
369/// Changing this value after an image is already loaded or loading will cause the image to reload, image cache allocates different
370/// entries for different downscale values, prefer setting samples of all possible sizes at once to
371/// avoid generating multiple image entries in the cache.
372///
373/// Rendering large (gigapixel) images can become slow if the image is scaled to fit as render
374/// scaling is GPU optimized, generating mipmap alternates here is a good optimization for large image viewers.
375///
376/// This property sets the [`IMAGE_DOWNSCALE_VAR`].
377///
378/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
379/// [`img_limits`]: fn@img_limits
380/// [`ImageEntryKind::Reduced`]: zng_ext_image::ImageEntryKind
381#[property(CONTEXT, default(IMAGE_DOWNSCALE_VAR), widget_impl(Image))]
382pub fn img_downscale(child: impl IntoUiNode, downscale: impl IntoVar<Option<ImageDownscaleMode>>) -> UiNode {
383    with_context_var(child, IMAGE_DOWNSCALE_VAR, downscale)
384}
385
386/// Defines what images are decoded from multi image containers.
387///
388/// By default container types like TIFF or ICO only decode the first/largest image, this property
389/// defines if other contained images are also requested.
390///
391/// If the image contains a [`Reduced`] alternate the best size is used during rendering, this is particularly
392/// useful for displaying icon files that have symbolic alternates that are more readable at a smaller size.
393///
394/// You can also configure [`img_downscale`] to generate a mipmap as an optimization for displaying very large images.
395///
396/// Note that although it is possible to request multi pages here the widget does not support pages, it always displays the
397/// first/primary page. The image pages are decoded if requested and you can access the image variable to get the pages.
398///
399/// This property sets the [`IMAGE_ENTRIES_MODE_VAR`].
400///
401/// [`Reduced`]: zng_ext_image::ImageEntryKind::Reduced
402/// [`img_downscale`]: fn@[`img_downscale`]
403#[property(CONTEXT, default(IMAGE_ENTRIES_MODE_VAR), widget_impl(Image))]
404pub fn img_entries_mode(child: impl IntoUiNode, mode: impl IntoVar<ImageEntriesMode>) -> UiNode {
405    with_context_var(child, IMAGE_ENTRIES_MODE_VAR, mode)
406}
407
408/// If the [`CONTEXT_IMAGE_VAR`] is an error.
409#[property(LAYOUT, widget_impl(Image))]
410pub fn is_error(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
411    bind_state(child, CONTEXT_IMAGE_VAR.map(|m| m.is_error()), state)
412}
413
414/// If the [`CONTEXT_IMAGE_VAR`] has successfully loaded.
415#[property(LAYOUT, widget_impl(Image))]
416pub fn is_loaded(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
417    bind_state(child, CONTEXT_IMAGE_VAR.map(|m| m.is_loaded()), state)
418}
419
420/// Gets the [`CONTEXT_IMAGE_VAR`].
421#[property(LAYOUT, widget_impl(Image))]
422pub fn get_img(child: impl IntoUiNode, state: impl IntoVar<Option<ImageEntry>>) -> UiNode {
423    bind_state(child, CONTEXT_IMAGE_VAR.map_into(), state)
424}
425
426/// Gets the [`CONTEXT_IMAGE_VAR`] ideal size.
427#[property(LAYOUT, widget_impl(Image))]
428pub fn get_img_layout_size(child: impl IntoUiNode, state: impl IntoVar<PxSize>) -> UiNode {
429    let state = state.into_var();
430    match_node(child, move |_, op| {
431        if let UiNodeOp::Layout { .. } = op {
432            let size = CONTEXT_IMAGE_VAR.with(|img| img.layout_size(&LAYOUT.metrics()));
433            if state.get() != size {
434                state.set(size);
435            }
436        }
437    })
438}
439
440/// Sets the [`wgt_fn!`] that is used to create a content for the error message.
441///
442/// [`wgt_fn!`]: zng_wgt::wgt_fn
443#[property(CONTEXT, default(IMAGE_ERROR_FN_VAR), widget_impl(Image))]
444pub fn img_error_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<ImgErrorArgs>>) -> UiNode {
445    with_context_var(child, IMAGE_ERROR_FN_VAR, wgt_fn)
446}
447
448/// Sets the [`wgt_fn!`] that is used to create a content for the loading message.
449///
450/// [`wgt_fn!`]: zng_wgt::wgt_fn
451#[property(CONTEXT, default(IMAGE_LOADING_FN_VAR), widget_impl(Image))]
452pub fn img_loading_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<ImgLoadingArgs>>) -> UiNode {
453    with_context_var(child, IMAGE_LOADING_FN_VAR, wgt_fn)
454}
455
456/// Arguments for [`img_loading_fn`].
457///
458/// [`img_loading_fn`]: fn@img_loading_fn
459#[derive(Clone, Default, Debug, PartialEq)]
460#[non_exhaustive]
461pub struct ImgLoadingArgs {}
462
463/// Arguments for [`on_load`].
464///
465/// [`on_load`]: fn@on_load
466#[derive(Clone, Default, Debug)]
467#[non_exhaustive]
468pub struct ImgLoadArgs {}
469
470/// Arguments for [`on_error`] and [`img_error_fn`].
471///
472/// [`on_error`]: fn@on_error
473/// [`img_error_fn`]: fn@img_error_fn
474#[derive(Clone, Debug, PartialEq)]
475#[non_exhaustive]
476pub struct ImgErrorArgs {
477    /// Error message.
478    pub error: Txt,
479}
480
481impl ImgErrorArgs {
482    /// New args.
483    pub fn new(error: impl Into<Txt>) -> Self {
484        Self { error: error.into() }
485    }
486}
487
488/// Image load or decode error event.
489///
490/// This property calls `handler` every time the [`CONTEXT_IMAGE_VAR`] updates with a different error or on the first update
491/// after init if the image is already in error on init.
492///
493/// # Handlers
494///
495/// This property accepts any [`Handler`], including the async handlers. Use one of the handler macros, [`hn!`],
496/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
497///
498/// # Route
499///
500/// This property is not routed, it works only inside a widget that loads images. There is also no *preview* event.
501///
502/// [`Handler`]: zng_wgt::prelude::Handler
503/// [`hn!`]: zng_wgt::prelude::hn!
504/// [`hn_once!`]: zng_wgt::prelude::hn_once!
505/// [`async_hn!`]: zng_wgt::prelude::async_hn!
506/// [`async_hn_once!`]: zng_wgt::prelude::async_hn_once!
507#[property(EVENT, widget_impl(Image))]
508pub fn on_error(child: impl IntoUiNode, handler: Handler<ImgErrorArgs>) -> UiNode {
509    let mut handler = handler.into_wgt_runner();
510    let mut error = Txt::from_str("");
511    let mut first_update = false;
512
513    match_node(child, move |_, op| match op {
514        UiNodeOp::Init => {
515            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
516
517            if CONTEXT_IMAGE_VAR.with(ImageEntry::is_error) {
518                first_update = true;
519                WIDGET.update();
520            }
521        }
522        UiNodeOp::Deinit => {
523            handler.deinit();
524        }
525        UiNodeOp::Update { .. } => {
526            if let Some(new_img) = CONTEXT_IMAGE_VAR.get_new() {
527                first_update = false;
528                if let Some(e) = new_img.error() {
529                    if error != e {
530                        error = e;
531                        handler.event(&ImgErrorArgs { error: error.clone() });
532                    }
533                } else {
534                    error = "".into();
535                }
536            } else if std::mem::take(&mut first_update) {
537                CONTEXT_IMAGE_VAR.with(|i| {
538                    if let Some(e) = i.error() {
539                        error = e;
540                        handler.event(&ImgErrorArgs { error: error.clone() });
541                    }
542                });
543            }
544
545            handler.update();
546        }
547        _ => {}
548    })
549}
550
551/// Image loaded event.
552///
553/// This property calls `handler` every time the [`CONTEXT_IMAGE_VAR`] updates with a successfully loaded image or on the first
554/// update after init if the image is already loaded on init.
555///
556/// # Handlers
557///
558/// This property accepts any [`Handler`], including the async handlers. Use one of the handler macros, [`hn!`],
559/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
560///
561/// # Route
562///
563/// This property is not routed, it works only inside a widget that loads images. There is also no *preview* event.
564///
565/// # Layout
566///
567/// Note that the handler is called on update before the new image is layout, use [`on_load_layout`] to handle after
568/// the new image is layout.
569///
570/// [`Handler`]: zng_wgt::prelude::Handler
571/// [`hn!`]: zng_wgt::prelude::hn!
572/// [`hn_once!`]: zng_wgt::prelude::hn_once!
573/// [`async_hn!`]: zng_wgt::prelude::async_hn!
574/// [`async_hn_once!`]: zng_wgt::prelude::async_hn_once!
575/// [`on_load_layout`]: fn@on_load_layout
576#[property(EVENT, widget_impl(Image))]
577pub fn on_load(child: impl IntoUiNode, handler: Handler<ImgLoadArgs>) -> UiNode {
578    let mut handler = handler.into_wgt_runner();
579    let mut first_update = false;
580
581    match_node(child, move |_, op| match op {
582        UiNodeOp::Init => {
583            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
584
585            if CONTEXT_IMAGE_VAR.with(ImageEntry::is_loaded) {
586                first_update = true;
587                WIDGET.update();
588            }
589        }
590        UiNodeOp::Deinit => {
591            handler.deinit();
592        }
593        UiNodeOp::Update { .. } => {
594            if let Some(new_img) = CONTEXT_IMAGE_VAR.get_new() {
595                first_update = false;
596                if new_img.is_loaded() {
597                    handler.event(&ImgLoadArgs {});
598                }
599            } else if std::mem::take(&mut first_update) && CONTEXT_IMAGE_VAR.with(ImageEntry::is_loaded) {
600                handler.event(&ImgLoadArgs {});
601            }
602
603            handler.update();
604        }
605        _ => {}
606    })
607}
608
609/// Image loaded and layout event.
610///
611/// This property calls `handler` every first layout after [`on_load`].
612///
613/// # Handlers
614///
615/// This property accepts any [`Handler`], including the async handlers. Use one of the handler macros, [`hn!`],
616/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
617///
618/// # Route
619///
620/// This property is not routed, it works only inside a widget that loads images. There is also no *preview* event.
621///
622/// [`Handler`]: zng_wgt::prelude::Handler
623/// [`hn!`]: zng_wgt::prelude::hn!
624/// [`hn_once!`]: zng_wgt::prelude::hn_once!
625/// [`async_hn!`]: zng_wgt::prelude::async_hn!
626/// [`async_hn_once!`]: zng_wgt::prelude::async_hn_once!
627/// [`on_load`]: fn@on_load
628#[property(EVENT, widget_impl(Image))]
629pub fn on_load_layout(child: impl IntoUiNode, handler: Handler<ImgLoadArgs>) -> UiNode {
630    let mut handler = handler.into_wgt_runner();
631    let mut update = false;
632
633    match_node(child, move |_, op| match op {
634        UiNodeOp::Init => {
635            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
636
637            update = CONTEXT_IMAGE_VAR.with(ImageEntry::is_loaded);
638            if update {
639                WIDGET.layout();
640            }
641        }
642        UiNodeOp::Deinit => {
643            handler.deinit();
644        }
645        UiNodeOp::Update { .. } => {
646            if let Some(new_img) = CONTEXT_IMAGE_VAR.get_new() {
647                update = new_img.is_loaded();
648                if update {
649                    WIDGET.layout();
650                }
651            }
652
653            handler.update();
654        }
655        UiNodeOp::Layout { .. } => {
656            if std::mem::take(&mut update) && CONTEXT_IMAGE_VAR.with(ImageEntry::is_loaded) {
657                handler.event(&ImgLoadArgs {});
658            }
659        }
660        _ => {}
661    })
662}
663
664/// Block window load until image is loaded.
665///
666/// If the image widget is in the initial window content a [`WindowLoadingHandle`] is used to delay the window
667/// visually opening until the source loads, fails to load or a timeout elapses. By default `true` sets the timeout to 1 second.
668///
669/// [`WindowLoadingHandle`]: zng_ext_window::WindowLoadingHandle
670#[property(LAYOUT, default(false), widget_impl(Image))]
671pub fn img_block_window_load(child: impl IntoUiNode, enabled: impl IntoValue<BlockWindowLoad>) -> UiNode {
672    let enabled = enabled.into();
673    let mut block = None;
674
675    match_node(child, move |_, op| match op {
676        UiNodeOp::Init => {
677            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
678
679            if let Some(delay) = enabled.deadline() {
680                block = WINDOW.loading_handle(delay, "img_block_window_load");
681            }
682        }
683        UiNodeOp::Update { .. } => {
684            if block.is_some() && !CONTEXT_IMAGE_VAR.with(ImageEntry::is_loading) {
685                block = None;
686            }
687        }
688        _ => {}
689    })
690}