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

            
7
use alloc::collections::BTreeMap;
8

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

            
15
use crate::managers::scroll_state::ScrollManager;
16

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

            
20
/// Manages `VirtualView` lifecycle, including re-invocation
21
///
22
/// Tracks which `VirtualViews` have been invoked, assigns unique DOM IDs to nested
23
/// virtual views, and determines when `VirtualViews` need to be re-invoked (e.g., when
24
/// the container bounds expand or the user scrolls near an edge).
25
#[derive(Debug, Clone, Default)]
26
pub struct VirtualViewManager {
27
    /// Per-`VirtualView` state keyed by (parent `DomId`, `NodeId` of virtualized view element)
28
    states: BTreeMap<(DomId, NodeId), VirtualViewState>,
29
    /// Counter for generating unique nested DOM IDs
30
    next_dom_id: usize,
31
    /// MWA-C-virtual_view: queue-time callback reasons, consumed by the very
32
    /// next `check_reinvoke` for the same view (set by
33
    /// `process_virtual_view_updates` right before the invoke). Replaces the
34
    /// `force_reinvoke` clear-flag trick that collapsed every delivered
35
    /// reason to `InitialRender`.
36
    reason_overrides: Vec<((DomId, NodeId), VirtualViewCallbackReason)>,
37
}
38

            
39
/// Internal state for a single `VirtualView` instance
40
///
41
/// Tracks invocation status, content dimensions, and edge triggers
42
/// to determine when the `VirtualView` callback needs to be re-invoked.
43
#[derive(Debug, Clone)]
44
struct VirtualViewState {
45
    /// WHAT IS MATERIALIZED RIGHT NOW, in VIRTUAL space.
46
    ///
47
    /// `origin` = where this window of content begins in the document (the
48
    /// `scroll_offset` the callback reported); `size` = how much it covers.
49
    /// This is the rect the content is PLACED by:
50
    /// `content is drawn at container.origin + (materialized.origin - scroll_offset)`.
51
    ///
52
    /// Deliberately a rect, not a loose position + size: the origin and the
53
    /// extent are one fact about one window and drift apart the moment they
54
    /// are stored separately (which is how the offset ended up write-only).
55
    materialized: Option<LogicalRect>,
56
    /// THE WHOLE DOCUMENT, in VIRTUAL space — the app's current best estimate
57
    /// (`virtual_scroll_size`), which background pagination refines over time.
58
    ///
59
    /// ONLY the scrollbar reads this. Placement above does not, which is the
60
    /// property that lets the estimate change without the content jumping:
61
    /// the user sees the thumb resize and nothing else move.
62
    virtual_rect: Option<LogicalRect>,
63
    /// Whether the `VirtualView` has ever been invoked
64
    virtual_view_was_invoked: bool,
65
    /// Whether invoked for current container expansion
66
    invoked_for_current_expansion: bool,
67
    /// Whether invoked for current edge scroll event
68
    invoked_for_current_edge: bool,
69
    /// Which edges have already triggered callbacks
70
    last_edge_triggered: EdgeFlags,
71
    /// Unique DOM ID assigned to this `VirtualView`'s content
72
    nested_dom_id: DomId,
73
    /// The `VirtualView`'s own on-screen box (the viewport), window coords.
74
    /// `size` is the scrollport the other two rects are compared against.
75
    container: LogicalRect,
76
    /// Scroll offset captured at `InitialRender`. Edge-scroll callbacks only fire
77
    /// once the user has scrolled away from this resting position — being at an
78
    /// edge from the very start (e.g. the top/left edge at offset 0) is the
79
    /// initial position, not a scroll-to-edge event.
80
    initial_scroll_offset: LogicalPosition,
81
}
82

            
83
/// Flags indicating which scroll edges have been triggered
84
///
85
/// Used to prevent repeated edge-scroll callbacks for the same edge
86
/// until the user scrolls away and back.
87
#[derive(Debug, Clone, Copy, PartialEq, Default)]
88
#[allow(clippy::struct_excessive_bools)] // one independent bool per box edge (top/bottom/left/right)
89
struct EdgeFlags {
90
    /// Near top edge
91
    top: bool,
92
    /// Near bottom edge
93
    bottom: bool,
94
    /// Near left edge
95
    left: bool,
96
    /// Near right edge
97
    right: bool,
98
}
99

            
100
impl VirtualViewManager {
101
    /// Creates a new `VirtualViewManager` with no tracked `VirtualViews`
102
5689
    #[must_use] pub fn new() -> Self {
103
5689
        Self {
104
5689
            next_dom_id: 1, // 0 is root
105
5689
            ..Default::default()
106
5689
        }
107
5689
    }
108

            
109
    /// Number of tracked `VirtualView` states. Used by `AZ_E2E_TEST` to watch growth.
110
35
    #[must_use] pub fn debug_counts(&self) -> usize {
111
35
        self.states.len()
112
35
    }
113

            
114
    /// MWA-C-virtual_view: stage the reason the next invoke of this view
115
    /// should deliver to the user callback (consumed by `check_reinvoke`).
116
1006
    pub fn set_reason_override(
117
1006
        &mut self,
118
1006
        dom_id: DomId,
119
1006
        node_id: NodeId,
120
1006
        reason: VirtualViewCallbackReason,
121
1006
    ) {
122
1006
        self.reason_overrides
123
1006
            .retain(|((d, n), _)| !(*d == dom_id && *n == node_id));
124
1006
        self.reason_overrides.push(((dom_id, node_id), reason));
125
1006
    }
126

            
127
    /// Gets or creates a unique nested DOM ID for a `VirtualView`
128
    ///
129
    /// Returns the existing DOM ID if the `VirtualView` was previously registered,
130
    /// otherwise allocates a new unique ID and initializes the `VirtualView` state.
131
565
    pub fn get_or_create_nested_dom_id(&mut self, dom_id: DomId, node_id: NodeId) -> DomId {
132
565
        let key = (dom_id, node_id);
133

            
134
        // Check if already exists
135
565
        if let Some(state) = self.states.get(&key) {
136
236
            return state.nested_dom_id;
137
329
        }
138

            
139
        // Create new nested DOM ID
140
329
        let nested_dom_id = DomId {
141
329
            inner: self.next_dom_id,
142
329
        };
143
329
        self.next_dom_id += 1;
144

            
145
329
        self.states.insert(key, VirtualViewState::new(nested_dom_id));
146
329
        nested_dom_id
147
565
    }
148

            
149
    /// Gets the nested DOM ID for a `VirtualView` if it exists
150
86
    #[must_use] pub fn get_nested_dom_id(&self, dom_id: DomId, node_id: NodeId) -> Option<DomId> {
151
86
        self.states.get(&(dom_id, node_id)).map(|s| s.nested_dom_id)
152
86
    }
153

            
154
    /// Returns whether the `VirtualView` has ever been invoked
155
41
    #[must_use] pub fn was_virtual_view_invoked(&self, dom_id: DomId, node_id: NodeId) -> bool {
156
41
        self.states
157
41
            .get(&(dom_id, node_id))
158
41
            .is_some_and(|s| s.virtual_view_was_invoked)
159
41
    }
160

            
161
    /// Updates the `VirtualView`'s content size information
162
    ///
163
    /// Called after the `VirtualView` callback returns to record the actual content
164
    /// dimensions. If the new size is larger than previously recorded, clears
165
    /// the expansion flag to allow `BoundsExpanded` re-invocation.
166
    /// The sizes the view's LAST invoke declared (`scroll_size`,
167
    /// `virtual_scroll_size`) — the reinvoke signal feeds these back so the
168
    /// callback's page math sees its own declared virtual extent (#16).
169
252
    #[must_use] pub fn get_declared_sizes(
170
252
        &self,
171
252
        dom_id: DomId,
172
252
        node_id: NodeId,
173
252
    ) -> (Option<LogicalSize>, Option<LogicalSize>) {
174
252
        self.states
175
252
            .get(&(dom_id, node_id))
176
252
            .map_or((None, None), |s| {
177
                (
178
252
                    s.materialized.map(|m| m.size),
179
252
                    s.virtual_rect.map(|v| v.size),
180
                )
181
252
            })
182
252
    }
183

            
184
    /// Record what the callback just materialized, as RECTS in virtual space.
185
    ///
186
    /// `window_origin` is where this window of content begins in the document
187
    /// (the callback's `scroll_offset`) — the piece that used to be dropped,
188
    /// which is why content could never be placed and the view never scrolled.
189
316
    pub fn update_virtual_view_info(
190
316
        &mut self,
191
316
        dom_id: DomId,
192
316
        node_id: NodeId,
193
316
        window_origin: LogicalPosition,
194
316
        scroll_size: LogicalSize,
195
316
        virtual_scroll_size: LogicalSize,
196
316
    ) -> Option<()> {
197
316
        let state = self.states.get_mut(&(dom_id, node_id))?;
198

            
199
        // Reset expansion flag if the materialized window grew
200
314
        if let Some(old) = state.materialized {
201
139
            if scroll_size.width > old.size.width || scroll_size.height > old.size.height {
202
24
                state.invoked_for_current_expansion = false;
203
115
            }
204
175
        }
205
314
        state.materialized = Some(LogicalRect::new(window_origin, scroll_size));
206
        // The document estimate lives at the virtual origin; only its SIZE is
207
        // the app's (refinable) claim. Changing it must move the scrollbar and
208
        // nothing else — placement reads `materialized`, never this.
209
314
        state.virtual_rect = Some(LogicalRect::new(
210
314
            LogicalPosition::zero(),
211
314
            virtual_scroll_size,
212
314
        ));
213

            
214
314
        Some(())
215
316
    }
216

            
217
    /// Where the materialized window sits in virtual space, if anything is
218
    /// materialized. The renderer places content at
219
    /// `container.origin + (window_origin - scroll_offset)`.
220
    #[must_use]
221
486
    pub fn materialized_window_origin(
222
486
        &self,
223
486
        dom_id: DomId,
224
486
        node_id: NodeId,
225
486
    ) -> Option<LogicalPosition> {
226
486
        self.states
227
486
            .get(&(dom_id, node_id))
228
486
            .and_then(|s| s.materialized)
229
486
            .map(|m| m.origin)
230
486
    }
231

            
232
    /// Marks a `VirtualView` as invoked for a specific reason
233
    ///
234
    /// Updates internal state flags based on the callback reason to prevent
235
    /// duplicate callbacks for the same trigger condition.
236
313
    pub fn mark_invoked(
237
313
        &mut self,
238
313
        dom_id: DomId,
239
313
        node_id: NodeId,
240
313
        reason: VirtualViewCallbackReason,
241
313
    ) -> Option<()> {
242
313
        let state = self.states.get_mut(&(dom_id, node_id))?;
243

            
244
312
        state.virtual_view_was_invoked = true;
245
312
        match reason {
246
15
            VirtualViewCallbackReason::BoundsExpanded => state.invoked_for_current_expansion = true,
247
17
            VirtualViewCallbackReason::EdgeScrolled(edge) => {
248
17
                state.invoked_for_current_edge = true;
249
17
                state.last_edge_triggered = edge.into();
250
17
            }
251
280
            _ => {}
252
        }
253

            
254
312
        Some(())
255
313
    }
