use alloc::vec::Vec;
use azul_core::{
dom::{DomId, DomNodeId, NodeId},
geom::{LogicalPosition, LogicalRect, LogicalSize},
styled_dom::NodeHierarchyItemId,
task::{Duration, Instant},
};
use azul_css::props::layout::LayoutOverflow;
use crate::{
managers::scroll_state::ScrollManager,
solver3::getters::{get_overflow_x, get_overflow_y, MultiValue},
window::DomLayoutResult,
};
pub use azul_core::events::{ScrollIntoViewBehavior, ScrollIntoViewOptions, ScrollLogicalPosition};
#[derive(Debug, Clone)]
pub struct ScrollAdjustment {
pub scroll_container_dom_id: DomId,
pub scroll_container_node_id: NodeId,
pub delta: LogicalPosition,
pub behavior: ScrollIntoViewBehavior,
}
#[derive(Debug, Clone)]
struct ScrollableAncestor {
dom_id: DomId,
node_id: NodeId,
visible_rect: LogicalRect,
scroll_x: bool,
scroll_y: bool,
}
pub fn scroll_rect_into_view(
target_rect: LogicalRect,
target_dom_id: DomId,
target_node_id: NodeId,
layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
scroll_manager: &mut ScrollManager,
options: ScrollIntoViewOptions,
now: Instant,
) -> Vec<ScrollAdjustment> {
let mut adjustments = Vec::new();
let scroll_ancestors = find_scrollable_ancestors(
target_dom_id,
target_node_id,
layout_results,
scroll_manager,
);
if scroll_ancestors.is_empty() {
return adjustments;
}
let mut current_rect = target_rect;
for ancestor in scroll_ancestors {
let delta = calculate_scroll_delta(
current_rect,
ancestor.visible_rect,
options.block,
options.inline_axis,
ancestor.scroll_x,
ancestor.scroll_y,
);
if delta.x.abs() > 0.5 || delta.y.abs() > 0.5 {
let behavior = resolve_scroll_behavior(
options.behavior,
ancestor.dom_id,
ancestor.node_id,
layout_results,
);
apply_scroll_adjustment(
scroll_manager,
ancestor.dom_id,
ancestor.node_id,
delta,
behavior,
now.clone(),
);
adjustments.push(ScrollAdjustment {
scroll_container_dom_id: ancestor.dom_id,
scroll_container_node_id: ancestor.node_id,
delta,
behavior,
});
current_rect.origin.x -= delta.x;
current_rect.origin.y -= delta.y;
}
}
adjustments
}
pub fn scroll_node_into_view(
node_id: DomNodeId,
layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
scroll_manager: &mut ScrollManager,
options: ScrollIntoViewOptions,
now: Instant,
) -> Vec<ScrollAdjustment> {
let target_rect = match get_node_rect(node_id, layout_results) {
Some(rect) => rect,
None => return Vec::new(),
};
let internal_node_id = match node_id.node.into_crate_internal() {
Some(nid) => nid,
None => return Vec::new(),
};
scroll_rect_into_view(
target_rect,
node_id.dom,
internal_node_id,
layout_results,
scroll_manager,
options,
now,
)
}
pub fn scroll_cursor_into_view(
cursor_rect: LogicalRect,
node_id: DomNodeId,
layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
scroll_manager: &mut ScrollManager,
options: ScrollIntoViewOptions,
now: Instant,
) -> Vec<ScrollAdjustment> {
let node_rect = match get_node_rect(node_id, layout_results) {
Some(rect) => rect,
None => return Vec::new(),
};
let absolute_cursor_rect = LogicalRect {
origin: LogicalPosition {
x: node_rect.origin.x + cursor_rect.origin.x,
y: node_rect.origin.y + cursor_rect.origin.y,
},
size: cursor_rect.size,
};
let internal_node_id = match node_id.node.into_crate_internal() {
Some(nid) => nid,
None => return Vec::new(),
};
scroll_rect_into_view(
absolute_cursor_rect,
node_id.dom,
internal_node_id,
layout_results,
scroll_manager,
options,
now,
)
}
fn find_scrollable_ancestors(
dom_id: DomId,
node_id: NodeId,
layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
scroll_manager: &ScrollManager,
) -> Vec<ScrollableAncestor> {
let mut ancestors = Vec::new();
let layout_result = match layout_results.get(&dom_id) {
Some(lr) => lr,
None => return ancestors,
};
let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
let mut current = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
while let Some(current_node_id) = current {
if let Some(ancestor) = check_if_scrollable(
dom_id,
current_node_id,
layout_result,
scroll_manager,
) {
ancestors.push(ancestor);
}
current = node_hierarchy.get(current_node_id).and_then(|h| h.parent_id());
}
ancestors
}
fn check_if_scrollable(
dom_id: DomId,
node_id: NodeId,
layout_result: &DomLayoutResult,
scroll_manager: &ScrollManager,
) -> Option<ScrollableAncestor> {
let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
let styled_node = styled_nodes.get(node_id)?;
let overflow_x = get_overflow_x(
&layout_result.styled_dom,
node_id,
&styled_node.styled_node_state,
);
let overflow_y = get_overflow_y(
&layout_result.styled_dom,
node_id,
&styled_node.styled_node_state,
);
let scroll_x = overflow_x.is_scroll();
let scroll_y = overflow_y.is_scroll();
if !scroll_x && !scroll_y {
return None;
}
let scroll_state = scroll_manager.get_scroll_state(dom_id, node_id)?;
let has_overflow_x = scroll_state.content_rect.size.width > scroll_state.container_rect.size.width;
let has_overflow_y = scroll_state.content_rect.size.height > scroll_state.container_rect.size.height;
if !has_overflow_x && !has_overflow_y {
return None;
}
let visible_rect = LogicalRect {
origin: LogicalPosition {
x: scroll_state.container_rect.origin.x + scroll_state.current_offset.x,
y: scroll_state.container_rect.origin.y + scroll_state.current_offset.y,
},
size: scroll_state.container_rect.size,
};
Some(ScrollableAncestor {
dom_id,
node_id,
visible_rect,
scroll_x: scroll_x && has_overflow_x,
scroll_y: scroll_y && has_overflow_y,
})
}
fn calculate_scroll_delta(
target: LogicalRect,
container: LogicalRect,
block: ScrollLogicalPosition,
inline: ScrollLogicalPosition,
scroll_x_enabled: bool,
scroll_y_enabled: bool,
) -> LogicalPosition {
LogicalPosition {
x: if scroll_x_enabled {
calculate_axis_delta(
target.origin.x,
target.size.width,
container.origin.x,
container.size.width,
inline,
)
} else {
0.0
},
y: if scroll_y_enabled {
calculate_axis_delta(
target.origin.y,
target.size.height,
container.origin.y,
container.size.height,
block,
)
} else {
0.0
},
}
}
fn calculate_axis_delta(
target_start: f32,
target_size: f32,
container_start: f32,
container_size: f32,
position: ScrollLogicalPosition,
) -> f32 {
let target_end = target_start + target_size;
let container_end = container_start + container_size;
match position {
ScrollLogicalPosition::Start => {
target_start - container_start
}
ScrollLogicalPosition::End => {
target_end - container_end
}
ScrollLogicalPosition::Center => {
let target_center = target_start + target_size / 2.0;
let container_center = container_start + container_size / 2.0;
target_center - container_center
}
ScrollLogicalPosition::Nearest => {
if target_start < container_start {
target_start - container_start
} else if target_end > container_end {
if target_size <= container_size {
target_end - container_end
} else {
target_start - container_start
}
} else {
0.0
}
}
}
}
fn resolve_scroll_behavior(
requested: ScrollIntoViewBehavior,
_dom_id: DomId,
_node_id: NodeId,
_layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
) -> ScrollIntoViewBehavior {
match requested {
ScrollIntoViewBehavior::Auto => {
ScrollIntoViewBehavior::Instant
}
other => other,
}
}
fn apply_scroll_adjustment(
scroll_manager: &mut ScrollManager,
dom_id: DomId,
node_id: NodeId,
delta: LogicalPosition,
behavior: ScrollIntoViewBehavior,
now: Instant,
) {
use azul_core::events::EasingFunction;
use azul_core::task::SystemTimeDiff;
let current = scroll_manager
.get_current_offset(dom_id, node_id)
.unwrap_or_default();
let new_position = LogicalPosition {
x: current.x + delta.x,
y: current.y + delta.y,
};
match behavior {
ScrollIntoViewBehavior::Instant | ScrollIntoViewBehavior::Auto => {
scroll_manager.set_scroll_position(dom_id, node_id, new_position, now);
}
ScrollIntoViewBehavior::Smooth => {
let duration = Duration::System(SystemTimeDiff::from_millis(300));
scroll_manager.scroll_to(
dom_id,
node_id,
new_position,
duration,
EasingFunction::EaseOut,
now,
);
}
}
}
fn get_node_rect(
node_id: DomNodeId,
layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
) -> Option<LogicalRect> {
let layout_result = layout_results.get(&node_id.dom)?;
let nid = node_id.node.into_crate_internal()?;
let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
let layout_index = *layout_indices.first()?;
let position = *layout_result.calculated_positions.get(layout_index)?;
let layout_node = layout_result.layout_tree.get(layout_index)?;
let size = layout_node.used_size?;
Some(LogicalRect::new(position, size))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_axis_delta_nearest_visible() {
let delta = calculate_axis_delta(
100.0, 50.0, 50.0, 200.0, ScrollLogicalPosition::Nearest,
);
assert_eq!(delta, 0.0);
}
#[test]
fn test_calculate_axis_delta_nearest_above() {
let delta = calculate_axis_delta(
20.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::Nearest,
);
assert_eq!(delta, -80.0); }
#[test]
fn test_calculate_axis_delta_nearest_below() {
let delta = calculate_axis_delta(
280.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::Nearest,
);
assert_eq!(delta, 30.0); }
#[test]
fn test_calculate_axis_delta_center() {
let delta = calculate_axis_delta(
50.0, 20.0, 100.0, 200.0, ScrollLogicalPosition::Center,
);
assert_eq!(delta, -140.0); }
#[test]
fn test_calculate_axis_delta_start() {
let delta = calculate_axis_delta(
150.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::Start,
);
assert_eq!(delta, 50.0); }
#[test]
fn test_calculate_axis_delta_end() {
let delta = calculate_axis_delta(
150.0, 50.0, 100.0, 200.0, ScrollLogicalPosition::End,
);
assert_eq!(delta, -100.0); }
}