use alloc::collections::BTreeMap;
#[cfg(feature = "std")]
use alloc::vec::Vec;
use azul_core::{
dom::{DomId, NodeId, ScrollbarOrientation},
events::EasingFunction,
geom::{LogicalPosition, LogicalRect, LogicalSize},
hit_test::{ExternalScrollId, ScrollPosition},
styled_dom::NodeHierarchyItemId,
task::{Duration, Instant},
};
#[cfg(feature = "std")]
use std::sync::{Arc, Mutex};
use crate::managers::hover::InputPointId;
use crate::solver3::scrollbar::compute_scrollbar_geometry_with_button_size;
const SCROLL_CHANGE_EPSILON: f32 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScrollInputSource {
TrackpadContinuous,
TrackpadEnd,
WheelDiscrete,
Programmatic,
}
#[derive(Debug, Clone)]
pub struct ScrollInput {
pub dom_id: DomId,
pub node_id: NodeId,
pub delta: LogicalPosition,
pub timestamp: Instant,
pub source: ScrollInputSource,
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, Default)]
pub struct ScrollInputQueue {
inner: Arc<Mutex<Vec<ScrollInput>>>,
}
#[cfg(feature = "std")]
impl ScrollInputQueue {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn push(&self, input: ScrollInput) {
if let Ok(mut queue) = self.inner.lock() {
queue.push(input);
}
}
pub fn take_all(&self) -> Vec<ScrollInput> {
if let Ok(mut queue) = self.inner.lock() {
core::mem::take(&mut *queue)
} else {
Vec::new()
}
}
pub fn take_recent(&self, max_events: usize) -> Vec<ScrollInput> {
if let Ok(mut queue) = self.inner.lock() {
let mut events = core::mem::take(&mut *queue);
if events.len() > max_events {
events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
events.drain(..events.len() - max_events);
}
events
} else {
Vec::new()
}
}
pub fn has_pending(&self) -> bool {
self.inner
.lock()
.map(|q| !q.is_empty())
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScrollbarComponent {
Track,
Thumb,
TopButton,
BottomButton,
}
#[derive(Debug, Clone)]
pub struct ScrollbarState {
pub visible: bool,
pub orientation: ScrollbarOrientation,
pub base_size: f32,
pub scale: LogicalPosition, pub thumb_position_ratio: f32,
pub thumb_size_ratio: f32,
pub track_rect: LogicalRect,
pub button_size: f32,
pub usable_track_length: f32,
pub thumb_length: f32,
pub thumb_offset: f32,
}
impl ScrollbarState {
pub fn hit_test_component(&self, local_pos: LogicalPosition) -> ScrollbarComponent {
match self.orientation {
ScrollbarOrientation::Vertical => {
if local_pos.y < self.button_size {
return ScrollbarComponent::TopButton;
}
let track_height = self.track_rect.size.height;
if local_pos.y > track_height - self.button_size {
return ScrollbarComponent::BottomButton;
}
let thumb_y_start = self.button_size + self.thumb_offset;
let thumb_y_end = thumb_y_start + self.thumb_length;
if local_pos.y >= thumb_y_start && local_pos.y <= thumb_y_end {
ScrollbarComponent::Thumb
} else {
ScrollbarComponent::Track
}
}
ScrollbarOrientation::Horizontal => {
if local_pos.x < self.button_size {
return ScrollbarComponent::TopButton;
}
let track_width = self.track_rect.size.width;
if local_pos.x > track_width - self.button_size {
return ScrollbarComponent::BottomButton;
}
let thumb_x_start = self.button_size + self.thumb_offset;
let thumb_x_end = thumb_x_start + self.thumb_length;
if local_pos.x >= thumb_x_start && local_pos.x <= thumb_x_end {
ScrollbarComponent::Thumb
} else {
ScrollbarComponent::Track
}
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarHit {
pub dom_id: DomId,
pub node_id: NodeId,
pub orientation: ScrollbarOrientation,
pub component: ScrollbarComponent,
pub local_position: LogicalPosition,
pub global_position: LogicalPosition,
}
#[derive(Debug, Clone, Default)]
pub struct ScrollManager {
states: BTreeMap<(DomId, NodeId), AnimatedScrollState>,
external_scroll_ids: BTreeMap<(DomId, NodeId), ExternalScrollId>,
next_external_scroll_id: u64,
scrollbar_states: BTreeMap<(DomId, NodeId, ScrollbarOrientation), ScrollbarState>,
#[cfg(feature = "std")]
pub scroll_input_queue: ScrollInputQueue,
scroll_dirty: bool,
}
#[derive(Debug, Clone)]
pub struct AnimatedScrollState {
pub current_offset: LogicalPosition,
pub animation: Option<ScrollAnimation>,
pub last_activity: Instant,
pub container_rect: LogicalRect,
pub content_rect: LogicalRect,
pub virtual_scroll_size: Option<LogicalSize>,
pub virtual_scroll_offset: Option<LogicalPosition>,
pub overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior,
pub overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior,
pub overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling,
pub scrollbar_thickness: f32,
pub visual_width_px: f32,
pub has_horizontal_scrollbar: bool,
pub has_vertical_scrollbar: bool,
}
#[derive(Debug, Clone)]
struct ScrollAnimation {
start_time: Instant,
duration: Duration,
start_offset: LogicalPosition,
target_offset: LogicalPosition,
easing: EasingFunction,
}
#[derive(Debug, Clone)]
pub struct ScrollNodeInfo {
pub current_offset: LogicalPosition,
pub container_rect: LogicalRect,
pub content_rect: LogicalRect,
pub max_scroll_x: f32,
pub max_scroll_y: f32,
pub overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior,
pub overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior,
pub overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling,
}
#[derive(Debug, Default)]
pub struct ScrollTickResult {
pub needs_repaint: bool,
pub updated_nodes: Vec<(DomId, NodeId)>,
}
impl ScrollManager {
pub fn new() -> Self {
Self::default()
}
pub fn debug_counts(&self) -> (usize, usize, usize) {
(
self.states.len(),
self.external_scroll_ids.len(),
self.scrollbar_states.len(),
)
}
pub fn has_pending_scroll_changes(&self) -> bool {
self.scroll_dirty
}
pub fn clear_scroll_dirty(&mut self) {
self.scroll_dirty = false;
}
pub fn build_scroll_offset_map(
&self,
dom_id: DomId,
scroll_ids: &std::collections::HashMap<usize, u64>,
) -> std::collections::HashMap<u64, (f32, f32)> {
let mut map = std::collections::HashMap::new();
for ((d, node_id), state) in &self.states {
if *d != dom_id { continue; }
let node_idx = node_id.index();
if let Some(&scroll_id) = scroll_ids.get(&node_idx) {
map.insert(scroll_id, (state.current_offset.x, state.current_offset.y));
}
}
map
}
#[cfg(feature = "std")]
pub fn record_scroll_input(&mut self, input: ScrollInput) -> bool {
let was_empty = !self.scroll_input_queue.has_pending();
self.scroll_input_queue.push(input);
was_empty }
#[cfg(feature = "std")]
pub fn record_scroll_from_hit_test(
&mut self,
delta_x: f32,
delta_y: f32,
source: ScrollInputSource,
hover_manager: &crate::managers::hover::HoverManager,
input_point_id: &InputPointId,
now: Instant,
) -> Option<(DomId, NodeId, bool)> {
let hit_test = hover_manager.get_current(input_point_id)?;
for (dom_id, hit_node) in &hit_test.hovered_nodes {
for (node_id, _scroll_item) in &hit_node.scroll_hit_test_nodes {
let scrollable = self.is_node_scrollable(*dom_id, *node_id);
if !scrollable {
continue;
}
let input = ScrollInput {
dom_id: *dom_id,
node_id: *node_id,
delta: LogicalPosition { x: delta_x, y: delta_y },
timestamp: now,
source,
};
let should_start_timer = self.record_scroll_input(input);
return Some((*dom_id, *node_id, should_start_timer));
}
}
None
}
#[cfg(feature = "std")]
pub fn get_input_queue(&self) -> ScrollInputQueue {
self.scroll_input_queue.clone()
}
pub fn tick(&mut self, now: Instant) -> ScrollTickResult {
let mut result = ScrollTickResult::default();
for ((dom_id, node_id), state) in self.states.iter_mut() {
if let Some(anim) = &state.animation {
let elapsed = now.duration_since(&anim.start_time);
let t = elapsed.div(&anim.duration).min(1.0);
let eased_t = apply_easing(t, anim.easing);
state.current_offset = LogicalPosition {
x: anim.start_offset.x + (anim.target_offset.x - anim.start_offset.x) * eased_t,
y: anim.start_offset.y + (anim.target_offset.y - anim.start_offset.y) * eased_t,
};
result.needs_repaint = true;
result.updated_nodes.push((*dom_id, *node_id));
if t >= 1.0 {
state.animation = None;
}
}
}
result
}
pub fn has_active_animations(&self) -> bool {
self.states.values().any(|s| s.animation.is_some())
}
pub fn find_scroll_parent(
&self,
dom_id: DomId,
node_id: NodeId,
node_hierarchy: &[azul_core::styled_dom::NodeHierarchyItem],
) -> Option<NodeId> {
let mut current = Some(node_id);
while let Some(nid) = current {
if self.states.contains_key(&(dom_id, nid)) && nid != node_id {
return Some(nid);
}
current = node_hierarchy
.get(nid.index())
.and_then(|item| item.parent_id());
}
None
}
fn is_node_scrollable(&self, dom_id: DomId, node_id: NodeId) -> bool {
let result = self.states.get(&(dom_id, node_id)).map_or(false, |state| {
let effective_width = state.virtual_scroll_size
.map(|s| s.width)
.unwrap_or(state.content_rect.size.width);
let effective_height = state.virtual_scroll_size
.map(|s| s.height)
.unwrap_or(state.content_rect.size.height);
let has_horizontal = effective_width > state.container_rect.size.width;
let has_vertical = effective_height > state.container_rect.size.height;
has_horizontal || has_vertical
});
result
}
pub fn set_scroll_position(
&mut self,
dom_id: DomId,
node_id: NodeId,
position: LogicalPosition,
now: Instant,
) {
let state = self
.states
.entry((dom_id, node_id))
.or_insert_with(|| AnimatedScrollState::new(now.clone()));
let clamped = state.clamp(position);
if (clamped.x - state.current_offset.x).abs() > SCROLL_CHANGE_EPSILON
|| (clamped.y - state.current_offset.y).abs() > SCROLL_CHANGE_EPSILON
{
self.scroll_dirty = true;
}
state.current_offset = clamped;
state.animation = None;
state.last_activity = now;
}
pub fn set_scroll_position_unclamped(
&mut self,
dom_id: DomId,
node_id: NodeId,
position: LogicalPosition,
now: Instant,
) {
let state = self
.states
.entry((dom_id, node_id))
.or_insert_with(|| AnimatedScrollState::new(now.clone()));
if (position.x - state.current_offset.x).abs() > SCROLL_CHANGE_EPSILON
|| (position.y - state.current_offset.y).abs() > SCROLL_CHANGE_EPSILON
{
self.scroll_dirty = true;
}
state.current_offset = position;
state.animation = None;
state.last_activity = now;
}
pub fn scroll_by(
&mut self,
dom_id: DomId,
node_id: NodeId,
delta: LogicalPosition,
duration: Duration,
easing: EasingFunction,
now: Instant,
) {
let current = self.get_current_offset(dom_id, node_id).unwrap_or_default();
let target = LogicalPosition {
x: current.x + delta.x,
y: current.y + delta.y,
};
self.scroll_to(dom_id, node_id, target, duration, easing, now);
}
pub fn scroll_to(
&mut self,
dom_id: DomId,
node_id: NodeId,
target: LogicalPosition,
duration: Duration,
easing: EasingFunction,
now: Instant,
) {
let is_zero = match &duration {
Duration::System(s) => s.secs == 0 && s.nanos == 0,
Duration::Tick(t) => t.tick_diff == 0,
};
if is_zero {
self.set_scroll_position(dom_id, node_id, target, now);
return;
}
let state = self
.states
.entry((dom_id, node_id))
.or_insert_with(|| AnimatedScrollState::new(now.clone()));
let clamped_target = state.clamp(target);
state.animation = Some(ScrollAnimation {
start_time: now.clone(),
duration,
start_offset: state.current_offset,
target_offset: clamped_target,
easing,
});
state.last_activity = now;
}
pub fn update_node_bounds(
&mut self,
dom_id: DomId,
node_id: NodeId,
container_rect: LogicalRect,
content_rect: LogicalRect,
now: Instant,
) {
let state = self
.states
.entry((dom_id, node_id))
.or_insert_with(|| AnimatedScrollState::new(now));
state.container_rect = container_rect;
state.content_rect = content_rect;
state.current_offset = state.clamp(state.current_offset);
}
pub fn update_virtual_scroll_bounds(
&mut self,
dom_id: DomId,
node_id: NodeId,
virtual_scroll_size: LogicalSize,
virtual_scroll_offset: Option<LogicalPosition>,
) {
let key = (dom_id, node_id);
let state = self.states.entry(key).or_insert_with(|| {
AnimatedScrollState::new(azul_core::task::Instant::now())
});
state.virtual_scroll_size = Some(virtual_scroll_size);
state.virtual_scroll_offset = virtual_scroll_offset;
state.current_offset = state.clamp(state.current_offset);
}
pub fn get_current_offset(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalPosition> {
self.states
.get(&(dom_id, node_id))
.map(|s| s.current_offset)
}
pub fn get_last_activity_time(&self, dom_id: DomId, node_id: NodeId) -> Option<Instant> {
self.states
.get(&(dom_id, node_id))
.map(|s| s.last_activity.clone())
}
pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
self.states.get(&(dom_id, node_id))
}
pub fn get_scroll_node_info(
&self,
dom_id: DomId,
node_id: NodeId,
) -> Option<ScrollNodeInfo> {
let state = self.states.get(&(dom_id, node_id))?;
let effective_content_width = state.virtual_scroll_size
.map(|s| s.width)
.unwrap_or(state.content_rect.size.width);
let effective_content_height = state.virtual_scroll_size
.map(|s| s.height)
.unwrap_or(state.content_rect.size.height);
let max_x = (effective_content_width - state.container_rect.size.width).max(0.0);
let max_y = (effective_content_height - state.container_rect.size.height).max(0.0);
Some(ScrollNodeInfo {
current_offset: state.current_offset,
container_rect: state.container_rect,
content_rect: state.content_rect,
max_scroll_x: max_x,
max_scroll_y: max_y,
overscroll_behavior_x: state.overscroll_behavior_x,
overscroll_behavior_y: state.overscroll_behavior_y,
overflow_scrolling: state.overflow_scrolling,
})
}
pub fn get_scroll_states_for_dom(&self, dom_id: DomId) -> BTreeMap<NodeId, ScrollPosition> {
if self.states.is_empty() {
return BTreeMap::new();
}
self.states
.iter()
.filter(|((d, _), _)| *d == dom_id)
.map(|((_, node_id), state)| {
let effective_content_size = state.virtual_scroll_size
.unwrap_or(state.content_rect.size);
(
*node_id,
ScrollPosition {
parent_rect: state.container_rect,
children_rect: LogicalRect::new(
state.current_offset,
effective_content_size,
),
},
)
})
.collect()
}
pub fn register_or_update_scroll_node(
&mut self,
dom_id: DomId,
node_id: NodeId,
container_rect: LogicalRect,
content_size: LogicalSize,
now: Instant,
scrollbar_thickness: f32,
visual_width_px: f32,
has_horizontal_scrollbar: bool,
has_vertical_scrollbar: bool,
) {
let key = (dom_id, node_id);
let content_rect = LogicalRect {
origin: LogicalPosition::zero(),
size: content_size,
};
if let Some(existing) = self.states.get_mut(&key) {
existing.container_rect = container_rect;
existing.content_rect = content_rect;
existing.scrollbar_thickness = scrollbar_thickness;
existing.visual_width_px = visual_width_px;
existing.has_horizontal_scrollbar = has_horizontal_scrollbar;
existing.has_vertical_scrollbar = has_vertical_scrollbar;
existing.current_offset = existing.clamp(existing.current_offset);
} else {
self.states.insert(
key,
AnimatedScrollState {
current_offset: LogicalPosition::zero(),
animation: None,
last_activity: now,
container_rect,
content_rect,
virtual_scroll_size: None,
virtual_scroll_offset: None,
overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling::Auto,
scrollbar_thickness,
visual_width_px,
has_horizontal_scrollbar,
has_vertical_scrollbar,
},
);
}
}
pub fn register_scroll_node(&mut self, dom_id: DomId, node_id: NodeId) -> ExternalScrollId {
use azul_core::hit_test::PipelineId;
let key = (dom_id, node_id);
if let Some(&existing_id) = self.external_scroll_ids.get(&key) {
return existing_id;
}
let pipeline_id = PipelineId(
dom_id.inner as u32, node_id.index() as u32,
);
let new_id = ExternalScrollId(self.next_external_scroll_id, pipeline_id);
self.next_external_scroll_id += 1;
self.external_scroll_ids.insert(key, new_id);
new_id
}
pub fn get_external_scroll_id(
&self,
dom_id: DomId,
node_id: NodeId,
) -> Option<ExternalScrollId> {
self.external_scroll_ids.get(&(dom_id, node_id)).copied()
}
pub fn iter_external_scroll_ids(
&self,
) -> impl Iterator<Item = ((DomId, NodeId), ExternalScrollId)> + '_ {
self.external_scroll_ids.iter().map(|(k, v)| (*k, *v))
}
pub fn calculate_scrollbar_states(&mut self) {
self.scrollbar_states.clear();
let vertical_states: Vec<_> = self
.states
.iter()
.filter(|(_, s)| {
let effective_height = s.virtual_scroll_size
.map(|vs| vs.height)
.unwrap_or(s.content_rect.size.height);
effective_height > s.container_rect.size.height
})
.map(|((dom_id, node_id), scroll_state)| {
let v_state = Self::calculate_scrollbar_state_from_geometry(
scroll_state,
ScrollbarOrientation::Vertical,
);
((*dom_id, *node_id, ScrollbarOrientation::Vertical), v_state)
})
.collect();
let horizontal_states: Vec<_> = self
.states
.iter()
.filter(|(_, s)| {
let effective_width = s.virtual_scroll_size
.map(|vs| vs.width)
.unwrap_or(s.content_rect.size.width);
effective_width > s.container_rect.size.width
})
.map(|((dom_id, node_id), scroll_state)| {
let h_state = Self::calculate_scrollbar_state_from_geometry(
scroll_state,
ScrollbarOrientation::Horizontal,
);
(
(*dom_id, *node_id, ScrollbarOrientation::Horizontal),
h_state,
)
})
.collect();
self.scrollbar_states.extend(vertical_states);
self.scrollbar_states.extend(horizontal_states);
}
fn calculate_scrollbar_state_from_geometry(
scroll_state: &AnimatedScrollState,
orientation: ScrollbarOrientation,
) -> ScrollbarState {
let scrollbar_thickness = if scroll_state.visual_width_px > 0.0 {
scroll_state.visual_width_px
} else if scroll_state.scrollbar_thickness > 0.0 {
scroll_state.scrollbar_thickness
} else {
crate::solver3::fc::DEFAULT_SCROLLBAR_WIDTH_PX
};
let content_size = scroll_state.virtual_scroll_size
.map(|vs| LogicalSize { width: vs.width, height: vs.height })
.unwrap_or(scroll_state.content_rect.size);
let scroll_offset = match orientation {
ScrollbarOrientation::Vertical => scroll_state.current_offset.y,
ScrollbarOrientation::Horizontal => scroll_state.current_offset.x,
};
let has_other_scrollbar = match orientation {
ScrollbarOrientation::Vertical => scroll_state.has_horizontal_scrollbar,
ScrollbarOrientation::Horizontal => scroll_state.has_vertical_scrollbar,
};
let is_overlay = scroll_state.scrollbar_thickness == 0.0;
let button_size = if is_overlay { 0.0 } else { scrollbar_thickness };
let geom = compute_scrollbar_geometry_with_button_size(
orientation,
scroll_state.container_rect,
content_size,
scroll_offset,
scrollbar_thickness,
has_other_scrollbar,
button_size,
);
let scale = match orientation {
ScrollbarOrientation::Vertical => {
LogicalPosition::new(1.0, geom.track_rect.size.height / scrollbar_thickness)
}
ScrollbarOrientation::Horizontal => {
LogicalPosition::new(geom.track_rect.size.width / scrollbar_thickness, 1.0)
}
};
ScrollbarState {
visible: true,
orientation,
base_size: scrollbar_thickness,
scale,
thumb_position_ratio: geom.scroll_ratio,
thumb_size_ratio: geom.thumb_size_ratio,
track_rect: geom.track_rect,
button_size: geom.button_size,
usable_track_length: geom.usable_track_length,
thumb_length: geom.thumb_length,
thumb_offset: geom.thumb_offset,
}
}
pub fn get_scrollbar_state(
&self,
dom_id: DomId,
node_id: NodeId,
orientation: ScrollbarOrientation,
) -> Option<&ScrollbarState> {
self.scrollbar_states.get(&(dom_id, node_id, orientation))
}
pub fn iter_scrollbar_states(
&self,
) -> impl Iterator<Item = ((DomId, NodeId, ScrollbarOrientation), &ScrollbarState)> + '_ {
self.scrollbar_states.iter().map(|(k, v)| (*k, v))
}
pub fn hit_test_scrollbar(
&self,
dom_id: DomId,
node_id: NodeId,
global_pos: LogicalPosition,
) -> Option<ScrollbarHit> {
for orientation in [
ScrollbarOrientation::Vertical,
ScrollbarOrientation::Horizontal,
] {
let Some(scrollbar_state) = self.scrollbar_states.get(&(dom_id, node_id, orientation)) else {
continue;
};
if !scrollbar_state.visible {
continue;
}
if !scrollbar_state.track_rect.contains(global_pos) {
continue;
}
let local_pos = LogicalPosition::new(
global_pos.x - scrollbar_state.track_rect.origin.x,
global_pos.y - scrollbar_state.track_rect.origin.y,
);
let component = scrollbar_state.hit_test_component(local_pos);
return Some(ScrollbarHit {
dom_id,
node_id,
orientation,
component,
local_position: local_pos,
global_position: global_pos,
});
}
None
}
pub fn hit_test_scrollbars(&self, global_pos: LogicalPosition) -> Option<ScrollbarHit> {
for ((dom_id, node_id, orientation), scrollbar_state) in self.scrollbar_states.iter().rev()
{
if !scrollbar_state.visible {
continue;
}
if !scrollbar_state.track_rect.contains(global_pos) {
continue;
}
let local_pos = LogicalPosition::new(
global_pos.x - scrollbar_state.track_rect.origin.x,
global_pos.y - scrollbar_state.track_rect.origin.y,
);
let component = scrollbar_state.hit_test_component(local_pos);
return Some(ScrollbarHit {
dom_id: *dom_id,
node_id: *node_id,
orientation: *orientation,
component,
local_position: local_pos,
global_position: global_pos,
});
}
None
}
}
impl AnimatedScrollState {
pub fn new(now: Instant) -> Self {
Self {
current_offset: LogicalPosition::zero(),
animation: None,
last_activity: now,
container_rect: LogicalRect::zero(),
content_rect: LogicalRect::zero(),
virtual_scroll_size: None,
virtual_scroll_offset: None,
overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling::Auto,
scrollbar_thickness: crate::solver3::fc::DEFAULT_SCROLLBAR_WIDTH_PX,
visual_width_px: 0.0,
has_horizontal_scrollbar: false,
has_vertical_scrollbar: false,
}
}
pub fn clamp(&self, position: LogicalPosition) -> LogicalPosition {
let effective_width = self.virtual_scroll_size
.map(|s| s.width)
.unwrap_or(self.content_rect.size.width);
let effective_height = self.virtual_scroll_size
.map(|s| s.height)
.unwrap_or(self.content_rect.size.height);
let max_x = (effective_width - self.container_rect.size.width).max(0.0);
let max_y = (effective_height - self.container_rect.size.height).max(0.0);
LogicalPosition {
x: position.x.max(0.0).min(max_x),
y: position.y.max(0.0).min(max_y),
}
}
}
pub fn apply_easing(t: f32, easing: EasingFunction) -> f32 {
match easing {
EasingFunction::Linear => t,
EasingFunction::EaseOut => 1.0 - (1.0 - t).powi(3),
EasingFunction::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
}
}
pub type ScrollStates = ScrollManager;
impl ScrollManager {
pub fn remap_node_ids(
&mut self,
dom_id: DomId,
node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
) {
for (&old_node_id, &new_node_id) in node_id_map.iter() {
if old_node_id != new_node_id {
if let Some(state) = self.states.remove(&(dom_id, old_node_id)) {
self.states.insert((dom_id, new_node_id), state);
}
}
}
for (&old_node_id, &new_node_id) in node_id_map.iter() {
if old_node_id != new_node_id {
if let Some(scroll_id) = self.external_scroll_ids.remove(&(dom_id, old_node_id)) {
self.external_scroll_ids.insert((dom_id, new_node_id), scroll_id);
}
}
}
let scrollbar_states_to_remap: Vec<_> = self.scrollbar_states.keys()
.filter(|(d, node_id, _)| {
*d == dom_id && node_id_map.get(node_id).map_or(false, |new_id| new_id != node_id)
})
.cloned()
.collect();
for (d, old_node_id, orientation) in scrollbar_states_to_remap {
if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
if let Some(state) = self.scrollbar_states.remove(&(d, old_node_id, orientation)) {
self.scrollbar_states.insert((d, new_node_id, orientation), state);
}
}
}
}
}