256

            
257
    /// Reset invocation flags for ALL tracked `VirtualViews`
258
    ///
259
    /// After `layout_results.clear()`, the child DOMs no longer exist in memory.
260
    /// This method ensures `check_reinvoke()` returns `InitialRender` for every
261
    /// `VirtualView`, so the callbacks re-run and re-populate `layout_results`.
262
    ///
263
    /// Called from `layout_and_generate_display_list()` after clearing layout results.
264
4343
    pub fn reset_all_invocation_flags(&mut self) {
265
4343
        for state in self.states.values_mut() {
266
93
            state.virtual_view_was_invoked = false;
267
93
            state.invoked_for_current_expansion = false;
268
93
            state.invoked_for_current_edge = false;
269
93
            state.last_edge_triggered = EdgeFlags::default();
270
93
        }
271
4343
    }
272

            
273
    /// Force a `VirtualView` to be re-invoked on the next layout pass
274
    ///
275
    /// Clears all invocation flags, causing `check_reinvoke()` to return `InitialRender`.
276
    /// Used by `trigger_virtual_view_rerender()` to manually refresh `VirtualView` content.
277
5
    pub fn force_reinvoke(&mut self, dom_id: DomId, node_id: NodeId) -> Option<()> {
278
5
        let state = self.states.get_mut(&(dom_id, node_id))?;
279

            
280
4
        state.virtual_view_was_invoked = false;
281
4
        state.invoked_for_current_expansion = false;
282
4
        state.invoked_for_current_edge = false;
283

            
284
4
        Some(())
285
5
    }
286

            
287
    /// `(DomId, NodeId)` of every `VirtualView` registered so far (invoked at
288
    /// least once). Used to re-invoke *all* views after a shared-dataset change
289
    /// arrives out-of-band (e.g. a background tile-fetch writeback) without
290
    /// needing to know which node the data belongs to.
291
    /// Which `VirtualView` HOSTS a nested dom: the inverse of
292
    /// [`Self::get_nested_dom_id`].
293
    ///
294
    /// A nested dom's display list is 0-relative — the rasteriser composites
295
    /// it at `host_bounds.origin + content_offset` — so every geometry
296
    /// accessor that must answer in WINDOW space has to walk back up through
297
    /// its hosts. Without this there was no way to ask "where does this dom
298
    /// actually sit", and a caret rect inside a `VirtualView` was handed to the
299
    /// platform IME as if the host were at the window origin.
300
    #[must_use]
301
45
    pub fn host_of_nested_dom(&self, nested: DomId) -> Option<(DomId, NodeId)> {
302
45
        self.states
303
45
            .iter()
304
45
            .find(|(_, state)| state.nested_dom_id == nested)
305
45
            .map(|((dom_id, node_id), _)| (*dom_id, *node_id))
306
45
    }
307

            
308
87
    #[must_use] pub fn all_view_keys(&self) -> Vec<(DomId, NodeId)> {
309
87
        self.states.keys().copied().collect()
310
87
    }
311

            
312
    /// Checks whether a `VirtualView` needs to be re-invoked and returns the reason
313
    ///
314
    /// Returns `Some(reason)` if the `VirtualView` callback should be invoked:
315
    /// - `InitialRender`: `VirtualView` has never been invoked
316
    /// - `BoundsExpanded`: Container grew larger than content
317
    /// - `EdgeScrolled`: User scrolled near an edge (for lazy loading)
318
    ///
319
    /// Returns `None` if no re-invocation is needed.
320
412
    pub fn check_reinvoke(
321
412
        &mut self,
322
412
        dom_id: DomId,
323
412
        node_id: NodeId,
324
412
        scroll_manager: &ScrollManager,
325
412
        layout_bounds: LogicalRect,
326
412
    ) -> Option<VirtualViewCallbackReason> {
327
        // MWA-C-virtual_view: a staged reason override wins (set by
328
        // process_virtual_view_updates immediately before the invoke). The
329
        // old force_reinvoke path cleared was_invoked instead, which
330
        // collapsed EVERY queued re-invocation to InitialRender at delivery
331
        // time — user callbacks could never see EdgeScrolled/BoundsExpanded/
332
        // DomRecreated (the latter had zero producers at all).
333
412
        if let Some(pos) = self
334
412
            .reason_overrides
335
412
            .iter()
336
412
            .position(|((d, n), _)| *d == dom_id && *n == node_id)
337
        {
338
5
            let (_, reason) = self.reason_overrides.remove(pos);
339
5
            return Some(reason);
340
407
        }
341

            
342
407
        let state = self.states.entry((dom_id, node_id)).or_insert_with(|| {
343
183
            let nested_dom_id = DomId {
344
183
                inner: self.next_dom_id,
345
183
            };
346
183
            self.next_dom_id += 1;
347
183
            VirtualViewState::new(nested_dom_id)
348
183
        });
349

            
350
407
        if !state.virtual_view_was_invoked {
351
            // Remember where we started, so edge callbacks fire on scroll-to-edge,
352
            // not for the edge we happen to rest on at the initial position.
353
302
            state.initial_scroll_offset = scroll_manager
354
302
                .get_current_offset(dom_id, node_id)
355
302
                .unwrap_or_default();
356
302
            return Some(VirtualViewCallbackReason::InitialRender);
357
105
        }
358

            
359
        // Check for bounds expansion
360
105
        if layout_bounds.size.width > state.container.size.width
361
52
            || layout_bounds.size.height > state.container.size.height
362
62
        {
363
62
            state.invoked_for_current_expansion = false;
364
62
        }
365
105
        state.container = layout_bounds;
366

            
367
105
        let scroll_offset = scroll_manager
368
105
            .get_current_offset(dom_id, node_id)
369
105
            .unwrap_or_default();
370

            
371
105
        state.check_reinvoke_condition(scroll_offset, layout_bounds.size)
372
412
    }
373

            
374
    /// Returns debug info for all tracked `VirtualViews`
375
    ///
376
    /// Each entry contains: (`parent_dom_id`, `parent_node_id`, `nested_dom_id`,
377
    /// `scroll_size`, `virtual_scroll_size`, `was_invoked`, `last_bounds`)
378
7
    #[must_use] pub fn get_all_virtual_view_infos(&self) -> Vec<VirtualViewDebugInfo> {
379
7
        self.states
380
7
            .iter()
381
7
            .map(|((dom_id, node_id), state)| VirtualViewDebugInfo {
382
15
                parent_dom_id: dom_id.inner,
383
15
                parent_node_id: node_id.index(),
384
15
                nested_dom_id: state.nested_dom_id.inner,
385
15
                scroll_size_width: state.materialized.map(|m| m.size.width),
386
15
                scroll_size_height: state.materialized.map(|m| m.size.height),
387
15
                virtual_scroll_size_width: state.virtual_rect.map(|v| v.size.width),
388
15
                virtual_scroll_size_height: state.virtual_rect.map(|v| v.size.height),
389
15
                was_invoked: state.virtual_view_was_invoked,
390
15
                last_bounds_x: state.container.origin.x,
391
15
                last_bounds_y: state.container.origin.y,
392
15
                last_bounds_width: state.container.size.width,
393
15
                last_bounds_height: state.container.size.height,
394
15
            })
395
7
            .collect()
396
7
    }
397
}
398

            
399
/// Debug info for a single `VirtualView`, returned by `get_all_virtual_view_infos`
400
#[derive(Copy, Debug, Clone)]
401
pub struct VirtualViewDebugInfo {
402
    pub parent_dom_id: usize,
403
    pub parent_node_id: usize,
404
    pub nested_dom_id: usize,
405
    pub scroll_size_width: Option<f32>,
406
    pub scroll_size_height: Option<f32>,
407
    pub virtual_scroll_size_width: Option<f32>,
408
    pub virtual_scroll_size_height: Option<f32>,
409
    pub was_invoked: bool,
410
    pub last_bounds_x: f32,
411
    pub last_bounds_y: f32,
412
    pub last_bounds_width: f32,
413
    pub last_bounds_height: f32,
414
}
415

            
416
impl VirtualViewState {
417
    /// Creates a new `VirtualViewState` with the given nested DOM ID
418
531
    fn new(nested_dom_id: DomId) -> Self {
419
531
        Self {
420
531
            materialized: None,
421
531
            virtual_rect: None,
422
531
            virtual_view_was_invoked: false,
423
531
            invoked_for_current_expansion: false,
424
531
            invoked_for_current_edge: false,
425
531
            last_edge_triggered: EdgeFlags::default(),
426
531
            nested_dom_id,
427
531
            container: LogicalRect::zero(),
428
531
            initial_scroll_offset: LogicalPosition::zero(),
429
531
        }
430
531
    }
431

            
432
    /// Determines if the `VirtualView` callback should be re-invoked based on
433
    /// scroll position
434
    ///
435
    /// Checks two conditions:
436
    /// 1. Container bounds expanded beyond content size
437
    /// 2. User scrolled within `EDGE_THRESHOLD` pixels of an edge (for lazy loading)
438
153
    fn check_reinvoke_condition(
439
153
        &self,
440
153
        current_offset: LogicalPosition,
441
153
        container_size: LogicalSize,
442
153
    ) -> Option<VirtualViewCallbackReason> {
443
        // Nothing is materialized yet — nothing to be near the edge OF.
444
153
        let materialized = self.materialized?;
445
        // The document estimate; falls back to the materialized window when
446
        // the app reports no virtual extent (a VirtualView used as a plain
447
        // windowed view: then materialized IS the document).
448
141
        let virtual_rect = self.virtual_rect.unwrap_or(materialized);
449

            
450
        // Check 1: Container grew larger than the materialized content — the
451
        // window no longer fills the viewport, so ask for more.
452
141
        if !self.invoked_for_current_expansion
453
128
            && (container_size.width > materialized.size.width
454
105
                || container_size.height > materialized.size.height)
455
        {
456
23
            return Some(VirtualViewCallbackReason::BoundsExpanded);
457
118
        }
458

            
459
        // Check 2: the user scrolled near an edge of WHAT IS MATERIALIZED.
460
        //
461
        // This is the rule that makes a VirtualView scrollable rather than
462
        // merely lazy-loadable. The old test compared a VIRTUAL-space offset
463
        // against the MATERIALIZED window's size — two different spaces — so
464
        // it only ever fired at the absolute top/bottom of the document, and
465
        // a document scrolled in the middle never re-materialized at all.
466
        //
467
        // Both rects are in virtual space, so the comparison is honest: the
468
        // visible window is `[current_offset, current_offset + container]`,
469
        // and we re-invoke when it comes within EDGE_THRESHOLD of the edge of
470
        // `materialized` — but only when the document actually extends past
471
        // that edge, otherwise we would spin at the ends forever.
472
118
        let vis_min_x = current_offset.x;
473
118
        let vis_min_y = current_offset.y;
474
118
        let vis_max_x = current_offset.x + container_size.width;
475
118
        let vis_max_y = current_offset.y + container_size.height;
476

            
477
118
        let mat_min_x = materialized.origin.x;
478
118
        let mat_min_y = materialized.origin.y;
479
118
        let mat_max_x = materialized.origin.x + materialized.size.width;
480
118
        let mat_max_y = materialized.origin.y + materialized.size.height;
481

            
482
118
        let doc_min_x = virtual_rect.origin.x;
483
118
        let doc_min_y = virtual_rect.origin.y;
484
118
        let doc_max_x = virtual_rect.origin.x + virtual_rect.size.width;
485
118
        let doc_max_y = virtual_rect.origin.y + virtual_rect.size.height;
486

            
487
118
        let current_edges = EdgeFlags {
488
            // More document above what we materialized, and the view is near
489
            // the materialized window's top.
490
118
            top: mat_min_y > doc_min_y && (vis_min_y - mat_min_y) <= EDGE_THRESHOLD,
491
118
            bottom: mat_max_y < doc_max_y && (mat_max_y - vis_max_y) <= EDGE_THRESHOLD,
492
118
            left: mat_min_x > doc_min_x && (vis_min_x - mat_min_x) <= EDGE_THRESHOLD,
493
118
            right: mat_max_x < doc_max_x && (mat_max_x - vis_max_x) <= EDGE_THRESHOLD,
494
        };
495

            
496
        // Only treat an edge as "scrolled to" once the user has actually moved
497
        // from the resting position captured at InitialRender — sitting at the
498
        // initial top/left edge from the start is not an edge-scroll event.
499
118
        let has_scrolled = current_offset != self.initial_scroll_offset;
500

            
501
        // Trigger edge callback if near an edge that hasn't been triggered yet
502
        // Prioritize bottom/right edges (common infinite scroll directions)
503
118
        if has_scrolled && !self.invoked_for_current_edge && current_edges.any() {
504
54
            if current_edges.bottom && !self.last_edge_triggered.bottom {
505
30
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom));
506
24
            }
507
24
            if current_edges.right && !self.last_edge_triggered.right {
508
11
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Right));
509
13
            }
