ScrollArea: scroll_bar_rect is not used for the scrollbar's interaction rect, creating a dead zone where no bar is drawn
- 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
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