Skip to content

Commit 4f6eabf

Browse files
authored
Fix jitter when hovering edge of scroll area close to resize splitter (#7803)
* Closes #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).
1 parent 4169d2c commit 4f6eabf

1 file changed

Lines changed: 46 additions & 47 deletions

File tree

crates/egui/src/containers/scroll_area.rs

Lines changed: 46 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,70 +1241,77 @@ impl Prepared {
12411241
continue;
12421242
}
12431243

1244+
let interact_id = id.with(d);
1245+
12441246
// Margin on either side of the scroll bar:
12451247
let inner_margin = show_factor * scroll_style.bar_inner_margin;
12461248
let outer_margin = show_factor * scroll_style.bar_outer_margin;
12471249

1250+
// bottom of a horizontal scroll (d==0).
1251+
// right of a vertical scroll (d==1).
1252+
let mut max_cross = outer_rect.max[1 - d] - outer_margin;
1253+
1254+
if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
1255+
// Move the scrollbar so it is visible. This is needed in some cases.
1256+
// For instance:
1257+
// * When we have a vertical-only scroll area in a top level panel,
1258+
// and that panel is not wide enough for the contents.
1259+
// * When one ScrollArea is nested inside another, and the outer
1260+
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
1261+
// is outside the clip rectangle.
1262+
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
1263+
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
1264+
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
1265+
}
1266+
1267+
let full_width = scroll_style.bar_width;
1268+
1269+
// The bounding rect of a fully visible bar.
1270+
// When we hover this area, we should show the full bar:
1271+
let max_bar_rect = if d == 0 {
1272+
outer_rect.with_min_y(max_cross - full_width)
1273+
} else {
1274+
outer_rect.with_min_x(max_cross - full_width)
1275+
};
1276+
1277+
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1278+
Sense::click_and_drag()
1279+
} else {
1280+
Sense::hover()
1281+
};
1282+
1283+
// We always sense interaction with the full width, even if we antimate it growing/shrinking.
1284+
// This is to present a more consistent target for our hit test code,
1285+
// and to avoid producing jitter in "thin widget" heuristics there.
1286+
// Also: it make sense to detect any hover where the scroll bar _will_ be.
1287+
let response = ui.interact(max_bar_rect, interact_id, sense);
1288+
12481289
// top/bottom of a horizontal scroll (d==0).
12491290
// left/rigth of a vertical scroll (d==1).
1250-
let mut cross = if scroll_style.floating {
1251-
// The bounding rect of a fully visible bar.
1252-
// When we hover this area, we should show the full bar:
1253-
let max_bar_rect = if d == 0 {
1254-
outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
1255-
} else {
1256-
outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
1257-
};
1258-
1259-
let is_hovering_bar_area = is_hovering_outer_rect
1260-
&& ui.rect_contains_pointer(max_bar_rect)
1261-
&& !is_dragging_background
1262-
|| state.scroll_bar_interaction[d];
1291+
let cross = if scroll_style.floating {
1292+
let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
12631293

12641294
let is_hovering_bar_area_t = ui
12651295
.ctx()
12661296
.animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
12671297

12681298
let width = show_factor
12691299
* lerp(
1270-
scroll_style.floating_width..=scroll_style.bar_width,
1300+
scroll_style.floating_width..=full_width,
12711301
is_hovering_bar_area_t,
12721302
);
12731303

1274-
let max_cross = outer_rect.max[1 - d] - outer_margin;
12751304
let min_cross = max_cross - width;
12761305
Rangef::new(min_cross, max_cross)
12771306
} else {
12781307
let min_cross = inner_rect.max[1 - d] + inner_margin;
1279-
let max_cross = outer_rect.max[1 - d] - outer_margin;
12801308
Rangef::new(min_cross, max_cross)
12811309
};
12821310

1283-
if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
1284-
// Move the scrollbar so it is visible. This is needed in some cases.
1285-
// For instance:
1286-
// * When we have a vertical-only scroll area in a top level panel,
1287-
// and that panel is not wide enough for the contents.
1288-
// * When one ScrollArea is nested inside another, and the outer
1289-
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
1290-
// is outside the clip rectangle.
1291-
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
1292-
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
1293-
let width = cross.max - cross.min;
1294-
cross.max = ui.clip_rect().max[1 - d] - outer_margin;
1295-
cross.min = cross.max - width;
1296-
}
1297-
12981311
let outer_scroll_bar_rect = if d == 0 {
1299-
Rect::from_min_max(
1300-
pos2(scroll_bar_rect.left(), cross.min),
1301-
pos2(scroll_bar_rect.right(), cross.max),
1302-
)
1312+
Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
13031313
} else {
1304-
Rect::from_min_max(
1305-
pos2(cross.min, scroll_bar_rect.top()),
1306-
pos2(cross.max, scroll_bar_rect.bottom()),
1307-
)
1314+
Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
13081315
};
13091316

13101317
let from_content = |content| {
@@ -1344,14 +1351,6 @@ impl Prepared {
13441351

13451352
let handle_rect = calculate_handle_rect(d, &state.offset);
13461353

1347-
let interact_id = id.with(d);
1348-
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1349-
Sense::click_and_drag()
1350-
} else {
1351-
Sense::hover()
1352-
};
1353-
let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
1354-
13551354
state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
13561355

13571356
if let Some(pointer_pos) = response.interact_pointer_pos() {

0 commit comments

Comments
 (0)