1
//! VirtualView lifecycle management for layout
2
//!
3
//! This module provides:
4
//! - VirtualView re-invocation logic for lazy loading
5
//! - WebRender PipelineId tracking
6
//! - Nested DOM ID management
7

            
8
use alloc::collections::BTreeMap;
9

            
10
use azul_core::{
11
    callbacks::{EdgeType, VirtualViewCallbackReason},
12
    dom::{DomId, NodeId},
13
    geom::{LogicalPosition, LogicalRect, LogicalSize},
14
    hit_test::PipelineId,
15
};
16

            
17
use crate::managers::scroll_state::ScrollManager;
18

            
19
/// Distance in pixels from edge that triggers edge-scrolled callback
20
const EDGE_THRESHOLD: f32 = 200.0;
21

            
22
/// Manages VirtualView lifecycle, including re-invocation and PipelineId generation
23
///
24
/// Tracks which VirtualViews have been invoked, assigns unique DOM IDs to nested
25
/// virtual views, and determines when VirtualViews need to be re-invoked (e.g., when
26
/// the container bounds expand or the user scrolls near an edge).
27
#[derive(Debug, Clone, Default)]
28
pub struct VirtualViewManager {
29
    /// Per-VirtualView state keyed by (parent DomId, NodeId of virtualized view element)
30
    states: BTreeMap<(DomId, NodeId), VirtualViewState>,
31
    /// WebRender PipelineId for each VirtualView
32
    pipeline_ids: BTreeMap<(DomId, NodeId), PipelineId>,
33
    /// Counter for generating unique nested DOM IDs
34
    next_dom_id: usize,
35
}
36

            
37
/// Internal state for a single VirtualView instance
38
///
39
/// Tracks invocation status, content dimensions, and edge triggers
40
/// to determine when the VirtualView callback needs to be re-invoked.
41
#[derive(Debug, Clone)]
42
struct VirtualViewState {
43
    /// Content size reported by VirtualView callback (actual rendered size)
44
    virtual_view_scroll_size: Option<LogicalSize>,
45
    /// Virtual scroll size for infinite scroll scenarios
46
    virtual_view_virtual_scroll_size: Option<LogicalSize>,
47
    /// Whether the VirtualView has ever been invoked
48
    virtual_view_was_invoked: bool,
49
    /// Whether invoked for current container expansion
50
    invoked_for_current_expansion: bool,
51
    /// Whether invoked for current edge scroll event
52
    invoked_for_current_edge: bool,
53
    /// Which edges have already triggered callbacks
54
    last_edge_triggered: EdgeFlags,
55
    /// Unique DOM ID assigned to this VirtualView's content
56
    nested_dom_id: DomId,
57
    /// Last known layout bounds of the VirtualView container
58
    last_bounds: LogicalRect,
59
}
60

            
61
/// Flags indicating which scroll edges have been triggered
62
///
63
/// Used to prevent repeated edge-scroll callbacks for the same edge
64
/// until the user scrolls away and back.
65
#[derive(Debug, Clone, Copy, PartialEq, Default)]
66
pub struct EdgeFlags {
67
    /// Near top edge
68
    pub top: bool,
69
    /// Near bottom edge
70
    pub bottom: bool,
71
    /// Near left edge
72
    pub left: bool,
73
    /// Near right edge
74
    pub right: bool,
75
}
76

            
77
impl VirtualViewManager {
78
    /// Creates a new VirtualViewManager with no tracked VirtualViews
79
3751
    pub fn new() -> Self {
80
3751
        Self {
81
3751
            next_dom_id: 1, // 0 is root
82
3751
            ..Default::default()
83
3751
        }
84
3751
    }
85

            
86
    /// (states, pipeline_ids). Used by `AZ_E2E_TEST` to watch growth.
87
    pub fn debug_counts(&self) -> (usize, usize) {
88
        (self.states.len(), self.pipeline_ids.len())
89
    }
90

            
91
    /// Called at the start of each frame (currently a no-op)
92
    pub fn begin_frame(&mut self) {
93
        // Nothing to do here for now, but good practice for stateful managers
94
    }
95

            
96
    /// Gets or creates a unique nested DOM ID for a VirtualView
97
    ///
98
    /// Returns the existing DOM ID if the VirtualView was previously registered,
99
    /// otherwise allocates a new unique ID and initializes the VirtualView state.
100
528
    pub fn get_or_create_nested_dom_id(&mut self, dom_id: DomId, node_id: NodeId) -> DomId {
101
528
        let key = (dom_id, node_id);
102

            
103
        // Check if already exists
104
528
        if let Some(state) = self.states.get(&key) {
105
396
            return state.nested_dom_id;
106
132
        }
107

            
108
        // Create new nested DOM ID
109
132
        let nested_dom_id = DomId {
110
132
            inner: self.next_dom_id,
111
132
        };
112
132
        self.next_dom_id += 1;
113

            
114
132
        self.states.insert(key, VirtualViewState::new(nested_dom_id));
115
132
        nested_dom_id
116
528
    }
117

            
118
    /// Gets the nested DOM ID for a VirtualView if it exists
119
132
    pub fn get_nested_dom_id(&self, dom_id: DomId, node_id: NodeId) -> Option<DomId> {
120
132
        self.states.get(&(dom_id, node_id)).map(|s| s.nested_dom_id)
121
132
    }
122

            
123
    /// Gets or creates a WebRender PipelineId for a VirtualView
124
    ///
125
    /// PipelineIds are used by WebRender to identify distinct rendering contexts.
126
    pub fn get_or_create_pipeline_id(&mut self, dom_id: DomId, node_id: NodeId) -> PipelineId {
127
        *self
128
            .pipeline_ids
129
            .entry((dom_id, node_id))
130
            .or_insert_with(|| PipelineId(dom_id.inner as u32, node_id.index() as u32))
131
    }
132

            
133
    /// Returns whether the VirtualView has ever been invoked
134
132
    pub fn was_virtual_view_invoked(&self, dom_id: DomId, node_id: NodeId) -> bool {
135
132
        self.states
136
132
            .get(&(dom_id, node_id))
137
132
            .map(|s| s.virtual_view_was_invoked)
138
132
            .unwrap_or(false)
139
132
    }
140

            
141
    /// Returns the virtual scroll size for a VirtualView (if set by the callback)
142
    pub fn get_virtual_scroll_size(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalSize> {
143
        self.states
144
            .get(&(dom_id, node_id))
145
            .and_then(|s| s.virtual_view_virtual_scroll_size)
146
    }
147

            
148
    /// Returns the scroll size for a VirtualView (actual content size, if set by the callback)
149
    pub fn get_scroll_size(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalSize> {
150
        self.states
151
            .get(&(dom_id, node_id))
152
            .and_then(|s| s.virtual_view_scroll_size)
153
    }
154

            
155
    /// Updates the VirtualView's content size information
156
    ///
157
    /// Called after the VirtualView callback returns to record the actual content
158
    /// dimensions. If the new size is larger than previously recorded, clears
159
    /// the expansion flag to allow BoundsExpanded re-invocation.
160
440
    pub fn update_virtual_view_info(
161
440
        &mut self,
162
440
        dom_id: DomId,
163
440
        node_id: NodeId,
164
440
        scroll_size: LogicalSize,
165
440
        virtual_scroll_size: LogicalSize,
166
440
    ) -> Option<()> {
167
440
        let state = self.states.get_mut(&(dom_id, node_id))?;
168

            
169
        // Reset expansion flag if content grew
170
440
        if let Some(old_size) = state.virtual_view_scroll_size {
171
88
            if scroll_size.width > old_size.width || scroll_size.height > old_size.height {
172
                state.invoked_for_current_expansion = false;
173
88
            }
174
352
        }
175
440
        state.virtual_view_scroll_size = Some(scroll_size);
176
440
        state.virtual_view_virtual_scroll_size = Some(virtual_scroll_size);
177

            
178
440
        Some(())
179
440
    }
180

            
181
    /// Marks a VirtualView as invoked for a specific reason
182
    ///
183
    /// Updates internal state flags based on the callback reason to prevent
184
    /// duplicate callbacks for the same trigger condition.
185
616
    pub fn mark_invoked(
186
616
        &mut self,
187
616
        dom_id: DomId,
188
616
        node_id: NodeId,
189
616
        reason: VirtualViewCallbackReason,
190
616
    ) -> Option<()> {
191
616
        let state = self.states.get_mut(&(dom_id, node_id))?;
192

            
193
616
        state.virtual_view_was_invoked = true;
194
616
        match reason {
195
44
            VirtualViewCallbackReason::BoundsExpanded => state.invoked_for_current_expansion = true,
196
44
            VirtualViewCallbackReason::EdgeScrolled(edge) => {
197
44
                state.invoked_for_current_edge = true;
198
44
                state.last_edge_triggered = edge.into();
199
44
            }
200
528
            _ => {}
201
        }
202

            
203
616
        Some(())
204
616
    }
205

            
206
    /// Reset invocation flags for ALL tracked VirtualViews
207
    ///
208
    /// After `layout_results.clear()`, the child DOMs no longer exist in memory.
209
    /// This method ensures `check_reinvoke()` returns `InitialRender` for every
210
    /// VirtualView, so the callbacks re-run and re-populate `layout_results`.
211
    ///
212
    /// Called from `layout_and_generate_display_list()` after clearing layout results.
213
3432
    pub fn reset_all_invocation_flags(&mut self) {
214
3432
        for state in self.states.values_mut() {
215
88
            state.virtual_view_was_invoked = false;
216
88
            state.invoked_for_current_expansion = false;
217
88
            state.invoked_for_current_edge = false;
218
88
            state.last_edge_triggered = EdgeFlags::default();
219
88
        }
220
3432
    }
221

            
222
    /// Force a VirtualView to be re-invoked on the next layout pass
223
    ///
224
    /// Clears all invocation flags, causing check_reinvoke() to return InitialRender.
225
    /// Used by trigger_virtual_view_rerender() to manually refresh VirtualView content.
226
    pub fn force_reinvoke(&mut self, dom_id: DomId, node_id: NodeId) -> Option<()> {
227
        let state = self.states.get_mut(&(dom_id, node_id))?;
228

            
229
        state.virtual_view_was_invoked = false;
230
        state.invoked_for_current_expansion = false;
231
        state.invoked_for_current_edge = false;
232

            
233
        Some(())
234
    }
235

            
236
    /// `(DomId, NodeId)` of every VirtualView registered so far (invoked at
237
    /// least once). Used to re-invoke *all* views after a shared-dataset change
238
    /// arrives out-of-band (e.g. a background tile-fetch writeback) without
239
    /// needing to know which node the data belongs to.
240
    pub fn all_view_keys(&self) -> alloc::vec::Vec<(DomId, NodeId)> {
241
        self.states.keys().copied().collect()
242
    }
243

            
244
    /// Checks whether a VirtualView needs to be re-invoked and returns the reason
245
    ///
246
    /// Returns `Some(reason)` if the VirtualView callback should be invoked:
247
    /// - `InitialRender`: VirtualView has never been invoked
248
    /// - `BoundsExpanded`: Container grew larger than content
249
    /// - `EdgeScrolled`: User scrolled near an edge (for lazy loading)
250
    ///
251
    /// Returns `None` if no re-invocation is needed.
252
924
    pub fn check_reinvoke(
253
924
        &mut self,
254
924
        dom_id: DomId,
255
924
        node_id: NodeId,
256
924
        scroll_manager: &ScrollManager,
257
924
        layout_bounds: LogicalRect,
258
924
    ) -> Option<VirtualViewCallbackReason> {
259
924
        let state = self.states.entry((dom_id, node_id)).or_insert_with(|| {
260
440
            let nested_dom_id = DomId {
261
440
                inner: self.next_dom_id,
262
440
            };
263
440
            self.next_dom_id += 1;
264
440
            VirtualViewState::new(nested_dom_id)
265
440
        });
266

            
267
924
        if !state.virtual_view_was_invoked {
268
572
            return Some(VirtualViewCallbackReason::InitialRender);
269
352
        }
270

            
271
        // Check for bounds expansion
272
352
        if layout_bounds.size.width > state.last_bounds.size.width
273
176
            || layout_bounds.size.height > state.last_bounds.size.height
274
220
        {
275
220
            state.invoked_for_current_expansion = false;
276
220
        }
277
352
        state.last_bounds = layout_bounds;
278

            
279
352
        let scroll_offset = scroll_manager
280
352
            .get_current_offset(dom_id, node_id)
281
352
            .unwrap_or_default();
282

            
283
352
        state.check_reinvoke_condition(scroll_offset, layout_bounds.size)
284
924
    }
285

            
286
    /// Returns debug info for all tracked VirtualViews
287
    ///
288
    /// Each entry contains: (parent_dom_id, parent_node_id, nested_dom_id,
289
    /// scroll_size, virtual_scroll_size, was_invoked, last_bounds)
290
    pub fn get_all_virtual_view_infos(&self) -> alloc::vec::Vec<VirtualViewDebugInfo> {
291
        self.states
292
            .iter()
293
            .map(|((dom_id, node_id), state)| VirtualViewDebugInfo {
294
                parent_dom_id: dom_id.inner,
295
                parent_node_id: node_id.index(),
296
                nested_dom_id: state.nested_dom_id.inner,
297
                scroll_size_width: state.virtual_view_scroll_size.map(|s| s.width),
298
                scroll_size_height: state.virtual_view_scroll_size.map(|s| s.height),
299
                virtual_scroll_size_width: state.virtual_view_virtual_scroll_size.map(|s| s.width),
300
                virtual_scroll_size_height: state.virtual_view_virtual_scroll_size.map(|s| s.height),
301
                was_invoked: state.virtual_view_was_invoked,
302
                last_bounds_x: state.last_bounds.origin.x,
303
                last_bounds_y: state.last_bounds.origin.y,
304
                last_bounds_width: state.last_bounds.size.width,
305
                last_bounds_height: state.last_bounds.size.height,
306
            })
307
            .collect()
308
    }
309
}
310

            
311
/// Debug info for a single VirtualView, returned by `get_all_virtual_view_infos`
312
#[derive(Debug, Clone)]
313
pub struct VirtualViewDebugInfo {
314
    pub parent_dom_id: usize,
315
    pub parent_node_id: usize,
316
    pub nested_dom_id: usize,
317
    pub scroll_size_width: Option<f32>,
318
    pub scroll_size_height: Option<f32>,
319
    pub virtual_scroll_size_width: Option<f32>,
320
    pub virtual_scroll_size_height: Option<f32>,
321
    pub was_invoked: bool,
322
    pub last_bounds_x: f32,
323
    pub last_bounds_y: f32,
324
    pub last_bounds_width: f32,
325
    pub last_bounds_height: f32,
326
}
327

            
328
impl VirtualViewState {
329
    /// Creates a new VirtualViewState with the given nested DOM ID
330
572
    fn new(nested_dom_id: DomId) -> Self {
331
572
        Self {
332
572
            virtual_view_scroll_size: None,
333
572
            virtual_view_virtual_scroll_size: None,
334
572
            virtual_view_was_invoked: false,
335
572
            invoked_for_current_expansion: false,
336
572
            invoked_for_current_edge: false,
337
572
            last_edge_triggered: EdgeFlags::default(),
338
572
            nested_dom_id,
339
572
            last_bounds: LogicalRect::zero(),
340
572
        }
341
572
    }
342

            
343
    /// Determines if the VirtualView callback should be re-invoked based on
344
    /// scroll position
345
    ///
346
    /// Checks two conditions:
347
    /// 1. Container bounds expanded beyond content size
348
    /// 2. User scrolled within EDGE_THRESHOLD pixels of an edge (for lazy loading)
349
352
    fn check_reinvoke_condition(
350
352
        &mut self,
351
352
        current_offset: LogicalPosition,
352
352
        container_size: LogicalSize,
353
352
    ) -> Option<VirtualViewCallbackReason> {
354
        // Need scroll_size to determine if we can scroll at all
355
352
        let Some(scroll_size) = self.virtual_view_scroll_size else {
356
44
            return None;
357
        };
358

            
359
        // Check 1: Container grew larger than content - need more content
360
308
        if !self.invoked_for_current_expansion
361
264
            && (container_size.width > scroll_size.width
362
176
                || container_size.height > scroll_size.height)
363
        {
364
88
            return Some(VirtualViewCallbackReason::BoundsExpanded);
365
220
        }
366

            
367
        // Check 2: Edge-based lazy loading
368
        // Determine if scrolling is possible in each direction
369
220
        let scrollable_width = scroll_size.width > container_size.width;
370
220
        let scrollable_height = scroll_size.height > container_size.height;
371

            
372
        // Calculate which edges the user is currently near
373
220
        let current_edges = EdgeFlags {
374
220
            top: scrollable_height && current_offset.y <= EDGE_THRESHOLD,
375
220
            bottom: scrollable_height
376
132
                && (scroll_size.height - container_size.height - current_offset.y)
377
132
                    <= EDGE_THRESHOLD,
378
220
            left: scrollable_width && current_offset.x <= EDGE_THRESHOLD,
379
220
            right: scrollable_width
380
44
                && (scroll_size.width - container_size.width - current_offset.x) <= EDGE_THRESHOLD,
381
        };
382

            
383
        // Trigger edge callback if near an edge that hasn't been triggered yet
384
        // Prioritize bottom/right edges (common infinite scroll directions)
385
220
        if !self.invoked_for_current_edge && current_edges.any() {
386
132
            if current_edges.bottom && !self.last_edge_triggered.bottom {
387
44
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
388
88
            }
389
88
            if current_edges.right && !self.last_edge_triggered.right {
390
44
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Right));
391
44
            }
392
88
        }
393

            
394
132
        None
395
352
    }
396
}
397

            
398
impl EdgeFlags {
399
    /// Returns true if any edge flag is set
400
176
    fn any(&self) -> bool {
401
176
        self.top || self.bottom || self.left || self.right
402
176
    }
403
}
404

            
405
impl From<EdgeType> for EdgeFlags {
406
44
    fn from(edge: EdgeType) -> Self {
407
44
        match edge {
408
            EdgeType::Top => Self {
409
                top: true,
410
                ..Default::default()
411
            },
412
44
            EdgeType::Bottom => Self {
413
44
                bottom: true,
414
44
                ..Default::default()
415
44
            },
416
            EdgeType::Left => Self {
417
                left: true,
418
                ..Default::default()
419
            },
420
            EdgeType::Right => Self {
421
                right: true,
422
                ..Default::default()
423
            },
424
        }
425
44
    }
426
}