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
2531
    pub fn new() -> Self {
80
2531
        Self {
81
2531
            next_dom_id: 1, // 0 is root
82
2531
            ..Default::default()
83
2531
        }
84
2531
    }
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
175
    pub fn get_or_create_nested_dom_id(&mut self, dom_id: DomId, node_id: NodeId) -> DomId {
101
175
        let key = (dom_id, node_id);
102

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

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

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

            
118
    /// Gets the nested DOM ID for a VirtualView if it exists
119
105
    pub fn get_nested_dom_id(&self, dom_id: DomId, node_id: NodeId) -> Option<DomId> {
120
105
        self.states.get(&(dom_id, node_id)).map(|s| s.nested_dom_id)
121
105
    }
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
105
    pub fn was_virtual_view_invoked(&self, dom_id: DomId, node_id: NodeId) -> bool {
135
105
        self.states
136
105
            .get(&(dom_id, node_id))
137
105
            .map(|s| s.virtual_view_was_invoked)
138
105
            .unwrap_or(false)
139
105
    }
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
105
    pub fn update_virtual_view_info(
161
105
        &mut self,
162
105
        dom_id: DomId,
163
105
        node_id: NodeId,
164
105
        scroll_size: LogicalSize,
165
105
        virtual_scroll_size: LogicalSize,
166
105
    ) -> Option<()> {
167
105
        let state = self.states.get_mut(&(dom_id, node_id))?;
168

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

            
178
105
        Some(())
179
105
    }
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
245
    pub fn mark_invoked(
186
245
        &mut self,
187
245
        dom_id: DomId,
188
245
        node_id: NodeId,
189
245
        reason: VirtualViewCallbackReason,
190
245
    ) -> Option<()> {
191
245
        let state = self.states.get_mut(&(dom_id, node_id))?;
192

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

            
203
245
        Some(())
204
245
    }
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
2275
    pub fn reset_all_invocation_flags(&mut self) {
214
2275
        for state in self.states.values_mut() {
215
            state.virtual_view_was_invoked = false;
216
            state.invoked_for_current_expansion = false;
217
            state.invoked_for_current_edge = false;
218
            state.last_edge_triggered = EdgeFlags::default();
219
        }
220
2275
    }
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
    /// Checks whether a VirtualView needs to be re-invoked and returns the reason
237
    ///
238
    /// Returns `Some(reason)` if the VirtualView callback should be invoked:
239
    /// - `InitialRender`: VirtualView has never been invoked
240
    /// - `BoundsExpanded`: Container grew larger than content
241
    /// - `EdgeScrolled`: User scrolled near an edge (for lazy loading)
242
    ///
243
    /// Returns `None` if no re-invocation is needed.
244
490
    pub fn check_reinvoke(
245
490
        &mut self,
246
490
        dom_id: DomId,
247
490
        node_id: NodeId,
248
490
        scroll_manager: &ScrollManager,
249
490
        layout_bounds: LogicalRect,
250
490
    ) -> Option<VirtualViewCallbackReason> {
251
490
        let state = self.states.entry((dom_id, node_id)).or_insert_with(|| {
252
175
            let nested_dom_id = DomId {
253
175
                inner: self.next_dom_id,
254
175
            };
255
175
            self.next_dom_id += 1;
256
175
            VirtualViewState::new(nested_dom_id)
257
175
        });
258

            
259
490
        if !state.virtual_view_was_invoked {
260
210
            return Some(VirtualViewCallbackReason::InitialRender);
261
280
        }
262

            
263
        // Check for bounds expansion
264
280
        if layout_bounds.size.width > state.last_bounds.size.width
265
140
            || layout_bounds.size.height > state.last_bounds.size.height
266
175
        {
267
175
            state.invoked_for_current_expansion = false;
268
175
        }
269
280
        state.last_bounds = layout_bounds;
270

            
271
280
        let scroll_offset = scroll_manager
272
280
            .get_current_offset(dom_id, node_id)
273
280
            .unwrap_or_default();
274

            
275
280
        state.check_reinvoke_condition(scroll_offset, layout_bounds.size)
276
490
    }
277

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

            
303
/// Debug info for a single VirtualView, returned by `get_all_virtual_view_infos`
304
#[derive(Debug, Clone)]
305
pub struct VirtualViewDebugInfo {
306
    pub parent_dom_id: usize,
307
    pub parent_node_id: usize,
308
    pub nested_dom_id: usize,
309
    pub scroll_size_width: Option<f32>,
310
    pub scroll_size_height: Option<f32>,
311
    pub virtual_scroll_size_width: Option<f32>,
312
    pub virtual_scroll_size_height: Option<f32>,
313
    pub was_invoked: bool,
314
    pub last_bounds_x: f32,
315
    pub last_bounds_y: f32,
316
    pub last_bounds_width: f32,
317
    pub last_bounds_height: f32,
318
}
319

            
320
impl VirtualViewState {
321
    /// Creates a new VirtualViewState with the given nested DOM ID
322
280
    fn new(nested_dom_id: DomId) -> Self {
323
280
        Self {
324
280
            virtual_view_scroll_size: None,
325
280
            virtual_view_virtual_scroll_size: None,
326
280
            virtual_view_was_invoked: false,
327
280
            invoked_for_current_expansion: false,
328
280
            invoked_for_current_edge: false,
329
280
            last_edge_triggered: EdgeFlags::default(),
330
280
            nested_dom_id,
331
280
            last_bounds: LogicalRect::zero(),
332
280
        }
333
280
    }
334

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

            
351
        // Check 1: Container grew larger than content - need more content
352
245
        if !self.invoked_for_current_expansion
353
210
            && (container_size.width > scroll_size.width
354
140
                || container_size.height > scroll_size.height)
355
        {
356
70
            return Some(VirtualViewCallbackReason::BoundsExpanded);
357
175
        }
358

            
359
        // Check 2: Edge-based lazy loading
360
        // Determine if scrolling is possible in each direction
361
175
        let scrollable_width = scroll_size.width > container_size.width;
362
175
        let scrollable_height = scroll_size.height > container_size.height;
363

            
364
        // Calculate which edges the user is currently near
365
175
        let current_edges = EdgeFlags {
366
175
            top: scrollable_height && current_offset.y <= EDGE_THRESHOLD,
367
175
            bottom: scrollable_height
368
105
                && (scroll_size.height - container_size.height - current_offset.y)
369
105
                    <= EDGE_THRESHOLD,
370
175
            left: scrollable_width && current_offset.x <= EDGE_THRESHOLD,
371
175
            right: scrollable_width
372
35
                && (scroll_size.width - container_size.width - current_offset.x) <= EDGE_THRESHOLD,
373
        };
374

            
375
        // Trigger edge callback if near an edge that hasn't been triggered yet
376
        // Prioritize bottom/right edges (common infinite scroll directions)
377
175
        if !self.invoked_for_current_edge && current_edges.any() {
378
105
            if current_edges.bottom && !self.last_edge_triggered.bottom {
379
35
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
380
70
            }
381
70
            if current_edges.right && !self.last_edge_triggered.right {
382
35
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Right));
383
35
            }
384
70
        }
385

            
386
105
        None
387
280
    }
388
}
389

            
390
impl EdgeFlags {
391
    /// Returns true if any edge flag is set
392
140
    fn any(&self) -> bool {
393
140
        self.top || self.bottom || self.left || self.right
394
140
    }
395
}
396

            
397
impl From<EdgeType> for EdgeFlags {
398
35
    fn from(edge: EdgeType) -> Self {
399
35
        match edge {
400
            EdgeType::Top => Self {
401
                top: true,
402
                ..Default::default()
403
            },
404
35
            EdgeType::Bottom => Self {
405
35
                bottom: true,
406
35
                ..Default::default()
407
35
            },
408
            EdgeType::Left => Self {
409
                left: true,
410
                ..Default::default()
411
            },
412
            EdgeType::Right => Self {
413
                right: true,
414
                ..Default::default()
415
            },
416
        }
417
35
    }
418
}