510
13
            if current_edges.top && !self.last_edge_triggered.top {
511
9
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top));
512
4
            }
513
4
            if current_edges.left && !self.last_edge_triggered.left {
514
2
                return Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Left));
515
2
            }
516
64
        }
517

            
518
66
        None
519
153
    }
520
}
521

            
522
impl EdgeFlags {
523
    /// Returns true if any edge flag is set
524
    #[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
525
93
    const fn any(&self) -> bool {
526
93
        self.top || self.bottom || self.left || self.right
527
93
    }
528
}
529

            
530
impl From<EdgeType> for EdgeFlags {
531
30
    fn from(edge: EdgeType) -> Self {
532
30
        match edge {
533
4
            EdgeType::Top => Self {
534
4
                top: true,
535
4
                ..Default::default()
536
4
            },
537
18
            EdgeType::Bottom => Self {
538
18
                bottom: true,
539
18
                ..Default::default()
540
18
            },
541
4
            EdgeType::Left => Self {
542
4
                left: true,
543
4
                ..Default::default()
544
4
            },
545
4
            EdgeType::Right => Self {
546
4
                right: true,
547
4
                ..Default::default()
548
4
            },
549
        }
550
30
    }
551
}
552

            
553
impl crate::managers::NodeIdRemap for VirtualViewManager {
554
    /// Remap the `(DomId, NodeId)` keys of every tracked `VirtualView`.
555
    ///
556
    /// A `VirtualView` whose host node was unmounted has its state dropped —
557
    /// including the `nested_dom_id` binding, which would otherwise resurface
558
    /// on whatever node inherited the index (rendering the *wrong* nested DOM
559
    /// into it) and leak forever.
560
26
    fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
561
26
        crate::managers::remap_dom_keys(&mut self.states, dom, map);
562

            
563
26
        self.reason_overrides.retain_mut(|((d, node_id), _)| {
564
            if *d != dom {
565
                return true;
566
            }
567
            map.resolve(*node_id).is_some_and(|new_id| {
568
                *node_id = new_id;
569
                true
570
            })
571
        });
572
26
    }
