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}