Revert "ui: Account for padding of parent container during scrollbar layout (#27402)" (#30544)

This reverts commit 82a7aca5a6.

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-05-12 11:47:04 +02:00 committed by GitHub
parent 907b2f0521
commit f0f0a52793
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 203 additions and 125 deletions

View file

@ -3,9 +3,9 @@ use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc};
use crate::{IntoElement, prelude::*, px, relative};
use gpui::{
Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element,
ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, IsZero, LayoutId, ListState,
ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, LayoutId, ListState,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent,
Size, Style, UniformListScrollHandle, Window, quad,
Size, Style, UniformListScrollHandle, Window, point, quad,
};
pub struct Scrollbar {
@ -15,8 +15,11 @@ pub struct Scrollbar {
}
impl ScrollableHandle for UniformListScrollHandle {
fn content_size(&self) -> Size<Pixels> {
self.0.borrow().base_handle.content_size()
fn content_size(&self) -> Option<ContentSize> {
Some(ContentSize {
size: self.0.borrow().last_item_size.map(|size| size.contents)?,
scroll_adjustment: None,
})
}
fn set_offset(&self, point: Point<Pixels>) {
@ -33,8 +36,11 @@ impl ScrollableHandle for UniformListScrollHandle {
}
impl ScrollableHandle for ListState {
fn content_size(&self) -> Size<Pixels> {
self.content_size_for_scrollbar()
fn content_size(&self) -> Option<ContentSize> {
Some(ContentSize {
size: self.content_size_for_scrollbar(),
scroll_adjustment: None,
})
}
fn set_offset(&self, point: Point<Pixels>) {
@ -59,8 +65,27 @@ impl ScrollableHandle for ListState {
}
impl ScrollableHandle for ScrollHandle {
fn content_size(&self) -> Size<Pixels> {
self.padded_content_size()
fn content_size(&self) -> Option<ContentSize> {
let last_children_index = self.children_count().checked_sub(1)?;
let mut last_item = self.bounds_for_item(last_children_index)?;
let mut scroll_adjustment = None;
if last_children_index != 0 {
// todo: PO: this is slightly wrong for horizontal scrollbar, as the last item is not necessarily the longest one.
let first_item = self.bounds_for_item(0)?;
last_item.size.height += last_item.origin.y;
last_item.size.width += last_item.origin.x;
scroll_adjustment = Some(first_item.origin);
last_item.size.height -= first_item.origin.y;
last_item.size.width -= first_item.origin.x;
}
Some(ContentSize {
size: last_item.size,
scroll_adjustment,
})
}
fn set_offset(&self, point: Point<Pixels>) {
@ -76,8 +101,14 @@ impl ScrollableHandle for ScrollHandle {
}
}
#[derive(Debug)]
pub struct ContentSize {
pub size: Size<Pixels>,
pub scroll_adjustment: Option<Point<Pixels>>,
}
pub trait ScrollableHandle: Any + Debug {
fn content_size(&self) -> Size<Pixels>;
fn content_size(&self) -> Option<ContentSize>;
fn set_offset(&self, point: Point<Pixels>);
fn offset(&self) -> Point<Pixels>;
fn viewport(&self) -> Bounds<Pixels>;
@ -118,26 +149,30 @@ impl ScrollbarState {
}
fn thumb_range(&self, axis: ScrollbarAxis) -> Option<Range<f32>> {
const MINIMUM_THUMB_SIZE: Pixels = px(25.);
let content_size = self.scroll_handle.content_size().along(axis);
let viewport_size = self.scroll_handle.viewport().size.along(axis);
if content_size.is_zero() || viewport_size.is_zero() || content_size < viewport_size {
const MINIMUM_THUMB_SIZE: f32 = 25.;
let ContentSize {
size: main_dimension_size,
scroll_adjustment,
} = self.scroll_handle.content_size()?;
let content_size = main_dimension_size.along(axis).0;
let mut current_offset = self.scroll_handle.offset().along(axis).min(px(0.)).abs().0;
if let Some(adjustment) = scroll_adjustment.and_then(|adjustment| {
let adjust = adjustment.along(axis).0;
if adjust < 0.0 { Some(adjust) } else { None }
}) {
current_offset -= adjustment;
}
let viewport_size = self.scroll_handle.viewport().size.along(axis).0;
if content_size < viewport_size {
return None;
}
let max_offset = content_size - viewport_size;
let current_offset = self
.scroll_handle
.offset()
.along(axis)
.clamp(-max_offset, Pixels::ZERO)
.abs();
let visible_percentage = viewport_size / content_size;
let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
if thumb_size > viewport_size {
return None;
}
let max_offset = content_size - viewport_size;
current_offset = current_offset.clamp(0., max_offset);
let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
let thumb_percentage_start = start_offset / viewport_size;
let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
@ -212,38 +247,57 @@ impl Element for Scrollbar {
window: &mut Window,
cx: &mut App,
) {
const EXTRA_PADDING: Pixels = px(5.0);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
let axis = self.kind;
let colors = cx.theme().colors();
let thumb_background = colors
.surface_background
.blend(colors.scrollbar_thumb_background);
let is_vertical = self.kind == ScrollbarAxis::Vertical;
let extra_padding = px(5.0);
let padded_bounds = if is_vertical {
Bounds::from_corners(
bounds.origin + point(Pixels::ZERO, extra_padding),
bounds.bottom_right() - point(Pixels::ZERO, extra_padding * 3),
)
} else {
Bounds::from_corners(
bounds.origin + point(extra_padding, Pixels::ZERO),
bounds.bottom_right() - point(extra_padding * 3, Pixels::ZERO),
)
};
let padded_bounds = Bounds::from_corners(
bounds
.origin
.apply_along(axis, |origin| origin + EXTRA_PADDING),
bounds
.bottom_right()
.apply_along(axis, |track_end| track_end - 3.0 * EXTRA_PADDING),
);
let thumb_offset = self.thumb.start * padded_bounds.size.along(axis);
let thumb_end = self.thumb.end * padded_bounds.size.along(axis);
let thumb_bounds = Bounds::new(
padded_bounds
.origin
.apply_along(axis, |origin| origin + thumb_offset),
padded_bounds
.size
.apply_along(axis, |_| thumb_end - thumb_offset)
.apply_along(axis.invert(), |width| width / 1.5),
);
let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0);
let mut thumb_bounds = if is_vertical {
let thumb_offset = self.thumb.start * padded_bounds.size.height;
let thumb_end = self.thumb.end * padded_bounds.size.height;
let thumb_upper_left = point(
padded_bounds.origin.x,
padded_bounds.origin.y + thumb_offset,
);
let thumb_lower_right = point(
padded_bounds.origin.x + padded_bounds.size.width,
padded_bounds.origin.y + thumb_end,
);
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
} else {
let thumb_offset = self.thumb.start * padded_bounds.size.width;
let thumb_end = self.thumb.end * padded_bounds.size.width;
let thumb_upper_left = point(
padded_bounds.origin.x + thumb_offset,
padded_bounds.origin.y,
);
let thumb_lower_right = point(
padded_bounds.origin.x + thumb_end,
padded_bounds.origin.y + padded_bounds.size.height,
);
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
};
let corners = if is_vertical {
thumb_bounds.size.width /= 1.5;
Corners::all(thumb_bounds.size.width / 2.0)
} else {
thumb_bounds.size.height /= 1.5;
Corners::all(thumb_bounds.size.height / 2.0)
};
window.paint_quad(quad(
thumb_bounds,
corners,
@ -254,39 +308,7 @@ impl Element for Scrollbar {
));
let scroll = self.state.scroll_handle.clone();
enum ScrollbarMouseEvent {
GutterClick,
ThumbDrag(Pixels),
}
let compute_click_offset =
move |event_position: Point<Pixels>,
item_size: Size<Pixels>,
event_type: ScrollbarMouseEvent| {
let viewport_size = padded_bounds.size.along(axis);
let thumb_size = thumb_bounds.size.along(axis);
let thumb_offset = match event_type {
ScrollbarMouseEvent::GutterClick => thumb_size / 2.,
ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
};
let thumb_start = (event_position.along(axis)
- padded_bounds.origin.along(axis)
- thumb_offset)
.clamp(px(0.), viewport_size - thumb_size);
let max_offset = (item_size.along(axis) - viewport_size).max(px(0.));
let percentage = if viewport_size > thumb_size {
thumb_start / (viewport_size - thumb_size)
} else {
0.
};
-max_offset * percentage
};
let axis = self.kind;
window.on_mouse_event({
let scroll = scroll.clone();
@ -301,17 +323,39 @@ impl Element for Scrollbar {
if thumb_bounds.contains(&event.position) {
let offset = event.position.along(axis) - thumb_bounds.origin.along(axis);
state.drag.set(Some(offset));
} else {
let click_offset = compute_click_offset(
event.position,
scroll.content_size(),
ScrollbarMouseEvent::GutterClick,
);
scroll.set_offset(scroll.offset().apply_along(axis, |_| click_offset));
} else if let Some(ContentSize {
size: item_size, ..
}) = scroll.content_size()
{
let click_offset = {
let viewport_size = padded_bounds.size.along(axis);
let thumb_size = thumb_bounds.size.along(axis);
let thumb_start = (event.position.along(axis)
- padded_bounds.origin.along(axis)
- (thumb_size / 2.))
.clamp(px(0.), viewport_size - thumb_size);
let max_offset = (item_size.along(axis) - viewport_size).max(px(0.));
let percentage = if viewport_size > thumb_size {
thumb_start / (viewport_size - thumb_size)
} else {
0.
};
-max_offset * percentage
};
match axis {
ScrollbarAxis::Horizontal => {
scroll.set_offset(point(click_offset, scroll.offset().y));
}
ScrollbarAxis::Vertical => {
scroll.set_offset(point(scroll.offset().x, click_offset));
}
}
}
}
});
window.on_mouse_event({
let scroll = scroll.clone();
move |event: &ScrollWheelEvent, phase, window, _| {
@ -323,19 +367,44 @@ impl Element for Scrollbar {
}
}
});
let state = self.state.clone();
let axis = self.kind;
window.on_mouse_event(move |event: &MouseMoveEvent, _, window, cx| {
if let Some(drag_state) = state.drag.get().filter(|_| event.dragging()) {
let drag_offset = compute_click_offset(
event.position,
scroll.content_size(),
ScrollbarMouseEvent::ThumbDrag(drag_state),
);
scroll.set_offset(scroll.offset().apply_along(axis, |_| drag_offset));
window.refresh();
if let Some(id) = state.parent_id {
cx.notify(id);
if let Some(ContentSize {
size: item_size, ..
}) = scroll.content_size()
{
let drag_offset = {
let viewport_size = padded_bounds.size.along(axis);
let thumb_size = thumb_bounds.size.along(axis);
let thumb_start = (event.position.along(axis)
- padded_bounds.origin.along(axis)
- drag_state)
.clamp(px(0.), viewport_size - thumb_size);
let max_offset = (item_size.along(axis) - viewport_size).max(px(0.));
let percentage = if viewport_size > thumb_size {
thumb_start / (viewport_size - thumb_size)
} else {
0.
};
-max_offset * percentage
};
match axis {
ScrollbarAxis::Horizontal => {
scroll.set_offset(point(drag_offset, scroll.offset().y));
}
ScrollbarAxis::Vertical => {
scroll.set_offset(point(scroll.offset().x, drag_offset));
}
};
window.refresh();
if let Some(id) = state.parent_id {
cx.notify(id);
}
}
} else {
state.drag.set(None);