zng_wgt_image/
mask.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
//! Mask properties, [`mask_image`], [`mask_mode`] and more.
//!
//! [`mask_image`]: fn@mask_image
//! [`mask_mode`]: fn@mask_mode

use zng_ext_image::{ImageCacheMode, ImageDownscale, ImageLimits, ImageMaskMode, ImageRenderArgs, ImageSource, IMAGES};
use zng_wgt::prelude::*;

use crate::ImageFit;

/// Sets an image mask.
///
/// The image alpha channel is used as a mask for the widget and descendants.
///
/// This property is configured by contextual values set by the properties in the [`mask`] module.
///
/// [`mask`]: crate::mask
#[property(FILL-1)]
pub fn mask_image(child: impl UiNode, source: impl IntoVar<ImageSource>) -> impl UiNode {
    let source = source.into_var();
    let mut img = None;
    let mut img_size = PxSize::zero();
    let mut rect = PxRect::zero();

    match_node(child, move |c, op| match op {
        UiNodeOp::Init => {
            // load
            WIDGET
                .sub_var(&source)
                .sub_var(&MASK_MODE_VAR)
                .sub_var(&MASK_IMAGE_CACHE_VAR)
                .sub_var(&MASK_IMAGE_DOWNSCALE_VAR);

            let mode = if MASK_IMAGE_CACHE_VAR.get() {
                ImageCacheMode::Cache
            } else {
                ImageCacheMode::Ignore
            };
            let limits = MASK_IMAGE_LIMITS_VAR.get();
            let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
            let mask_mode = MASK_MODE_VAR.get();

            let mut source = source.get();
            if let ImageSource::Render(_, args) = &mut source {
                *args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
            }
            let i = IMAGES.image(source, mode, limits, downscale, Some(mask_mode));
            let s = i.subscribe(UpdateOp::Update, WIDGET.id());
            img = Some((i, s));

            // present

            WIDGET
                .sub_var_layout(&MASK_FIT_VAR)
                .sub_var_layout(&MASK_ALIGN_VAR)
                .sub_var_layout(&MASK_OFFSET_VAR);
        }
        UiNodeOp::Deinit => {
            c.deinit();
            img = None;
        }
        UiNodeOp::Update { .. } => {
            // load
            if source.is_new() || MASK_MODE_VAR.is_new() || MASK_IMAGE_DOWNSCALE_VAR.is_new() {
                let mut source = source.get();

                if let ImageSource::Render(_, args) = &mut source {
                    *args = Some(ImageRenderArgs { parent: Some(WINDOW.id()) });
                }

                let mode = if MASK_IMAGE_CACHE_VAR.get() {
                    ImageCacheMode::Cache
                } else {
                    ImageCacheMode::Ignore
                };
                let limits = MASK_IMAGE_LIMITS_VAR.get();
                let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
                let mask_mode = MASK_MODE_VAR.get();

                let i = IMAGES.image(source, mode, limits, downscale, Some(mask_mode));
                let s = i.subscribe(UpdateOp::Layout, WIDGET.id());
                img = Some((i, s));

                WIDGET.layout();
            } else if let Some(enabled) = MASK_IMAGE_CACHE_VAR.get_new() {
                // cache-mode update:
                let is_cached = img.as_ref().unwrap().0.with(|i| IMAGES.is_cached(i));
                if enabled != is_cached {
                    let i = if is_cached {
                        // must not cache, but is cached, detach from cache.

                        let img = img.take().unwrap().0;
                        IMAGES.detach(img)
                    } else {
                        // must cache, but image is not cached, get source again.

                        let source = source.get();
                        let limits = MASK_IMAGE_LIMITS_VAR.get();
                        let downscale = MASK_IMAGE_DOWNSCALE_VAR.get();
                        let mask_mode = MASK_MODE_VAR.get();
                        IMAGES.image(source, ImageCacheMode::Cache, limits, downscale, Some(mask_mode))
                    };

                    let s = i.subscribe(UpdateOp::Update, WIDGET.id());
                    img = Some((i, s));

                    WIDGET.layout();
                }
            } else if let Some(img) = img.as_ref().unwrap().0.get_new() {
                let s = img.size();
                if s != img_size {
                    img_size = s;
                    WIDGET.layout().render();
                } else {
                    WIDGET.render();
                }
            }
        }
        UiNodeOp::Layout { wl, final_size } => {
            *final_size = c.layout(wl);

            let wgt_size = *final_size;
            let constraints = PxConstraints2d::new_fill_size(wgt_size);
            LAYOUT.with_constraints(constraints, || {
                let mut img_size = img_size;
                let mut img_origin = PxPoint::zero();

                let mut fit = MASK_FIT_VAR.get();
                if let ImageFit::ScaleDown = fit {
                    if img_size.width < wgt_size.width && img_size.height < wgt_size.height {
                        fit = ImageFit::None;
                    } else {
                        fit = ImageFit::Contain;
                    }
                }

                let mut align = MASK_ALIGN_VAR.get();
                match fit {
                    ImageFit::Fill => {
                        align = Align::FILL;
                    }
                    ImageFit::Contain => {
                        let container = wgt_size.to_f32();
                        let content = img_size.to_f32();
                        let scale = (container.width / content.width).min(container.height / content.height).fct();
                        img_size *= scale;
                    }
                    ImageFit::Cover => {
                        let container = wgt_size.to_f32();
                        let content = img_size.to_f32();
                        let scale = (container.width / content.width).max(container.height / content.height).fct();
                        img_size *= scale;
                    }
                    ImageFit::None => {}
                    ImageFit::ScaleDown => unreachable!(),
                }

                if align.is_fill_x() {
                    let factor = wgt_size.width.0 as f32 / img_size.width.0 as f32;
                    img_size.width *= factor;
                } else {
                    let diff = wgt_size.width - img_size.width;
                    let offset = diff * align.x(LAYOUT.direction());
                    img_origin.x += offset;
                }
                if align.is_fill_y() {
                    let factor = wgt_size.height.0 as f32 / img_size.height.0 as f32;
                    img_size.height *= factor;
                } else {
                    let diff = wgt_size.height - img_size.height;
                    let offset = diff * align.y();
                    img_origin.y += offset;
                }

                img_origin += MASK_OFFSET_VAR.layout();

                let new_rect = PxRect::new(img_origin, img_size);
                if rect != new_rect {
                    rect = new_rect;
                    WIDGET.render();
                }
            });
        }
        UiNodeOp::Render { frame } => {
            img.as_ref().unwrap().0.with(|img| {
                if img.is_loaded() && !rect.size.is_empty() {
                    frame.push_mask(img, rect, |frame| c.render(frame));
                }
            });
        }
        _ => {}
    })
}

