zng_wgt_image/
image_properties.rs

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