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