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