Skip to content

scrollbar: Let the vertical scrollbar sit on the left side - #3416

Merged
huacnlee merged 3 commits into
longbridge:nextfrom
GigLaboCom:scrollbar-left-side
Oct 9, 2026
Merged

huacnlee merged 3 commits into
longbridge:nextfrom
GigLaboCom:scrollbar-left-side

Conversation

@glani

@glani glani commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

The vertical scrollbar is hard-coded to the right edge and the horizontal one to the bottom. In a side-by-side diff the left pane's scrollbar belongs on its outer (left) edge, so the two panes mirror each other.

This adds ScrollbarPlacement, one value for both axes, set with one call — Scrollbar::placement, and scrollbar_placement / set_scrollbar_placement on the editor state:

vertical horizontal
BottomRight (default, unchanged) right bottom
BottomLeft left bottom
TopRight right top
TopLeft left top

Each axis follows its half of the placement: the track sits on that edge, the thumb and its hit area are anchored to it, and the bar slides in from that edge. When both bars show, the vertical one keeps its full height and the horizontal one stops short of it — at its start when the vertical bar is on the left, at its end when it is on the right. A single-axis scrollbar uses only its own half. The editor reserves no room for its horizontal bar at the bottom today, so at the top it overlays the first line the same way. Nothing changes by default.

Targets next: it is the first half of a mirrored side-by-side diff whose second half, #3417, builds on #3359's gutter markers, which are on next.

Screenshot

Before After
before scrollbar-left

The four placements (ScrollbarMode::Always); the story switches Dataset and Placement from one Options menu (d8057f92):

scrollbar placements, light

scrollbar placements, dark

Public API

gpui_base (re-exported by gpui_component, also from gpui_component::scroll):

  • ScrollbarPlacement { BottomRight, BottomLeft, TopRight, TopLeft } — default BottomRight; is_left() (vertical bar on the left), is_top() (horizontal bar at the top).
  • Scrollbar::placement(mut self, placement: ScrollbarPlacement) -> Self.
  • InputBaseState::scrollbar_placement(mut self, placement: ScrollbarPlacement) -> Self — builder for the editor's scrollbars.
  • InputBaseState::set_scrollbar_placement(&mut self, placement: ScrollbarPlacement, cx: &mut Context<Self>) — replaces the whole placement at runtime.

How to Test

  • cargo test -p gpui-base --lib scrollbar: the Scrollbar harness clicks and drags on each placement — the vertical track and thumb on the left, the horizontal track and thumb at the top, the horizontal track starting after a left bar and ending before a right one with both bars at the top, a thumb drag and a track click moving the offset the right way — and visibility_translation_moves_toward_the_nearest_edge covers all four placements on both axes; an editor layout test for TopLeft. Each fails with its code reverted.
  • cargo run -p gpui-component-story -- scrollbar, then Options → Placement.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

@glani
glani force-pushed the scrollbar-left-side branch from eb4366c to afe8e70 Compare October 8, 2026 20:32
@glani
glani marked this pull request as ready for review October 8, 2026 20:32
@glani
glani force-pushed the scrollbar-left-side branch from afe8e70 to 0338bdc Compare October 8, 2026 21:26
@glani
glani changed the base branch from main to next October 8, 2026 21:26

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds left-side vertical scrollbars for mirrored diff panes, while preserving the existing right-side default. The requirement is reasonable, and the geometry changes appear consistent with that purpose.

Before merging, please reconsider the public placement API so it accounts for horizontal scrollbars as well. Scrollbar::side(Side) and the editor's scrollbar_side currently expose a general scrollbar setting that only describes the vertical axis. We should establish a coherent placement contract for both axes before publishing this API.

One option to consider is a single placement value representing the complete arrangement:

Scrollbar::new(&handle)
    .placement(ScrollbarPlacement::BottomLeft);

EditorState::new(window, cx)
    .scrollbar_placement(ScrollbarPlacement::BottomLeft);

