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::{ImageDownscale, 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
81context_var! {
82    /// The Image scaling algorithm in the renderer.
83    ///
84    /// Is `ImageRendering::Auto` by default.
85    pub static IMAGE_RENDERING_VAR: ImageRendering = ImageRendering::Auto;
86
87    /// If the image is cached.
88    ///
89    /// Is `true` by default.
90    pub static IMAGE_CACHE_VAR: bool = true;
91
92    /// Widget function for the content shown when the image does not load.
93    pub static IMAGE_ERROR_FN_VAR: WidgetFn<ImgErrorArgs> = WidgetFn::nil();
94
95    /// Widget function for the content shown when the image is still loading.
96    pub static IMAGE_LOADING_FN_VAR: WidgetFn<ImgLoadingArgs> = WidgetFn::nil();
97
98    /// Custom image load and decode limits.
99    ///
100    /// Set to `None` to use the [`IMAGES.limits`].
101    ///
102    /// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
103    pub static IMAGE_LIMITS_VAR: Option<ImageLimits> = None;
104
105    /// Custom resize applied during image decode.
106    ///
107    /// Is `None` by default.
108    pub static IMAGE_DOWNSCALE_VAR: Option<ImageDownscale> = None;
109
110    /// The image layout mode.
111    ///
112    /// Is [`ImageFit::Contain`] by default.
113    pub static IMAGE_FIT_VAR: ImageFit = ImageFit::Contain;
114
115    /// Scaling applied to the image desired size.
116    ///
117    /// Does not scale by default, `1.0`.
118    pub static IMAGE_SCALE_VAR: Factor2d = Factor2d::identity();
119
120    /// If the image desired size is scaled by the screen scale factor.
121    ///
122    /// Is `true` by default.
123    pub static IMAGE_SCALE_FACTOR_VAR: bool = true;
124
125    /// If the image desired size is scaled considering the image and screen PPIs.
126    ///
127    /// Is `false` by default.
128    pub static IMAGE_SCALE_DENSITY_VAR: bool = false;
129
130    /// Align of the image in relation to the image widget final size.
131    ///
132    /// Is `Align::CENTER` by default.
133    pub static IMAGE_ALIGN_VAR: Align = Align::CENTER;
134
135    /// Offset applied to the image after all measure and arrange.
136    pub static IMAGE_OFFSET_VAR: Vector = Vector::default();
137
138    /// Simple clip applied to the image before layout.
139    ///
140    /// No cropping is done by default.
141    pub static IMAGE_CROP_VAR: Rect = Rect::default();
142
143    /// Pattern repeat applied on the final image.
144    ///
145    /// Is `ImageRepeat::None` by default.
146    pub static IMAGE_REPEAT_VAR: ImageRepeat = ImageRepeat::None;
147
148    /// Spacing between repeated image copies.
149    ///
150    /// is `Size::zero` by default.
151    pub static IMAGE_REPEAT_SPACING_VAR: Size = Size::zero();
152}
153
154/// Sets the [`ImageFit`] of all inner images.
155///
156/// This property sets the [`IMAGE_FIT_VAR`].
157#[property(CONTEXT, default(IMAGE_FIT_VAR), widget_impl(Image))]
158pub fn img_fit(child: impl IntoUiNode, fit: impl IntoVar<ImageFit>) -> UiNode {
159    with_context_var(child, IMAGE_FIT_VAR, fit)
160}
161
162/// Sets the scale applied to all inner images.
163///
164/// The scaling is applied after [`img_scale_density`] if active.
165///
166/// By default not scaling is done.
167///
168/// [`img_scale_density`]: fn@img_scale_density
169#[property(CONTEXT, default(IMAGE_SCALE_VAR), widget_impl(Image))]
170pub fn img_scale(child: impl IntoUiNode, scale: impl IntoVar<Factor2d>) -> UiNode {
171    with_context_var(child, IMAGE_SCALE_VAR, scale)
172}
173
174/// Sets if the image desired size is scaled by the screen scale factor.
175///
176/// The image desired size is its original size after [`img_crop`], it is a pixel value, but widgets are layout using
177/// device independent pixels that automatically scale in higher definition displays, when this property is enabled
178/// the image size is also scaled so that the image will take the same screen space in all devices.
179///
180/// This is enabled by default.
181///
182/// See also [`img_scale_density`], for physical scaling in calibrated monitors.
183///
184/// [`img_crop`]: fn@img_crop
185/// [`img_scale_density`]: fn@img_scale_density
186#[property(CONTEXT, default(IMAGE_SCALE_FACTOR_VAR), widget_impl(Image))]
187pub fn img_scale_factor(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
188    with_context_var(child, IMAGE_SCALE_FACTOR_VAR, enabled)
189}
190
191/// Sets if the image desired size is scaled considering the image and monitor pixel density.
192///
193/// The image desired size is its original size, after [`img_crop`], and it can be in pixels or scaled considering
194/// the image pixel density, monitor pixel density and scale factor.
195///
196/// By default this is `false`, if `true` the image is scaled in a attempt to recreate the original physical dimensions, this
197/// only works if the image and monitor pixel density are set correctly. The monitor pixel density can be set using the [`MONITORS`] service.
198///
199/// This value supersedes [`img_scale_factor`], this this enabled the scale factor is ignored.
200///
201/// [`img_crop`]: fn@img_crop
202/// [`MONITORS`]: zng_ext_window::MONITORS
203/// [`img_scale_factor`]: fn@img_scale_factor
204#[property(CONTEXT, default(IMAGE_SCALE_DENSITY_VAR), widget_impl(Image))]
205pub fn img_scale_density(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
206    with_context_var(child, IMAGE_SCALE_DENSITY_VAR, enabled) // TODO(breaking) merge this property with `img_scale_factor`?
207}
208
209/// Sets the [`Align`] of all inner images within each image widget area.
210///
211/// 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,
212/// for example, alignment [`BOTTOM_RIGHT`] makes a smaller image sit at the bottom-right of the widget and makes
213/// a larger image bottom-right fill the widget, clipping the rest.
214///
215/// By default the alignment is [`CENTER`]. The [`BASELINE`] alignment is treaded the same as [`BOTTOM`].
216///
217/// [`BOTTOM_RIGHT`]: zng_wgt::prelude::Align::BOTTOM_RIGHT
218/// [`CENTER`]: zng_wgt::prelude::Align::CENTER
219/// [`BASELINE`]: zng_wgt::prelude::Align::BASELINE
220/// [`BOTTOM`]: zng_wgt::prelude::Align::BOTTOM
221/// [`Align`]: zng_wgt::prelude::Align
222/// [`img_align`]: fn@crate::img_align
223#[property(CONTEXT, default(IMAGE_ALIGN_VAR), widget_impl(Image))]
224pub fn img_align(child: impl IntoUiNode, align: impl IntoVar<Align>) -> UiNode {
225    with_context_var(child, IMAGE_ALIGN_VAR, align)
226}
227
228/// Sets a [`Point`] that is an offset applied to all inner images within each image widget area.
229///
230/// Relative values are calculated from the widget final size. Note that this is different the applying the
231/// `offset` property on the widget itself, the widget is not moved just the image within the widget area.
232///
233/// This property sets the [`IMAGE_OFFSET_VAR`]. By default no offset is applied.
234///
235/// [`img_offset`]: fn@crate::img_offset
236/// [`Point`]: zng_wgt::prelude::Point
237#[property(CONTEXT, default(IMAGE_OFFSET_VAR), widget_impl(Image))]
238pub fn img_offset(child: impl IntoUiNode, offset: impl IntoVar<Vector>) -> UiNode {
239    with_context_var(child, IMAGE_OFFSET_VAR, offset)
240}
241
242/// Sets a [`Rect`] that is a clip applied to all inner images before their layout.
243///
244/// Relative values are calculated from the image pixel size, the [`img_scale_density`] is only considered after.
245/// Note that more complex clipping can be applied after to the full widget, this property exists primarily to
246/// render selections of a [texture atlas].
247///
248/// By default no cropping is done.
249///
250/// [`img_scale_density`]: #fn@img_scale_density
251/// [texture atlas]: https://en.wikipedia.org/wiki/Texture_atlas
252/// [`Rect`]: zng_wgt::prelude::Rect
253#[property(CONTEXT, default(IMAGE_CROP_VAR), widget_impl(Image))]
254pub fn img_crop(child: impl IntoUiNode, crop: impl IntoVar<Rect>) -> UiNode {
255    with_context_var(child, IMAGE_CROP_VAR, crop)
256}
257
258/// Sets the [`ImageRepeat`] of all inner images.
259///
260/// Note that `repeat` converts from `bool` so you can set this property to `img_repeat = true;` to
261/// enable repeat in all inner images.
262///
263/// See also [`img_repeat_spacing`] to control the space between repeated tiles.
264///
265/// This property sets the [`IMAGE_REPEAT_VAR`].
266///
267/// [`img_repeat_spacing`]: fn@img_repeat_spacing
268#[property(CONTEXT, default(IMAGE_REPEAT_VAR), widget_impl(Image))]
269pub fn img_repeat(child: impl IntoUiNode, repeat: impl IntoVar<ImageRepeat>) -> UiNode {
270    with_context_var(child, IMAGE_REPEAT_VAR, repeat)
271}
272
273/// Sets the spacing between copies of the image if it is repeated.
274///
275/// Relative lengths are computed on the size of a single repeated tile image, so `100.pct()` is *skips*
276/// an entire image of space. The leftover size is set to the space taken by tile images that do not fully
277/// fit inside the clip area, `1.lft()` will insert space to cause only fully visible tiles to remain on screen.
278///
279/// This property sets the [`IMAGE_REPEAT_SPACING_VAR`].
280#[property(CONTEXT, default(IMAGE_REPEAT_SPACING_VAR), widget_impl(Image))]
281pub fn img_repeat_spacing(child: impl IntoUiNode, spacing: impl IntoVar<Size>) -> UiNode {
282    with_context_var(child, IMAGE_REPEAT_SPACING_VAR, spacing)
283}
284
285/// Sets the [`ImageRendering`] of all inner images.
286///
287/// If the image layout size is not the same as the `source` pixel size the image must be re-scaled
288/// during rendering, this property selects what algorithm is used to do this re-scaling.
289///
290/// Note that the algorithms used in the renderer value performance over quality and do a good
291/// enough job for small or temporary changes in scale only. If the image stays at a very different scale
292/// after a short time a CPU re-scale task is automatically started to generate a better quality re-scaling.
293///
294/// If the image is an app resource known during build time you should consider pre-scaling it to match the screen
295/// size at different DPIs using mipmaps.
296///
297/// This is [`ImageRendering::Auto`] by default.
298///
299/// [`ImageRendering`]: zng_app::render::ImageRendering
300/// [`ImageRendering::Auto`]: zng_app::render::ImageRendering::Auto
301#[property(CONTEXT, default(IMAGE_RENDERING_VAR), widget_impl(Image))]
302pub fn img_rendering(child: impl IntoUiNode, rendering: impl IntoVar<ImageRendering>) -> UiNode {
303    with_context_var(child, IMAGE_RENDERING_VAR, rendering)
304}
305
306/// Sets the cache mode of all inner images.
307///
308/// Sets if the [`source`] is cached.
309///
310/// By default this is `true`, meaning the image is loaded from cache and if not present it is inserted into
311/// the cache, the cache lives for the app in the [`IMAGES`] service, the image can be manually removed from cache.
312///
313/// If set to `false` the image is always loaded and decoded on init or when [`source`] updates and is dropped when
314/// the widget is deinited or dropped.
315///
316/// [`source`]: fn@crate::source
317/// [`IMAGES`]: zng_ext_image::IMAGES
318#[property(CONTEXT, default(IMAGE_CACHE_VAR), widget_impl(Image))]
319pub fn img_cache(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
320    with_context_var(child, IMAGE_CACHE_VAR, enabled)
321}
322
323/// Sets custom image load and decode limits.
324///
325/// If not set or set to `None` the [`IMAGES.limits`] is used.
326///
327/// See also [`img_downscale`] for a way to still display unexpected large images.
328///
329/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
330/// [`img_downscale`]: fn@img_downscale
331#[property(CONTEXT, default(IMAGE_LIMITS_VAR), widget_impl(Image))]
332pub fn img_limits(child: impl IntoUiNode, limits: impl IntoVar<Option<ImageLimits>>) -> UiNode {
333    with_context_var(child, IMAGE_LIMITS_VAR, limits)
334}
335
336/// Custom pixel resize applied during image load/decode.
337///
338/// Note that this resize affects the image actual pixel size directly when it is loading to force the image pixels to be within an expected size.
339/// This property primary use is as error recover before the [`img_limits`] error happens, you set the limits to the size that should not even
340/// be processed and set this property to the maximum size expected.
341///
342/// Changing this value after an image is already loaded or loading will cause the image to reload, image cache allocates different
343/// entries for different downscale values, this means that this property should never be used for responsive resize,use the widget
344/// size and other properties to efficiently resize an image on screen.
345///
346/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
347/// [`img_limits`]: fn@img_limits
348#[property(CONTEXT, default(IMAGE_DOWNSCALE_VAR), widget_impl(Image))]
349pub fn img_downscale(child: impl IntoUiNode, downscale: impl IntoVar<Option<ImageDownscale>>) -> UiNode {
350    with_context_var(child, IMAGE_DOWNSCALE_VAR, downscale)
351}
352
353/// If the [`CONTEXT_IMAGE_VAR`] is an error.
354#[property(LAYOUT, widget_impl(Image))]
355pub fn is_error(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
356    bind_state(child, CONTEXT_IMAGE_VAR.map(|m| m.is_error()), state)
357}
358
359/// If the [`CONTEXT_IMAGE_VAR`] has successfully loaded.
360#[property(LAYOUT, widget_impl(Image))]
361pub fn is_loaded(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
362    bind_state(child, CONTEXT_IMAGE_VAR.map(|m| m.is_loaded()), state)
363}
364
365/// Gets the [`CONTEXT_IMAGE_VAR`].
366#[property(LAYOUT, widget_impl(Image))]
367pub fn get_img(child: impl IntoUiNode, state: impl IntoVar<Option<Img>>) -> UiNode {
368    bind_state(child, CONTEXT_IMAGE_VAR.map_into(), state)
369}
370
371/// Gets the [`CONTEXT_IMAGE_VAR`] ideal size.
372#[property(LAYOUT, widget_impl(Image))]
373pub fn get_img_layout_size(child: impl IntoUiNode, state: impl IntoVar<PxSize>) -> UiNode {
374    let state = state.into_var();
375    match_node(child, move |_, op| {
376        if let UiNodeOp::Layout { .. } = op {
377            let size = CONTEXT_IMAGE_VAR.with(|img| img.layout_size(&LAYOUT.metrics()));
378            if state.get() != size {
379                state.set(size);
380            }
381        }
382    })
383}
384
385/// Sets the [`wgt_fn!`] that is used to create a content for the error message.
386///
387/// [`wgt_fn!`]: zng_wgt::wgt_fn
388#[property(CONTEXT, default(IMAGE_ERROR_FN_VAR), widget_impl(Image))]
389pub fn img_error_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<ImgErrorArgs>>) -> UiNode {
390    with_context_var(child, IMAGE_ERROR_FN_VAR, wgt_fn)
391}
392
393/// Sets the [`wgt_fn!`] that is used to create a content for the loading message.
394///
395/// [`wgt_fn!`]: zng_wgt::wgt_fn
396#[property(CONTEXT, default(IMAGE_LOADING_FN_VAR), widget_impl(Image))]
397pub fn img_loading_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<ImgLoadingArgs>>) -> UiNode {
398    with_context_var(child, IMAGE_LOADING_FN_VAR, wgt_fn)
399}
400
401/// Arguments for [`img_loading_fn`].
402///
403/// [`img_loading_fn`]: fn@img_loading_fn
404#[derive(Clone, Default, Debug, PartialEq)]
405#[non_exhaustive]
406pub struct ImgLoadingArgs {}
407
408/// Arguments for [`on_load`].
409///
410/// [`on_load`]: fn@on_load
411#[derive(Clone, Default, Debug)]
412#[non_exhaustive]
413pub struct ImgLoadArgs {}
414
415/// Arguments for [`on_error`] and [`img_error_fn`].
416///
417/// [`on_error`]: fn@on_error
418/// [`img_error_fn`]: fn@img_error_fn
419#[derive(Clone, Debug, PartialEq)]
420#[non_exhaustive]
421pub struct ImgErrorArgs {
422    /// Error message.
423    pub error: Txt,
424}
425
426impl ImgErrorArgs {
427    /// New args.
428    pub fn new(error: impl Into<Txt>) -> Self {
429        Self { error: error.into() }
430    }
431}
432
433/// Image load or decode error event.
434///
435/// This property calls `handler` every time the [`CONTEXT_IMAGE_VAR`] updates with a different error or on the first update
436/// after init if the image is already in error on init.
437///
438/// # Handlers
439///
440/// This property accepts any [`Handler`], including the async handlers. Use one of the handler macros, [`hn!`],
441/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
442///
443/// # Route
444///
445/// This property is not routed, it works only inside a widget that loads images. There is also no *preview* event.
446///
447/// [`Handler`]: zng_wgt::prelude::Handler
448/// [`hn!`]: zng_wgt::prelude::hn!
449/// [`hn_once!`]: zng_wgt::prelude::hn_once!
450/// [`async_hn!`]: zng_wgt::prelude::async_hn!
451/// [`async_hn_once!`]: zng_wgt::prelude::async_hn_once!
452#[property(EVENT, widget_impl(Image))]
453pub fn on_error(child: impl IntoUiNode, handler: Handler<ImgErrorArgs>) -> UiNode {
454    let mut handler = handler.into_wgt_runner();
455    let mut error = Txt::from_str("");
456    let mut first_update = false;
457
458    match_node(child, move |_, op| match op {
459        UiNodeOp::Init => {
460            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
461
462            if CONTEXT_IMAGE_VAR.with(Img::is_error) {
463                first_update = true;
464                WIDGET.update();
465            }
466        }
467        UiNodeOp::Deinit => {
468            handler.deinit();
469        }
470        UiNodeOp::Update { .. } => {
471            if let Some(new_img) = CONTEXT_IMAGE_VAR.get_new() {
472                first_update = false;
473                if let Some(e) = new_img.error() {
474                    if error != e {
475                        error = e;
476                        handler.event(&ImgErrorArgs { error: error.clone() });
477                    }
478                } else {
479                    error = "".into();
480                }
481            } else if std::mem::take(&mut first_update) {
482                CONTEXT_IMAGE_VAR.with(|i| {
483                    if let Some(e) = i.error() {
484                        error = e;
485                        handler.event(&ImgErrorArgs { error: error.clone() });
486                    }
487                });
488            }
489
490            handler.update();
491        }
492        _ => {}
493    })
494}
495
496/// Image loaded event.
497///
498/// This property calls `handler` every time the [`CONTEXT_IMAGE_VAR`] updates with a successfully loaded image or on the first
499/// update after init if the image is already loaded on init.
500///
501/// # Handlers
502///
503/// This property accepts any [`Handler`], including the async handlers. Use one of the handler macros, [`hn!`],
504/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
505///
506/// # Route
507///
508/// This property is not routed, it works only inside a widget that loads images. There is also no *preview* event.
509///
510/// [`Handler`]: zng_wgt::prelude::Handler
511/// [`hn!`]: zng_wgt::prelude::hn!
512/// [`hn_once!`]: zng_wgt::prelude::hn_once!
513/// [`async_hn!`]: zng_wgt::prelude::async_hn!
514/// [`async_hn_once!`]: zng_wgt::prelude::async_hn_once!
515#[property(EVENT, widget_impl(Image))]
516pub fn on_load(child: impl IntoUiNode, handler: Handler<ImgLoadArgs>) -> UiNode {
517    let mut handler = handler.into_wgt_runner();
518    let mut first_update = false;
519
520    match_node(child, move |_, op| match op {
521        UiNodeOp::Init => {
522            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
523
524            if CONTEXT_IMAGE_VAR.with(Img::is_loaded) {
525                first_update = true;
526                WIDGET.update();
527            }
528        }
529        UiNodeOp::Deinit => {
530            handler.deinit();
531        }
532        UiNodeOp::Update { .. } => {
533            if let Some(new_img) = CONTEXT_IMAGE_VAR.get_new() {
534                first_update = false;
535                if new_img.is_loaded() {
536                    handler.event(&ImgLoadArgs {});
537                }
538            } else if std::mem::take(&mut first_update) && CONTEXT_IMAGE_VAR.with(Img::is_loaded) {
539                handler.event(&ImgLoadArgs {});
540            }
541
542            handler.update();
543        }
544        _ => {}
545    })
546}
547
548/// Block window load until image is loaded.
549///
550/// If the image widget is in the initial window content a [`WindowLoadingHandle`] is used to delay the window
551/// visually opening until the source loads, fails to load or a timeout elapses. By default `true` sets the timeout to 1 second.
552///
553/// [`WindowLoadingHandle`]: zng_ext_window::WindowLoadingHandle
554#[property(LAYOUT, default(false), widget_impl(Image))]
555pub fn img_block_window_load(child: impl IntoUiNode, enabled: impl IntoValue<BlockWindowLoad>) -> UiNode {
556    let enabled = enabled.into();
557    let mut block = None;
558
559    match_node(child, move |_, op| match op {
560        UiNodeOp::Init => {
561            WIDGET.sub_var(&CONTEXT_IMAGE_VAR);
562
563            if let Some(delay) = enabled.deadline() {
564                block = WINDOW.loading_handle(delay);
565            }
566        }
567        UiNodeOp::Update { .. } => {
568            if block.is_some() && !CONTEXT_IMAGE_VAR.with(Img::is_loading) {
569                block = None;
570            }
571        }
572        _ => {}
573    })
574}