context_var! {
    /// Defines how the A8 image mask pixels are to be derived from a source mask image.
    pub static MASK_MODE_VAR: ImageMaskMode = ImageMaskMode::default();

    /// Defines if the mask image is cached.
    pub static MASK_IMAGE_CACHE_VAR: bool = true;

    /// Custom mask image load and decode limits.
    ///
    /// Set to `None` to use the `IMAGES::limits`.
    pub static MASK_IMAGE_LIMITS_VAR: Option<ImageLimits> = None;

    /// Custom resize applied during mask image decode.
    ///
    /// Is `None` by default.
    pub static MASK_IMAGE_DOWNSCALE_VAR: Option<ImageDownscale> = None;

    /// Defines how the mask image fits the widget bounds.
    pub static MASK_FIT_VAR: ImageFit = ImageFit::Fill;

    /// Align of the mask image in relation to the image widget final size.
    ///
    /// Is `Align::CENTER` by default.
    pub static MASK_ALIGN_VAR: Align = Align::CENTER;

    /// Offset applied to the mask image after all measure and arrange.
    pub static MASK_OFFSET_VAR: Vector = Vector::default();
}

/// Defines how the A8 image mask pixels are to be derived from a source mask image in all [`mask_image`] inside
/// the widget in descendants.
///
/// This property sets the [`MASK_MODE_VAR`].
///
/// [`mask_image`]: fn@mask_image
#[property(CONTEXT, default(MASK_MODE_VAR))]
pub fn mask_mode(child: impl UiNode, mode: impl IntoVar<ImageMaskMode>) -> impl UiNode {
    with_context_var(child, MASK_MODE_VAR, mode)
}

