zng_wgt_image/
mask.rs

1//! Mask properties, [`mask_image`], [`mask_mode`] and more.
2//!
3//! [`mask_image`]: fn@mask_image
4//! [`mask_mode`]: fn@mask_mode
5
6use zng_ext_image::{
7    IMAGES, ImageCacheMode, ImageDownscaleMode, ImageEntriesMode, ImageLimits, ImageMaskMode, ImageOptions, ImageRenderArgs, ImageSource,
8};
9use zng_wgt::prelude::*;
10
11use crate::ImageFit;
12
13/// Sets an image mask.
14///
15/// This property is configured by contextual values set by the properties in the [`mask`] module.
16/// By default the image alpha channel is used as mask, this can be changed by the [`mask_mode`] property.
17///
18/// [`mask`]: crate::mask
19/// [`mask_mode`]: fn@mask_mode
20#[property(BORDER-1)]
21pub fn mask_image(child: impl IntoUiNode, source: impl IntoVar<ImageSource>) -> UiNode {
22    let source = source.into_var();
23    let mut img = None;
24    let mut best_reduce = None;
25    let mut img_size = PxSize::zero();
26    let mut rect = PxRect::zero();
27
28    match_node(child, move |c, op| match op {
29        UiNodeOp::Init => {
30            // load
31            WIDGET
32                .sub_var(&source)
33                .sub_var(&MASK_MODE_VAR)
34                .sub_var(&MASK_IMAGE_CACHE_VAR)
35                .sub_var(&MASK_IMAGE_DOWNSCALE_VAR);
36
37            let mode = if MASK_IMAGE_CACHE_VAR.get() {
38                ImageCacheMode::Cache
39            } else {
40                ImageCacheMode::Ignore
41            };
42            let limits = MASK_IMAGE_LIMITS_VAR.get();
43            let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
44            let mask_mode = MASK_MODE_VAR.get();
45
46            let mut source = source.get();
47            if let ImageSource::Render(_, args) = &mut source {
48                *args = Some(ImageRenderArgs::new(WINDOW.id()));
49            }
50            let opt = ImageOptions::new(mode, downscale, Some(mask_mode), ImageEntriesMode::PRIMARY);
51            let i = IMAGES.image(source, opt, limits);
52            let s = i.subscribe(UpdateOp::Update, WIDGET.id());
53            img = Some((i, s));
54
55            // present
56
57            WIDGET
58                .sub_var_layout(&MASK_FIT_VAR)
59                .sub_var_layout(&MASK_ALIGN_VAR)
60                .sub_var_layout(&MASK_OFFSET_VAR);
61        }
62        UiNodeOp::Deinit => {
63            c.deinit();
64            img = None;
65            best_reduce = None;
66        }
67        UiNodeOp::Update { .. } => {
68            // load
69            if source.is_new() || MASK_MODE_VAR.is_new() || MASK_IMAGE_DOWNSCALE_VAR.is_new() {
70                let mut source = source.get();
71
72                if let ImageSource::Render(_, args) = &mut source {
73                    *args = Some(ImageRenderArgs::new(WINDOW.id()));
74                }
75
76                let mode = if MASK_IMAGE_CACHE_VAR.get() {
77                    ImageCacheMode::Cache
78                } else {
79                    ImageCacheMode::Ignore
80                };
81                let limits = MASK_IMAGE_LIMITS_VAR.get();
82                let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
83                let mask_mode = MASK_MODE_VAR.get();
84                let opt = ImageOptions::new(mode, downscale, Some(mask_mode), ImageEntriesMode::PRIMARY);
85                let i = IMAGES.image(source, opt, limits);
86                let s = i.subscribe(UpdateOp::Layout, WIDGET.id());
87                img = Some((i, s));
88
89                WIDGET.layout();
90            } else if let Some(enabled) = MASK_IMAGE_CACHE_VAR.get_new() {
91                // cache-mode update:
92                let is_cached = img.as_ref().unwrap().0.with(|i| IMAGES.is_cached(i));
93                if enabled != is_cached {
94                    let source = source.get();
95                    let limits = MASK_IMAGE_LIMITS_VAR.get();
96                    let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
97                    let mask_mode = MASK_MODE_VAR.get();
98                    let mut opt = ImageOptions::new(ImageCacheMode::Cache, downscale, Some(mask_mode), ImageEntriesMode::PRIMARY);
99
100                    if is_cached {
101                        let _ = img.take().unwrap().0;
102                        if let Some(h) = source.hash128(&opt) {
103                            IMAGES.clean(h);
104                        }
105
106                        opt.cache_mode = ImageCacheMode::Ignore;
107                    }
108
109                    let i = IMAGES.image(source, opt, limits);
110
111                    let s = i.subscribe(UpdateOp::Update, WIDGET.id());
112                    img = Some((i, s));
113
114                    WIDGET.layout();
115                }
116            } else if let Some(img) = img.as_ref().unwrap().0.get_new() {
117                let s = img.size();
118                if s != img_size {
119                    img_size = s;
120                    best_reduce = None;
121                    WIDGET.layout().render();
122                } else {
123                    if img.has_entries() {
124                        let b = img.best_reduce(rect.size);
125                        let h = b.subscribe(UpdateOp::Render, WIDGET.id());
126                        best_reduce = Some((b, h));
127                    } else {
128                        best_reduce = None;
129                    }
130                    WIDGET.render();
131                }
132            }
133        }
134        UiNodeOp::Layout { wl, final_size } => {
135            *final_size = c.layout(wl);
136
137            let wgt_size = *final_size;
138            let constraints = PxConstraints2d::new_fill_size(wgt_size);
139            LAYOUT.with_constraints(constraints, || {
140                let mut img_size = img_size;
141                let mut img_origin = PxPoint::zero();
142
143                let mut fit = MASK_FIT_VAR.get();
144                if let ImageFit::ScaleDown = fit {
145                    if img_size.width < wgt_size.width && img_size.height < wgt_size.height {
146                        fit = ImageFit::None;
147                    } else {
148                        fit = ImageFit::Contain;
149                    }
150                }
151
152                let mut align = MASK_ALIGN_VAR.get();
153                match fit {
154                    ImageFit::Fill => {
155                        align = Align::FILL;
156                    }
157                    ImageFit::Contain => {
158                        let container = wgt_size.to_f32();
159                        let content = img_size.to_f32();
160                        let scale = (container.width / content.width).min(container.height / content.height).fct();
161                        img_size *= scale;
162                    }
163                    ImageFit::Cover => {
164                        let container = wgt_size.to_f32();
165                        let content = img_size.to_f32();
166                        let scale = (container.width / content.width).max(container.height / content.height).fct();
167                        img_size *= scale;
168                    }
169                    ImageFit::None => {}
170                    ImageFit::ScaleDown => unreachable!(),
171                }
172
173                if align.is_fill_x() {
174                    let factor = wgt_size.width.0 as f32 / img_size.width.0 as f32;
175                    img_size.width *= factor;
176                } else {
177                    let diff = wgt_size.width - img_size.width;
178                    let offset = diff * align.x(LAYOUT.direction());
179                    img_origin.x += offset;
180                }
181                if align.is_fill_y() {
182                    let factor = wgt_size.height.0 as f32 / img_size.height.0 as f32;
183                    img_size.height *= factor;
184                } else {
185                    let diff = wgt_size.height - img_size.height;
186                    let offset = diff * align.y();
187                    img_origin.y += offset;
188                }
189
190                img_origin += MASK_OFFSET_VAR.layout();
191
192                let new_rect = PxRect::new(img_origin, img_size);
193                if rect != new_rect {
194                    if rect.size != new_rect.size {
195                        img.as_ref().unwrap().0.with(|img| {
196                            if img.has_entries() {
197                                let b = img.best_reduce(new_rect.size);
198                                let h = b.subscribe(UpdateOp::Render, WIDGET.id());
199                                best_reduce = Some((b, h));
200                            } else {
201                                best_reduce = None;
202                            }
203                        });
204                    }
205
206                    rect = new_rect;
207                    WIDGET.render();
208                }
209            });
210        }
211        UiNodeOp::Render { frame } => {
212            if rect.size.is_empty() {
213                return;
214            }
215            best_reduce.as_ref().or(img.as_ref()).unwrap().0.with(|img| {
216                if img.is_loaded() && !img.is_error() {
217                    frame.push_mask(img, rect, |frame| c.render(frame));
218                }
219            });
220        }
221        _ => {}
222    })
223}
224
225context_var! {
226    /// Defines how the A8 image mask pixels are to be derived from a source mask image.
227    pub static MASK_MODE_VAR: ImageMaskMode = ImageMaskMode::default();
228
229    /// Defines if the mask image is cached.
230    pub static MASK_IMAGE_CACHE_VAR: bool = true;
231
232    /// Custom mask image load and decode limits.
233    ///
234    /// Set to `None` to use the `IMAGES::limits`.
235    pub static MASK_IMAGE_LIMITS_VAR: Option<ImageLimits> = None;
236
237    /// Custom resize applied during mask image decode.
238    ///
239    /// Is `None` by default.
240    pub static MASK_IMAGE_DOWNSCALE_VAR: Option<ImageDownscaleMode> = None;
241
242    /// Defines what mask images are decoded from multi image containers.
243    pub static MASK_IMAGE_ENTRIES_MODE_VAR: ImageEntriesMode = ImageEntriesMode::PRIMARY;
244
245    /// Defines how the mask image fits the widget bounds.
246    pub static MASK_FIT_VAR: ImageFit = ImageFit::Fill;
247
248    /// Align of the mask image in relation to the image widget final size.
249    ///
250    /// Is `Align::CENTER` by default.
251    pub static MASK_ALIGN_VAR: Align = Align::CENTER;
252
253    /// Offset applied to the mask image after all measure and arrange.
254    pub static MASK_OFFSET_VAR: Vector = Vector::default();
255}
256
257/// Defines how the A8 image mask pixels are to be derived from a source mask image in all [`mask_image`] inside
258/// the widget in descendants.
259///
260/// This property sets the [`MASK_MODE_VAR`].
261///
262/// [`mask_image`]: fn@mask_image
263#[property(CONTEXT, default(MASK_MODE_VAR))]
264pub fn mask_mode(child: impl IntoUiNode, mode: impl IntoVar<ImageMaskMode>) -> UiNode {
265    with_context_var(child, MASK_MODE_VAR, mode)
266}
267
268/// Defines if the mask images loaded in all [`mask_image`] inside
269/// the widget in descendants are cached.
270///
271/// This property sets the [`MASK_IMAGE_CACHE_VAR`].
272///
273/// [`mask_image`]: fn@mask_image
274#[property(CONTEXT, default(MASK_IMAGE_CACHE_VAR))]
275pub fn mask_image_cache(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
276    with_context_var(child, MASK_IMAGE_CACHE_VAR, enabled)
277}
278
279/// Sets custom mask image load and decode limits.
280///
281/// If not set or set to `None` the [`IMAGES.limits`] is used.
282///
283/// This property sets the [`MASK_IMAGE_LIMITS_VAR`].
284///
285/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
286#[property(CONTEXT, default(MASK_IMAGE_LIMITS_VAR))]
287pub fn mask_image_limits(child: impl IntoUiNode, limits: impl IntoVar<Option<ImageLimits>>) -> UiNode {
288    with_context_var(child, MASK_IMAGE_LIMITS_VAR, limits)
289}
290
291/// Custom pixel resize applied during mask image load/decode.
292///
293/// Note that this resize affects the image actual pixel size directly when it is loading, it can also generate multiple image entries.
294///
295/// If the image is smaller than the requested size it is not upscaled. If multiple downscale samples are requested they are generated as
296/// synthetic [`ImageEntryKind::Reduced`].
297///
298/// Changing this value after an image is already loaded or loading will cause the mask image to reload, image cache allocates different
299/// entries for different downscale values, prefer setting samples of all possible sizes at once to
300/// avoid generating multiple image entries in the cache.
301///
302/// This property sets the [`MASK_IMAGE_DOWNSCALE_VAR`].
303///
304/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
305/// [`img_limits`]: fn@crate::img_limits
306/// [`ImageEntryKind::Reduced`]: zng_ext_image::ImageEntryKind
307#[property(CONTEXT, default(MASK_IMAGE_DOWNSCALE_VAR))]
308pub fn mask_image_downscale(child: impl IntoUiNode, downscale: impl IntoVar<Option<ImageDownscaleMode>>) -> UiNode {
309    with_context_var(child, MASK_IMAGE_DOWNSCALE_VAR, downscale)
310}
311
312/// Defines what mask images are decoded from multi image containers.
313///
314/// By default container types like TIFF or ICO only decode the first/largest image, this property
315/// defines if other contained images are also requested.
316///
317/// If the image contains a [`Reduced`] alternate the best size is used during rendering, this is particularly
318/// useful for displaying icon files that have symbolic alternates that are more readable at a smaller size.
319///
320///
321/// This property sets the [`MASK_IMAGE_ENTRIES_MODE_VAR`].
322///
323/// [`Reduced`]: zng_ext_image::ImageEntryKind::Reduced
324/// [`img_downscale`]: fn@[`img_downscale`]
325#[property(CONTEXT, default(MASK_IMAGE_ENTRIES_MODE_VAR))]
326pub fn mask_image_entries_mode(child: impl IntoUiNode, mode: impl IntoVar<ImageEntriesMode>) -> UiNode {
327    with_context_var(child, MASK_IMAGE_ENTRIES_MODE_VAR, mode)
328}
329
330/// Defines how the mask image fits the widget bounds in all [`mask_image`] inside
331/// the widget in descendants.
332///
333/// This property sets the [`MASK_FIT_VAR`].
334///
335/// [`mask_image`]: fn@mask_image
336#[property(CONTEXT, default(MASK_FIT_VAR))]
337pub fn mask_fit(child: impl IntoUiNode, fit: impl IntoVar<ImageFit>) -> UiNode {
338    with_context_var(child, MASK_FIT_VAR, fit)
339}
340
341/// Defines the align of the mask image in relation to the widget bounds in all [`mask_image`] inside
342/// the widget in descendants.
343///
344/// This property sets the [`MASK_ALIGN_VAR`].
345///
346/// [`mask_image`]: fn@mask_image
347#[property(CONTEXT, default(MASK_ALIGN_VAR))]
348pub fn mask_align(child: impl IntoUiNode, align: impl IntoVar<Align>) -> UiNode {
349    with_context_var(child, MASK_ALIGN_VAR, align)
350}
351
352/// Defines the offset applied to the mask image after all measure and arrange. in all [`mask_image`] inside
353/// the widget in descendants.
354///
355/// This property sets the [`MASK_OFFSET_VAR`].
356///
357/// [`mask_image`]: fn@mask_image
358#[property(CONTEXT, default(MASK_OFFSET_VAR))]
359pub fn mask_offset(child: impl IntoUiNode, offset: impl IntoVar<Vector>) -> UiNode {
360    with_context_var(child, MASK_OFFSET_VAR, offset)
361}