For example:

  • BottomRight: vertical on the right, horizontal at the bottom (the existing default).
  • BottomLeft: vertical on the left, horizontal at the bottom.
  • TopRight: vertical on the right, horizontal at the top.
  • TopLeft: vertical on the left, horizontal at the top.

The editor's runtime setter would replace the complete placement with one call as well. For a single-axis scrollbar, only the corresponding part of the placement would apply.

This is a suggested design, not a requirement to use these exact names or this exact enum. Please use it as a reference, or propose a clearer alternative that covers both axes with one configuration method. Avoid requiring two axis-specific methods or making repeated calls to the same setter implicitly accumulate different axis settings.

The resulting implementation should consistently handle track placement, corner avoidance, thumb and hitbox geometry, and entrance direction for both axes. Please also keep the default behavior unchanged and update both documentation locales.

Validation: reviewed the diff at 0338bdc73 and the repository guides; git diff --check passed. Tests and real-window validation were not run. This request concerns the public API design, not a demonstrated regression in the current left-side geometry.

glani and others added 2 commits October 9, 2026 14:00
Replace `Scrollbar::side(Side)` with `Scrollbar::placement(ScrollbarPlacement)`,
which places both axes at once: `BottomRight` (the default), `BottomLeft`,
`TopRight` and `TopLeft`. A horizontal scrollbar at the top gets its track,
thumb and hitbox on the top edge and slides in from the top; when both
scrollbars show, the horizontal track gives way at the vertical scrollbar's
end. The editor's `scrollbar_side` and `set_scrollbar_side` become
`scrollbar_placement` and `set_scrollbar_placement`.
@huacnlee

huacnlee commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

I added the Story changes locally: a single test area with an Options menu for switching Dataset and Placement, instead of displaying all four placements at once.

Please cherry-pick commit d8057f92 from the pr-3416-story-options branch into this PR:

git fetch https://github.com/longbridge/gpui-kit.git pr-3416-story-options
git cherry-pick d8057f92

For future PRs, please use a fork under your personal GitHub account and enable “Allow edits from maintainers.” This PR uses an organization-owned fork, and both SSH and HTTPS pushes to your branch were denied with permission errors despite maintainer_can_modify being enabled, so I could not apply the changes directly.

Being able to make small adjustments directly would reduce back-and-forth and help us move the PR forward faster.

@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — took d8057f92 as is (fast-forward, same hash), and the description now points at the Options menu. Noted on the fork: further PRs will come from a personal fork with maintainer edits allowed.

@huacnlee
huacnlee merged commit 63068d0 into longbridge:next Oct 9, 2026
12 checks passed
huacnlee added a commit that referenced this pull request Oct 9, 2026
## Description

Builds on #3416 (`ScrollbarPlacement`, merged into `next`) and #3359's
gutter markers.

In a side-by-side diff, both gutters can face the center, so the line
numbers of corresponding lines sit next to each other across the divider
and can be compared directly. The editor's gutter is always on the left
of the text today, so the left pane cannot do that.

This adds `gutter_side(Side)` / `set_gutter_side` to the editor state
for the left pane of such a diff, default `Side::Left` (unchanged). On
the right the gutter is mirrored: the same columns, in the same order
counted from the text, with the line numbers aligned toward it. The text
and gutter x offsets are computed once per layout and used for painting,
hit testing, IME bounds and scroll-into-view. A scrollbar on the
gutter's side stays outermost. With the gutter on the right and the
scrollbar on the left — the left pane's outer edge — the text keeps
clear of the scrollbar's whole track: its effective width, the active
thumb included, less the editor's left padding, at least the usual 10 px
margin; that one reservation sets the text origin, width, wrap width and
scroll size.

Line numbers are placed by their shaped width rather than padded with
spaces: in a proportional font a space is about a third of a digit, so
the default left gutter left right-aligned numbers ragged and 6–11 px
short of the text; they now line up against it on either side. `Editor`
puts its narrow padding on the gutter's side.

