Fix two `ScrollArea` bugs: leaking scroll target and broken animation to target offset (#4174)

This PR fixes two issues related to `ScrollArea`.

1) When a `ScrollArea` would have `drag_to_scroll` set to `false` (e.g.
because some custom logic is at play or some other reason), it would not
animate to the `target_offset`, effectively making
`Response::scroll_to_me()` ineffective.
2) Single-direction `ScrollArea`s would leak the `scroll_target`'s other
direction. In certain specific circumstances (e.g. an horizontal area
nested in a vertical one, or inversely), this _could_ work as intended,
but in many other cases it could cause unwanted effects. With this PR,
both `scroll_target` directions are consumed by nearest enclosing
`ScrollArea`, regardless of the actually enabled scroll axes.
This commit is contained in:
Antoine Beyeler 2024-03-17 17:12:41 +01:00 committed by GitHub
parent bf7ffb982a
commit 3258cd2a7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 47 additions and 42 deletions

View File

@ -553,6 +553,7 @@ impl ScrollArea {
} }
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
if (scrolling_enabled && drag_to_scroll) if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1]) && (state.content_is_too_large[0] || state.content_is_too_large[1])
@ -577,8 +578,27 @@ impl ScrollArea {
} }
} else { } else {
for d in 0..2 { for d in 0..2 {
let dt = ui.input(|i| i.stable_dt).at_most(0.1); // Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
}
}
}
}
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
// above).
for d in 0..2 {
if let Some(scroll_target) = state.offset_target[d] { if let Some(scroll_target) = state.offset_target[d] {
state.vel[d] = 0.0; state.vel[d] = 0.0;
@ -604,23 +624,6 @@ impl ScrollArea {
state.offset_target[d] = None; state.offset_target[d] = None;
} }
} }
} else {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
}
}
}
} }
} }
@ -753,11 +756,13 @@ impl Prepared {
let content_size = content_ui.min_size(); let content_size = content_ui.min_size();
for d in 0..2 { for d in 0..2 {
if scroll_enabled[d] { // We always take both scroll targets regardless of which scroll axes are enabled. This
// We take the scroll target so only this ScrollArea will use it: // is to avoid them leaking to other scroll areas.
let scroll_target = content_ui let scroll_target = content_ui
.ctx() .ctx()
.frame_state_mut(|state| state.scroll_target[d].take()); .frame_state_mut(|state| state.scroll_target[d].take());
if scroll_enabled[d] {
if let Some((target_range, align)) = scroll_target { if let Some((target_range, align)) = scroll_target {
let min = content_ui.min_rect().min[d]; let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect(); let clip_rect = content_ui.clip_rect();