Skip to main content

zng_wgt_scroll/
lazy_prop.rs

1use std::{fmt, mem};
2
3use crate::ScrollMode;
4use zng_wgt::prelude::*;
5
6/// Lazy init mode of a widget.
7///
8/// See [`lazy`] property for more details.
9///
10/// [`lazy`]: fn@lazy
11#[derive(Clone, PartialEq)]
12pub enum LazyMode {
13    /// Node always inited.
14    Disabled,
15    /// Node lazy inited.
16    Enabled {
17        /// Function that instantiates the node that replaces the widget when it is not in the init viewport.
18        ///
19        /// All node methods are called on the placeholder, except the render methods, it should efficiently estimate
20        /// the size of the inited widget.
21        placeholder: WidgetFn<()>,
22        /// If the node is deinited when is moved out of the viewport.
23        ///
24        /// If `false` the node stays inited after the first lazy init.
25        ///
26        /// If `true` the placeholder size is always used, this is to avoid the app entering a "flickering" loop
27        /// when the actual bounds are different causing an immediate deinit. An error is logged if the placeholder
28        /// size does not match.
29        deinit: bool,
30
31        /// The scroll directions that are considered for intersection with the viewport.
32        ///
33        /// If set to [`ScrollMode::VERTICAL`] the widget is inited if it intersects on the vertical dimension only,
34        /// even if it is not actually in the viewport due to horizontal offset, and if `deinit` is flagged only the placeholder
35        /// height is enforced, the width can be different from the actual.
36        ///
37        /// If set to [`ScrollMode::NONE`] this value is ignored, behaving like [`ScrollMode::PAN`].
38        intersect: ScrollMode,
39    },
40}
41impl LazyMode {
42    /// Enable lazy mode with a node that estimates the widget size.
43    ///
44    /// The widget function must produce a node that is used as the layout placeholder for the actual widget content.
45    ///
46    /// The widget will init when the placeholder stops being culled by render, and deinit when it starts being culled.
47    /// Note that in this mode the placeholder size is always used as the widget size, see the `deinit` docs in [`LazyMode::Enabled`]
48    /// for more details.
49    ///
50    /// See [`FrameBuilder::auto_hide_rect`] for more details about render culling.
51    ///
52    /// [`FrameBuilder::auto_hide_rect`]: zng_wgt::prelude::FrameBuilder::auto_hide_rect
53    pub fn lazy(placeholder: WidgetFn<()>) -> Self {
54        Self::Enabled {
55            placeholder,
56            deinit: true,
57            intersect: ScrollMode::PAN,
58        }
59    }
60
61    /// Like [`lazy`], but only considers the height and vertical offset to init and deinit. Like [`lazy`]
62    /// the placeholder height is enforced, but the width is allowed to change between placeholder and actual.
63    ///
64    /// Note that if the widget is inlined the full size of the widget placeholder is enforced like [`lazy`],
65    /// the widget will still init and deinit considering only the vertical intersection.
66    ///
67    /// [`lazy`]: Self::lazy
68    pub fn lazy_vertical(placeholder: WidgetFn<()>) -> Self {
69        Self::Enabled {
70            placeholder,
71            deinit: true,
72            intersect: ScrollMode::VERTICAL,
73        }
74    }
75
76    /// Like [`lazy`], but only considers the width and horizontal offset to init and deinit. Like [`lazy`]
77    /// the placeholder width is enforced, but the height is allowed to change between placeholder and actual.
78    ///
79    /// Note that if the widget is inlined the full size of the widget placeholder is enforced like [`lazy`],
80    /// the widget will still init and deinit considering only the horizontal intersection.
81    ///
82    /// [`lazy`]: Self::lazy
83    pub fn lazy_horizontal(placeholder: WidgetFn<()>) -> Self {
84        Self::Enabled {
85            placeholder,
86            deinit: true,
87            intersect: ScrollMode::HORIZONTAL,
88        }
89    }
90
91    /// Like [`lazy`] but the widget stays inited after the first, even if it is culled by render it will be present in the UI tree.
92    ///
93    /// Note that this mode allows the actual size to be different from the placeholder size, so it can be used for items that
94    /// can't estimate their own size exactly.
95    ///
96    /// This mode is only recommended for items that are "heavy" to init, but light after, otherwise the app will show degraded
97    /// performance after many items are inited.
98    ///
99    /// [`lazy`]: Self::lazy
100    pub fn once(placeholder: WidgetFn<()>) -> Self {
101        Self::Enabled {
102            placeholder,
103            deinit: false,
104            intersect: ScrollMode::PAN,
105        }
106    }
107
108    /// Like [`once`], but only considers the height and vertical offset to init.
109    ///
110    /// [`once`]: Self::once
111    pub fn once_vertical(placeholder: WidgetFn<()>) -> Self {
112        Self::Enabled {
113            placeholder,
114            deinit: false,
115            intersect: ScrollMode::VERTICAL,
116        }
117    }
118
119    /// Like [`once`], but only considers the width and horizontal offset to init.
120    ///
121    /// [`once`]: Self::once
122    pub fn once_horizontal(placeholder: WidgetFn<()>) -> Self {
123        Self::Enabled {
124            placeholder,
125            deinit: false,
126            intersect: ScrollMode::HORIZONTAL,
127        }
128    }
129
130    /// If lazy init is mode.
131    pub fn is_enabled(&self) -> bool {
132        matches!(self, Self::Enabled { .. })
133    }
134
135    /// If lazy init is disabled.
136    pub fn is_disabled(&self) -> bool {
137        matches!(self, Self::Disabled)
138    }
139
140    /// Unwrap and correct the intersect mode.
141    fn unwrap_intersect(&self) -> ScrollMode {
142        match self {
143            LazyMode::Disabled => panic!("expected `LazyMode::Enabled`"),
144            LazyMode::Enabled { intersect, .. } => {
145                let m = *intersect;
146                if m.is_empty() { ScrollMode::PAN } else { m }
147            }
148        }
149    }
150}
151impl fmt::Debug for LazyMode {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            Self::Disabled => write!(f, "Disabled"),
155            Self::Enabled { deinit, .. } => f.debug_struct("Enabled").field("deinit", deinit).finish_non_exhaustive(),
156        }
157    }
158}
159
160/// Enables lazy init for the widget.
161///
162/// See [`LazyMode`] for details.
163#[property(WIDGET, default(LazyMode::Disabled))]
164pub fn lazy(child: impl IntoUiNode, mode: impl IntoVar<LazyMode>) -> UiNode {
165    let mode = mode.into_var();
166    // max two nodes:
167    // * in `deinit` mode can be two [0]: placeholder, [1]: actual.
168    // * or can be only placeholder or only actual.
169    let children = ui_vec![];
170    // actual child, not inited
171    let mut not_inited = Some(child.into_node());
172    let mut in_viewport = false;
173
174    match_node(children, move |c, op| match op {
175        UiNodeOp::Init => {
176            WIDGET.sub_var(&mode);
177
178            if let LazyMode::Enabled { placeholder, deinit, .. } = mode.get() {
179                if mem::take(&mut in_viewport) {
180                    // init
181
182                    if deinit {
183                        // Keep placeholder, layout will still use it to avoid glitches when the actual layout causes a deinit,
184                        // and the placeholder another init on a loop.
185                        //
186                        // This time we have the actual widget content, so the placeholder is upgraded to a full widget to
187                        // have a place to store the layout info.
188
189                        let placeholder = placeholder(()).into_widget();
190                        c.node_impl::<UiVec>().push(placeholder);
191                    }
192                    c.node_impl::<UiVec>().push(not_inited.take().unwrap());
193                } else {
194                    // only placeholder
195
196                    let placeholder = placeholder(());
197                    let placeholder = zng_app::widget::base::node::widget_inner(placeholder);
198
199                    // just placeholder, and as the `widget_inner`, first render may init
200                    c.node_impl::<UiVec>().push(placeholder);
201                }
202            } else {
203                // not enabled, just init actual
204                c.node_impl::<UiVec>().push(not_inited.take().unwrap());
205            }
206        }
207        UiNodeOp::Deinit => {
208            c.deinit();
209
210            if not_inited.is_none() {
211                not_inited = c.node_impl::<UiVec>().pop(); // pop actual
212            }
213            c.node_impl::<UiVec>().clear(); // drop placeholder, if any
214        }
215        UiNodeOp::Update { .. } if mode.is_new() => {
216            WIDGET.reinit();
217        }
218        UiNodeOp::Measure { wm, desired_size } => {
219            c.delegated();
220            let mut size = c.node().with_child(0, |n| n.measure(wm));
221
222            if not_inited.is_none() && c.node().children_len() == 2 {
223                // is inited and can deinit, measure the actual child and validate
224
225                let lazy_size = size;
226                let actual_size = c.node().with_child(1, |n| n.measure(wm));
227
228                let mut intersect_mode = ScrollMode::empty();
229
230                let lazy_inline = c.node_impl::<UiVec>()[0]
231                    .as_widget()
232                    .and_then(|mut w| w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().measure_inline()));
233                if let Some(actual_inline) = wm.inline() {
234                    if let Some(lazy_inline) = lazy_inline {
235                        fn validate<T: PartialEq + fmt::Debug>(actual: T, lazy: T, name: &'static str) {
236                            if actual != lazy {
237                                tracing::debug!(
238                                    target: "lazy",
239                                    "widget `{}` measure inline {name} `{actual:?}` not equal to lazy `{lazy:?}`",
240                                    WIDGET.id()
241                                );
242                            }
243                        }
244                        validate(actual_inline.first, lazy_inline.first, "first");
245                        validate(actual_inline.first_wrapped, lazy_inline.first_wrapped, "first_wrapped");
246                        validate(actual_inline.last, lazy_inline.last, "last");
247                        validate(actual_inline.last_wrapped, lazy_inline.last_wrapped, "last_wrapped");
248
249                        actual_inline.first = lazy_inline.first;
250                        actual_inline.first_wrapped = lazy_inline.first_wrapped;
251                        actual_inline.last = lazy_inline.last;
252                        actual_inline.last_wrapped = lazy_inline.last_wrapped;
253
254                        intersect_mode = ScrollMode::PAN;
255                    } else {
256                        tracing::debug!(target: "lazy", "widget `{}` measure inlined, but lazy did not inline", WIDGET.id());
257                    }
258                } else {
259                    if lazy_inline.is_some() {
260                        tracing::debug!(target: "lazy", "widget `{}` measure did not inline, but lazy did", WIDGET.id());
261                    }
262
263                    intersect_mode = mode.with(|s| s.unwrap_intersect());
264                }
265
266                if intersect_mode == ScrollMode::PAN {
267                    if lazy_size != actual_size {
268                        tracing::debug!(
269                            target: "lazy",
270                            "widget `{}` measure size `{actual_size:?}` not equal to lazy size `{lazy_size:?}`",
271                            WIDGET.id()
272                        );
273                    }
274                } else if intersect_mode == ScrollMode::VERTICAL {
275                    if lazy_size.height != actual_size.height {
276                        tracing::debug!(
277                            target: "lazy",
278                            "widget `{}` measure height `{:?}` not equal to lazy height `{:?}`",
279                            WIDGET.id(),
280                            actual_size.height,
281                            lazy_size.height,
282                        );
283                    }
284
285                    size.width = actual_size.width;
286                } else if intersect_mode == ScrollMode::HORIZONTAL {
287                    if lazy_size.width != actual_size.width {
288                        tracing::debug!(
289                            target: "lazy",
290                            "widget `{}` measure width `{:?}` not equal to lazy width `{:?}`",
291                            WIDGET.id(),
292                            actual_size.width,
293                            lazy_size.width,
294                        );
295                    }
296
297                    size.height = actual_size.height;
298                }
299            }
300
301            *desired_size = size;
302        }
303        UiNodeOp::Layout { wl, final_size } => {
304            c.delegated();
305
306            let mut size = c.node().with_child(0, |n| n.layout(wl));
307
308            if not_inited.is_none() && c.node().children_len() == 2 {
309                // is inited and can deinit, layout the actual child and validate
310
311                let lazy_size = size;
312                let actual_size = c.node().with_child(1, |n| n.layout(wl));
313
314                let mut intersect_mode = ScrollMode::empty();
315
316                let lazy_inlined = c.node_impl::<UiVec>()[0]
317                    .as_widget()
318                    .unwrap()
319                    .with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().inline().is_some());
320                if wl.inline().is_some() {
321                    if !lazy_inlined {
322                        tracing::debug!(target: "lazy", "widget `{}` inlined, but lazy did not inline", WIDGET.id());
323                    } else {
324                        intersect_mode = ScrollMode::PAN;
325                    }
326                } else {
327                    if lazy_inlined {
328                        tracing::debug!(target: "lazy", "widget `{}` layout did not inline, but lazy did", WIDGET.id());
329                    }
330
331                    intersect_mode = mode.with(|s| s.unwrap_intersect());
332                }
333
334                if intersect_mode == ScrollMode::PAN {
335                    if lazy_size != actual_size {
336                        tracing::debug!(
337                            target: "lazy",
338                            "widget `{}` layout size `{actual_size:?}` not equal to lazy size `{lazy_size:?}`",
339                            WIDGET.id()
340                        );
341                    }
342                } else if intersect_mode == ScrollMode::VERTICAL {
343                    if lazy_size.height != actual_size.height {
344                        tracing::debug!(
345                            target: "lazy",
346                            "widget `{}` layout height `{:?}` not equal to lazy height `{:?}`",
347                            WIDGET.id(),
348                            actual_size.height,
349                            lazy_size.height,
350                        );
351                    }
352
353                    size.width = actual_size.width;
354                } else if intersect_mode == ScrollMode::HORIZONTAL {
355                    if lazy_size.width != actual_size.width {
356                        tracing::debug!(
357                            target: "lazy",
358                            "widget `{}` layout width `{:?}` not equal to lazy width `{:?}`",
359                            WIDGET.id(),
360                            actual_size.width,
361                            lazy_size.width,
362                        );
363                    }
364
365                    size.height = actual_size.height;
366                }
367            }
368
369            *final_size = size;
370        }
371        UiNodeOp::Render { frame } => {
372            c.delegated();
373
374            if not_inited.is_some() {
375                // not inited, verify
376
377                c.node_impl::<UiVec>()[0].render(frame); // update bounds
378
379                let intersect_mode = mode.with(|s| s.unwrap_intersect());
380                let outer_bounds = WIDGET.bounds().outer_bounds();
381                let viewport = frame.auto_hide_rect();
382
383                in_viewport = if intersect_mode == ScrollMode::VERTICAL {
384                    outer_bounds.min_y() < viewport.max_y() && outer_bounds.max_y() > viewport.min_y()
385                } else if intersect_mode == ScrollMode::HORIZONTAL {
386                    outer_bounds.min_x() < viewport.max_x() && outer_bounds.max_x() > viewport.min_x()
387                } else {
388                    outer_bounds.intersects(&viewport)
389                };
390                if in_viewport {
391                    // request init
392                    WIDGET.reinit();
393                }
394            } else if c.node().children_len() == 2 {
395                // is inited and can deinit, check viewport on placeholder
396
397                c.node_impl::<UiVec>()[1].render(frame); // render + update bounds
398
399                frame.hide(|f| {
400                    f.with_hit_tests_disabled(|f| {
401                        // update bounds (not used but can be inspected)
402                        c.node_impl::<UiVec>()[0].render(f);
403                    });
404                });
405
406                let intersect_mode = mode.with(|s| s.unwrap_intersect());
407                let viewport = frame.auto_hide_rect();
408                let outer_bounds = WIDGET.bounds().outer_bounds();
409
410                in_viewport = if intersect_mode == ScrollMode::VERTICAL {
411                    outer_bounds.min_y() < viewport.max_y() && outer_bounds.max_y() > viewport.min_y()
412                } else if intersect_mode == ScrollMode::HORIZONTAL {
413                    outer_bounds.min_x() < viewport.max_x() && outer_bounds.max_x() > viewport.min_x()
414                } else {
415                    outer_bounds.intersects(&viewport)
416                };
417                if !in_viewport {
418                    // request deinit
419                    WIDGET.reinit();
420                }
421            } else {
422                // is inited and cannot deinit
423                c.node_impl::<UiVec>()[0].render(frame);
424            }
425        }
426        UiNodeOp::RenderUpdate { update } => {
427            c.delegated();
428            if not_inited.is_none() {
429                // child is actual child
430                let last = c.node().children_len() - 1;
431
432                c.node_impl::<UiVec>()[last].render_update(update);
433
434                if last == 1 {
435                    update.hidden(|u| {
436                        // update bounds (not used but can be inspected)
437                        c.node_impl::<UiVec>()[0].render_update(u);
438                    });
439                }
440            } else {
441                // update bounds
442                c.node_impl::<UiVec>().render_update(update);
443            }
444        }
445        _ => {}
446    })
447}