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::{IMAGES, ImageCacheMode, ImageDownscale, ImageLimits, ImageMaskMode, ImageRenderArgs, ImageSource};
7use zng_wgt::prelude::*;
8
9use crate::ImageFit;
10
11/// Sets an image mask.
12///
13/// The image alpha channel is used as a mask for the widget and descendants.
14///
15/// This property is configured by contextual values set by the properties in the [`mask`] module.
16///
17/// [`mask`]: crate::mask
18#[property(FILL-1)]
19pub fn mask_image(child: impl UiNode, source: impl IntoVar<ImageSource>) -> impl UiNode {
20    let source = source.into_var();
21    let mut img = None;
22    let mut img_size = PxSize::zero();
23    let mut rect = PxRect::zero();
24
25    match_node(child, move |c, op| match op {
26        UiNodeOp::Init => {
27            // load
28            WIDGET
29                .sub_var(&source)
30                .sub_var(&MASK_MODE_VAR)
31                .sub_var(&MASK_IMAGE_CACHE_VAR)
32                .sub_var(&MASK_IMAGE_DOWNSCALE_VAR);
33
34            let mode = if MASK_IMAGE_CACHE_VAR.get() {
35                ImageCacheMode::Cache
36            } else {
37                ImageCacheMode::Ignore
38            };
39            let limits = MASK_IMAGE_LIMITS_VAR.get();
40            let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
41            let mask_mode = MASK_MODE_VAR.get();
42
43            let mut source = source.get();
44            if let ImageSource::Render(_, args) = &mut source {
45                *args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
46            }
47            let i = IMAGES.image(source, mode, limits, downscale, Some(mask_mode));
48            let s = i.subscribe(UpdateOp::Update, WIDGET.id());
49            img = Some((i, s));
50
51            // present
52
53            WIDGET
54                .sub_var_layout(&MASK_FIT_VAR)
55                .sub_var_layout(&MASK_ALIGN_VAR)
56                .sub_var_layout(&MASK_OFFSET_VAR);
57        }
58        UiNodeOp::Deinit => {
59            c.deinit();
60            img = None;
61        }
62        UiNodeOp::Update { .. } => {
63            // load
64            if source.is_new() || MASK_MODE_VAR.is_new() || MASK_IMAGE_DOWNSCALE_VAR.is_new() {
65                let mut source = source.get();
66
67                if let ImageSource::Render(_, args) = &mut source {
68                    *args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
69                }
70
71                let mode = if MASK_IMAGE_CACHE_VAR.get() {
72                    ImageCacheMode::Cache
73                } else {
74                    ImageCacheMode::Ignore
75                };
76                let limits = MASK_IMAGE_LIMITS_VAR.get();
77                let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
78                let mask_mode = MASK_MODE_VAR.get();
79
80                let i = IMAGES.image(source, mode, limits, downscale, Some(mask_mode));
81                let s = i.subscribe(UpdateOp::Layout, WIDGET.id());
82                img = Some((i, s));
83
84                WIDGET.layout();
85            } else if let Some(enabled) = MASK_IMAGE_CACHE_VAR.get_new() {
86                // cache-mode update:
87                let is_cached = img.as_ref().unwrap().0.with(|i| IMAGES.is_cached(i));
88                if enabled != is_cached {
89                    let i = if is_cached {
90                        // must not cache, but is cached, detach from cache.
91
92                        let img = img.take().unwrap().0;
93                        IMAGES.detach(img)
94                    } else {
95                        // must cache, but image is not cached, get source again.
96
97                        let source = source.get();
98                        let limits = MASK_IMAGE_LIMITS_VAR.get();
99                        let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
100                        let mask_mode = MASK_MODE_VAR.get();
101                        IMAGES.image(source, ImageCacheMode::Cache, limits, downscale, Some(mask_mode))
102                    };
103
104                    let s = i.subscribe(UpdateOp::Update, WIDGET.id());
105                    img = Some((i, s));
106
107                    WIDGET.layout();
108                }
109            } else if let Some(img) = img.as_ref().unwrap().0.get_new() {
110                let s = img.size();
111                if s != img_size {
112                    img_size = s;
113                    WIDGET.layout().render();
114                } else {
115                    WIDGET.render();
116                }
117            }
118        }
119        UiNodeOp::Layout { wl, final_size } => {
120            *final_size = c.layout(wl);
121
122            let wgt_size = *final_size;
123            let constraints = PxConstraints2d::new_fill_size(wgt_size);
124            LAYOUT.with_constraints(constraints, || {
125                let mut img_size = img_size;
126                let mut img_origin = PxPoint::zero();
127
128                let mut fit = MASK_FIT_VAR.get();
129                if let ImageFit::ScaleDown = fit {
130                    if img_size.width < wgt_size.width && img_size.height < wgt_size.height {
131                        fit = ImageFit::None;
132                    } else {
133                        fit = ImageFit::Contain;
134                    }
135                }
136
137                let mut align = MASK_ALIGN_VAR.get();
138                match fit {
139                    ImageFit::Fill => {
140                        align = Align::FILL;
141                    }
142                    ImageFit::Contain => {
143                        let container = wgt_size.to_f32();
144                        let content = img_size.to_f32();
145                        let scale = (container.width / content.width).min(container.height / content.height).fct();
146                        img_size *= scale;
147                    }
148                    ImageFit::Cover => {
149                        let container = wgt_size.to_f32();
150                        let content = img_size.to_f32();
151                        let scale = (container.width / content.width).max(container.height / content.height).fct();
152                        img_size *= scale;
153                    }
154                    ImageFit::None => {}
155                    ImageFit::ScaleDown => unreachable!(),
156                }
157
158                if align.is_fill_x() {
159                    let factor = wgt_size.width.0 as f32 / img_size.width.0 as f32;
160                    img_size.width *= factor;
161                } else {
162                    let diff = wgt_size.width - img_size.width;
163                    let offset = diff * align.x(LAYOUT.direction());
164                    img_origin.x += offset;
165                }
166                if align.is_fill_y() {
167                    let factor = wgt_size.height.0 as f32 / img_size.height.0 as f32;
168                    img_size.height *= factor;
169                } else {
170                    let diff = wgt_size.height - img_size.height;
171                    let offset = diff * align.y();
172                    img_origin.y += offset;
173                }
174
175                img_origin += MASK_OFFSET_VAR.layout();
176
177                let new_rect = PxRect::new(img_origin, img_size);
178                if rect != new_rect {
179                    rect = new_rect;
180                    WIDGET.render();
181                }
182            });
183        }
184        UiNodeOp::Render { frame } => {
185            img.as_ref().unwrap().0.with(|img| {
186                if img.is_loaded() && !rect.size.is_empty() {
187                    frame.push_mask(img, rect, |frame| c.render(frame));
188                }
189            });
190        }
191        _ => {}
192    })
193}
194
195context_var! {
196    /// Defines how the A8 image mask pixels are to be derived from a source mask image.
197    pub static MASK_MODE_VAR: ImageMaskMode = ImageMaskMode::default();
198
199    /// Defines if the mask image is cached.
200    pub static MASK_IMAGE_CACHE_VAR: bool = true;
201
202    /// Custom mask image load and decode limits.
203    ///
204    /// Set to `None` to use the `IMAGES::limits`.
205    pub static MASK_IMAGE_LIMITS_VAR: Option<ImageLimits> = None;
206
207    /// Custom resize applied during mask image decode.
208    ///
209    /// Is `None` by default.
210    pub static MASK_IMAGE_DOWNSCALE_VAR: Option<ImageDownscale> = None;
211
212    /// Defines how the mask image fits the widget bounds.
213    pub static MASK_FIT_VAR: ImageFit = ImageFit::Fill;
214
215    /// Align of the mask image in relation to the image widget final size.
216    ///
217    /// Is `Align::CENTER` by default.
218    pub static MASK_ALIGN_VAR: Align = Align::CENTER;
219
220    /// Offset applied to the mask image after all measure and arrange.
221    pub static MASK_OFFSET_VAR: Vector = Vector::default();
222}
223
224/// Defines how the A8 image mask pixels are to be derived from a source mask image in all [`mask_image`] inside
225/// the widget in descendants.
226///
227/// This property sets the [`MASK_MODE_VAR`].
228///
229/// [`mask_image`]: fn@mask_image
230#[property(CONTEXT, default(MASK_MODE_VAR))]
231pub fn mask_mode(child: impl UiNode, mode: impl IntoVar<ImageMaskMode>) -> impl UiNode {
232    with_context_var(child, MASK_MODE_VAR, mode)
233}
234
235/// Defines if the mask images loaded in all [`mask_image`] inside
236/// the widget in descendants are cached.
237///
238/// This property sets the [`MASK_IMAGE_CACHE_VAR`].
239///
240/// [`mask_image`]: fn@mask_image
241#[property(CONTEXT, default(MASK_IMAGE_CACHE_VAR))]
242pub fn mask_image_cache(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
243    with_context_var(child, MASK_IMAGE_CACHE_VAR, enabled)
244}
245
246/// Sets custom mask image load and decode limits.
247///
248/// If not set or set to `None` the [`IMAGES.limits`] is used.
249///
250/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
251#[property(CONTEXT, default(MASK_IMAGE_LIMITS_VAR))]
252pub fn mask_image_limits(child: impl UiNode, limits: impl IntoVar<Option<ImageLimits>>) -> impl UiNode {
253    with_context_var(child, MASK_IMAGE_LIMITS_VAR, limits)
254}
255
256/// Custom pixel resize applied during mask image load/decode.
257///
258/// Note that this resize affects the image actual pixel size directly when it is loading to force the image pixels to
259/// be within an expected size.
260/// This property primary use is as error recover before the [`mask_image_limits`] error happens, you set the limits to
261/// the size that should not even be processed and set this property to the maximum size expected.
262///
263/// Changing this value after an image is already loaded or loading will cause the image to reload, image cache allocates different
264/// entries for different downscale values, this means that this property should never be used for responsive resize,use the widget
265/// size and other properties to efficiently resize an image on screen.
266///
267/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
268/// [`mask_image_limits`]: fn@mask_image_limits
269#[property(CONTEXT, default(MASK_IMAGE_DOWNSCALE_VAR))]
270pub fn mask_image_downscale(child: impl UiNode, downscale: impl IntoVar<Option<ImageDownscale>>) -> impl UiNode {
271    with_context_var(child, MASK_IMAGE_DOWNSCALE_VAR, downscale)
272}
273
274/// Defines how the mask image fits the widget bounds in all [`mask_image`] inside
275/// the widget in descendants.
276///
277/// This property sets the [`MASK_FIT_VAR`].
278///
279/// [`mask_image`]: fn@mask_image
280#[property(CONTEXT, default(MASK_FIT_VAR))]
281pub fn mask_fit(child: impl UiNode, fit: impl IntoVar<ImageFit>) -> impl UiNode {
282    with_context_var(child, MASK_FIT_VAR, fit)
283}
284
285/// Defines the align of the mask image in relation to the widget bounds in all [`mask_image`] inside
286/// the widget in descendants.
287///
288/// This property sets the [`MASK_ALIGN_VAR`].
289///
290/// [`mask_image`]: fn@mask_image
291#[property(CONTEXT, default(MASK_ALIGN_VAR))]
292pub fn mask_align(child: impl UiNode, align: impl IntoVar<Align>) -> impl UiNode {
293    with_context_var(child, MASK_ALIGN_VAR, align)
294}
295
296/// Defines the offset applied to the mask image after all measure and arrange. in all [`mask_image`] inside
297/// the widget in descendants.
298///
299/// This property sets the [`MASK_OFFSET_VAR`].
300///
301/// [`mask_image`]: fn@mask_image
302#[property(CONTEXT, default(MASK_OFFSET_VAR))]
303pub fn mask_offset(child: impl UiNode, offset: impl IntoVar<Vector>) -> impl UiNode {
304    with_context_var(child, MASK_OFFSET_VAR, offset)
305}