1use alloc::collections::BTreeMap;
8
9use azul_core::{
10 callbacks::{EdgeType, VirtualViewCallbackReason},
11 dom::{DomId, NodeId},
12 geom::{LogicalPosition, LogicalRect, LogicalSize},
13};
14
15use crate::managers::scroll_state::ScrollManager;
16
17const EDGE_THRESHOLD: f32 = 200.0;
19
20#[derive(Debug, Clone, Default)]
26pub struct VirtualViewManager {
27 states: BTreeMap<(DomId, NodeId), VirtualViewState>,
29 next_dom_id: usize,
31 reason_overrides: Vec<((DomId, NodeId), VirtualViewCallbackReason)>,
37}
38
39#[derive(Debug, Clone)]
44struct VirtualViewState {
45 virtual_view_scroll_size: Option<LogicalSize>,
47 virtual_view_virtual_scroll_size: Option<LogicalSize>,
49 virtual_view_was_invoked: bool,
51 invoked_for_current_expansion: bool,
53 invoked_for_current_edge: bool,
55 last_edge_triggered: EdgeFlags,
57 nested_dom_id: DomId,
59 last_bounds: LogicalRect,
61 initial_scroll_offset: LogicalPosition,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Default)]
73#[allow(clippy::struct_excessive_bools)] struct EdgeFlags {
75 top: bool,
77 bottom: bool,
79 left: bool,
81 right: bool,
83}
84
85impl VirtualViewManager {
86 #[must_use] pub fn new() -> Self {
88 Self {
89 next_dom_id: 1, ..Default::default()
91 }
92 }
93
94 #[must_use] pub fn debug_counts(&self) -> usize {
96 self.states.len()
97 }
98
99 pub fn set_reason_override(
102 &mut self,
103 dom_id: DomId,
104 node_id: NodeId,
105 reason: VirtualViewCallbackReason,
106 ) {
107 self.reason_overrides
108 .retain(|((d, n), _)| !(*d == dom_id && *n == node_id));
109 self.reason_overrides.push(((dom_id, node_id), reason));
110 }
111
112 pub fn get_or_create_nested_dom_id(&mut self, dom_id: DomId, node_id: NodeId) -> DomId {
117 let key = (dom_id, node_id);
118
119 if let Some(state) = self.states.get(&key) {
121 return state.nested_dom_id;
122 }
123
124 let nested_dom_id = DomId {
126 inner: self.next_dom_id,
127 };
128 self.next_dom_id += 1;
129
130 self.states.insert(key, VirtualViewState::new(nested_dom_id));
131 nested_dom_id
132 }
133
134 #[must_use] pub fn get_nested_dom_id(&self, dom_id: DomId, node_id: NodeId) -> Option<DomId> {
136 self.states.get(&(dom_id, node_id)).map(|s| s.nested_dom_id)
137 }
138
139 #[must_use] pub fn was_virtual_view_invoked(&self, dom_id: DomId, node_id: NodeId) -> bool {
141 self.states
142 .get(&(dom_id, node_id))
143 .is_some_and(|s| s.virtual_view_was_invoked)
144 }
145
146 pub fn update_virtual_view_info(
152 &mut self,
153 dom_id: DomId,
154 node_id: NodeId,
155 scroll_size: LogicalSize,
156 virtual_scroll_size: LogicalSize,
157 ) -> Option<()> {
158 let state = self.states.get_mut(&(dom_id, node_id))?;
159
160 if let Some(old_size) = state.virtual_view_scroll_size {
162 if scroll_size.width > old_size.width || scroll_size.height > old_size.height {
163 state.invoked_for_current_expansion = false;
164 }
165 }
166 state.virtual_view_scroll_size = Some(scroll_size);
167 state.virtual_view_virtual_scroll_size = Some(virtual_scroll_size);
168
169 Some(())
170 }
171
172 pub fn mark_invoked(
177 &mut self,
178 dom_id: DomId,
179 node_id: NodeId,
180 reason: VirtualViewCallbackReason,
181 ) -> Option<()> {
182 let state = self.states.get_mut(&(dom_id, node_id))?;
183
184 state.virtual_view_was_invoked = true;
185 match reason {
186 VirtualViewCallbackReason::BoundsExpanded => state.invoked_for_current_expansion = true,
187 VirtualViewCallbackReason::EdgeScrolled(edge) => {
188 state.invoked_for_current_edge = true;
189 state.last_edge_triggered = edge.into();
190 }
191 _ => {}
192 }
193
194 Some(())
195 }
196
197 pub fn reset_all_invocation_flags(&mut self) {
205 for state in self.states.values_mut() {
206 state.virtual_view_was_invoked = false;
207 state.invoked_for_current_expansion = false;
208 state.invoked_for_current_edge = false;
209 state.last_edge_triggered = EdgeFlags::default();
210 }
211 }
212
213 pub fn force_reinvoke(&mut self, dom_id: DomId, node_id: NodeId) -> Option<()> {
218 let state = self.states.get_mut(&(dom_id, node_id))?;
219
220 state.virtual_view_was_invoked = false;
221 state.invoked_for_current_expansion = false;
222 state.invoked_for_current_edge = false;
223
224 Some(())
225 }
226
227 #[must_use] pub fn all_view_keys(&self) -> Vec<(DomId, NodeId)> {
232 self.states.keys().copied().collect()
233 }
234
235 pub fn check_reinvoke(
244 &mut self,
245 dom_id: DomId,
246 node_id: NodeId,
247 scroll_manager: &ScrollManager,
248 layout_bounds: LogicalRect,
249 ) -> Option<VirtualViewCallbackReason> {
250 if let Some(pos) = self
257 .reason_overrides
258 .iter()
259 .position(|((d, n), _)| *d == dom_id && *n == node_id)
260 {
261 let (_, reason) = self.reason_overrides.remove(pos);
262 return Some(reason);
263 }
264
265 let state = self.states.entry((dom_id, node_id)).or_insert_with(|| {
266 let nested_dom_id = DomId {
267 inner: self.next_dom_id,
268 };
269 self.next_dom_id += 1;
270 VirtualViewState::new(nested_dom_id)
271 });
272
273 if !state.virtual_view_was_invoked {
274 state.initial_scroll_offset = scroll_manager
277 .get_current_offset(dom_id, node_id)
278 .unwrap_or_default();
279 return Some(VirtualViewCallbackReason::InitialRender);
280 }
281
282 if layout_bounds.size.width > state.last_bounds.size.width
284 || layout_bounds.size.height > state.last_bounds.size.height
285 {
286 state.invoked_for_current_expansion = false;
287 }
288 state.last_bounds = layout_bounds;
289
290 let scroll_offset = scroll_manager
291 .get_current_offset(dom_id, node_id)
292 .unwrap_or_default();
293
294 state.check_reinvoke_condition(scroll_offset, layout_bounds.size)
295 }
296
297 #[must_use] pub fn get_all_virtual_view_infos(&self) -> Vec<VirtualViewDebugInfo> {
302 self.states
303 .iter()
304 .map(|((dom_id, node_id), state)| VirtualViewDebugInfo {
305 parent_dom_id: dom_id.inner,
306 parent_node_id: node_id.index(),
307 nested_dom_id: state.nested_dom_id.inner,
308 scroll_size_width: state.virtual_view_scroll_size.map(|s| s.width),
309 scroll_size_height: state.virtual_view_scroll_size.map(|s| s.height),
310 virtual_scroll_size_width: state.virtual_view_virtual_scroll_size.map(|s| s.width),
311 virtual_scroll_size_height: state.virtual_view_virtual_scroll_size.map(|s| s.height),
312 was_invoked: state.virtual_view_was_invoked,
313 last_bounds_x: state.last_bounds.origin.x,
314 last_bounds_y: state.last_bounds.origin.y,
315 last_bounds_width: state.last_bounds.size.width,
316 last_bounds_height: state.last_bounds.size.height,
317 })
318 .collect()
319 }
320}
321
322#[derive(Copy, Debug, Clone)]
324pub struct VirtualViewDebugInfo {
325 pub parent_dom_id: usize,
326 pub parent_node_id: usize,
327 pub nested_dom_id: usize,
328 pub scroll_size_width: Option<f32>,
329 pub scroll_size_height: Option<f32>,
330 pub virtual_scroll_size_width: Option<f32>,
331 pub virtual_scroll_size_height: Option<f32>,
332 pub was_invoked: bool,
333 pub last_bounds_x: f32,
334 pub last_bounds_y: f32,
335 pub last_bounds_width: f32,
336 pub last_bounds_height: f32,
337}
338
339impl VirtualViewState {
340 fn new(nested_dom_id: DomId) -> Self {
342 Self {
343 virtual_view_scroll_size: None,
344 virtual_view_virtual_scroll_size: None,
345 virtual_view_was_invoked: false,
346 invoked_for_current_expansion: false,
347 invoked_for_current_edge: false,
348 last_edge_triggered: EdgeFlags::default(),
349 nested_dom_id,
350 last_bounds: LogicalRect::zero(),
351 initial_scroll_offset: LogicalPosition::zero(),
352 }
353 }
354
355 fn check_reinvoke_condition(
362 &self,
363 current_offset: LogicalPosition,
364 container_size: LogicalSize,
365 ) -> Option<VirtualViewCallbackReason> {
366 let scroll_size = self.virtual_view_scroll_size?;
368
369 if !self.invoked_for_current_expansion
371 && (container_size.width > scroll_size.width
372 || container_size.height > scroll_size.height)
373 {
374 return Some(VirtualViewCallbackReason::BoundsExpanded);
375 }
376
377 let scrollable_width = scroll_size.width > container_size.width;
380 let scrollable_height = scroll_size.height > container_size.height;
381
382 let current_edges = EdgeFlags {
384 top: scrollable_height && current_offset.y <= EDGE_THRESHOLD,
385 bottom: scrollable_height
386 && (scroll_size.height - container_size.height - current_offset.y)
387 <= EDGE_THRESHOLD,
388 left: scrollable_width && current_offset.x <= EDGE_THRESHOLD,
389 right: scrollable_width
390 && (scroll_size.width - container_size.width - current_offset.x) <= EDGE_THRESHOLD,
391 };
392
393 let has_scrolled = current_offset != self.initial_scroll_offset;
397
398 if has_scrolled && !self.invoked_for_current_edge && current_edges.any() {
401 if current_edges.bottom && !self.last_edge_triggered.bottom {
402 return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
403 }
404 if current_edges.right && !self.last_edge_triggered.right {
405 return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Right));
406 }
407 if current_edges.top && !self.last_edge_triggered.top {
408 return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top));
409 }
410 if current_edges.left && !self.last_edge_triggered.left {
411 return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Left));
412 }
413 }
414
415 None
416 }
417}
418
419impl EdgeFlags {
420 #[allow(clippy::trivially_copy_pass_by_ref)] const fn any(&self) -> bool {
423 self.top || self.bottom || self.left || self.right
424 }
425}
426
427impl From<EdgeType> for EdgeFlags {
428 fn from(edge: EdgeType) -> Self {
429 match edge {
430 EdgeType::Top => Self {
431 top: true,
432 ..Default::default()
433 },
434 EdgeType::Bottom => Self {
435 bottom: true,
436 ..Default::default()
437 },
438 EdgeType::Left => Self {
439 left: true,
440 ..Default::default()
441 },
442 EdgeType::Right => Self {
443 right: true,
444 ..Default::default()
445 },
446 }
447 }
448}
449
450impl crate::managers::NodeIdRemap for VirtualViewManager {
451 fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
458 crate::managers::remap_dom_keys(&mut self.states, dom, map);
459
460 self.reason_overrides.retain_mut(|((d, node_id), _)| {
461 if *d != dom {
462 return true;
463 }
464 match map.resolve(*node_id) {
465 Some(new_id) => {
466 *node_id = new_id;
467 true
468 }
469 None => false,
470 }
471 });
472 }
473}