Add auto-scrolling support for scrollable widget

This commit is contained in:
nz366 2025-05-30 17:03:58 +00:00 • committed by Héctor Ramón Jiménez
parent d94c080dd4
commit 71e57fe889
No known key found for this signature in database
GPG key ID: 7CC46565708259A7

View file

@ -567,6 +567,9 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, _viewport: &Rectangle,
) { ) {
const AUTOSCROLL_DEADZONE: f32 = 20.0;
const AUTOSCROLL_SPEED: f32 = 10.0;
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
let bounds = layout.bounds(); let bounds = layout.bounds();
let cursor_over_scrollable = cursor.position_over(bounds); let cursor_over_scrollable = cursor.position_over(bounds);
@ -601,7 +604,7 @@ where
} }
let mut update = || { let mut update = || {
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at() {
match event { match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => { | Event::Touch(touch::Event::FingerMoved { .. }) => {
@ -657,8 +660,9 @@ where
content_bounds, content_bounds,
); );
state.y_scroller_grabbed_at = state.interaction = Interaction::YScrollerGrabbed(
Some(scroller_grabbed_at); scroller_grabbed_at,
);
let _ = notify_scroll( let _ = notify_scroll(
state, state,
@ -675,7 +679,7 @@ where
} }
} }
if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at() {
match event { match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => { | Event::Touch(touch::Event::FingerMoved { .. }) => {
@ -730,8 +734,9 @@ where
content_bounds, content_bounds,
); );
state.x_scroller_grabbed_at = state.interaction = Interaction::XScrollerGrabbed(
Some(scroller_grabbed_at); scroller_grabbed_at,
);
let _ = notify_scroll( let _ = notify_scroll(
state, state,
@ -800,10 +805,19 @@ where
| touch::Event::FingerLost { .. } | touch::Event::FingerLost { .. }
) )
) { ) {
state.scroll_area_touched_at = None; state.interaction = Interaction::None;
state.x_scroller_grabbed_at = None; return;
state.y_scroller_grabbed_at = None; }
if matches!(state.interaction, Interaction::AutoScrolling { .. })
&& matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(_))
| Event::Touch(_)
| Event::Keyboard(_)
)
{
state.interaction = Interaction::None;
return; return;
} }
@ -811,15 +825,6 @@ where
return; return;
} }
if let Event::Keyboard(keyboard::Event::ModifiersChanged(
modifiers,
)) = event
{
state.keyboard_modifiers = *modifiers;
return;
}
match event { match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => { Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if cursor_over_scrollable.is_none() { if cursor_over_scrollable.is_none() {
@ -874,58 +879,172 @@ where
shell.capture_event(); shell.capture_event();
} }
} }
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Middle,
)) if matches!(state.interaction, Interaction::None) => {
let Some(origin) = cursor_over_scrollable else {
return;
};
state.interaction = Interaction::AutoScrolling {
origin,
current: origin,
last_frame: None,
};
shell.capture_event();
}
Event::Touch(event) Event::Touch(event)
if state.scroll_area_touched_at.is_some() if matches!(
|| (!mouse_over_y_scrollbar state.interaction,
&& !mouse_over_x_scrollbar) => Interaction::TouchScrolling(_)
) || (!mouse_over_y_scrollbar
&& !mouse_over_x_scrollbar) =>
{ {
match event { match event {
touch::Event::FingerPressed { .. } => { touch::Event::FingerPressed { .. } => {
if cursor_over_scrollable.is_none() { let Some(position) = cursor_over_scrollable else {
return; return;
} };
state.scroll_area_touched_at = cursor.position(); state.interaction =
Interaction::TouchScrolling(position);
} }
touch::Event::FingerMoved { .. } => { touch::Event::FingerMoved { .. } => {
if let Some(scroll_box_touched_at) = let Interaction::TouchScrolling(
state.scroll_area_touched_at scroll_box_touched_at,
{ ) = state.interaction
let Some(cursor_position) = cursor.position() else {
else { return;
return; };
};
let delta = Vector::new( let Some(cursor_position) = cursor.position()
scroll_box_touched_at.x - cursor_position.x, else {
scroll_box_touched_at.y - cursor_position.y, return;
); };
state.scroll( let delta = Vector::new(
self.direction.align(delta), scroll_box_touched_at.x - cursor_position.x,
bounds, scroll_box_touched_at.y - cursor_position.y,
content_bounds, );
);
state.scroll_area_touched_at = state.scroll(
Some(cursor_position); self.direction.align(delta),
bounds,
content_bounds,
);
// TODO: bubble up touch movements if not consumed. state.interaction =
let _ = notify_scroll( Interaction::TouchScrolling(cursor_position);
state,
&self.on_scroll, // TODO: bubble up touch movements if not consumed.
bounds, let _ = notify_scroll(
content_bounds, state,
shell, &self.on_scroll,
); bounds,
} content_bounds,
shell,
);
} }
_ => {} _ => {}
} }
shell.capture_event(); shell.capture_event();
} }
Event::Window(window::Event::RedrawRequested(_)) => { Event::Mouse(mouse::Event::CursorMoved { position }) => {
if let Interaction::AutoScrolling {
origin,
last_frame,
..
} = state.interaction
{
let delta = *position - origin;
if delta.x.abs() >= AUTOSCROLL_DEADZONE
|| delta.y.abs() >= AUTOSCROLL_DEADZONE
{
state.interaction = Interaction::AutoScrolling {
origin,
current: *position,
last_frame,
};
if last_frame.is_none() {
shell.request_redraw();
}
}
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(
modifiers,
)) => {
state.keyboard_modifiers = *modifiers;
}
Event::Window(window::Event::RedrawRequested(now)) => {
if let Interaction::AutoScrolling {
origin,
current,
last_frame,
} = state.interaction
{
if last_frame == Some(*now) {
shell.request_redraw();
return;
}
state.interaction = Interaction::AutoScrolling {
origin,
current,
last_frame: None,
};
let delta = current - origin;
if delta.x.abs() >= AUTOSCROLL_DEADZONE
|| delta.y.abs() >= AUTOSCROLL_DEADZONE
{
let time_delta =
if let Some(last_frame) = last_frame {
*now - last_frame
} else {
Duration::ZERO
};
let scroll_factor =
time_delta.as_secs_f32() * AUTOSCROLL_SPEED;
state.scroll(
self.direction.align(Vector::new(
delta.x * scroll_factor,
delta.y * scroll_factor,
)),
bounds,
content_bounds,
);
let has_scrolled = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
if has_scrolled || time_delta.is_zero() {
state.interaction =
Interaction::AutoScrolling {
origin,
current,
last_frame: Some(*now),
};
shell.request_redraw();
}
return;
}
}
let _ = notify_viewport( let _ = notify_viewport(
state, state,
&self.on_scroll, &self.on_scroll,
@ -940,15 +1059,13 @@ where
update(); update();
let status = if state.y_scroller_grabbed_at.is_some() let status = if state.scrollers_grabbed() {
|| state.x_scroller_grabbed_at.is_some()
{
Status::Dragged { Status::Dragged {
is_horizontal_scrollbar_dragged: state is_horizontal_scrollbar_dragged: state
.x_scroller_grabbed_at .x_scroller_grabbed_at()
.is_some(), .is_some(),
is_vertical_scrollbar_dragged: state is_vertical_scrollbar_dragged: state
.y_scroller_grabbed_at .y_scroller_grabbed_at()
.is_some(), .is_some(),
is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(), is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(), is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
@ -1318,25 +1435,34 @@ fn notify_viewport<Message>(
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct State { struct State {
scroll_area_touched_at: Option<Point>,
offset_y: Offset, offset_y: Offset,
y_scroller_grabbed_at: Option<f32>,
offset_x: Offset, offset_x: Offset,
x_scroller_grabbed_at: Option<f32>, interaction: Interaction,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
last_notified: Option<Viewport>, last_notified: Option<Viewport>,
last_scrolled: Option<Instant>, last_scrolled: Option<Instant>,
is_scrollbar_visible: bool, is_scrollbar_visible: bool,
} }
#[derive(Debug, Clone, Copy)]
enum Interaction {
None,
YScrollerGrabbed(f32),
XScrollerGrabbed(f32),
TouchScrolling(Point),
AutoScrolling {
origin: Point,
current: Point,
last_frame: Option<Instant>,
},
}
impl Default for State { impl Default for State {
fn default() -> Self { fn default() -> Self {
Self { Self {
scroll_area_touched_at: None,
offset_y: Offset::Absolute(0.0), offset_y: Offset::Absolute(0.0),
y_scroller_grabbed_at: None,
offset_x: Offset::Absolute(0.0), offset_x: Offset::Absolute(0.0),
x_scroller_grabbed_at: None, interaction: Interaction::None,
keyboard_modifiers: keyboard::Modifiers::default(), keyboard_modifiers: keyboard::Modifiers::default(),
last_notified: None, last_notified: None,
last_scrolled: None, last_scrolled: None,
@ -1455,14 +1581,11 @@ impl Viewport {
} }
impl State { impl State {
/// Creates a new [`State`] with the scrollbar(s) at the beginning. fn new() -> Self {
pub fn new() -> Self {
State::default() State::default()
} }
/// Apply a scrolling offset to the current [`State`], given the bounds of fn scroll(
/// the [`Scrollable`] and its contents.
pub fn scroll(
&mut self, &mut self,
delta: Vector<f32>, delta: Vector<f32>,
bounds: Rectangle, bounds: Rectangle,
@ -1485,11 +1608,7 @@ impl State {
} }
} }
/// Scrolls the [`Scrollable`] to a relative amount along the y axis. fn scroll_y_to(
///
/// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
/// the end.
pub fn scroll_y_to(
&mut self, &mut self,
percentage: f32, percentage: f32,
bounds: Rectangle, bounds: Rectangle,
@ -1499,11 +1618,7 @@ impl State {
self.unsnap(bounds, content_bounds); self.unsnap(bounds, content_bounds);
} }
/// Scrolls the [`Scrollable`] to a relative amount along the x axis. fn scroll_x_to(
///
/// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
/// the end.
pub fn scroll_x_to(
&mut self, &mut self,
percentage: f32, percentage: f32,
bounds: Rectangle, bounds: Rectangle,
@ -1513,14 +1628,12 @@ impl State {
self.unsnap(bounds, content_bounds); self.unsnap(bounds, content_bounds);
} }
/// Snaps the scroll position to a [`RelativeOffset`]. fn snap_to(&mut self, offset: RelativeOffset) {
pub fn snap_to(&mut self, offset: RelativeOffset) {
self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0)); self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0)); self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
} }
/// Scroll to the provided [`AbsoluteOffset`]. fn scroll_to(&mut self, offset: AbsoluteOffset) {
pub fn scroll_to(&mut self, offset: AbsoluteOffset) {
self.offset_x = Offset::Absolute(offset.x.max(0.0)); self.offset_x = Offset::Absolute(offset.x.max(0.0));
self.offset_y = Offset::Absolute(offset.y.max(0.0)); self.offset_y = Offset::Absolute(offset.y.max(0.0));
} }
@ -1580,10 +1693,27 @@ impl State {
) )
} }
/// Returns whether any scroller is currently grabbed or not. fn scrollers_grabbed(&self) -> bool {
pub fn scrollers_grabbed(&self) -> bool { matches!(
self.x_scroller_grabbed_at.is_some() self.interaction,
|| self.y_scroller_grabbed_at.is_some() Interaction::YScrollerGrabbed(_) | Interaction::XScrollerGrabbed(_),
)
}
pub fn y_scroller_grabbed_at(&self) -> Option<f32> {
let Interaction::YScrollerGrabbed(at) = self.interaction else {
return None;
};
Some(at)
}
pub fn x_scroller_grabbed_at(&self) -> Option<f32> {
let Interaction::XScrollerGrabbed(at) = self.interaction else {
return None;
};
Some(at)
} }
} }