1
//! Scroll-into-view implementation
2
//!
3
//! Provides W3C CSSOM View Module compliant scroll-into-view functionality.
4
//! This module contains the core primitive `scroll_rect_into_view` which all
5
//! higher-level scroll-into-view APIs build upon.
6
//!
7
//! # Architecture
8
//!
9
//! The core principle is that all scroll-into-view operations reduce to scrolling
10
//! a rectangle into the visible area of its scroll container ancestry:
11
//!
12
//! - `scroll_rect_into_view`: Core primitive - scroll any rect into view
13
//! - `scroll_node_into_view`: Scroll a DOM node's bounding rect into view
14
//! - `scroll_cursor_into_view`: Scroll a text cursor position into view
15
//!
16
//! # W3C Compliance
17
//!
18
//! This implementation follows the W3C CSSOM View Module specification:
19
//! - ScrollLogicalPosition: start, center, end, nearest
20
//! - ScrollBehavior: auto, instant, smooth
21
//! - Proper scroll ancestor chain traversal
22

            
23
use alloc::vec::Vec;
24

            
25
use azul_core::{
26
    dom::{DomId, DomNodeId, NodeId},
27
    geom::{LogicalPosition, LogicalRect, LogicalSize},
28
    task::{Duration, Instant},
29
};
30

            
31
use crate::{
32
    managers::scroll_state::ScrollManager,
33
    solver3::getters::{get_overflow_x, get_overflow_y},
34
    window::DomLayoutResult,
35
};
36

            
37
// Re-export types from core for public API
38
pub use azul_core::events::{ScrollIntoViewBehavior, ScrollIntoViewOptions, ScrollLogicalPosition};
39

            
40
/// Minimum scroll delta (in logical pixels) below which scrolling is skipped
41
const SCROLL_DELTA_THRESHOLD: f32 = 0.5;
42
/// Duration of smooth scroll animations in milliseconds
43
const SMOOTH_SCROLL_DURATION_MS: u64 = 300;
44

            
45
/// Calculated scroll adjustment for one scroll container
46
#[derive(Debug, Clone)]
47
pub struct ScrollAdjustment {
48
    /// The DOM containing the scroll container
49
    pub scroll_container_dom_id: DomId,
50
    /// The node ID of the scroll container within the DOM
51
    pub scroll_container_node_id: NodeId,
52
    /// The scroll delta to apply
53
    pub delta: LogicalPosition,
54
    /// The scroll behavior to use
55
    pub behavior: ScrollIntoViewBehavior,
56
}
57

            
58
/// Information about a scrollable ancestor
59
#[derive(Debug, Clone)]
60
struct ScrollableAncestor {
61
    dom_id: DomId,
62
    node_id: NodeId,
63
    /// The visible rect of the scroll container (content area)
64
    visible_rect: LogicalRect,
65
    /// Whether horizontal scroll is enabled
66
    scroll_x: bool,
67
    /// Whether vertical scroll is enabled
68
    scroll_y: bool,
69
}
70

            
71
// ============================================================================
72
// Core API: scroll_rect_into_view
73
// ============================================================================
74

            
75
/// Core function: scroll a rect into the visible area of its scroll containers
76
///
77
/// This is the ONLY scroll-into-view primitive. All higher-level APIs call this.
78
///
79
/// # Arguments
80
///
81
/// * `target_rect` - The rectangle to make visible (in absolute coordinates)
82
/// * `target_dom_id` - The DOM containing the target node
83
/// * `target_node_id` - The target node (used for finding scroll ancestors)
84
/// * `layout_results` - Layout data for all DOMs
85
/// * `scroll_manager` - Current scroll state
86
/// * `options` - How to scroll (alignment and animation)
87
/// * `now` - Current timestamp for animation
88
///
89
/// # Returns
90
///
91
/// A vector of scroll adjustments for each scroll container in the ancestry chain.
92
/// The adjustments are ordered from innermost (closest to target) to outermost.
93
pub fn scroll_rect_into_view(
94
    target_rect: LogicalRect,
95
    target_dom_id: DomId,
96
    target_node_id: NodeId,
97
    layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
98
    scroll_manager: &mut ScrollManager,
99
    options: ScrollIntoViewOptions,
100
    now: Instant,
101
) -> Vec<ScrollAdjustment> {
102
    let mut adjustments = Vec::new();
103
    
104
    // Find scrollable ancestors from target to root
105
    let scroll_ancestors = find_scrollable_ancestors(
106
        target_dom_id,
107
        target_node_id,
108
        layout_results,
109
        scroll_manager,
110
    );
111
    
112
    if scroll_ancestors.is_empty() {
113
        return adjustments;
114
    }
115
    
116
    // Transform target_rect relative to each scroll container and calculate deltas
117
    let mut current_rect = target_rect;
118
    
119
    for ancestor in scroll_ancestors {
120
        // Calculate the scroll delta based on options
121
        let delta = calculate_scroll_delta(
122
            current_rect,
123
            ancestor.visible_rect,
124
            options.block,
125
            options.inline_axis,
126
            ancestor.scroll_x,
127
            ancestor.scroll_y,
128
        );
129
        
130
        // Only add adjustment if there's actual scrolling to do
131
        if delta.x.abs() > SCROLL_DELTA_THRESHOLD || delta.y.abs() > SCROLL_DELTA_THRESHOLD {
132
            // Resolve scroll behavior
133
            let behavior = resolve_scroll_behavior(
134
                options.behavior,
135
                ancestor.dom_id,
136
                ancestor.node_id,
137
                layout_results,
138
            );
139
            
140
            // Apply the scroll adjustment
141
            apply_scroll_adjustment(
142
                scroll_manager,
143
                ancestor.dom_id,
144
                ancestor.node_id,
145
                delta,
146
                behavior,
147
                now.clone(),
148
            );
149
            
150
            adjustments.push(ScrollAdjustment {
151
                scroll_container_dom_id: ancestor.dom_id,
152
                scroll_container_node_id: ancestor.node_id,
153
                delta,
154
                behavior,
155
            });
156
            
157
            // Adjust current_rect for next iteration (relative to new scroll position)
158
            current_rect.origin.x -= delta.x;
159
            current_rect.origin.y -= delta.y;
160
        }
161
    }
162
    
163
    adjustments
164
}
165

            
166
// ============================================================================
167
// Higher-Level APIs
168
// ============================================================================
169

            
170
/// Scroll a DOM node's bounding rect into view
171
///
172
/// This is a convenience wrapper around `scroll_rect_into_view` that
173
/// automatically gets the node's bounding rect from layout results.
174
pub fn scroll_node_into_view(
175
    node_id: DomNodeId,
176
    layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
177
    scroll_manager: &mut ScrollManager,
178
    options: ScrollIntoViewOptions,
179
    now: Instant,
180
) -> Vec<ScrollAdjustment> {
181
    // Get node's bounding rect from layout
182
    let target_rect = match get_node_rect(node_id, layout_results) {
183
        Some(rect) => rect,
184
        None => return Vec::new(),
185
    };
186
    
187
    let internal_node_id = match node_id.node.into_crate_internal() {
188
        Some(nid) => nid,
189
        None => return Vec::new(),
190
    };
191
    
192
    // Call the core rect-based API
193
    scroll_rect_into_view(
194
        target_rect,
195
        node_id.dom,
196
        internal_node_id,
197
        layout_results,
198
        scroll_manager,
199
        options,
200
        now,
201
    )
202
}
203

            
204
/// Scroll a text cursor position into view
205
///
206
/// Transforms the cursor's visual rect (in node-local coordinates) to absolute
207
/// coordinates before scrolling.
208
pub fn scroll_cursor_into_view(
209
    cursor_rect: LogicalRect,
210
    node_id: DomNodeId,
211
    layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
212
    scroll_manager: &mut ScrollManager,
213
    options: ScrollIntoViewOptions,
214
    now: Instant,
215
) -> Vec<ScrollAdjustment> {
216
    // Get node's position to transform cursor_rect to absolute coordinates
217
    let node_rect = match get_node_rect(node_id, layout_results) {
218
        Some(rect) => rect,
219
        None => return Vec::new(),
220
    };
221
    
222
    // Transform cursor rect to absolute coordinates
223
    let absolute_cursor_rect = LogicalRect {
224
        origin: LogicalPosition {
225
            x: node_rect.origin.x + cursor_rect.origin.x,
226
            y: node_rect.origin.y + cursor_rect.origin.y,
227
        },
228
        size: cursor_rect.size,
229
    };
230
    
231
    let internal_node_id = match node_id.node.into_crate_internal() {
232
        Some(nid) => nid,
233
        None => return Vec::new(),
234
    };
235
    
236
    // Call the core rect-based API
237
    scroll_rect_into_view(
238
        absolute_cursor_rect,
239
        node_id.dom,
240
        internal_node_id,
241
        layout_results,
242
        scroll_manager,
243
        options,
244
        now,
245
    )
246
}
247

            
248
// ============================================================================
249
// Helper Functions
250
// ============================================================================
251

            
252
/// Find all scrollable ancestors from a node to the root
253
///
254
/// Returns ancestors ordered from innermost (closest to target) to outermost (root).
255
fn find_scrollable_ancestors(
256
    dom_id: DomId,
257
    node_id: NodeId,
258
    layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
259
    scroll_manager: &ScrollManager,
260
) -> Vec<ScrollableAncestor> {
261
    let mut ancestors = Vec::new();
262
    
263
    let layout_result = match layout_results.get(&dom_id) {
264
        Some(lr) => lr,
265
        None => return ancestors,
266
    };
267
    
268
    let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
269

            
270
    // Walk up the DOM tree from parent of target node
271
    let mut current = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
272
    
273
    while let Some(current_node_id) = current {
274
        // Check if this node is scrollable
275
        if let Some(ancestor) = check_if_scrollable(
276
            dom_id,
277
            current_node_id,
278
            layout_result,
279
            scroll_manager,
280
        ) {
281
            ancestors.push(ancestor);
282
        }
283
        
284
        // Move to parent
285
        current = node_hierarchy.get(current_node_id).and_then(|h| h.parent_id());
286
    }
287
    
288
    ancestors
289
}
290

            
291
/// Check if a node is scrollable and return its scroll info
292
fn check_if_scrollable(
293
    dom_id: DomId,
294
    node_id: NodeId,
295
    layout_result: &DomLayoutResult,
296
    scroll_manager: &ScrollManager,
297
) -> Option<ScrollableAncestor> {
298
    let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
299
    let styled_node = styled_nodes.get(node_id)?;
300
    
301
    let overflow_x = get_overflow_x(
302
        &layout_result.styled_dom,
303
        node_id,
304
        &styled_node.styled_node_state,
305
    );
306
    let overflow_y = get_overflow_y(
307
        &layout_result.styled_dom,
308
        node_id,
309
        &styled_node.styled_node_state,
310
    );
311
    
312
    let scroll_x = overflow_x.is_scroll();
313
    let scroll_y = overflow_y.is_scroll();
314
    
315
    // If neither axis is scrollable, skip this node
316
    if !scroll_x && !scroll_y {
317
        return None;
318
    }
319
    
320
    // Check if the scroll manager has scroll state for this node
321
    // (which means it actually has overflowing content)
322
    let scroll_state = scroll_manager.get_scroll_state(dom_id, node_id)?;
323
    
324
    // Check if content actually overflows (use virtual_scroll_size when set, e.g. for VirtualView)
325
    let effective_width = scroll_state.virtual_scroll_size.map(|s| s.width).unwrap_or(scroll_state.content_rect.size.width);
326
    let effective_height = scroll_state.virtual_scroll_size.map(|s| s.height).unwrap_or(scroll_state.content_rect.size.height);
327
    let has_overflow_x = effective_width > scroll_state.container_rect.size.width;
328
    let has_overflow_y = effective_height > scroll_state.container_rect.size.height;
329
    
330
    if !has_overflow_x && !has_overflow_y {
331
        return None;
332
    }
333
    
334
    // Get the visible rect (container rect minus current scroll offset)
335
    let visible_rect = LogicalRect {
336
        origin: LogicalPosition {
337
            x: scroll_state.container_rect.origin.x + scroll_state.current_offset.x,
338
            y: scroll_state.container_rect.origin.y + scroll_state.current_offset.y,
339
        },
340
        size: scroll_state.container_rect.size,
341
    };
342
    
343
    Some(ScrollableAncestor {
344
        dom_id,
345
        node_id,
346
        visible_rect,
347
        scroll_x: scroll_x && has_overflow_x,
348
        scroll_y: scroll_y && has_overflow_y,
349
    })
350
}
351

            
352
/// Calculate the scroll delta needed to bring target into view within container
353
fn calculate_scroll_delta(
354
    target: LogicalRect,
355
    container: LogicalRect,
356
    block: ScrollLogicalPosition,
357
    inline: ScrollLogicalPosition,
358
    scroll_x_enabled: bool,
359
    scroll_y_enabled: bool,
360
) -> LogicalPosition {
361
    LogicalPosition {
362
        x: if scroll_x_enabled {
363
            calculate_axis_delta(
364
                target.origin.x,
365
                target.size.width,
366
                container.origin.x,
367
                container.size.width,
368
                inline,
369
            )
370
        } else {
371
            0.0
372
        },
373
        y: if scroll_y_enabled {
374
            calculate_axis_delta(
375
                target.origin.y,
376
                target.size.height,
377
                container.origin.y,
378
                container.size.height,
379
                block,
380
            )
381
        } else {
382
            0.0
383
        },
384
    }
385
}
386

            
387
/// Calculate scroll delta for a single axis
388
pub fn calculate_axis_delta(
389
    target_start: f32,
390
    target_size: f32,
391
    container_start: f32,
392
    container_size: f32,
393
    position: ScrollLogicalPosition,
394
) -> f32 {
395
    let target_end = target_start + target_size;
396
    let container_end = container_start + container_size;
397
    
398
    match position {
399
        ScrollLogicalPosition::Start => {
400
            // Align target start with container start
401
            target_start - container_start
402
        }
403
        ScrollLogicalPosition::End => {
404
            // Align target end with container end
405
            target_end - container_end
406
        }
407
        ScrollLogicalPosition::Center => {
408
            // Center target in container
409
            let target_center = target_start + target_size / 2.0;
410
            let container_center = container_start + container_size / 2.0;
411
            target_center - container_center
412
        }
413
        ScrollLogicalPosition::Nearest => {
414
            // Minimum scroll to make target fully visible
415
            if target_start < container_start {
416
                // Target is above/left of visible area - scroll up/left
417
                target_start - container_start
418
            } else if target_end > container_end {
419
                // Target is below/right of visible area
420
                if target_size <= container_size {
421
                    // Target fits, align end with container end
422
                    target_end - container_end
423
                } else {
424
                    // Target doesn't fit, align start with container start
425
                    target_start - container_start
426
                }
427
            } else {
428
                // Target is already fully visible
429
                0.0
430
            }
431
        }
432
    }
433
}
434

            
435
/// Resolve scroll behavior based on options and CSS properties
436
// +spec:containing-block:03528c - scroll-behavior on root element applies to viewport
437
fn resolve_scroll_behavior(
438
    requested: ScrollIntoViewBehavior,
439
    _dom_id: DomId,
440
    _node_id: NodeId,
441
    _layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
442
) -> ScrollIntoViewBehavior {
443
    match requested {
444
        ScrollIntoViewBehavior::Auto => {
445
            // TODO: Check CSS scroll-behavior property on the scroll container
446
            // For now, default to instant
447
            ScrollIntoViewBehavior::Instant
448
        }
449
        other => other,
450
    }
451
}
452

            
453
/// Apply a scroll adjustment to the scroll manager
454
fn apply_scroll_adjustment(
455
    scroll_manager: &mut ScrollManager,
456
    dom_id: DomId,
457
    node_id: NodeId,
458
    delta: LogicalPosition,
459
    behavior: ScrollIntoViewBehavior,
460
    now: Instant,
461
) {
462
    use azul_core::events::EasingFunction;
463
    use azul_core::task::SystemTimeDiff;
464
    
465
    let current = scroll_manager
466
        .get_current_offset(dom_id, node_id)
467
        .unwrap_or_default();
468
    
469
    let new_position = LogicalPosition {
470
        x: current.x + delta.x,
471
        y: current.y + delta.y,
472
    };
473
    
474
    match behavior {
475
        ScrollIntoViewBehavior::Instant | ScrollIntoViewBehavior::Auto => {
476
            scroll_manager.set_scroll_position(dom_id, node_id, new_position, now);
477
        }
478
        ScrollIntoViewBehavior::Smooth => {
479
            // Use smooth scroll with 300ms duration
480
            let duration = Duration::System(SystemTimeDiff::from_millis(SMOOTH_SCROLL_DURATION_MS));
481
            scroll_manager.scroll_to(
482
                dom_id,
483
                node_id,
484
                new_position,
485
                duration,
486
                EasingFunction::EaseOut,
487
                now,
488
            );
489
        }
490
    }
491
}
492

            
493
/// Get a node's bounding rect from layout results
494
fn get_node_rect(
495
    node_id: DomNodeId,
496
    layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
497
) -> Option<LogicalRect> {
498
    let layout_result = layout_results.get(&node_id.dom)?;
499
    let nid = node_id.node.into_crate_internal()?;
500
    
501
    // Get position
502
    let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
503
    let layout_index = *layout_indices.first()?;
504
    let position = *layout_result.calculated_positions.get(layout_index)?;
505
    
506
    // Get size
507
    let layout_node = layout_result.layout_tree.get(layout_index)?;
508
    let size = layout_node.used_size?;
509
    
510
    Some(LogicalRect::new(position, size))
511
}