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:
parent
bf7ffb982a
commit
3258cd2a7f
|
|
@ -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,48 +578,50 @@ 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.
|
||||||
|
|
||||||
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;
|
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 {
|
} else {
|
||||||
// Kinetic scrolling
|
state.vel[d] -= friction * state.vel[d].signum();
|
||||||
let stop_speed = 20.0; // Pixels per second.
|
// Offset has an inverted coordinate system compared to
|
||||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
// 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;
|
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
||||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
// above).
|
||||||
state.vel[d] = 0.0;
|
for d in 0..2 {
|
||||||
} else {
|
if let Some(scroll_target) = state.offset_target[d] {
|
||||||
state.vel[d] -= friction * state.vel[d].signum();
|
state.vel[d] = 0.0;
|
||||||
// Offset has an inverted coordinate system compared to
|
|
||||||
// the velocity, so we subtract it instead of adding it
|
if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
|
||||||
state.offset[d] -= state.vel[d] * dt;
|
// Arrived
|
||||||
ctx.request_repaint();
|
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();
|
let content_size = content_ui.min_size();
|
||||||
|
|
||||||
for d in 0..2 {
|
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] {
|
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 {
|
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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue