use std::rc::Rc;
use dioxus::html::geometry::PixelsVector2D;
use dioxus::html::{MountedData, ScrollBehavior};
use dioxus::prelude::*;
use crate::core::hooks::use_rect_refresh_provider;
use crate::core::{Point, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollAxis {
#[default]
Y,
X,
Both,
}
pub fn edge_delta(
pos: Point,
rect: Rect,
threshold: f64,
speed: f64,
axis: ScrollAxis,
) -> (f64, f64) {
if !rect.contains(pos) {
return (0.0, 0.0);
}
let ramp = |dist_into_band: f64| (dist_into_band / threshold.max(1.0)).clamp(0.0, 1.0) * speed;
let edge = |lo: f64, hi: f64| -> f64 {
if lo <= hi {
if lo < threshold {
-ramp(threshold - lo)
} else {
0.0
}
} else if hi < threshold {
ramp(threshold - hi)
} else {
0.0
}
};
let mut dx = 0.0;
let mut dy = 0.0;
if matches!(axis, ScrollAxis::X | ScrollAxis::Both) {
dx = edge(pos.x - rect.x, rect.x + rect.width - pos.x);
}
if matches!(axis, ScrollAxis::Y | ScrollAxis::Both) {
dy = edge(pos.y - rect.y, rect.y + rect.height - pos.y);
}
(dx, dy)
}
fn pointer_move_should_scroll(
pointer_type: &str,
pressure: f32,
has_held_button: bool,
active: Option<bool>,
) -> bool {
match active {
Some(active) => active,
None => has_held_button || (pointer_type != "mouse" && pressure > 0.0),
}
}
#[component]
pub fn AutoScroll(
#[props(default = 48.0)]
threshold: f64,
#[props(default = 24.0)]
speed: f64,
#[props(default)]
axis: ScrollAxis,
#[props(default)]
active: Option<bool>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut mounted = use_signal(|| None::<Rc<MountedData>>);
let busy = use_signal(|| false);
let refresh = use_rect_refresh_provider();
let scroll_for = move |point: Point| {
let Some(m) = mounted.peek().clone() else {
return;
};
if *busy.peek() {
return;
}
let mut busy = busy;
busy.set(true);
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
let rect = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
let (dx, dy) = edge_delta(point, rect, threshold, speed, axis);
if dx != 0.0 || dy != 0.0 {
if let Ok(offset) = m.get_scroll_offset().await {
let _ = m
.scroll(
PixelsVector2D::new(offset.x + dx, offset.y + dy),
ScrollBehavior::Instant,
)
.await;
refresh.refresh_all();
}
}
}
busy.set(false);
});
};
rsx! {
div {
onmounted: move |evt: Event<MountedData>| {
mounted.set(Some(evt.data()));
},
onscroll: move |_| {
refresh.refresh_all();
},
ondragover: move |evt: DragEvent| {
let c = evt.client_coordinates();
scroll_for(Point::new(c.x, c.y));
},
onpointermove: move |evt: PointerEvent| {
if pointer_move_should_scroll(
&evt.pointer_type(),
evt.pressure(),
!evt.held_buttons().is_empty(),
active,
) {
let c = evt.client_coordinates();
scroll_for(Point::new(c.x, c.y));
}
},
..attributes,
{children}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deltas_ramp_toward_edges() {
let rect = Rect::new(0.0, 0.0, 200.0, 400.0);
assert_eq!(
edge_delta(Point::new(100.0, 200.0), rect, 48.0, 24.0, ScrollAxis::Both),
(0.0, 0.0)
);
let (_, dy) = edge_delta(Point::new(100.0, 10.0), rect, 48.0, 24.0, ScrollAxis::Y);
assert!((-24.0..0.0).contains(&dy));
let (_, dy) = edge_delta(Point::new(100.0, 400.0), rect, 48.0, 24.0, ScrollAxis::Y);
assert_eq!(dy, 24.0);
let (dx, _) = edge_delta(Point::new(1.0, 200.0), rect, 48.0, 24.0, ScrollAxis::Y);
assert_eq!(dx, 0.0);
}
#[test]
fn no_scroll_when_pointer_leaves_the_container() {
let rect = Rect::new(0.0, 0.0, 200.0, 400.0);
assert_eq!(
edge_delta(Point::new(100.0, 900.0), rect, 48.0, 24.0, ScrollAxis::Both),
(0.0, 0.0)
);
assert_eq!(
edge_delta(Point::new(-50.0, 200.0), rect, 48.0, 24.0, ScrollAxis::Both),
(0.0, 0.0)
);
}
#[test]
fn narrow_container_scrolls_toward_the_nearer_edge() {
let rect = Rect::new(0.0, 0.0, 40.0, 400.0);
let (dx, _) = edge_delta(Point::new(35.0, 200.0), rect, 48.0, 24.0, ScrollAxis::X);
assert!(
dx > 0.0,
"near the right edge should scroll right, got {dx}"
);
let (dx, _) = edge_delta(Point::new(5.0, 200.0), rect, 48.0, 24.0, ScrollAxis::X);
assert!(dx < 0.0, "near the left edge should scroll left, got {dx}");
}
#[test]
fn pointer_scroll_predicate_matches_active_pointer_drags() {
assert!(
pointer_move_should_scroll("mouse", 0.0, true, None),
"default mouse pointer drags keep a held button during movement"
);
assert!(
!pointer_move_should_scroll("mouse", 0.0, false, None),
"passive mouse hover must not scroll"
);
assert!(
pointer_move_should_scroll("touch", 0.5, false, None),
"touch contact can report pressure instead of held buttons"
);
assert!(
pointer_move_should_scroll("pen", 0.0, true, None),
"pen contact can also surface as held buttons"
);
assert!(
!pointer_move_should_scroll("touch", 0.5, false, Some(false)),
"callers that track drag state can explicitly gate scrolling off"
);
assert!(
pointer_move_should_scroll("mouse", 0.0, false, Some(true)),
"callers that track drag state can explicitly gate scrolling on"
);
}
}