1use alloc::vec::Vec;
24
25use azul_core::{
26 dom::{DomId, DomNodeId, NodeId},
27 geom::{LogicalPosition, LogicalRect, LogicalSize},
28 task::{Duration, Instant},
29};
30
31use crate::{
32 managers::scroll_state::ScrollManager,
33 solver3::getters::{get_overflow_x, get_overflow_y},
34 window::DomLayoutResult,
35};
36
37pub use azul_core::events::{ScrollIntoViewBehavior, ScrollIntoViewOptions, ScrollLogicalPosition};
39
40const SCROLL_DELTA_THRESHOLD: f32 = 0.5;
42const SMOOTH_SCROLL_DURATION_MS: u64 = 300;
44
45#[derive(Debug, Clone)]
47pub struct ScrollAdjustment {
48 pub scroll_container_dom_id: DomId,
50 pub scroll_container_node_id: NodeId,
52 pub delta: LogicalPosition,
54 pub behavior: ScrollIntoViewBehavior,
56}
57
58#[derive(Debug, Clone)]
60struct ScrollableAncestor {
61 dom_id: DomId,
62 node_id: NodeId,
63 visible_rect: LogicalRect,
65 scroll_x: bool,
67 scroll_y: bool,
69}
70
71pub 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 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 let mut current_rect = target_rect;
118
119 for ancestor in scroll_ancestors {
120 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 if delta.x.abs() > SCROLL_DELTA_THRESHOLD || delta.y.abs() > SCROLL_DELTA_THRESHOLD {
132 let behavior = resolve_scroll_behavior(
134 options.behavior,
135 ancestor.dom_id,
136 ancestor.node_id,
137 layout_results,
138 );
139
140 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 current_rect.origin.x -= delta.x;
159 current_rect.origin.y -= delta.y;
160 }
161 }
162
163 adjustments
164}
165
166pub 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 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 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
204pub 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 let node_rect = match get_node_rect(node_id, layout_results) {
218 Some(rect) => rect,
219 None => return Vec::new(),
220 };
221
222 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 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
248fn 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 let mut current = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
272
273 while let Some(current_node_id) = current {
274 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 current = node_hierarchy.get(current_node_id).and_then(|h| h.parent_id());
286 }
287
288 ancestors
289}
290
291fn 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 !scroll_x && !scroll_y {
317 return None;
318 }
319
320 let scroll_state = scroll_manager.get_scroll_state(dom_id, node_id)?;
323
324 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 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
352fn 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
387pub 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 target_start - container_start
402 }
403 ScrollLogicalPosition::End => {
404 target_end - container_end
406 }
407 ScrollLogicalPosition::Center => {
408 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 if target_start < container_start {
416 target_start - container_start
418 } else if target_end > container_end {
419 if target_size <= container_size {
421 target_end - container_end
423 } else {
424 target_start - container_start
426 }
427 } else {
428 0.0
430 }
431 }
432 }
433}
434
435fn 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 ScrollIntoViewBehavior::Instant
448 }
449 other => other,
450 }
451}
452
453fn 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 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
493fn 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 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 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}