/// Defines if the mask images loaded in all [`mask_image`] inside
/// the widget in descendants are cached.
///
/// This property sets the [`MASK_IMAGE_CACHE_VAR`].
///
/// [`mask_image`]: fn@mask_image
#[property(CONTEXT, default(MASK_IMAGE_CACHE_VAR))]
pub fn mask_image_cache(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
    with_context_var(child, MASK_IMAGE_CACHE_VAR, enabled)
}

/// Sets custom mask image load and decode limits.
///
/// If not set or set to `None` the [`IMAGES.limits`] is used.
///
/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
/// [`img_downscale`]: fn@img_downscale
#[property(CONTEXT, default(MASK_IMAGE_LIMITS_VAR))]
pub fn mask_image_limits(child: impl UiNode, limits: impl IntoVar<Option<ImageLimits>>) -> impl UiNode {
    with_context_var(child, MASK_IMAGE_LIMITS_VAR, limits)
}

/// Custom pixel resize applied during mask image load/decode.
///
/// 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.
/// This property primary use is as error recover before the [`mask_image_limits`] error happens, you set the limits to
/// the size that should not even be processed and set this property to the maximum size expected.
///
/// Changing this value after an image is already loaded or loading will cause the image to reload, image cache allocates different
/// entries for different downscale values, this means that this property should never be used for responsive resize,use the widget
/// size and other properties to efficiently resize an image on screen.
///
/// [`IMAGES.limits`]: zng_ext_image::IMAGES::limits
/// [`mask_image_limits`]: fn@mask_image_limits
#[property(CONTEXT, default(MASK_IMAGE_DOWNSCALE_VAR))]
pub fn mask_image_downscale(child: impl UiNode, downscale: impl IntoVar<Option<ImageDownscale>>) -> impl UiNode {
    with_context_var(child, MASK_IMAGE_DOWNSCALE_VAR, downscale)
}

/// Defines how the mask image fits the widget bounds in all [`mask_image`] inside
/// the widget in descendants.
///
/// This property sets the [`MASK_FIT_VAR`].
///
/// [`mask_image`]: fn@mask_image
#[property(CONTEXT, default(MASK_FIT_VAR))]
pub fn mask_fit(child: impl UiNode, fit: impl IntoVar<ImageFit>) -> impl UiNode {
    with_context_var(child, MASK_FIT_VAR, fit)
}

/// Defines the align of the mask image in relation to the widget bounds in all [`mask_image`] inside
/// the widget in descendants.
///
/// This property sets the [`MASK_ALIGN_VAR`].
///
/// [`mask_image`]: fn@mask_image
#[property(CONTEXT, default(MASK_ALIGN_VAR))]
pub fn mask_align(child: impl UiNode, align: impl IntoVar<Align>) -> impl UiNode {
    with_context_var(child, MASK_ALIGN_VAR, align)
}

/// Defines the offset applied to the mask image after all measure and arrange. in all [`mask_image`] inside
/// the widget in descendants.
///
/// This property sets the [`MASK_OFFSET_VAR`].
///
/// [`mask_image`]: fn@mask_image
#[property(CONTEXT, default(MASK_OFFSET_VAR))]
pub fn mask_offset(child: impl UiNode, offset: impl IntoVar<Vector>) -> impl UiNode {
    with_context_var(child, MASK_OFFSET_VAR, offset)
}