`gutter_order([GutterColumn])` orders the gutter's columns from the text
outward, on either side: the default is `[FoldIcons, LineNumbers,
Markers]` (unchanged); `[FoldIcons, Markers, LineNumbers]` puts a diff's
change markers between the text and the line numbers, as IntelliJ's diff
does. A column left out follows the listed ones in its default order.

The **Editor Diff** story (`70b271ad`, @huacnlee) shows original and
modified source with change markers; its Options menu switches between
both gutters on the left and center-facing gutters, and optionally puts
the markers before the line numbers. The two panes scroll together, by
wheel and by dragging either scrollbar. The ordinary Editor story is
unchanged.

## Screenshot

| Before | After |
| ------ | ----- |
| <img width="800" alt="scrollbar-left"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/scrollbar-left.png"
/> | <img width="800" alt="mirrored"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/mirrored.png"
/> |

A side-by-side diff with changed lines marked; the left pane uses
`.scrollbar_placement(ScrollbarPlacement::BottomLeft).gutter_side(Side::Right)`,
mirrored:

| Both gutters on the left | Mirrored |
| ------------------------ | -------- |
| <img width="800" alt="both gutters on the left"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/gutter-before.png"
/> | <img width="800" alt="mirrored"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/gutter-mirrored.png"
/> |

The columns in IntelliJ's order,
`.gutter_order([GutterColumn::FoldIcons, GutterColumn::Markers,
GutterColumn::LineNumbers])` on both panes — without and with folding
(an empty fold column here):

| IntelliJ's order | With folding |
| ---------------- | ------------ |
| <img width="800" alt="IntelliJ's order"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/gutter-intellij.png"
/> | <img width="800" alt="IntelliJ's order with folding"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/gutter-intellij-fold.png"
/> |

## Public API

`gpui_base` (re-exported by `gpui_component`):

- `InputBaseState::gutter_side(mut self, side: Side) -> Self` — builder:
which side of the text the gutter is drawn on; default `Side::Left`,
`Side::Right` for the left pane of a side-by-side diff.
- `InputBaseState::set_gutter_side(&mut self, side: Side, cx: &mut
Context<Self>)` — the same, at runtime.
- `InputPresentation::gutter_side(&self) -> Side` — the side the
editor's gutter sits on.
- `GutterColumn { FoldIcons, LineNumbers, Markers }` — a column of the
gutter.
- `InputBaseState::gutter_order(mut self, columns: impl
IntoIterator<Item = GutterColumn>) -> Self` / `set_gutter_order(&mut
self, columns, cx: &mut Context<Self>)` — the columns from the text
outward; default `[FoldIcons, LineNumbers, Markers]`; a column left out
follows in its default order, a repeat is ignored.

## How to Test

- `cargo test -p gpui-base --lib`: new tests for the text and gutter
origins with an unchanged wrap width, the mirrored fold icons and
columns, the scrollbar staying outermost (left/left, right/left,
right/right), clicks in the text and in the gutter on both sides, caret
/ IME / `range_to_bounds` / selection-path x including a long line
scrolled clear of the gutter, a right-side case in the existing
gutter-bounds test, every gutter column — markers included — mirrored to
0.01 px for all six orders with folding on and off, the order normalised
and counted from the text on both sides, clicks on the fold icon and the
line number in every side/order combination, `Editor` keeping its
gutter-side padding, and the text clear of the whole track of a left
scrollbar with zero padding and with a wider themed track. Each fails
with its code reverted.
- `cargo test -p gpui-component-story --lib editor_diff`: layout
switching keeps both sources, and the panes stay aligned on wheel input
and while either scrollbar is dragged.
- `cargo run -p gpui-component-story -- 'Editor Diff'`, then Options.

## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [x] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)

---------

Co-authored-by: Jason Lee <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants