emilk / emilk/egui

ScrollArea: scroll_bar_rect is not used for the scrollbar's interaction rect, creating a dead zone where no bar is drawn

Open
#8,495 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
30.6k
Forks
2.1k
Avg merge
1d 9h
Merged PRs (30d)
72

Description

### Summary

`ScrollArea::scroll_bar_rect` is respected when drawing the scrollbar and when computing the handle's geometry and travel, but **not** when computing the rect the scrollbar senses interaction on. That rect is derived from `outer_rect`, so it covers area where no scrollbar is drawn. A press there is treated as a scrollbar press and snaps the offset to its minimum, and during a drag that area behaves as a dead zone.

### Version

egui 0.36.1. `crates/egui/src/containers/scroll_area.rs` appears unchanged on `main` since shortly before that release, so I believe this is current.

### Detail

In `scroll_area.rs`, `scroll_bar_rect` is resolved at line 1278:

```rust
let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
```

The handle geometry and its travel are computed from it — `from_content` (1366-1372) maps content onto `scroll_bar_rect`, and `handle_travel` (1421-1422) is `scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d])`.

But the interaction rect comes from `outer_rect` (1318-1322, then 1334):

```rust
let max_bar_rect = if d == 0 {
outer_rect.with_min_y(max_cross - full_width)
} else {
outer_rect.with_min_x(max_cross - full_width)
};
// ...
let response = ui.interact(max_bar_rect, interact_id, sense);
```

When a caller passes a `scroll_bar_rect` smaller than `outer_rect`, the two disagree along the scrolling axis. Pointer positions inside `max_bar_rect` but outside `handle_travel` yield a `new_handle_top` below `handle_travel.start()`; `remap` at 1426 is unclamped, so `state.offset[d]` goes negative and is then clamped to 0 at 1437-1438.

Two consequences:

1. A press in the region where no scrollbar is drawn is sensed as a scrollbar press, and the offset snaps to the minimum.
2. During a drag, that region is dead. Because `scroll_start_offset_from_top_left` is latched for the whole drag (`get_or_insert_with`, 1406-1418) and cleared only when the pointer interaction ends (1433), the offset stays pinned until the pointer comes back past `handle_travel.start() + latch`. Releasing and re-grabbing recovers faster than dragging back, so it presents as the scrollbar "sticking" until the mouse button is released.

The dead zone's width is the gap between the two rects' near edges.

### Who hits this

Any caller of `ScrollArea::scroll_bar_rect`. `egui_table`'s `SplitScroll` is one: it hands the `ScrollArea` the full rect while confining the bars to the bottom-right quadrant, so they are not painted over the sticky row/column regions — see the `ScrollArea::new(...).scroll_bar_rect(bottom_right_rect)` call against `ui.new_child(UiBuilder::new().max_rect(rect))` in https://github.com/rerun-io/egui_table/blob/main/src/split_scroll.rs

For a table with a fixed left column, the strip above that column along the horizontal bar's axis therefore behaves as scrollbar even though no bar is drawn there, and the horizontal dead zone becomes asymmetric — the left side is wider than the right by the fixed region's width. I measured this in an app using `egui_table` with one sticky column: of 14 observed clamp events during horizontal drags, 12 were at the minimum end.

Note that dead zones at the ends of a handle's travel are normal and expected; the report here is specifically the *extra* width contributed by the rect mismatch, in a region where nothing is drawn.

### Expected

The scrollbar should sense interaction only where it is drawn — `max_bar_rect` derived from `scroll_bar_rect` rather than `outer_rect` along the scrolling axis, so the interactive region and the handle's travel share bounds.

### Note

No PR, deliberately. The change looks small, and in the default case (`scroll_bar_rect` is `None`, so it resolves to `inner_rect`) I would expect switching to be close to a no-op — but whether `outer_rect` is intentional there, and how it should interact with the `ui.clip_rect()` adjustment at 1303-1312, is a maintainer call rather than something I should guess at.

Possibly the same family as #7849, which was closed by its author without a diagnosis, though that one describes the cursor leaving the window rather than a fixed dead zone.

Contributor guide

Open the contributing guide

Research direction

Start in crates/egui/src/containers/scroll_area.rs around the scroll_bar_rect resolution, max_bar_rect construction, and handle interaction logic at the referenced lines. Compare the interaction bounds with scroll_bar_rect and handle_travel, including the ui.clip_rect() adjustment. Done means regions where no scrollbar is drawn no longer respond as scrollbar interaction, while the default scroll_bar_rect behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.