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