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 { .. } => {
216            if mode.is_new() {
217                WIDGET.reinit();
218            }
219        }
220        UiNodeOp::Measure { wm, desired_size } => {
221            c.delegated();
222            let mut size = c.node().with_child(0, |n| n.measure(wm));
223
224            if not_inited.is_none() && c.node().children_len() == 2 {
225                // is inited and can deinit, measure the actual child and validate
226
227                let lazy_size = size;
228                let actual_size = c.node().with_child(1, |n| n.measure(wm));
229
230                let mut intersect_mode = ScrollMode::empty();
231
232                let lazy_inline = c.node_impl::<UiVec>()[0]
233                    .as_widget()
234                    .and_then(|mut w| w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().measure_inline()));
235                if let Some(actual_inline) = wm.inline() {
236                    if let Some(lazy_inline) = lazy_inline {
237                        fn validate<T: PartialEq + fmt::Debug>(actual: T, lazy: T, name: &'static str) {
238                            if actual != lazy {
239                                tracing::debug!(
240                                    target: "lazy",
241                                    "widget `{}` measure inline {name} `{actual:?}` not equal to lazy `{lazy:?}`",
242                                    WIDGET.id()
243                                );
244                            }
245                        }
246                        validate(actual_inline.first, lazy_inline.first, "first");
247                        validate(actual_inline.first_wrapped, lazy_inline.first_wrapped, "first_wrapped");
248                        validate(actual_inline.last, lazy_inline.last, "last");
249                        validate(actual_inline.last_wrapped, lazy_inline.last_wrapped, "last_wrapped");
250
251                        actual_inline.first = lazy_inline.first;
252                        actual_inline.first_wrapped = lazy_inline.first_wrapped;
253                        actual_inline.last = lazy_inline.last;
254                        actual_inline.last_wrapped = lazy_inline.last_wrapped;
255
256                        intersect_mode = ScrollMode::PAN;
257                    } else {
258                        tracing::debug!(target: "lazy", "widget `{}` measure inlined, but lazy did not inline", WIDGET.id());
259                    }
260                } else {
261                    if lazy_inline.is_some() {
262                        tracing::debug!(target: "lazy", "widget `{}` measure did not inline, but lazy did", WIDGET.id());
263                    }
264
265                    intersect_mode = mode.with(|s| s.unwrap_intersect());
266                }
267
268                if intersect_mode == ScrollMode::PAN {
269                    if lazy_size != actual_size {
270                        tracing::debug!(
271                            target: "lazy",
272                            "widget `{}` measure size `{actual_size:?}` not equal to lazy size `{lazy_size:?}`",
273                            WIDGET.id()
274                        );
275                    }
276                } else if intersect_mode == ScrollMode::VERTICAL {
277                    if lazy_size.height != actual_size.height {
278                        tracing::debug!(
279                            target: "lazy",
280                            "widget `{}` measure height `{:?}` not equal to lazy height `{:?}`",
281                            WIDGET.id(),
282                            actual_size.height,
283                            lazy_size.height,
284                        );
285                    }
286
287                    size.width = actual_size.width;
288                } else if intersect_mode == ScrollMode::HORIZONTAL {
289                    if lazy_size.width != actual_size.width {
290                        tracing::debug!(
291                            target: "lazy",
292                            "widget `{}` measure width `{:?}` not equal to lazy width `{:?}`",
293                            WIDGET.id(),
294                            actual_size.width,
295                            lazy_size.width,
296                        );
297                    }
298
299                    size.height = actual_size.height;
300                }
301            }
302
303            *desired_size = size;
304        }
305        UiNodeOp::Layout { wl, final_size } => {
306            c.delegated();
307
308            let mut size = c.node().with_child(0, |n| n.layout(wl));
309
310            if not_inited.is_none() && c.node().children_len() == 2 {
311                // is inited and can deinit, layout the actual child and validate
312
313                let lazy_size = size;
314                let actual_size = c.node().with_child(1, |n| n.layout(wl));
315
316                let mut intersect_mode = ScrollMode::empty();
317
318                let lazy_inlined = c.node_impl::<UiVec>()[0]
319                    .as_widget()
320                    .unwrap()
321                    .with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().inline().is_some());
322                if wl.inline().is_some() {
323                    if !lazy_inlined {
324                        tracing::debug!(target: "lazy", "widget `{}` inlined, but lazy did not inline", WIDGET.id());
325                    } else {
326                        intersect_mode = ScrollMode::PAN;
327                    }
328                } else {
329                    if lazy_inlined {
330                        tracing::debug!(target: "lazy", "widget `{}` layout did not inline, but lazy did", WIDGET.id());
331                    }
332
333                    intersect_mode = mode.with(|s| s.unwrap_intersect());
334                }
335
336                if intersect_mode == ScrollMode::PAN {
337                    if lazy_size != actual_size {
338                        tracing::debug!(
339                            target: "lazy",
340                            "widget `{}` layout size `{actual_size:?}` not equal to lazy size `{lazy_size:?}`",
341                            WIDGET.id()
342                        );
343                    }
344                } else if intersect_mode == ScrollMode::VERTICAL {
345                    if lazy_size.height != actual_size.height {
346                        tracing::debug!(
347                            target: "lazy",
348                            "widget `{}` layout height `{:?}` not equal to lazy height `{:?}`",
349                            WIDGET.id(),
350                            actual_size.height,
351                            lazy_size.height,
352                        );
353                    }
354
355                    size.width = actual_size.width;
356                } else if intersect_mode == ScrollMode::HORIZONTAL {
357                    if lazy_size.width != actual_size.width {
358                        tracing::debug!(
359                            target: "lazy",
360                            "widget `{}` layout width `{:?}` not equal to lazy width `{:?}`",
361                            WIDGET.id(),
362                            actual_size.width,
363                            lazy_size.width,
364                        );
365                    }
366
367                    size.height = actual_size.height;
368                }
369            }
370
371            *final_size = size;
372        }
373        UiNodeOp::Render { frame } => {
374            c.delegated();
375
376            if not_inited.is_some() {
377                // not inited, verify
378
379                c.node_impl::<UiVec>()[0].render(frame); // update bounds
380
381                let intersect_mode = mode.with(|s| s.unwrap_intersect());
382                let outer_bounds = WIDGET.bounds().outer_bounds();
383                let viewport = frame.auto_hide_rect();
384
385                in_viewport = if intersect_mode == ScrollMode::VERTICAL {
386                    outer_bounds.min_y() < viewport.max_y() && outer_bounds.max_y() > viewport.min_y()
387                } else if intersect_mode == ScrollMode::HORIZONTAL {
388                    outer_bounds.min_x() < viewport.max_x() && outer_bounds.max_x() > viewport.min_x()
389                } else {
390                    outer_bounds.intersects(&viewport)
391                };
392                if in_viewport {
393                    // request init
394                    WIDGET.reinit();
395                }
396            } else if c.node().children_len() == 2 {
397                // is inited and can deinit, check viewport on placeholder
398
399                c.node_impl::<UiVec>()[1].render(frame); // render + update bounds
400
401                frame.hide(|f| {
402                    f.with_hit_tests_disabled(|f| {
403                        // update bounds (not used but can be inspected)
404                        c.node_impl::<UiVec>()[0].render(f);
405                    });
406                });
407
408                let intersect_mode = mode.with(|s| s.unwrap_intersect());
409                let viewport = frame.auto_hide_rect();
410                let outer_bounds = WIDGET.bounds().outer_bounds();
411
412                in_viewport = if intersect_mode == ScrollMode::VERTICAL {
413                    outer_bounds.min_y() < viewport.max_y() && outer_bounds.max_y() > viewport.min_y()
414                } else if intersect_mode == ScrollMode::HORIZONTAL {
415                    outer_bounds.min_x() < viewport.max_x() && outer_bounds.max_x() > viewport.min_x()
416                } else {
417                    outer_bounds.intersects(&viewport)
418                };
419                if !in_viewport {
420                    // request deinit
421                    WIDGET.reinit();
422                }
423            } else {
424                // is inited and cannot deinit
425                c.node_impl::<UiVec>()[0].render(frame);
426            }
427        }
428        UiNodeOp::RenderUpdate { update } => {
429            c.delegated();
430            if not_inited.is_none() {
431                // child is actual child
432                let last = c.node().children_len() - 1;
433
434                c.node_impl::<UiVec>()[last].render_update(update);
435
436                if last == 1 {
437                    update.hidden(|u| {
438                        // update bounds (not used but can be inspected)
439                        c.node_impl::<UiVec>()[0].render_update(u);
440                    });
441                }
442            } else {
443                // update bounds
444                c.node_impl::<UiVec>().render_update(update);
445            }
446        }
447        _ => {}
448    })
449}