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}