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 dt = ui.input(|i| i.stable_dt).at_most(0.1);
if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
@ -577,48 +578,50 @@ impl ScrollArea {
}
} else {
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.
if let Some(scroll_target) = state.offset_target[d] {
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
// Arrived
state.offset[d] = scroll_target.target_offset;
state.offset_target[d] = None;
} else {
// Move towards target
let t = emath::interpolation_factor(
scroll_target.animation_time_span,
ui.input(|i| i.time),
dt,
emath::ease_in_ease_out,
);
if t < 1.0 {
state.offset[d] =
emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
ctx.request_repaint();
} else {
// Arrived
state.offset[d] = scroll_target.target_offset;
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.
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();
}
}
}
}
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] {
state.vel[d] = 0.0;
if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
// Arrived
state.offset[d] = scroll_target.target_offset;
state.offset_target[d] = None;
} else {
// Move towards target
let t = emath::interpolation_factor(
scroll_target.animation_time_span,
ui.input(|i| i.time),
dt,
emath::ease_in_ease_out,
);
if t < 1.0 {
state.offset[d] =
emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
ctx.request_repaint();
} else {
// Arrived
state.offset[d] = scroll_target.target_offset;
state.offset_target[d] = None;
}
}
}
@ -753,11 +756,13 @@ impl Prepared {
let content_size = content_ui.min_size();
for d in 0..2 {
// We always take both scroll targets regardless of which scroll axes are enabled. This
// is to avoid them leaking to other scroll areas.
let scroll_target = content_ui
.ctx()
.frame_state_mut(|state| state.scroll_target[d].take());
if scroll_enabled[d] {
// We take the scroll target so only this ScrollArea will use it:
let scroll_target = content_ui
.ctx()
.frame_state_mut(|state| state.scroll_target[d].take());
if let Some((target_range, align)) = scroll_target {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();