573
}
574

            
575
// ============================================================================
576
// Adversarial unit tests (autotest fleet)
577
//
578
// Hostile inputs for every category in the task file: constructors (extreme
579
// args + post-construction invariants), getters/predicates (defined value on a
580
// default/empty instance), and the numeric decision functions
581
// (`check_reinvoke` / `check_reinvoke_condition` / `update_virtual_view_info`)
582
// under NaN / ±inf / f32::MAX / negative-overscroll / zero and at the exact
583
// EDGE_THRESHOLD boundary.
584
//
585
// An inline module can reach the private `states` / `next_dom_id` /
586
// `reason_overrides` fields and the private `VirtualViewState`, so the flag
587
// invariants are asserted directly rather than inferred.
588
//
589
// Every assertion documents the *actual* behavior — nothing is weakened to
590
// make it pass. Where the actual behavior looks wrong (stale `last_edge_triggered`
591
// suppressing repeat bottom-edge loads; NaN poisoning the growth check;
592
// `Default` handing out DomId 0), the test pins the current behavior and says so
593
// in a comment.
594
// ============================================================================
595
#[cfg(all(test, feature = "std"))]
596
mod autotest_generated {
597
    #![allow(clippy::float_cmp)] // deterministic inputs: exact float compares are intended
598

            
599
    use std::collections::BTreeSet;
600

            
601
    use azul_core::task::{Instant, SystemTick};
602

            
603
    use super::*;
604

            
605
    // ---------------------------------------------------------------- helpers
606

            
607
    const DOM: DomId = DomId::ROOT_ID;
608
    const DOM1: DomId = DomId { inner: 1 };
609
    const DOM_MAX: DomId = DomId {
610
        inner: usize::MAX,
611
    };
612

            
613
    fn n(i: usize) -> NodeId {
614
        NodeId::new(i)
615
    }
616

            
617
    fn sz(width: f32, height: f32) -> LogicalSize {
618
        LogicalSize::new(width, height)
619
    }
620

            
621
    fn pos(x: f32, y: f32) -> LogicalPosition {
622
        LogicalPosition::new(x, y)
623
    }
624

            
625
    fn rect(width: f32, height: f32) -> LogicalRect {
626
        LogicalRect::new(LogicalPosition::zero(), sz(width, height))
627
    }
628

            
629
    /// Deterministic tick-clock instant — no wall clock, no flakiness.
630
    fn at(t: u64) -> Instant {
631
        Instant::Tick(SystemTick::new(t))
632
    }
633

            
634
    /// A `ScrollManager` reporting exactly `(x, y)` for `(dom, node)`.
635
    /// Unclamped, so overscroll / absurd offsets survive to `check_reinvoke`.
636
    fn scrolled(dom: DomId, node: NodeId, x: f32, y: f32) -> ScrollManager {
637
        let mut sm = ScrollManager::new();
638
        sm.set_scroll_position_unclamped(dom, node, pos(x, y), at(0));
639
        sm
640
    }
641

            
642
    fn st(m: &VirtualViewManager, dom: DomId, node: NodeId) -> &VirtualViewState {
643
        m.states.get(&(dom, node)).expect("state must exist")
644
    }
645

            
646
    /// How much document the manager-level fixture leaves UNMATERIALIZED below
647
    /// its window. A view whose materialized window IS the whole document is
648
    /// fully loaded, and by the edge rule nothing may fire for it — so a
649
    /// fixture that wants to observe an edge has to leave something to load.
650
    const FIXTURE_DOC_TAIL: f32 = 1000.0;
651

            
652
    /// Steady state: view `(DOM, n(1))` created, invoked once, `scroll`
653
    /// materialized at the document's top-left corner — with the document
654
    /// estimate `FIXTURE_DOC_TAIL` px taller than what is materialized, i.e. a
655
    /// real virtual view that still has content to load BELOW the window (and,
656
    /// deliberately, none above it and none to either side: the window spans
657
    /// the document's full width, as a vertically-scrolling list does).
658
    /// `initial_scroll_offset` stays at the (0, 0) default, so any nonzero offset
659
    /// counts as "the user has scrolled".
660
    fn ready_view(scroll: LogicalSize) -> VirtualViewManager {
661
        let mut m = VirtualViewManager::new();
662
        m.get_or_create_nested_dom_id(DOM, n(1));
663
        m.mark_invoked(DOM, n(1), VirtualViewCallbackReason::InitialRender)
664
            .expect("view exists");
665
        m.update_virtual_view_info(
666
            DOM,
667
            n(1),
668
            LogicalPosition::zero(),
669
            scroll,
670
            sz(scroll.width, scroll.height + FIXTURE_DOC_TAIL),
671
        )
672
        .expect("view exists");
673
        m
674
    }
675

            
676
    /// The general fixture for driving the private `check_reinvoke_condition`
677
    /// directly: an already-invoked state whose materialized window is `mat`
678
    /// and whose document estimate is `doc`, BOTH in virtual space. Spelling
679
    /// the two rects out is the point — the rule is entirely about where the
680
    /// window sits inside the document, and every "why did/didn't this fire?"
681
    /// answer is read off these two rects.
682
    fn windowed_state(mat: LogicalRect, doc: LogicalRect) -> VirtualViewState {
683
        let mut s = VirtualViewState::new(DomId { inner: 7 });
684
        s.virtual_view_was_invoked = true;
685
        s.materialized = Some(mat);
686
        s.virtual_rect = Some(doc);
687
        s
688
    }
689

            
690
    /// How far into the document the fixture's materialized window starts.
691
    /// The document extends this far ABOVE and BELOW it, so both the top and
692
    /// the bottom edge of the window have more document past them — which is
693
    /// what the edge rule is about. `vpos` maps a window-relative y (what the
694
    /// tests reason in) into virtual space.
695
    const FIXTURE_WINDOW_ORIGIN_Y: f32 = 1000.0;
696

            
697
    /// Same, for the x axis of the both-axes fixture `invoked_state_2d`.
698
    const FIXTURE_WINDOW_ORIGIN_X: f32 = 1000.0;
699

            
700
    fn vpos(y: f32) -> LogicalPosition {
701
        pos(0.0, y + FIXTURE_WINDOW_ORIGIN_Y)
702
    }
703

            
704
    /// A view windowed on the Y AXIS ONLY: a `scroll`-sized window parked
705
    /// 1000 px down a document that is 1000 px taller than the window at each
706
    /// end, and exactly as WIDE as the window.
707
    ///
708
    /// HORIZONTAL JUDGEMENT (deliberate, not an oversight): left/right can
709
    /// never fire on this fixture, because `mat_min_x == doc_min_x` and
710
    /// `mat_max_x == doc_max_x` — there is no document to either side, so
711
    /// there is nothing to load and silence is the correct answer. That is the
712
    /// shape a `VirtualView` actually ships in (a vertically-scrolling list),
713
    /// and it keeps the vertical assertions free of cross-axis noise: any edge
714
    /// these tests observe is unambiguously the one they aimed at.
715
    /// `invoked_state_2d` is the both-axes fixture, used wherever left/right is
716
    /// itself the property under test.
717
    fn invoked_state(scroll: LogicalSize) -> VirtualViewState {
718
        windowed_state(
719
            LogicalRect::new(pos(0.0, FIXTURE_WINDOW_ORIGIN_Y), scroll),
720
            LogicalRect::new(
721
                LogicalPosition::zero(),
722
                sz(scroll.width, FIXTURE_WINDOW_ORIGIN_Y * 2.0 + scroll.height),
723
            ),
724
        )
725
    }
726

            
727
    /// The same idea on BOTH axes: a `scroll`-sized window parked 1000 px into
728
    /// a document that extends 1000 px past it on all four sides. Left/right
729
    /// are exactly symmetric with top/bottom here, which is what makes edge
730
    /// priority and the horizontal threshold observable at all.
731
    fn invoked_state_2d(scroll: LogicalSize) -> VirtualViewState {
732
        windowed_state(
733
            LogicalRect::new(
734
                pos(FIXTURE_WINDOW_ORIGIN_X, FIXTURE_WINDOW_ORIGIN_Y),
735
                scroll,
736
            ),
737
            LogicalRect::new(
738
                LogicalPosition::zero(),
739
                sz(
740
                    FIXTURE_WINDOW_ORIGIN_X * 2.0 + scroll.width,
741
                    FIXTURE_WINDOW_ORIGIN_Y * 2.0 + scroll.height,
742
                ),
743
            ),
744
        )
745
    }
746

            
747
    // `Option`-returning mutators (the crate denies `unused_must_use`): these
748
    // wrappers also assert that the view actually existed.
749
    fn mark(m: &mut VirtualViewManager, dom: DomId, node: NodeId, r: VirtualViewCallbackReason) {
750
        m.mark_invoked(dom, node, r).expect("view exists");
751
    }
752

            
753
    fn set_sizes(
754
        m: &mut VirtualViewManager,
755
        dom: DomId,
756
        node: NodeId,
757
        scroll: LogicalSize,
758
        virt: LogicalSize,
759
    ) {
760
        m.update_virtual_view_info(dom, node, LogicalPosition::zero(), scroll, virt)
761
            .expect("view exists");
762
    }
763

            
764
    // ------------------------------------------------------- constructors
765

            
766
    #[test]
767
    fn new_is_empty_and_reserves_dom_id_zero_for_root() {
768
        let m = VirtualViewManager::new();
769

            
770
        assert_eq!(m.debug_counts(), 0);
771
        assert!(m.all_view_keys().is_empty());
772
        assert!(m.get_all_virtual_view_infos().is_empty());
773
        assert!(m.reason_overrides.is_empty());
774
        // 0 is the root DOM — nested ids start at 1.
775
        assert_eq!(m.next_dom_id, 1);
776

            
777
        // Getters on the empty instance are defined, not panicking.
778
        assert_eq!(m.get_nested_dom_id(DOM, n(0)), None);
779
        assert_eq!(m.get_nested_dom_id(DOM_MAX, n(usize::MAX)), None);
780
        assert!(!m.was_virtual_view_invoked(DOM, n(0)));
781
        assert!(!m.was_virtual_view_invoked(DOM_MAX, n(usize::MAX)));
782
    }
783

            
784
    #[test]
785
    fn derived_default_hands_out_root_dom_id_unlike_new() {
786
        // HAZARD (pinned, not a live bug): `new()` skips 0 because "0 is root",
787
        // but the derived `Default` starts the counter at 0, so a Default-built
788
        // manager hands out DomId::ROOT_ID as its first *nested* DOM id. Every
789
        // production site builds via `new()` (LayoutWindow does not derive
790
        // Default), so this is only reachable by a future caller.
791
        assert_eq!(VirtualViewManager::default().next_dom_id, 0);
792
        assert_eq!(VirtualViewManager::new().next_dom_id, 1);
793

            
794
        let mut d = VirtualViewManager::default();
795
        assert_eq!(d.get_or_create_nested_dom_id(DOM, n(0)), DomId::ROOT_ID);
796

            
797
        let mut fresh = VirtualViewManager::new();
798
        assert_ne!(fresh.get_or_create_nested_dom_id(DOM, n(0)), DomId::ROOT_ID);
799
    }
800

            
801
    #[test]
802
    fn virtual_view_state_new_invariants_at_extreme_dom_id() {
803
        let s = VirtualViewState::new(DomId { inner: usize::MAX });
804

            
805
        assert_eq!(s.nested_dom_id.inner, usize::MAX);
806
        assert!(s.materialized.is_none());
807
        assert!(s.virtual_rect.is_none());
808
        assert!(!s.virtual_view_was_invoked);
809
        assert!(!s.invoked_for_current_expansion);
810
        assert!(!s.invoked_for_current_edge);
811
        assert_eq!(s.last_edge_triggered, EdgeFlags::default());
812
        assert!(!s.last_edge_triggered.any());
813
        assert_eq!(s.container, LogicalRect::zero());
814
        assert_eq!(s.initial_scroll_offset, LogicalPosition::zero());
815

            
816
        // A brand-new state has no content size, so it can never ask to be
817
        // re-invoked, however absurd the container.
818
        assert_eq!(
819
            s.check_reinvoke_condition(pos(0.0, 0.0), sz(f32::INFINITY, f32::INFINITY)),
820
            None
821
        );
822
    }
823

            
824
    // --------------------------------------------- nested DOM id allocation
825

            
826
    #[test]
827
    fn get_or_create_is_idempotent_and_unique_per_key() {
828
        let mut m = VirtualViewManager::new();
829

            
830
        let a = m.get_or_create_nested_dom_id(DOM, n(0));
831
        let a_again = m.get_or_create_nested_dom_id(DOM, n(0));
832
        assert_eq!(a, a_again, "re-registering a view must not re-allocate");
833
        assert_eq!(a, DomId { inner: 1 });
834
        assert_eq!(m.debug_counts(), 1);
835

            
836
        // Saturated key components must not panic and must get a fresh id.
837
        let b = m.get_or_create_nested_dom_id(DOM_MAX, n(usize::MAX));
838
        assert_eq!(b, DomId { inner: 2 });
839
        assert_ne!(a, b);
840
        assert_eq!(m.debug_counts(), 2);
841

            
842
        assert_eq!(m.get_nested_dom_id(DOM, n(0)), Some(a));
843
        assert_eq!(m.get_nested_dom_id(DOM_MAX, n(usize::MAX)), Some(b));
844
        assert_eq!(m.get_nested_dom_id(DOM, n(1)), None);
845
        assert_eq!(m.get_nested_dom_id(DOM1, n(0)), None);
846
    }
847

            
848
    #[test]
849
    fn nested_dom_ids_are_unique_across_many_views() {
850
        let mut m = VirtualViewManager::new();
851
        let mut seen = BTreeSet::new();
852

            
853
        for dom in 0..8_usize {
854
            for node in 0..32_usize {
855
                let id = m.get_or_create_nested_dom_id(DomId { inner: dom }, n(node));
856
                assert!(id.inner >= 1, "nested id must never collide with the root");
857
                assert!(seen.insert(id.inner), "nested DOM id {id:?} handed out twice");
858
            }
859
        }
860

            
861
        assert_eq!(seen.len(), 8 * 32);
862
        assert_eq!(m.debug_counts(), 8 * 32);
863
        assert_eq!(m.next_dom_id, 8 * 32 + 1);
864
    }
865

            
866
    #[test]
867
    fn all_view_keys_is_sorted_and_matches_the_tracked_states() {
868
        let mut m = VirtualViewManager::new();
869
        assert!(m.all_view_keys().is_empty());
870

            
871
        // Insert in deliberately reversed order — BTreeMap must still yield
872
        // ascending (DomId, NodeId).
873
        m.get_or_create_nested_dom_id(DOM1, n(9));
874
        m.get_or_create_nested_dom_id(DOM1, n(2));
875
        m.get_or_create_nested_dom_id(DOM, n(7));
876

            
877
        let keys = m.all_view_keys();
878
        assert_eq!(keys, vec![(DOM, n(7)), (DOM1, n(2)), (DOM1, n(9))]);
879
        assert_eq!(keys.len(), m.debug_counts());
880

            
881
        let mut sorted = keys.clone();
882
        sorted.sort_unstable();
883
        assert_eq!(keys, sorted);
884
    }
885

            
886
    // ------------------------------------------------ Option-returning mutators
887

            
888
    #[test]
889
    fn mutators_return_none_for_unknown_view_and_never_insert() {
890
        let mut m = VirtualViewManager::new();
891

            
892
        assert_eq!(
893
            m.update_virtual_view_info(DOM, n(3), LogicalPosition::zero(), sz(1.0, 1.0), sz(1.0, 1.0)),
894
            None
895
        );
896
        assert_eq!(
897
            m.mark_invoked(DOM, n(3), VirtualViewCallbackReason::InitialRender),
898
            None
899
        );
900
        assert_eq!(m.force_reinvoke(DOM, n(3)), None);
901
        assert_eq!(
902
            m.update_virtual_view_info(DOM_MAX, n(usize::MAX), LogicalPosition::zero(), sz(0.0, 0.0), sz(0.0, 0.0)),
903
            None
904
        );
905

            
906
        // Unlike check_reinvoke, none of these may lazily create a state.
907
        assert_eq!(m.debug_counts(), 0);
908
        assert_eq!(m.next_dom_id, 1);
909
    }
910

            
911
    // ------------------------------------------------------ reason overrides
912

            
913
    #[test]
914
    fn set_reason_override_keeps_only_the_latest_per_key() {
915
        let mut m = VirtualViewManager::new();
916

            
917
        for _ in 0..1_000 {
918
            m.set_reason_override(DOM, n(2), VirtualViewCallbackReason::DomRecreated);
919
        }
920
        m.set_reason_override(DOM, n(2), VirtualViewCallbackReason::BoundsExpanded);
921

            
922
        // Re-staging must overwrite, not accumulate.
923
        assert_eq!(m.reason_overrides.len(), 1);
924

            
925
        let sm = ScrollManager::new();
926
        assert_eq!(
927
            m.check_reinvoke(DOM, n(2), &sm, rect(10.0, 10.0)),
928
            Some(VirtualViewCallbackReason::BoundsExpanded)
929
        );
930
    }
931

            
932
    #[test]
933
    fn reason_override_is_consumed_exactly_once_and_does_not_create_state() {
934
        let mut m = VirtualViewManager::new();
935
        let sm = ScrollManager::new();
936

            
937
        m.set_reason_override(DOM, n(2), VirtualViewCallbackReason::ScrollBeyondContent);
938
        assert_eq!(
939
            m.check_reinvoke(DOM, n(2), &sm, rect(10.0, 10.0)),
940
            Some(VirtualViewCallbackReason::ScrollBeyondContent)
941
        );
942

            
943
        // The override short-circuits before the entry() call, so no state yet.
944
        assert!(m.reason_overrides.is_empty());
945
        assert_eq!(m.debug_counts(), 0);
946

            
947
        // Second call falls through to the normal path, which *does* create it.
948
        assert_eq!(
949
            m.check_reinvoke(DOM, n(2), &sm, rect(10.0, 10.0)),
950
            Some(VirtualViewCallbackReason::InitialRender)
951
        );
952
        assert_eq!(m.debug_counts(), 1);
953
        assert_eq!(m.get_nested_dom_id(DOM, n(2)), Some(DomId { inner: 1 }));
954
    }
955

            
956
    #[test]
957
    fn reason_overrides_do_not_leak_across_keys() {
958
        let mut m = VirtualViewManager::new();
959
        let sm = ScrollManager::new();
960

            
961
        m.set_reason_override(
962
            DOM,
963
            n(1),
964
            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Left),
965
        );
966
        m.set_reason_override(DOM1, n(1), VirtualViewCallbackReason::DomRecreated);
967
        m.set_reason_override(DOM, n(2), VirtualViewCallbackReason::BoundsExpanded);
968
        assert_eq!(m.reason_overrides.len(), 3);
969

            
970
        // A different node of the same DOM must not steal DOM/n(1)'s override.
971
        assert_eq!(
972
            m.check_reinvoke(DOM, n(2), &sm, rect(1.0, 1.0)),
973
            Some(VirtualViewCallbackReason::BoundsExpanded)
974
        );
975
        assert_eq!(
976
            m.check_reinvoke(DOM1, n(1), &sm, rect(1.0, 1.0)),
977
            Some(VirtualViewCallbackReason::DomRecreated)
978
        );
979
        assert_eq!(
980
            m.check_reinvoke(DOM, n(1), &sm, rect(1.0, 1.0)),
981
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Left))
982
        );
