Fix scroll handle extending outside of `ScrollArea` (#5286)

* [x] I have followed the instructions in the PR template
* Closes https://github.com/emilk/egui/issues/3734

## Info

This PR addresses an issue where resizing a scroll handle can lead to
unwanted overlap.

It is also happening on the egui-demo.

![Screenshot 2024-10-18 at 17 35
25](https://github.com/user-attachments/assets/3a9527d9-fc46-4b25-b95a-9ba2fa54978e)

## Cause

*Note: The following explanation assumes a vertical scroll; however, the
logic applies equally to horizontal scrolling.*

When the scroll handle is positioned at the top or bottom of the scroll
area and the handle is resized to fit the minimum handle size, there is
a risk of overlap. This occurs if the handle’s new size extends beyond
the bounds of the scroll area.

## Proposed Solution

1. Check whether increasing the handle size will cause it to overlap
with the scroll area.
2. If an overlap is detected, adjust the handle’s center position by the
overlap amount, moving it towards the center of the scroll area.
This commit is contained in:
Gilberto Santos 2025-03-05 10:49:04 +01:00 committed by GitHub
parent 159ccb2fef
commit 1dea8fac9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 32 additions and 42 deletions

View File

@ -1,8 +1,8 @@
#![allow(clippy::needless_range_loop)]
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef,
Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
};
#[derive(Clone, Copy, Debug)]
@ -1090,21 +1090,35 @@ impl Prepared {
)
};
let handle_rect = if d == 0 {
Rect::from_min_max(
pos2(from_content(state.offset.x), cross.min),
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
)
} else {
Rect::from_min_max(
pos2(cross.min, from_content(state.offset.y)),
pos2(
cross.max,
from_content(state.offset.y + inner_rect.height()),
),
)
let calculate_handle_rect = |d, offset: &Vec2| {
let handle_size = if d == 0 {
from_content(offset.x + inner_rect.width()) - from_content(offset.x)
} else {
from_content(offset.y + inner_rect.height()) - from_content(offset.y)
}
.max(scroll_style.handle_min_length);
let handle_start_point = remap_clamp(
offset[d],
0.0..=max_offset[d],
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
);
if d == 0 {
Rect::from_min_max(
pos2(handle_start_point, cross.min),
pos2(handle_start_point + handle_size, cross.max),
)
} else {
Rect::from_min_max(
pos2(cross.min, handle_start_point),
pos2(cross.max, handle_start_point + handle_size),
)
}
};
let handle_rect = calculate_handle_rect(d, &state.offset);
let interact_id = id.with(d);
let sense = if self.scrolling_enabled {
Sense::click_and_drag()
@ -1133,8 +1147,8 @@ impl Prepared {
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
state.offset[d] = remap(
new_handle_top,
scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
0.0..=content_size[d],
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]),
0.0..=max_offset[d],
);
// some manual action taken, scroll not stuck
@ -1154,31 +1168,7 @@ impl Prepared {
if ui.is_rect_visible(outer_scroll_bar_rect) {
// Avoid frame-delay by calculating a new handle rect:
let mut handle_rect = if d == 0 {
Rect::from_min_max(
pos2(from_content(state.offset.x), cross.min),
pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
)
} else {
Rect::from_min_max(
pos2(cross.min, from_content(state.offset.y)),
pos2(
cross.max,
from_content(state.offset.y + inner_rect.height()),
),
)
};
let min_handle_size = scroll_style.handle_min_length;
if handle_rect.size()[d] < min_handle_size {
handle_rect = Rect::from_center_size(
handle_rect.center(),
if d == 0 {
vec2(min_handle_size, handle_rect.size().y)
} else {
vec2(handle_rect.size().x, min_handle_size)
},
);
}
let handle_rect = calculate_handle_rect(d, &state.offset);
let visuals = if scrolling_enabled {
// Pick visuals based on interaction with the handle.