1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 06:40:06 -04:00

Fix jitter when hovering edge of scroll area close to resize splitter (#7803)

* Closes https://github.com/emilk/egui/issues/7749

## What
In our hit test code we have special handling for thin widgets, to make
them easier to hit.
The widths of our scroll bars are animated.
Together, the heuristic would trigger as the scroll bars were shrinking,
leading to the scroll bars being _hovered_ which lead them being too
wide, making the heuristic fail, which would again make them shrink,
causing the bug.

## Bonus
Now the scroll bar is only shown as hovered if the mouse is actually
hovering them and nothing else. Previously they would show as long as
the cursor were in the general area (but maybe actually hovering
something else).
This commit is contained in:
Emil Ernerfeldt
2025-12-22 21:13:24 +01:00
committed by GitHub
parent 4169d2c196
commit 4f6eabffb8

View File

@@ -1241,46 +1241,17 @@ impl Prepared {
continue; continue;
} }
let interact_id = id.with(d);
// Margin on either side of the scroll bar: // Margin on either side of the scroll bar:
let inner_margin = show_factor * scroll_style.bar_inner_margin; let inner_margin = show_factor * scroll_style.bar_inner_margin;
let outer_margin = show_factor * scroll_style.bar_outer_margin; let outer_margin = show_factor * scroll_style.bar_outer_margin;
// top/bottom of a horizontal scroll (d==0). // bottom of a horizontal scroll (d==0).
// left/rigth of a vertical scroll (d==1). // right of a vertical scroll (d==1).
let mut cross = if scroll_style.floating { let mut max_cross = outer_rect.max[1 - d] - outer_margin;
// The bounding rect of a fully visible bar.
// When we hover this area, we should show the full bar:
let max_bar_rect = if d == 0 {
outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
} else {
outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
};
let is_hovering_bar_area = is_hovering_outer_rect if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
&& ui.rect_contains_pointer(max_bar_rect)
&& !is_dragging_background
|| state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui
.ctx()
.animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
let width = show_factor
* lerp(
scroll_style.floating_width..=scroll_style.bar_width,
is_hovering_bar_area_t,
);
let max_cross = outer_rect.max[1 - d] - outer_margin;
let min_cross = max_cross - width;
Rangef::new(min_cross, max_cross)
} else {
let min_cross = inner_rect.max[1 - d] + inner_margin;
let max_cross = outer_rect.max[1 - d] - outer_margin;
Rangef::new(min_cross, max_cross)
};
if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
// Move the scrollbar so it is visible. This is needed in some cases. // Move the scrollbar so it is visible. This is needed in some cases.
// For instance: // For instance:
// * When we have a vertical-only scroll area in a top level panel, // * When we have a vertical-only scroll area in a top level panel,
@@ -1290,21 +1261,57 @@ impl Prepared {
// is outside the clip rectangle. // is outside the clip rectangle.
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that. // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
// clip_rect_margin is quite a hack. It would be nice to get rid of it. // clip_rect_margin is quite a hack. It would be nice to get rid of it.
let width = cross.max - cross.min; max_cross = ui.clip_rect().max[1 - d] - outer_margin;
cross.max = ui.clip_rect().max[1 - d] - outer_margin;
cross.min = cross.max - width;
} }
let outer_scroll_bar_rect = if d == 0 { let full_width = scroll_style.bar_width;
Rect::from_min_max(
pos2(scroll_bar_rect.left(), cross.min), // The bounding rect of a fully visible bar.
pos2(scroll_bar_rect.right(), cross.max), // When we hover this area, we should show the full bar:
) let max_bar_rect = if d == 0 {
outer_rect.with_min_y(max_cross - full_width)
} else { } else {
Rect::from_min_max( outer_rect.with_min_x(max_cross - full_width)
pos2(cross.min, scroll_bar_rect.top()), };
pos2(cross.max, scroll_bar_rect.bottom()),
) let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::click_and_drag()
} else {
Sense::hover()
};
// We always sense interaction with the full width, even if we antimate it growing/shrinking.
// This is to present a more consistent target for our hit test code,
// and to avoid producing jitter in "thin widget" heuristics there.
// Also: it make sense to detect any hover where the scroll bar _will_ be.
let response = ui.interact(max_bar_rect, interact_id, sense);
// top/bottom of a horizontal scroll (d==0).
// left/rigth of a vertical scroll (d==1).
let cross = if scroll_style.floating {
let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui
.ctx()
.animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
let width = show_factor
* lerp(
scroll_style.floating_width..=full_width,
is_hovering_bar_area_t,
);
let min_cross = max_cross - width;
Rangef::new(min_cross, max_cross)
} else {
let min_cross = inner_rect.max[1 - d] + inner_margin;
Rangef::new(min_cross, max_cross)
};
let outer_scroll_bar_rect = if d == 0 {
Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
} else {
Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
}; };
let from_content = |content| { let from_content = |content| {
@@ -1344,14 +1351,6 @@ impl Prepared {
let handle_rect = calculate_handle_rect(d, &state.offset); let handle_rect = calculate_handle_rect(d, &state.offset);
let interact_id = id.with(d);
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::click_and_drag()
} else {
Sense::hover()
};
let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
state.scroll_bar_interaction[d] = response.hovered() || response.dragged(); state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
if let Some(pointer_pos) = response.interact_pointer_pos() { if let Some(pointer_pos) = response.interact_pointer_pos() {