mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Fix ScrollArea::scroll_to_* calls when stick_to_bottom is Active (#8033)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Closes #8032 * [x] I have followed the instructions in the PR template It includes: * Fix for `ScrollArea` when `scroll_to_*` could be ignored when `stick_to_bottom(true)` was active and the viewport was already stuck to the bottom. * The fix is by making explicit per-axis scroll movement take priority over sticky-end snapping for that frame, and avoid immediately re-marking animated scrolls as still stuck. * I've also added a regression test for this issue to ensure it will be caught on further code changes. The code snippets form the original issue can be used for testing here as well Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
committed by
lucasmerlin
parent
dd72bda544
commit
872587836c
@@ -1062,6 +1062,8 @@ impl Prepared {
|
||||
.ctx()
|
||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
|
||||
|
||||
let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
|
||||
|
||||
for d in 0..2 {
|
||||
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
|
||||
let mut delta = -scroll_delta.0[d];
|
||||
@@ -1133,6 +1135,10 @@ impl Prepared {
|
||||
ui.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
if delta != 0.0 {
|
||||
had_explicit_scroll_adjustment[d] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll target meant for ScrollAreas up the stack (if any)
|
||||
@@ -1234,8 +1240,10 @@ impl Prepared {
|
||||
// Paint the bars:
|
||||
let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
|
||||
for d in 0..2 {
|
||||
// maybe force increase in offset to keep scroll stuck to end position
|
||||
if stick_to_end[d] && state.scroll_stuck_to_end[d] {
|
||||
// maybe force increase in offset to keep scroll stuck to end position,
|
||||
// unless this axis had an explicit scroll adjustment.
|
||||
if stick_to_end[d] && state.scroll_stuck_to_end[d] && !had_explicit_scroll_adjustment[d]
|
||||
{
|
||||
state.offset[d] = content_size[d] - inner_rect.size()[d];
|
||||
}
|
||||
|
||||
@@ -1487,16 +1495,25 @@ impl Prepared {
|
||||
state.offset = state.offset.min(available_offset);
|
||||
state.offset = state.offset.max(Vec2::ZERO);
|
||||
|
||||
let suppress_stuck_recompute = Vec2b::new(
|
||||
had_explicit_scroll_adjustment[0] && state.offset_target[0].is_some(),
|
||||
had_explicit_scroll_adjustment[1] && state.offset_target[1].is_some(),
|
||||
);
|
||||
|
||||
// Is scroll handle at end of content, or is there no scrollbar
|
||||
// yet (not enough content), but sticking is requested? If so, enter sticky mode.
|
||||
// Only has an effect if stick_to_end is enabled but we save in
|
||||
// state anyway so that entering sticky mode at an arbitrary time
|
||||
// has appropriate effect.
|
||||
// Keep explicit target requests from being reclassified as "still stuck" in the same
|
||||
// frame, otherwise animated scroll-to requests never get a chance to pull away from the end.
|
||||
state.scroll_stuck_to_end = Vec2b::new(
|
||||
(state.offset[0] == available_offset[0])
|
||||
|| (self.stick_to_end[0] && available_offset[0] < 0.0),
|
||||
(state.offset[1] == available_offset[1])
|
||||
|| (self.stick_to_end[1] && available_offset[1] < 0.0),
|
||||
!suppress_stuck_recompute[0]
|
||||
&& ((state.offset[0] == available_offset[0])
|
||||
|| (stick_to_end[0] && available_offset[0] < 0.0)),
|
||||
!suppress_stuck_recompute[1]
|
||||
&& ((state.offset[1] == available_offset[1])
|
||||
|| (stick_to_end[1] && available_offset[1] < 0.0)),
|
||||
);
|
||||
|
||||
state.show_scroll = show_scroll_this_frame;
|
||||
|
||||
@@ -407,3 +407,44 @@ fn horizontal_wrapped_multiline_row_height_reference() {
|
||||
|
||||
harness.snapshot("horizontal_wrapped_multiline_row_height_reference");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_scroll_beats_sticky_bottom() {
|
||||
let mut harness = Harness::builder()
|
||||
.with_size((200.0, 120.0))
|
||||
.with_max_steps(8)
|
||||
.build_ui_state(
|
||||
|ui, state: &mut (bool, f32, f32)| {
|
||||
ui.style_mut().scroll_animation = ScrollAnimation::duration(0.5);
|
||||
|
||||
let output = ScrollArea::vertical()
|
||||
.max_height(60.0)
|
||||
.stick_to_bottom(true)
|
||||
.animated(true)
|
||||
.show(ui, |ui| {
|
||||
for row in 0..40 {
|
||||
let response = ui.label(format!("Row {row}"));
|
||||
if state.0 && row == 0 {
|
||||
response.scroll_to_me(Some(Align::TOP));
|
||||
state.0 = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.1 = output.state.offset.y;
|
||||
state.2 = (output.content_size.y - output.inner_rect.height()).max(0.0);
|
||||
},
|
||||
(false, 0.0, 0.0),
|
||||
);
|
||||
|
||||
assert!((harness.state().1 - harness.state().2).abs() <= 1.0);
|
||||
|
||||
harness.state_mut().0 = true;
|
||||
harness.step();
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
harness.state().1 + 1.0 < harness.state().2,
|
||||
"animated explicit scroll should leave the sticky bottom"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user