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}