983
        assert!(m.reason_overrides.is_empty());
984
    }
985

            
986
    // ------------------------------------------- update_virtual_view_info (numeric)
987

            
988
    #[test]
989
    fn update_virtual_view_info_zero_and_extreme_sizes_do_not_panic() {
990
        let mut m = VirtualViewManager::new();
991
        m.get_or_create_nested_dom_id(DOM, n(1));
992

            
993
        for size in [
994
            sz(0.0, 0.0),
995
            sz(-0.0, -0.0),
996
            sz(f32::MAX, f32::MAX),
997
            sz(f32::MIN, f32::MIN),
998
            sz(f32::INFINITY, f32::NEG_INFINITY),
999
            sz(-1.0e30, 1.0e30),
            sz(f32::MIN_POSITIVE, f32::EPSILON),
        ] {
            assert_eq!(
                m.update_virtual_view_info(DOM, n(1), LogicalPosition::zero(), size, size),
                Some(()),
                "size {size:?} must be recorded without panicking"
            );
            assert_eq!(st(&m, DOM, n(1)).materialized.map(|r| r.size), Some(size));
            assert_eq!(
                st(&m, DOM, n(1)).virtual_rect.map(|r| r.size),
                Some(size)
            );
        }
        // NaN is stored verbatim (no normalization, no panic).
        assert_eq!(
            m.update_virtual_view_info(DOM, n(1), LogicalPosition::zero(), sz(f32::NAN, f32::NAN), sz(f32::NAN, 1.0)),
            Some(())
        );
        let stored = st(&m, DOM, n(1)).materialized.map(|r| r.size).unwrap();
        assert!(stored.width.is_nan() && stored.height.is_nan());
    }
    #[test]
    fn update_virtual_view_info_clears_expansion_flag_only_when_content_grows() {
        let mut m = ready_view(sz(100.0, 100.0));
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::BoundsExpanded);
        assert!(st(&m, DOM, n(1)).invoked_for_current_expansion);
        // Shrink: not a growth, flag survives.
        set_sizes(&mut m, DOM, n(1), sz(50.0, 50.0), sz(50.0, 50.0));
        assert!(st(&m, DOM, n(1)).invoked_for_current_expansion);
        // Same size: still not a growth (strict `>`).
        set_sizes(&mut m, DOM, n(1), sz(50.0, 50.0), sz(50.0, 50.0));
        assert!(st(&m, DOM, n(1)).invoked_for_current_expansion);
        // One axis grows by an epsilon: flag cleared, BoundsExpanded can re-fire.
        set_sizes(&mut m, DOM, n(1), sz(50.000_01, 50.0), sz(50.0, 50.0));
        assert!(!st(&m, DOM, n(1)).invoked_for_current_expansion);
    }
    #[test]
    fn update_virtual_view_info_infinite_growth_clears_the_flag() {
        let mut m = ready_view(sz(100.0, 100.0));
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::BoundsExpanded);
        set_sizes(
            &mut m,
            DOM,
            n(1),
            sz(f32::INFINITY, 100.0),
            sz(f32::INFINITY, 100.0),
        );
        assert!(!st(&m, DOM, n(1)).invoked_for_current_expansion);
    }
    #[test]
    fn update_virtual_view_info_nan_size_poisons_the_growth_check() {
        // PINNED QUIRK: growth is `new > old`, and every comparison against NaN
        // is false. Once a NaN content size is recorded, *no* later size — not
        // even 1e9 — is seen as growth, so `invoked_for_current_expansion` can
        // never be cleared here again. Only force_reinvoke/reset_all recover.
        let mut m = ready_view(sz(100.0, 100.0));
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::BoundsExpanded);
        assert!(st(&m, DOM, n(1)).invoked_for_current_expansion);
        // NaN is not > 100.0 → no clear (and no panic).
        set_sizes(
            &mut m,
            DOM,
            n(1),
            sz(f32::NAN, f32::NAN),
            sz(f32::NAN, f32::NAN),
        );
        assert!(st(&m, DOM, n(1)).invoked_for_current_expansion);
        // 1e9 is not > NaN either → still no clear.
        set_sizes(&mut m, DOM, n(1), sz(1.0e9, 1.0e9), sz(1.0e9, 1.0e9));
        assert!(st(&m, DOM, n(1)).invoked_for_current_expansion);
        // The recovery path still works.
        m.force_reinvoke(DOM, n(1)).expect("view exists");
        assert!(!st(&m, DOM, n(1)).invoked_for_current_expansion);
    }
    // ----------------------------------------------------------- mark_invoked
    #[test]
    fn mark_invoked_sets_only_the_flags_the_reason_owns() {
        for reason in [
            VirtualViewCallbackReason::InitialRender,
            VirtualViewCallbackReason::DomRecreated,
            VirtualViewCallbackReason::ScrollBeyondContent,
        ] {
            let mut m = VirtualViewManager::new();
            m.get_or_create_nested_dom_id(DOM, n(1));
            assert_eq!(m.mark_invoked(DOM, n(1), reason), Some(()));
            let s = st(&m, DOM, n(1));
            assert!(s.virtual_view_was_invoked, "{reason:?} must mark invoked");
            assert!(!s.invoked_for_current_expansion, "{reason:?}");
            assert!(!s.invoked_for_current_edge, "{reason:?}");
            assert_eq!(s.last_edge_triggered, EdgeFlags::default(), "{reason:?}");
            assert!(m.was_virtual_view_invoked(DOM, n(1)));
        }
        let mut m = VirtualViewManager::new();
        m.get_or_create_nested_dom_id(DOM, n(1));
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::BoundsExpanded);
        let s = st(&m, DOM, n(1));
        assert!(s.virtual_view_was_invoked);
        assert!(s.invoked_for_current_expansion);
        assert!(!s.invoked_for_current_edge);
        assert_eq!(s.last_edge_triggered, EdgeFlags::default());
    }
    #[test]
    fn edge_type_round_trips_through_mark_invoked_into_edge_flags() {
        for edge in [
            EdgeType::Top,
            EdgeType::Bottom,
            EdgeType::Left,
            EdgeType::Right,
        ] {
            let mut m = VirtualViewManager::new();
            m.get_or_create_nested_dom_id(DOM, n(1));
            mark(
                &mut m,
                DOM,
                n(1),
                VirtualViewCallbackReason::EdgeScrolled(edge),
            );
            let s = st(&m, DOM, n(1));
            assert!(s.virtual_view_was_invoked);
            assert!(s.invoked_for_current_edge);
            // encode(edge) == decode: the stored flags are exactly EdgeFlags::from(edge).
            assert_eq!(s.last_edge_triggered, EdgeFlags::from(edge), "{edge:?}");
            assert!(s.last_edge_triggered.any(), "{edge:?}");
            let f = s.last_edge_triggered;
            let set = usize::from(f.top)
                + usize::from(f.bottom)
                + usize::from(f.left)
                + usize::from(f.right);
            assert_eq!(set, 1, "{edge:?} must set exactly one flag");
            // Expansion is a different trigger and must stay untouched.
            assert!(!s.invoked_for_current_expansion, "{edge:?}");
        }
    }
    // ---------------------------------------------------- reset / force_reinvoke
    #[test]
    fn reset_all_invocation_flags_on_empty_manager_is_a_noop() {
        let mut m = VirtualViewManager::new();
        m.reset_all_invocation_flags();
        assert_eq!(m.debug_counts(), 0);
        assert_eq!(m.next_dom_id, 1);
        assert!(m.all_view_keys().is_empty());
    }
    #[test]
    fn reset_all_clears_every_flag_but_preserves_identity_sizes_and_bounds() {
        let mut m = ready_view(sz(100.0, 1000.0));
        let nested = m.get_nested_dom_id(DOM, n(1)).expect("view exists");
        m.get_or_create_nested_dom_id(DOM1, n(4));
        mark(
            &mut m,
            DOM,
            n(1),
            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
        );
        mark(&mut m, DOM1, n(4), VirtualViewCallbackReason::BoundsExpanded);
        // Record a non-zero last_bounds through the normal path.
        let sm = scrolled(DOM, n(1), 0.0, 900.0);
        let _ = m.check_reinvoke(DOM, n(1), &sm, rect(100.0, 100.0));
        // Overrides are a separate queue: reset must not touch them.
        m.set_reason_override(DOM, n(1), VirtualViewCallbackReason::DomRecreated);
        m.reset_all_invocation_flags();
        for (dom, node) in [(DOM, n(1)), (DOM1, n(4))] {
            let s = st(&m, dom, node);
            assert!(!s.virtual_view_was_invoked);
            assert!(!s.invoked_for_current_expansion);
            assert!(!s.invoked_for_current_edge);
            assert_eq!(s.last_edge_triggered, EdgeFlags::default());
            assert!(!m.was_virtual_view_invoked(dom, node));
        }
        // Identity, content size and bounds survive — only the flags reset.
        let s = st(&m, DOM, n(1));
        assert_eq!(s.nested_dom_id, nested);
        assert_eq!(s.materialized.map(|r| r.size), Some(sz(100.0, 1000.0)));
        assert_eq!(s.container, rect(100.0, 100.0));
        assert_eq!(m.debug_counts(), 2);
        assert_eq!(m.reason_overrides.len(), 1);
    }
    #[test]
    fn force_reinvoke_yields_initial_render_but_leaves_last_edge_triggered_set() {
        let mut m = ready_view(sz(100.0, 1000.0));
        mark(
            &mut m,
            DOM,
            n(1),
            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
        );
        assert_eq!(m.force_reinvoke(DOM, n(1)), Some(()));
        let s = st(&m, DOM, n(1));
        assert!(!s.virtual_view_was_invoked);
        assert!(!s.invoked_for_current_expansion);
        assert!(!s.invoked_for_current_edge);
        // ASYMMETRY (pinned): unlike reset_all_invocation_flags, force_reinvoke
        // does NOT clear last_edge_triggered — see the bottom-edge suppression
        // test below for the consequence.
        assert_eq!(s.last_edge_triggered, EdgeFlags::from(EdgeType::Bottom));
        // The documented effect still holds: the next check is an InitialRender.
        let sm = scrolled(DOM, n(1), 0.0, 900.0);
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &sm, rect(100.0, 100.0)),
            Some(VirtualViewCallbackReason::InitialRender)
        );
    }
    // -------------------------------------------------- check_reinvoke (numeric)
    #[test]
    fn check_reinvoke_creates_the_state_for_an_unknown_view() {
        let mut m = VirtualViewManager::new();
        let sm = ScrollManager::new();
        assert_eq!(
            m.check_reinvoke(DOM, n(3), &sm, rect(100.0, 100.0)),
            Some(VirtualViewCallbackReason::InitialRender)
        );
        assert_eq!(m.debug_counts(), 1);
        assert_eq!(m.get_nested_dom_id(DOM, n(3)), Some(DomId { inner: 1 }));
        assert!(!m.was_virtual_view_invoked(DOM, n(3)));
        // Re-checking without marking must keep returning InitialRender and must
        // NOT keep allocating states/ids (unbounded growth guard).
        for _ in 0..16 {
            assert_eq!(
                m.check_reinvoke(DOM, n(3), &sm, rect(100.0, 100.0)),
                Some(VirtualViewCallbackReason::InitialRender)
            );
        }
        assert_eq!(m.debug_counts(), 1);
        assert_eq!(m.next_dom_id, 2);
        // Saturated key: no panic, fresh id.
        assert_eq!(
            m.check_reinvoke(DOM_MAX, n(usize::MAX), &sm, rect(0.0, 0.0)),
            Some(VirtualViewCallbackReason::InitialRender)
        );
        assert_eq!(
            m.get_nested_dom_id(DOM_MAX, n(usize::MAX)),
            Some(DomId { inner: 2 })
        );
    }
    #[test]
    fn check_reinvoke_is_none_while_no_content_size_is_known() {
        let mut m = VirtualViewManager::new();
        m.get_or_create_nested_dom_id(DOM, n(1));
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::InitialRender);
        let sm = scrolled(DOM, n(1), 0.0, 5_000.0);
        // scroll_size is still None → the `?` bails out, whatever the bounds.
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &sm, rect(f32::MAX, f32::MAX)),
            None
        );
        assert_eq!(m.check_reinvoke(DOM, n(1), &sm, rect(0.0, 0.0)), None);
    }
    #[test]
    fn check_reinvoke_bounds_expanded_fires_once_per_growth() {
        let mut m = ready_view(sz(100.0, 100.0));
        let sm = ScrollManager::new();
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &sm, rect(200.0, 200.0)),
            Some(VirtualViewCallbackReason::BoundsExpanded)
        );
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::BoundsExpanded);
        // Same bounds again → already invoked for this expansion → quiet.
        assert_eq!(m.check_reinvoke(DOM, n(1), &sm, rect(200.0, 200.0)), None);
        // Shrinking is never a re-invoke trigger.
        assert_eq!(m.check_reinvoke(DOM, n(1), &sm, rect(150.0, 150.0)), None);
        // Growing past the last bounds re-arms it.
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &sm, rect(300.0, 300.0)),
            Some(VirtualViewCallbackReason::BoundsExpanded)
        );
        assert_eq!(st(&m, DOM, n(1)).container, rect(300.0, 300.0));
    }
    #[test]
    fn check_reinvoke_does_not_fire_an_edge_for_the_resting_start_position() {
        // Regression guard for the initial_scroll_offset rule: a view that
        // starts at offset 0 is *at* the top edge, but that is the initial
        // position, not a scroll-to-edge event.
        let mut m = VirtualViewManager::new();
        m.get_or_create_nested_dom_id(DOM, n(1));
        let sm = scrolled(DOM, n(1), 0.0, 0.0);
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &sm, rect(100.0, 100.0)),
            Some(VirtualViewCallbackReason::InitialRender)
        );
        assert_eq!(st(&m, DOM, n(1)).initial_scroll_offset, pos(0.0, 0.0));
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::InitialRender);
        set_sizes(&mut m, DOM, n(1), sz(100.0, 1000.0), sz(100.0, 1000.0));
        // Still parked at the top edge, hasn't moved → no EdgeScrolled(Top).
        assert_eq!(m.check_reinvoke(DOM, n(1), &sm, rect(100.0, 100.0)), None);
    }
    #[test]
    fn check_reinvoke_edge_scrolled_bottom_then_stays_quiet() {
        let mut m = ready_view(sz(100.0, 1000.0));
        let sm = scrolled(DOM, n(1), 0.0, 900.0);
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &sm, rect(100.0, 100.0)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        mark(
            &mut m,
            DOM,
            n(1),
            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
        );
        // invoked_for_current_edge gates the whole edge block → no duplicate.
        assert_eq!(m.check_reinvoke(DOM, n(1), &sm, rect(100.0, 100.0)), None);
    }
    #[test]
    fn bottom_edge_is_suppressed_after_a_force_reinvoke_but_not_after_a_reset() {
        // PINNED BUG-SHAPED BEHAVIOR: force_reinvoke clears invoked_for_current_edge
        // but NOT last_edge_triggered, so a second genuine scroll-to-bottom produces
        // no EdgeScrolled(Bottom) — an infinite-scroll list stops lazy-loading after
        // the first page. reset_all_invocation_flags (which does clear the edge
        // memory) re-arms it; the two halves below are identical except for that
        // one call, which isolates the stale flag as the cause.
        //
        // Geometry (`ready_view`): the 100x1000 window is materialized at the
        // document's ORIGIN, and the document is 2000 px tall — so offsets here
        // are already virtual-space offsets and `vpos` (which is for the
        // 1000-px-down `invoked_state` window) must NOT be applied to them.
        // Scrolling to y=900 puts the viewport's bottom flush against the
        // window's bottom edge, with 1000 px of document still to load below.
        let bottom = scrolled(DOM, n(1), 0.0, 900.0);
        let middle = scrolled(DOM, n(1), 0.0, 400.0);
        let bounds = rect(100.0, 100.0);
        let mut m = ready_view(sz(100.0, 1000.0));
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &bottom, bounds),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        mark(
            &mut m,
            DOM,
            n(1),
            VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom),
        );
        // --- half 1: the trigger_virtual_view_rerender() path -----------------
        m.force_reinvoke(DOM, n(1)).expect("view exists");
        // Re-invoked while the user sits mid-list, so the resting position is 400.
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &middle, bounds),
            Some(VirtualViewCallbackReason::InitialRender)
        );
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::InitialRender);
        assert_eq!(st(&m, DOM, n(1)).initial_scroll_offset, pos(0.0, 400.0));
        // The user now really scrolls 400 → 900 (a scroll-to-edge, and the
        // invoked_for_current_edge gate is open), yet nothing fires.
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &bottom, bounds),
            None,
            "stale last_edge_triggered.bottom suppresses the second bottom-edge load"
        );
        assert!(st(&m, DOM, n(1)).last_edge_triggered.bottom);
        // --- half 2: the same sequence, but through reset_all -----------------
        m.reset_all_invocation_flags();
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &middle, bounds),
            Some(VirtualViewCallbackReason::InitialRender)
        );
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::InitialRender);
        assert_eq!(
            m.check_reinvoke(DOM, n(1), &bottom, bounds),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom)),
            "reset_all clears the edge memory, so the identical scroll does fire"
        );
    }
    #[test]
    fn check_reinvoke_with_nan_bounds_is_quiet_and_stores_nan() {
        let mut m = ready_view(sz(100.0, 100.0));
        let sm = ScrollManager::new();
        let nan_rect = LogicalRect::new(pos(f32::NAN, f32::NAN), sz(f32::NAN, f32::NAN));
        // Every NaN comparison is false → no expansion, no scrollable axis.
        assert_eq!(m.check_reinvoke(DOM, n(1), &sm, nan_rect), None);
        let info = m.get_all_virtual_view_infos();
        assert_eq!(info.len(), 1);
        assert!(info[0].last_bounds_x.is_nan());
        assert!(info[0].last_bounds_width.is_nan());
        assert!(info[0].last_bounds_height.is_nan());
    }
    #[test]
    fn check_reinvoke_with_infinite_bounds_reports_bounds_expanded() {
        let mut m = ready_view(sz(100.0, 100.0));
        let sm = ScrollManager::new();
        assert_eq!(
            m.check_reinvoke(
                DOM,
                n(1),
                &sm,
                rect(f32::INFINITY, f32::INFINITY)
            ),
            Some(VirtualViewCallbackReason::BoundsExpanded)
        );
    }
    // ------------------------------- check_reinvoke_condition (private, numeric)
    #[test]
    fn edge_threshold_is_exactly_200_px_and_inclusive() {
        assert_eq!(EDGE_THRESHOLD, 200.0);
        // Every distance below is measured between the VISIBLE window
        // (`[offset, offset + container]`) and the MATERIALIZED window's edge —
        // never the document's. The document only decides whether an edge is
        // allowed to fire at all (is there anything left to load past it?).
        let s = invoked_state(sz(100.0, 1000.0)); // window y 1000..2000 of a 0..3000 doc
        let container = sz(100.0, 100.0);
        // Bottom edge: mat_max_y - vis_max_y == 2000 - 1800 == 200 → inclusive hit.
        assert_eq!(
            s.check_reinvoke_condition(vpos(700.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // One px further from the bottom (201) and not near the top → quiet.
        assert_eq!(s.check_reinvoke_condition(vpos(699.0), container), None);
        // Top edge: vis_min_y - mat_min_y == 1200 - 1000 == 200 → inclusive hit.
        assert_eq!(
            s.check_reinvoke_condition(vpos(200.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        // Just past it, and still 699 px from the bottom → quiet.
        assert_eq!(s.check_reinvoke_condition(vpos(201.0), container), None);
        // The horizontal axis uses the identical threshold with identical
        // inclusivity. It needs the both-axes fixture: on `invoked_state` the
        // window spans the document's full width, so left/right have nothing to
        // load and correctly never fire whatever the distance.
        let s2 = invoked_state_2d(sz(1000.0, 1000.0)); // window 1000..2000 of a 0..3000 doc, both axes
        // y is parked dead centre of the window (450 px from either vertical
        // edge) so that only the x axis can speak.
        let quiet_y = FIXTURE_WINDOW_ORIGIN_Y + 450.0;
        // Left edge: vis_min_x - mat_min_x == 1200 - 1000 == 200 → inclusive hit.
        assert_eq!(
            s2.check_reinvoke_condition(pos(1200.0, quiet_y), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Left))
        );
        assert_eq!(
            s2.check_reinvoke_condition(pos(1201.0, quiet_y), container),
            None
        );
        // Right edge: mat_max_x - vis_max_x == 2000 - 1800 == 200 → inclusive hit.
        assert_eq!(
            s2.check_reinvoke_condition(pos(1700.0, quiet_y), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Right))
        );
        assert_eq!(
            s2.check_reinvoke_condition(pos(1699.0, quiet_y), container),
            None
        );
    }
    #[test]
    fn edge_priority_is_bottom_right_top_left_and_drains() {
        // Several edges have to be near AT ONCE for priority to mean anything,
        // which takes a small viewport inside a window that has document past
        // it on all four sides — hence the both-axes fixture. A 900 px viewport
        // sitting on the top-left corner of the 1000x1000 window is 0 px from
        // its top and left edges and 100 px from its bottom and right ones, so
        // all four are inside EDGE_THRESHOLD simultaneously.
        let mut s = invoked_state_2d(sz(1000.0, 1000.0));
        let container = sz(900.0, 900.0);
        let offset = pos(FIXTURE_WINDOW_ORIGIN_X, FIXTURE_WINDOW_ORIGIN_Y);
        assert_eq!(
            s.check_reinvoke_condition(offset, container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        s.last_edge_triggered.bottom = true;
        assert_eq!(
            s.check_reinvoke_condition(offset, container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Right))
        );
        s.last_edge_triggered.right = true;
        assert_eq!(
            s.check_reinvoke_condition(offset, container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        s.last_edge_triggered.top = true;
        assert_eq!(
            s.check_reinvoke_condition(offset, container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Left))
        );
        // All four drained: near an edge, but nothing left to report.
        s.last_edge_triggered.left = true;
        assert_eq!(s.check_reinvoke_condition(offset, container), None);
    }
    #[test]
    fn check_reinvoke_condition_at_zero_is_quiet() {
        // Zero content, zero container, zero offset: `0 > 0` is false so there
        // is no expansion, and the offset is exactly the resting
        // `initial_scroll_offset`, so no edge may fire either.
        let s = invoked_state(sz(0.0, 0.0));
        assert_eq!(s.check_reinvoke_condition(pos(0.0, 0.0), sz(0.0, 0.0)), None);
        // Zero-size content inside a real container *is* an expansion.
        assert_eq!(
            s.check_reinvoke_condition(pos(0.0, 0.0), sz(1.0, 1.0)),
            Some(VirtualViewCallbackReason::BoundsExpanded)
        );
    }
    #[test]
    fn check_reinvoke_condition_handles_nan_offset_and_nan_sizes() {
        let nan = f32::NAN;
        // JUDGEMENT (NaN). The rule is four independent comparisons, and every
        // comparison against NaN is false — so NaN does NOT poison the whole
        // decision, it silences exactly the edges whose arithmetic touches it
        // while the others still answer. Which edges those are depends on where
        // the NaN is, so each case is verified in both directions (the edge
        // that survives fires; the edge that was poisoned stays quiet) rather
        // than asserted wholesale as `None`. Nothing here may panic.
        // (1) NaN materialized/document SIZES. `bottom`/`right` are derived
        // from those sizes → NaN → false. `top`/`left` are derived from ORIGINS
        // only, so they are NaN-free: parked on the window's top edge with
        // 1000 px of document above it, Top still fires.
        let s = invoked_state(sz(nan, nan));
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), sz(100.0, 100.0)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top)),
            "the top edge is computed from origins alone, so a NaN size cannot silence it"
        );
        // Far below the top, where only the (NaN-poisoned) bottom edge could
        // have fired — proof that the NaN really did silence it.
        assert_eq!(
            s.check_reinvoke_condition(vpos(5_000.0), sz(100.0, 100.0)),
            None
        );
        // (2) NaN offset AND NaN sizes: nothing is left that can compare true.
        assert_eq!(s.check_reinvoke_condition(pos(nan, nan), sz(nan, nan)), None);
        // (3) NaN CONTAINER size against real content. The container size only
        // enters the bottom/right distances, so Top survives again...
        let s = invoked_state(sz(100.0, 1000.0));
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), sz(nan, nan)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        // ...and the bottom edge is the one that goes quiet, at an offset that
        // would otherwise be a dead-on bottom hit.
        assert_eq!(s.check_reinvoke_condition(vpos(900.0), sz(nan, nan)), None);
        // (4) NaN scroll OFFSET. It appears in all four distances, so every
        // edge predicate is false and NO edge fires — verified, not assumed —
        // even though `has_scrolled` is true: NaN quantizes to the dedicated
        // i64::MIN sentinel, which is != the quantized 0 of the resting offset.
        assert_ne!(
            pos(nan, nan),
            LogicalPosition::zero(),
            "a NaN offset counts as 'has scrolled', so the edge block IS entered"
        );
        assert_eq!(
            s.check_reinvoke_condition(pos(nan, nan), sz(100.0, 100.0)),
            None
        );
    }
    #[test]
    fn check_reinvoke_condition_handles_negative_overscroll_offsets() {
        let s = invoked_state(sz(100.0, 1000.0));
        let container = sz(100.0, 100.0);
        // Rubber-band overscroll far above the materialized top: the top edge
        // fires, because this window has 1000 px of document above it. (The
        // bottom is ~1e9 px away, and the fixture is not windowed on x.)
        assert_eq!(
            s.check_reinvoke_condition(vpos(-1.0e9), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        assert_eq!(
            s.check_reinvoke_condition(pos(-50.0, -1.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        // JUDGEMENT: a negative offset is not a top-edge event by ITSELF — the
        // sign of the offset is irrelevant, what matters is whether there is
        // document above the materialized window. With the window flush against
        // the document's top there is nothing left to load up there, so the
        // very same rubber-band overscroll is silence, not a reload loop
        // (which is precisely what a rubber-band bounce would otherwise cause,
        // once per frame, for the whole duration of the bounce).
        let at_doc_top = windowed_state(
            LogicalRect::new(LogicalPosition::zero(), sz(100.0, 1000.0)),
            LogicalRect::new(LogicalPosition::zero(), sz(100.0, 3000.0)),
        );
        assert_eq!(
            at_doc_top.check_reinvoke_condition(pos(0.0, -1.0e9), container),
            None
        );
        assert_eq!(
            at_doc_top.check_reinvoke_condition(pos(-1.0e9, -1.0e9), container),
            None
        );
        // Overscrolled past the bottom: still the bottom edge, no panic.
        assert_eq!(
            s.check_reinvoke_condition(vpos(1.0e9), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // JUDGEMENT: negative container/content sizes are nonsense input — an
        // INVERTED rect, whose max lies below its min. The rule stays total on
        // them (no panic, no NaN, a deterministic answer), but it cannot be
        // *meaningful*, and pinning `None` here would pretend the inversion is
        // detected when it is not. What actually happens: the inverted window's
        // "bottom" (y=900) sits 100 px short of the inverted document's
        // (y=1900), so the bottom edge reports. Totality is the guarantee; the
        // particular edge is an artefact of the garbage, recorded so a future
        // change to it is noticed rather than silently absorbed.
        let s = invoked_state(sz(-100.0, -100.0));
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), sz(-200.0, -200.0)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
    }
    #[test]
    fn check_reinvoke_condition_saturates_at_f32_extremes() {
        // JUDGEMENT (extremes): the requirement is that saturated arithmetic
        // neither panics NOR invents an edge. The second half is the subtle
        // one — at f32::MAX the distances collapse to 0 and *look* like a
        // permanent edge hit, and the only thing standing between that and an
        // infinite re-materialize loop is the "does the document actually
        // extend past this edge?" guard. Both halves are pinned below.
        // A MAX-sized window inside a document that saturates to the SAME MAX
        // (`1000 + MAX == MAX` and `2000 + MAX == MAX` in f32): the window
        // covers everything there is. Scrolled to the far end the bottom
        // distance is MAX - MAX == 0 — inside the threshold — yet nothing
        // fires, because there is nothing beyond the window to load.
        let s = invoked_state(sz(f32::MAX, f32::MAX));
        assert_eq!(
            s.check_reinvoke_condition(pos(f32::MAX, f32::MAX), sz(0.0, 0.0)),
            None,
            "a saturated distance of 0 must not fake an edge when the window covers the document"
        );
        // The mirrored extreme: -MAX is far above the window, and the 1000 px
        // of document above it survive saturation (the window's ORIGIN is
        // still 1000, the document's still 0), so the top edge does fire.
        assert_eq!(
            s.check_reinvoke_condition(pos(-f32::MAX, -f32::MAX), sz(0.0, 0.0)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        // MAX container over MAX content: `>` is strict, so nothing grew and
        // there is no BoundsExpanded — but the viewport starts exactly on the
        // window's top edge, which has document above it, so this is a Top.
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), sz(f32::MAX, f32::MAX)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
        // Saturation where the document genuinely DOES extend past the window:
        // a MAX/2 window at the document's origin inside a MAX-tall document.
        // The bottom distance underflows to -MAX/2 (the viewport is absurdly
        // far past the window) — still <= EDGE_THRESHOLD, so the bottom edge
        // reports instead of overflowing.
        let huge = windowed_state(
            LogicalRect::new(
                LogicalPosition::zero(),
                sz(f32::MAX / 2.0, f32::MAX / 2.0),
            ),
            LogicalRect::new(LogicalPosition::zero(), sz(f32::MAX, f32::MAX)),
        );
        assert_eq!(
            huge.check_reinvoke_condition(pos(f32::MAX, f32::MAX), sz(0.0, 0.0)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // Mirrored: that window is flush with the document's top, so an -MAX
        // offset has nothing to load above it. The bottom distance
        // (MAX/2 + MAX) overflows to +inf, which reads as "very far away" —
        // not as a panic and not as an edge.
        assert_eq!(
            huge.check_reinvoke_condition(pos(-f32::MAX, -f32::MAX), sz(0.0, 0.0)),
            None
        );
        // Infinite container over finite content is an expansion...
        let s = invoked_state(sz(100.0, 100.0));
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), sz(f32::INFINITY, f32::INFINITY)),
            Some(VirtualViewCallbackReason::BoundsExpanded)
        );
        // ...and once that expansion has been served, an infinite viewport is
        // "at" every edge at once (`mat_max - inf == -inf <= 200`), so priority
        // answers Bottom. Absurd, but deterministic and bounded: the document
        // guard is still the thing that decides, as the next case shows.
        let mut s = invoked_state(sz(100.0, 100.0));
        s.invoked_for_current_expansion = true;
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), sz(f32::INFINITY, f32::INFINITY)),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // Same infinite viewport over a FULLY materialized view: every edge is
        // 0/-inf away and every one of them is vetoed, so it goes quiet instead
        // of looping.
        let mut covered = windowed_state(
            LogicalRect::new(LogicalPosition::zero(), sz(100.0, 100.0)),
            LogicalRect::new(LogicalPosition::zero(), sz(100.0, 100.0)),
        );
        covered.invoked_for_current_expansion = true;
        assert_eq!(
            covered.check_reinvoke_condition(pos(0.0, 50.0), sz(f32::INFINITY, f32::INFINITY)),
            None
        );
    }
    #[test]
    fn check_reinvoke_condition_needs_a_real_scroll_before_any_edge_fires() {
        // The view is re-invoked while the user sits at the BOTTOM of the
        // materialized window, so the resting position is itself an edge: the
        // bottom distance is 0 from the very first check. Only movement away
        // from that resting position may fire.
        let mut s = invoked_state(sz(100.0, 1000.0));
        s.initial_scroll_offset = vpos(900.0);
        let container = sz(100.0, 100.0);
        // Parked exactly where it started (on the window's bottom edge, with
        // 1000 px of document still below it): not a scroll-to-edge.
        assert_eq!(s.check_reinvoke_condition(vpos(900.0), container), None);
        // One pixel of real movement, still within the bottom threshold → fires.
        assert_eq!(
            s.check_reinvoke_condition(vpos(899.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // Already invoked for this edge event → quiet regardless of movement.
        s.invoked_for_current_edge = true;
        assert_eq!(s.check_reinvoke_condition(vpos(899.0), container), None);
    }
    #[test]
    fn a_window_scrolled_in_the_middle_of_a_document_rematerializes_at_its_edges() {
        // THE REGRESSION THIS RULE EXISTS FOR. The old rule compared a
        // VIRTUAL-space scroll offset against the MATERIALIZED window's SIZE —
        // two different coordinate spaces — so an edge could only ever fire at
        // the absolute top or bottom of the DOCUMENT, and a view parked in the
        // middle of one never asked for more content: a VirtualView could not
        // scroll. The edges that matter belong to the materialized WINDOW, and
        // they sit in the middle of the document, which is exactly what this
        // pins.
        let s = invoked_state(sz(100.0, 1000.0)); // window y 1000..2000 of a 0..3000 doc
        let container = sz(100.0, 100.0);
        // Dead centre of the window — 450 px from either of its edges, and also
        // the middle of the document: nothing to do.
        assert_eq!(s.check_reinvoke_condition(vpos(450.0), container), None);
        // On the window's BOTTOM edge while still 1100 px short of the
        // document's end. Under the old rule this was "not at the bottom" and
        // nothing loaded; the view could never grow downwards.
        assert_eq!(
            s.check_reinvoke_condition(vpos(900.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // Symmetrically on the window's TOP edge, 1000 px INTO the document
        // rather than at its start — the case a paginated document hits every
        // time the user scrolls back up.
        assert_eq!(
            s.check_reinvoke_condition(vpos(0.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Top))
        );
    }
    #[test]
    fn no_edge_fires_when_the_materialized_window_already_covers_that_side() {
        // The other half of the rule: proximity alone is not enough, the
        // document has to actually extend past that edge. Without this the
        // ends of every document would re-materialize once per frame forever.
        let container = sz(100.0, 100.0);
        // Window flush against the document's TOP: its top edge has nothing
        // behind it, so resting on it is silence — while the BOTTOM edge of the
        // very same window, which does have document behind it, still fires.
        // (Same fixture, same offsets: the only difference is which side has
        // content left.)
        let head = windowed_state(
            LogicalRect::new(LogicalPosition::zero(), sz(100.0, 1000.0)),
            LogicalRect::new(LogicalPosition::zero(), sz(100.0, 3000.0)),
        );
        assert_eq!(head.check_reinvoke_condition(pos(0.0, 1.0), container), None);
        assert_eq!(
            head.check_reinvoke_condition(pos(0.0, 900.0), container),
            Some(VirtualViewCallbackReason::EdgeScrolled(EdgeType::Bottom))
        );
        // Fully materialized (window == document): nothing is left to load on
        // ANY side, so no offset can produce an edge — including the corners,
        // where all four distances are 0 and every edge "looks" reachable.
        let whole = windowed_state(
            LogicalRect::new(LogicalPosition::zero(), sz(1000.0, 1000.0)),
            LogicalRect::new(LogicalPosition::zero(), sz(1000.0, 1000.0)),
        );
        for offset in [
            pos(0.0, 1.0),
            pos(1.0, 0.0),
            pos(900.0, 900.0),
            pos(500.0, 500.0),
        ] {
            assert_eq!(
                whole.check_reinvoke_condition(offset, container),
                None,
                "fully materialized: nothing to load at {offset:?}"
            );
        }
    }
    // ------------------------------------------------------- getters / predicates
    #[test]
    fn debug_counts_matches_the_number_of_tracked_views() {
        let mut m = VirtualViewManager::new();
        assert_eq!(m.debug_counts(), 0);
        for i in 0..10_usize {
            m.get_or_create_nested_dom_id(DOM, n(i));
            assert_eq!(m.debug_counts(), i + 1);
        }
        // Re-registering the same keys must not grow the map.
        for i in 0..10_usize {
            m.get_or_create_nested_dom_id(DOM, n(i));
        }
        assert_eq!(m.debug_counts(), 10);
        assert_eq!(m.debug_counts(), m.all_view_keys().len());
        assert_eq!(m.debug_counts(), m.get_all_virtual_view_infos().len());
    }
    #[test]
    fn was_virtual_view_invoked_is_false_until_marked() {
        let mut m = VirtualViewManager::new();
        assert!(!m.was_virtual_view_invoked(DOM, n(1)));
        m.get_or_create_nested_dom_id(DOM, n(1));
        assert!(
            !m.was_virtual_view_invoked(DOM, n(1)),
            "registration alone is not an invocation"
        );
        mark(&mut m, DOM, n(1), VirtualViewCallbackReason::InitialRender);
        assert!(m.was_virtual_view_invoked(DOM, n(1)));
        // A sibling node must not inherit the flag.
        assert!(!m.was_virtual_view_invoked(DOM, n(2)));
        assert!(!m.was_virtual_view_invoked(DOM1, n(1)));
        assert_eq!(m.force_reinvoke(DOM, n(1)), Some(()));
        assert!(!m.was_virtual_view_invoked(DOM, n(1)));
    }
    #[test]
    fn get_all_virtual_view_infos_reports_every_field() {
        let m = VirtualViewManager::new();
        assert!(m.get_all_virtual_view_infos().is_empty());
        let mut m = VirtualViewManager::new();
        let nested = m.get_or_create_nested_dom_id(DOM1, n(5));
        // Before any callback: sizes are None, not 0.0.
        let info = m.get_all_virtual_view_infos();
        assert_eq!(info.len(), 1);
        assert_eq!(info[0].parent_dom_id, 1);
        assert_eq!(info[0].parent_node_id, 5);
        assert_eq!(info[0].nested_dom_id, nested.inner);
        assert!(info[0].scroll_size_width.is_none());
        assert!(info[0].scroll_size_height.is_none());
        assert!(info[0].virtual_scroll_size_width.is_none());
        assert!(info[0].virtual_scroll_size_height.is_none());
        assert!(!info[0].was_invoked);
        assert_eq!(info[0].last_bounds_x, 0.0);
        assert_eq!(info[0].last_bounds_y, 0.0);
        assert_eq!(info[0].last_bounds_width, 0.0);
        assert_eq!(info[0].last_bounds_height, 0.0);
        set_sizes(&mut m, DOM1, n(5), sz(3.0, 4.0), sz(5.0, 6.0));
        mark(&mut m, DOM1, n(5), VirtualViewCallbackReason::InitialRender);
        let info = m.get_all_virtual_view_infos();
        assert_eq!(info[0].scroll_size_width, Some(3.0));
        assert_eq!(info[0].scroll_size_height, Some(4.0));
        assert_eq!(info[0].virtual_scroll_size_width, Some(5.0));
        assert_eq!(info[0].virtual_scroll_size_height, Some(6.0));
        assert!(info[0].was_invoked);
        // Infos are emitted in the same (sorted) order as all_view_keys.
        m.get_or_create_nested_dom_id(DOM, n(9));
        let keys = m.all_view_keys();
        let infos = m.get_all_virtual_view_infos();
        assert_eq!(keys.len(), infos.len());
        for (k, i) in keys.iter().zip(infos.iter()) {
            assert_eq!(k.0.inner, i.parent_dom_id);
            assert_eq!(k.1.index(), i.parent_node_id);
        }
    }
    #[test]
    fn edge_flags_any_is_the_or_of_all_four_edges() {
        assert!(!EdgeFlags::default().any());
        let mut all = EdgeFlags::default();
        for edge in [
            EdgeType::Top,
            EdgeType::Bottom,
            EdgeType::Left,
            EdgeType::Right,
        ] {
            let f = EdgeFlags::from(edge);
            assert!(f.any(), "{edge:?} alone must satisfy any()");
            all.top |= f.top;
            all.bottom |= f.bottom;
            all.left |= f.left;
            all.right |= f.right;
        }
        assert_eq!(
            all,
            EdgeFlags {
                top: true,
                bottom: true,
                left: true,
                right: true,
            }
        );
        assert!(all.any());
    }
    #[test]
    fn edge_flags_from_edge_type_sets_exactly_that_edge() {
        assert_eq!(
            EdgeFlags::from(EdgeType::Top),
            EdgeFlags {
                top: true,
                ..Default::default()
            }
        );
        assert_eq!(
            EdgeFlags::from(EdgeType::Bottom),
            EdgeFlags {
                bottom: true,
                ..Default::default()
            }
        );
        assert_eq!(
            EdgeFlags::from(EdgeType::Left),
            EdgeFlags {
                left: true,
                ..Default::default()
            }
        );
        assert_eq!(
            EdgeFlags::from(EdgeType::Right),
            EdgeFlags {
                right: true,
                ..Default::default()
            }
        );
    }
}