zng_wgt_image/
border.rs

1//! 9-patch image border.
2
3use zng_app::render::RepeatMode;
4use zng_ext_image::{IMAGES, ImageCacheMode, ImageEntry, ImageOptions, ImageRenderArgs, ImageSource};
5use zng_wgt::prelude::*;
6
7use crate::{IMAGE_CACHE_VAR, IMAGE_LIMITS_VAR, IMAGE_RENDERING_VAR};
8
9/// 9-patch image border.
10///
11/// The `source` image is sliced by `slices` and projected onto `widths` in the widget layout.
12///
13/// See also [`border_img_repeat`] and [`border_img_fill`].
14///
15/// [`border_img_repeat`]: fn@border_img_repeat
16/// [`border_img_fill`]: fn@border_img_fill
17#[property(BORDER)]
18pub fn border_img(
19    child: impl IntoUiNode,
20    widths: impl IntoVar<SideOffsets>,
21    source: impl IntoVar<ImageSource>,
22    slices: impl IntoVar<SideOffsets>,
23) -> UiNode {
24    let widths = widths.into_var();
25    let source = source.into_var();
26    let slices = slices.into_var();
27
28    let mut img = var(ImageEntry::new_loading()).read_only();
29    let mut _img_sub = VarHandle::dummy();
30    let mut slices_img_size = PxSize::zero();
31    let mut slices_px = PxSideOffsets::zero();
32
33    border_node(
34        child,
35        widths,
36        match_node_leaf(move |op| match op {
37            UiNodeOp::Init => {
38                WIDGET
39                    .sub_var(&source)
40                    .sub_var(&IMAGE_CACHE_VAR)
41                    .sub_var_render(&slices)
42                    .sub_var_render(&BORDER_IMG_REPEAT_VAR)
43                    .sub_var_render(&IMAGE_RENDERING_VAR)
44                    .sub_var_render(&BORDER_IMG_FILL_VAR);
45
46                let mode = if IMAGE_CACHE_VAR.get() {
47                    ImageCacheMode::Cache
48                } else {
49                    ImageCacheMode::Ignore
50                };
51                let limits = IMAGE_LIMITS_VAR.get();
52
53                let mut source = source.get();
54                if let ImageSource::Render(_, args) = &mut source {
55                    *args = Some(ImageRenderArgs::new(WINDOW.id()));
56                }
57
58                let mut opt = ImageOptions::cache();
59                opt.cache_mode = mode;
60                img = IMAGES.image(source, opt, limits);
61                _img_sub = img.subscribe(UpdateOp::Update, WIDGET.id());
62
63                let img = img.get();
64                if img.is_loaded() {
65                    slices_img_size = img.size();
66                }
67            }
68            UiNodeOp::Deinit => {
69                img = var(ImageEntry::new_loading()).read_only();
70                _img_sub = VarHandle::dummy();
71            }
72            UiNodeOp::Update { .. } => {
73                if source.is_new() {
74                    // source update:
75
76                    let mut source = source.get();
77
78                    if let ImageSource::Render(_, args) = &mut source {
79                        *args = Some(ImageRenderArgs::new(WINDOW.id()));
80                    }
81
82                    let mode = if IMAGE_CACHE_VAR.get() {
83                        ImageCacheMode::Cache
84                    } else {
85                        ImageCacheMode::Ignore
86                    };
87                    let limits = IMAGE_LIMITS_VAR.get();
88                    let mut opt = ImageOptions::cache();
89                    opt.cache_mode = mode;
90                    img = IMAGES.image(source, opt, limits);
91                } else if let Some(enabled) = IMAGE_CACHE_VAR.get_new() {
92                    // cache-mode update:
93                    let is_cached = img.with(|img| IMAGES.is_cached(img));
94                    if enabled != is_cached {
95                        let source = source.get();
96                        let limits = IMAGE_LIMITS_VAR.get();
97
98                        let mut opt = ImageOptions::cache();
99
100                        if is_cached {
101                            // must not cache, but is cached, detach from cache.
102
103                            img = const_var(ImageEntry::new_loading()); // drop
104                            if let Some(h) = source.hash128(&opt) {
105                                IMAGES.clean(h);
106                            }
107                            opt.cache_mode = ImageCacheMode::Ignore;
108                        }
109                        img = IMAGES.image(source, opt, limits);
110                    }
111                }
112
113                if img.is_new() {
114                    let img = img.get();
115                    if img.is_loaded() {
116                        let s = img.size();
117                        if s != slices_img_size {
118                            slices_img_size = s;
119                            WIDGET.layout();
120                        }
121                    }
122                    WIDGET.render();
123                }
124            }
125            UiNodeOp::Measure { desired_size, .. } => {
126                *desired_size = LAYOUT.constraints().fill_size();
127            }
128            UiNodeOp::Layout { final_size, .. } => {
129                *final_size = LAYOUT.constraints().fill_size();
130
131                let metrics = LAYOUT
132                    .metrics()
133                    .with_constraints(PxConstraints2d::new_exact_size(slices_img_size))
134                    .with_scale_factor(1.fct());
135                let s = LAYOUT.with_context(metrics, || slices.layout());
136                if s != slices_px {
137                    slices_px = s;
138                    WIDGET.render();
139                }
140            }
141            UiNodeOp::Render { frame } => {
142                let img = img.get();
143                if img.is_loaded() {
144                    let (rect, offsets) = BORDER.border_layout();
145                    if !rect.size.is_empty() {
146                        let repeats = BORDER_IMG_REPEAT_VAR.get();
147                        frame.push_border_image(
148                            rect,
149                            offsets,
150                            slices_px,
151                            BORDER_IMG_FILL_VAR.get(),
152                            repeats.top_bottom,
153                            repeats.left_right,
154                            &img,
155                            IMAGE_RENDERING_VAR.get(),
156                        );
157                    }
158                }
159            }
160            _ => {}
161        }),
162    )
163}
164
165/// Defines how the 9-patch edge slices are used to fill the widths.
166///
167/// This property sets the [`BORDER_IMG_REPEAT_VAR`].
168#[property(CONTEXT, default(BORDER_IMG_REPEAT_VAR))]
169pub fn border_img_repeat(child: impl IntoUiNode, repeats: impl IntoVar<BorderRepeats>) -> UiNode {
170    with_context_var(child, BORDER_IMG_REPEAT_VAR, repeats)
171}
172
173/// Defines if the middle slice of the 9-patch image is also rendered.
174///
175/// This property sets the [`BORDER_IMG_FILL_VAR`].
176#[property(CONTEXT, default(BORDER_IMG_FILL_VAR))]
177pub fn border_img_fill(child: impl IntoUiNode, fill: impl IntoVar<bool>) -> UiNode {
178    with_context_var(child, BORDER_IMG_FILL_VAR, fill)
179}
180
181/// Defines how the 9-patch edge slices are used to fill the widths.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
183pub struct BorderRepeats {
184    /// Top and bottom edges.
185    ///
186    /// Also middle if *fill* is set.
187    pub top_bottom: RepeatMode,
188    /// Left and right edges.
189    pub left_right: RepeatMode,
190}
191impl BorderRepeats {
192    /// Top-bottom and left-right equal. From any [`RepeatMode`] type.
193    pub fn new<TB: Into<RepeatMode>, LR: Into<RepeatMode>>(top_bottom: TB, left_right: LR) -> Self {
194        BorderRepeats {
195            top_bottom: top_bottom.into(),
196            left_right: left_right.into(),
197        }
198    }
199
200    /// All sides equal. From any [`RepeatMode`] type.
201    pub fn new_all<T: Into<RepeatMode>>(all_sides: T) -> Self {
202        let all_sides = all_sides.into();
203        Self::new(all_sides, all_sides)
204    }
205}
206context_var! {
207    /// Defines how the 9-patch edge slices are used to fill the widths.
208    pub static BORDER_IMG_REPEAT_VAR: BorderRepeats = BorderRepeats::default();
209
210    /// If the middle slice is rendered in a 9-patch border.
211    pub static BORDER_IMG_FILL_VAR: bool = false;
212}
213
214impl_from_and_into_var! {
215    fn from(repeat: RepeatMode) -> BorderRepeats {
216        BorderRepeats::new_all(repeat)
217    }
218
219    /// `true` is `Repeat`, `false` is `Stretch`.
220    fn from(repeat_or_stretch: bool) -> BorderRepeats {
221        BorderRepeats::new_all(repeat_or_stretch)
222    }
223
224    fn from<TB: Into<RepeatMode>, LR: Into<RepeatMode>>((top_bottom, left_right): (TB, LR)) -> BorderRepeats {
225        BorderRepeats::new(top_bottom, left_right)
226    }
227}