Files
tty7/src/ui/scrollbar.rs
T
l0ng-ai 081e191bb0 perf(diff-overlay): draw the patch as a virtualised row list (#799)
The overlay built its whole patch as a nested element tree on every frame:
a card per file, a header per hunk, six elements per line. gpui notifies the
view on each scroll wheel event, so a few hundred lines of diff rebuilt tens
of thousands of elements tens of times a second, and the window stalled.

Flatten the tree into one row per line in a new `diff_list` module and draw it
with `gpui::list`, which builds only the rows on screen. The rows are rebuilt
only when what they are built from changes, so scrolling no longer re-splits
hunks or re-clones every line, and a change to one file splices just the rows
it touched rather than resetting the list and losing the scroll position.

The key that decides a rebuild takes the snapshot each frame was asked about
even when it matched only by contents. A probe that finds nothing new still
lands a fresh `Arc` over an equal snapshot; a key left pointing at the old one
would go on walking the whole patch to prove the two equal, once per wheel
event, which is the cost the key exists to avoid.

A list counts a row it has not laid out yet as zero tall, which left the
scrollbar reading an 800-line patch as one viewport: its thumb filled the
track, and a drag from top to bottom travelled 248px and stopped. The rows
below the fold are counted at the 19px both views already give a line of a
patch, through `ListState::with_size_hint` — added to the gpui fork for this,
`Cargo.lock` following its `tty7` branch to `ece710e3`.

A card cannot survive that flattening — its rows are separate items now — so
the frame it drew is gone, and with it the grey header bars and hunk bands
that made the overlay the one view in the app still speaking gpui-component's
default container language. The rows take the source control panel's own
measurements instead: 26px, 10px inset, 5px radius, colour only under the
pointer. The title bar's view switch loses its border for the same reason.
2026-09-07 22:13:57 +08:00

85 lines
3.3 KiB
Rust

use gpui::{AnyElement, ElementId, Pixels, ScrollHandle, div, prelude::*, px};
use gpui_component::scroll::{Scrollbar, ScrollbarHandle};
use gpui_component::v_flex;
/// Overlays the shared vertical scrollbar on a scroll area.
///
/// The returned element takes its height from `flex_1`, so it wants a flex
/// column with a height of its own to grow inside — give it one rather than
/// dropping it straight into whatever is around it. Without that the wrapper
/// sizes to its content, the `size_full` scroll area inside grows with it, and
/// the pane stops scrolling because nothing overflows any more.
pub(crate) fn with_vertical_scrollbar<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
scroll_area: impl IntoElement,
handle: &H,
) -> AnyElement {
with_inset_vertical_scrollbar(id, scroll_area, handle, px(0.))
}
/// The same bar, held `inset_y` clear of the top and bottom of the area.
///
/// Every list inside a bordered panel wants the bar to run the full height —
/// the border is already the edge. A scroll area that *is* the window has no
/// such edge, and a bar that runs to the last pixel lands on the rounded
/// corner and reads as if it had been clipped.
pub(crate) fn with_inset_vertical_scrollbar<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
scroll_area: impl IntoElement,
handle: &H,
inset_y: Pixels,
) -> AnyElement {
v_flex()
.relative()
.flex_1()
.min_h_0()
// Stretching would size this the same, but not *definitely*: a `w_full`
// inside the scroll area would then have no width to be a percentage
// of, and would fall back to its content. That is how the settings
// reading column lost its 640px cap on the Chinese page — one wide row
// measured wider, and every row followed it.
.w_full()
.child(scroll_area)
.child(
div()
.absolute()
.top(inset_y)
.left_0()
.right_0()
.bottom(inset_y)
// No `scrollbar_show` override: it falls back to
// `cx.theme().scrollbar_show`, which `apply_theme` pins to
// `Scrolling` for every list in the app. Overriding it here
// would be one list disagreeing with the rest.
.child(Scrollbar::vertical(handle).id(id)),
)
.into_any_element()
}
/// The same bar, for a scroll area that already carries its own height —
/// a `max_h` box, say.
///
/// `with_vertical_scrollbar` grows into a flex parent, which is wrong for a box
/// that is already the size it wants to be: taking `flex_1` there would either
/// stretch it past its cap or collapse it. This wrapper only lays the bar over
/// whatever the area measured, so the layout is exactly what it was without it.
pub(crate) fn over_vertical_scroll(
id: impl Into<ElementId>,
scroll_area: impl IntoElement,
handle: &ScrollHandle,
) -> AnyElement {
div()
.relative()
.child(scroll_area)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(handle).id(id)),
)
.into_any_element()
}