1use alloc::vec::Vec;
24
25use azul_core::{
26 dom::{DomId, DomNodeId, NodeId},
27 geom::{LogicalPosition, LogicalRect, LogicalSize},
28 styled_dom::NodeHierarchyItemId,
29 task::{Duration, Instant},
30};
31use azul_css::props::layout::LayoutOverflow;
32
33use crate::{
34 managers::scroll_state::ScrollManager,
35 solver3::getters::{get_overflow_x, get_overflow_y, MultiValue},
36 window::DomLayoutResult,
37};
38
39pub use azul_core::events::{ScrollIntoViewBehavior, ScrollIntoViewOptions, ScrollLogicalPosition};
41
42#[derive(Debug, Clone)]
44pub struct ScrollAdjustment {
45 pub scroll_container_dom_id: DomId,
47 pub scroll_container_node_id: NodeId,
48 pub delta: LogicalPosition,
50 pub behavior: ScrollIntoViewBehavior,
52}
53
54#[derive(Debug, Clone)]
56struct ScrollableAncestor {
57 dom_id: DomId,
58 node_id: NodeId,
59 visible_rect: LogicalRect,
61 scroll_x: bool,
63 scroll_y: bool,
65}
66
67pub fn scroll_rect_into_view(
90 target_rect: LogicalRect,
91 target_dom_id: DomId,
92 target_node_id: NodeId,
93 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
94 scroll_manager: &mut ScrollManager,
95 options: ScrollIntoViewOptions,
96 now: Instant,
97) -> Vec<ScrollAdjustment> {
98 let mut adjustments = Vec::new();
99
100 let scroll_ancestors = find_scrollable_ancestors(
102 target_dom_id,
103 target_node_id,
104 layout_results,
105 scroll_manager,
106 );
107
108 if scroll_ancestors.is_empty() {
109 return adjustments;
110 }
111
112 let mut current_rect = target_rect;
114
115 for ancestor in scroll_ancestors {
116 let delta = calculate_scroll_delta(
118 current_rect,
119 ancestor.visible_rect,
120 options.block,
121 options.inline_axis,
122 ancestor.scroll_x,
123 ancestor.scroll_y,
124 );
125
126 if delta.x.abs() > 0.5 || delta.y.abs() > 0.5 {
128 let behavior = resolve_scroll_behavior(
130 options.behavior,
131 ancestor.dom_id,
132 ancestor.node_id,
133 layout_results,
134 );
135
136 apply_scroll_adjustment(
138 scroll_manager,
139 ancestor.dom_id,
140 ancestor.node_id,
141 delta,
142 behavior,
143 now.clone(),
144 );
145
146 adjustments.push(ScrollAdjustment {
147 scroll_container_dom_id: ancestor.dom_id,
148 scroll_container_node_id: ancestor.node_id,
149 delta,
150 behavior,
151 });
152
153 current_rect.origin.x -= delta.x;
155 current_rect.origin.y -= delta.y;
156 }
157 }
158
159 adjustments
160}
161
162pub fn scroll_node_into_view(
171 node_id: DomNodeId,
172 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
173 scroll_manager: &mut ScrollManager,
174 options: ScrollIntoViewOptions,
175 now: Instant,
176) -> Vec<ScrollAdjustment> {
177 let target_rect = match get_node_rect(node_id, layout_results) {
179 Some(rect) => rect,
180 None => return Vec::new(),
181 };
182
183 let internal_node_id = match node_id.node.into_crate_internal() {
184 Some(nid) => nid,
185 None => return Vec::new(),
186 };
187
188 scroll_rect_into_view(
190 target_rect,
191 node_id.dom,
192 internal_node_id,
193 layout_results,
194 scroll_manager,
195 options,
196 now,
197 )
198}
199
200pub fn scroll_cursor_into_view(
214 cursor_rect: LogicalRect,
215 node_id: DomNodeId,
216 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
217 scroll_manager: &mut ScrollManager,
218 options: ScrollIntoViewOptions,
219 now: Instant,
220) -> Vec<ScrollAdjustment> {
221 let node_rect = match get_node_rect(node_id, layout_results) {
223 Some(rect) => rect,
224 None => return Vec::new(),
225 };
226
227 let absolute_cursor_rect = LogicalRect {
229 origin: LogicalPosition {
230 x: node_rect.origin.x + cursor_rect.origin.x,
231 y: node_rect.origin.y + cursor_rect.origin.y,
232 },
233 size: cursor_rect.size,
234 };
235
236 let internal_node_id = match node_id.node.into_crate_internal() {
237 Some(nid) => nid,
238 None => return Vec::new(),
239 };
240
241 scroll_rect_into_view(
243 absolute_cursor_rect,
244 node_id.dom,
245 internal_node_id,
246 layout_results,
247 scroll_manager,
248 options,
249 now,
250 )
251}
252
253fn find_scrollable_ancestors(
261 dom_id: DomId,
262 node_id: NodeId,
263 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
264 scroll_manager: &ScrollManager,
265) -> Vec<ScrollableAncestor> {
266 let mut ancestors = Vec::new();
267
268 let layout_result = match layout_results.get(&dom_id) {
269 Some(lr) => lr,
270 None => return ancestors,
271 };
272
273 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
274 let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
275
276 let mut current = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
278
279 while let Some(current_node_id) = current {
280 if let Some(ancestor) = check_if_scrollable(
282 dom_id,
283 current_node_id,
284 layout_result,
285 scroll_manager,
286 ) {
287 ancestors.push(ancestor);
288 }
289
290 current = node_hierarchy.get(current_node_id).and_then(|h| h.parent_id());
292 }
293
294 ancestors
295}
296
297fn check_if_scrollable(
299 dom_id: DomId,
300 node_id: NodeId,
301 layout_result: &DomLayoutResult,
302 scroll_manager: &ScrollManager,
303) -> Option<ScrollableAncestor> {
304 let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
305 let styled_node = styled_nodes.get(node_id)?;
306
307 let overflow_x = get_overflow_x(
308 &layout_result.styled_dom,
309 node_id,
310 &styled_node.styled_node_state,
311 );
312 let overflow_y = get_overflow_y(
313 &layout_result.styled_dom,
314 node_id,
315 &styled_node.styled_node_state,
316 );
317
318 let scroll_x = overflow_x.is_scroll();
319 let scroll_y = overflow_y.is_scroll();
320
321 if !scroll_x && !scroll_y {
323 return None;
324 }
325
326 let scroll_state = scroll_manager.get_scroll_state(dom_id, node_id)?;
329
330 let has_overflow_x = scroll_state.content_rect.size.width > scroll_state.container_rect.size.width;
332 let has_overflow_y = scroll_state.content_rect.size.height > scroll_state.container_rect.size.height;
333
334 if !has_overflow_x && !has_overflow_y {
335 return None;
336 }
337
338 let visible_rect = LogicalRect {
340 origin: LogicalPosition {
341 x: scroll_state.container_rect.origin.x + scroll_state.current_offset.x,
342 y: scroll_state.container_rect.origin.y + scroll_state.current_offset.y,
343 },
344 size: scroll_state.container_rect.size,
345 };
346
347 Some(ScrollableAncestor {
348 dom_id,
349 node_id,
350 visible_rect,
351 scroll_x: scroll_x && has_overflow_x,
352 scroll_y: scroll_y && has_overflow_y,
353 })
354}
355
356fn calculate_scroll_delta(
358 target: LogicalRect,
359 container: LogicalRect,
360 block: ScrollLogicalPosition,
361 inline: ScrollLogicalPosition,
362 scroll_x_enabled: bool,
363 scroll_y_enabled: bool,
364) -> LogicalPosition {
365 LogicalPosition {
366 x: if scroll_x_enabled {
367 calculate_axis_delta(
368 target.origin.x,
369 target.size.width,
370 container.origin.x,
371 container.size.width,
372 inline,
373 )
374 } else {
375 0.0
376 },
377 y: if scroll_y_enabled {
378 calculate_axis_delta(
379 target.origin.y,
380 target.size.height,
381 container.origin.y,
382 container.size.height,
383 block,
384 )
385 } else {
386 0.0
387 },
388 }
389}
390
391fn calculate_axis_delta(
393 target_start: f32,
394 target_size: f32,
395 container_start: f32,
396 container_size: f32,
397 position: ScrollLogicalPosition,
398) -> f32 {
399 let target_end = target_start + target_size;
400 let container_end = container_start + container_size;
401
402 match position {
403 ScrollLogicalPosition::Start => {
404 target_start - container_start
406 }
407 ScrollLogicalPosition::End => {
408 target_end - container_end
410 }
411 ScrollLogicalPosition::Center => {
412 let target_center = target_start + target_size / 2.0;
414 let container_center = container_start + container_size / 2.0;
415 target_center - container_center
416 }
417 ScrollLogicalPosition::Nearest => {
418 if target_start < container_start {
420 target_start - container_start
422 } else if target_end > container_end {
423 if target_size <= container_size {
425 target_end - container_end
427 } else {
428 target_start - container_start
430 }
431 } else {
432 0.0
434 }
435 }
436 }
437}
438
439fn resolve_scroll_behavior(
441 requested: ScrollIntoViewBehavior,
442 _dom_id: DomId,
443 _node_id: NodeId,
444 _layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
445) -> ScrollIntoViewBehavior {
446 match requested {
447 ScrollIntoViewBehavior::Auto => {
448 ScrollIntoViewBehavior::Instant
451 }
452 other => other,
453 }
454}
455
456fn apply_scroll_adjustment(
458 scroll_manager: &mut ScrollManager,
459 dom_id: DomId,
460 node_id: NodeId,
461 delta: LogicalPosition,
462 behavior: ScrollIntoViewBehavior,
463 now: Instant,
464) {
465 use azul_core::events::EasingFunction;
466 use azul_core::task::SystemTimeDiff;
467
468 let current = scroll_manager
469 .get_current_offset(dom_id, node_id)
470 .unwrap_or_default();
471
472 let new_position = LogicalPosition {
473 x: current.x + delta.x,
474 y: current.y + delta.y,
475 };
476
477 match behavior {
478 ScrollIntoViewBehavior::Instant | ScrollIntoViewBehavior::Auto => {
479 scroll_manager.set_scroll_position(dom_id, node_id, new_position, now);
480 }
481 ScrollIntoViewBehavior::Smooth => {
482 let duration = Duration::System(SystemTimeDiff::from_millis(300));
484 scroll_manager.scroll_to(
485 dom_id,
486 node_id,
487 new_position,
488 duration,
489 EasingFunction::EaseOut,
490 now,
491 );
492 }
493 }
494}
495
496fn get_node_rect(
498 node_id: DomNodeId,
499 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
500) -> Option<LogicalRect> {
501 let layout_result = layout_results.get(&node_id.dom)?;
502 let nid = node_id.node.into_crate_internal()?;
503
504 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
506 let layout_index = *layout_indices.first()?;
507 let position = *layout_result.calculated_positions.get(&layout_index)?;
508
509 let layout_node = layout_result.layout_tree.get(layout_index)?;
511 let size = layout_node.used_size?;
512
513 Some(LogicalRect::new(position, size))
514}
515
516#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_calculate_axis_delta_nearest_visible() {
526 let delta = calculate_axis_delta(
528 100.0, 50.0, 50.0, 200.0, ScrollLogicalPosition::Nearest,
533 );
534 assert_eq!(delta, 0.0);
535 }
536
537 #[test]
538 fn test_calculate_axis_delta_nearest_above() {
539 let delta = calculate_axis_delta(
541 20.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::Nearest,
546 );
547 assert_eq!(delta, -80.0); }
549
550 #[test]
551 fn test_calculate_axis_delta_nearest_below() {
552 let delta = calculate_axis_delta(
554 280.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::Nearest,
559 );
560 assert_eq!(delta, 30.0); }
562
563 #[test]
564 fn test_calculate_axis_delta_center() {
565 let delta = calculate_axis_delta(
567 50.0, 20.0, 100.0, 200.0, ScrollLogicalPosition::Center,
572 );
573 assert_eq!(delta, -140.0); }
575
576 #[test]
577 fn test_calculate_axis_delta_start() {
578 let delta = calculate_axis_delta(
579 150.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::Start,
584 );
585 assert_eq!(delta, 50.0); }
587
588 #[test]
589 fn test_calculate_axis_delta_end() {
590 let delta = calculate_axis_delta(
591 150.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::End,
596 );
597 assert_eq!(delta, -100.0); }
599}