1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -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:
Ammar Abou Zor
2026-05-26 16:01:55 +02:00
committed by lucasmerlin
parent dd72bda544
commit 872587836c
2 changed files with 64 additions and 6 deletions

View File

@@ -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;

View File

@@ -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"
);
}