diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index ed1666c530..4655c92409 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1664,6 +1664,11 @@ impl Interactivity { window: &mut Window, _cx: &mut App, ) -> Point { + fn round_to_two_decimals(pixels: Pixels) -> Pixels { + const ROUNDING_FACTOR: f32 = 100.0; + (pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR + } + if let Some(scroll_offset) = self.scroll_offset.as_ref() { let mut scroll_to_bottom = false; let mut tracked_scroll_handle = self @@ -1678,8 +1683,16 @@ impl Interactivity { let rem_size = window.rem_size(); let padding = style.padding.to_pixels(bounds.size.into(), rem_size); let padding_size = size(padding.left + padding.right, padding.top + padding.bottom); + // The floating point values produced by Taffy and ours often vary + // slightly after ~5 decimal places. This can lead to cases where after + // subtracting these, the container becomes scrollable for less than + // 0.00000x pixels. As we generally don't benefit from a precision that + // high for the maximum scroll, we round the scroll max to 2 decimal + // places here. let padded_content_size = self.content_size + padding_size; - let scroll_max = (padded_content_size - bounds.size).max(&Size::default()); + let scroll_max = (padded_content_size - bounds.size) + .map(round_to_two_decimals) + .max(&Default::default()); // Clamp scroll offset in case scroll max is smaller now (e.g., if children // were removed or the bounds became larger). let mut scroll_offset = scroll_offset.borrow_mut(); @@ -1692,7 +1705,7 @@ impl Interactivity { } if let Some(mut scroll_handle_state) = tracked_scroll_handle { - scroll_handle_state.padded_content_size = padded_content_size; + scroll_handle_state.max_offset = scroll_max; } *scroll_offset @@ -2936,7 +2949,7 @@ impl ScrollAnchor { struct ScrollHandleState { offset: Rc>>, bounds: Bounds, - padded_content_size: Size, + max_offset: Size, child_bounds: Vec>, scroll_to_bottom: bool, overflow: Point, @@ -2965,6 +2978,11 @@ impl ScrollHandle { *self.0.borrow().offset.borrow() } + /// Get the maximum scroll offset. + pub fn max_offset(&self) -> Size { + self.0.borrow().max_offset + } + /// Get the top child that's scrolled into view. pub fn top_item(&self) -> usize { let state = self.0.borrow(); @@ -2999,11 +3017,6 @@ impl ScrollHandle { self.0.borrow().child_bounds.get(ix).cloned() } - /// Get the size of the content with padding of the container. - pub fn padded_content_size(&self) -> Size { - self.0.borrow().padded_content_size - } - /// scroll_to_item scrolls the minimal amount to ensure that the child is /// fully visible pub fn scroll_to_item(&self, ix: usize) { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 35a3b622b2..f24d38794f 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -411,9 +411,9 @@ impl ListState { self.0.borrow_mut().set_offset_from_scrollbar(point); } - /// Returns the size of items we have measured. + /// Returns the maximum scroll offset according to the items we have measured. /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly. - pub fn content_size_for_scrollbar(&self) -> Size { + pub fn max_offset_for_scrollbar(&self) -> Size { let state = self.0.borrow(); let bounds = state.last_layout_bounds.unwrap_or_default(); @@ -421,7 +421,7 @@ impl ListState { .scrollbar_drag_start_height .unwrap_or_else(|| state.items.summary().height); - Size::new(bounds.size.width, height) + Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height)) } /// Returns the current scroll offset adjusted for the scrollbar diff --git a/crates/terminal_view/src/terminal_scrollbar.rs b/crates/terminal_view/src/terminal_scrollbar.rs index 18e135be2e..c8565a42be 100644 --- a/crates/terminal_view/src/terminal_scrollbar.rs +++ b/crates/terminal_view/src/terminal_scrollbar.rs @@ -46,9 +46,16 @@ impl TerminalScrollHandle { } impl ScrollableHandle for TerminalScrollHandle { - fn content_size(&self) -> Size { + fn max_offset(&self) -> Size { let state = self.state.borrow(); - size(Pixels::ZERO, state.total_lines as f32 * state.line_height) + size( + Pixels::ZERO, + state + .total_lines + .checked_sub(state.viewport_lines) + .unwrap_or(0) as f32 + * state.line_height, + ) } fn offset(&self) -> Point { diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 2a8c4885ac..17ab2e788f 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -29,8 +29,8 @@ impl ThumbState { } impl ScrollableHandle for UniformListScrollHandle { - fn content_size(&self) -> Size { - self.0.borrow().base_handle.content_size() + fn max_offset(&self) -> Size { + self.0.borrow().base_handle.max_offset() } fn set_offset(&self, point: Point) { @@ -47,8 +47,8 @@ impl ScrollableHandle for UniformListScrollHandle { } impl ScrollableHandle for ListState { - fn content_size(&self) -> Size { - self.content_size_for_scrollbar() + fn max_offset(&self) -> Size { + self.max_offset_for_scrollbar() } fn set_offset(&self, point: Point) { @@ -73,8 +73,8 @@ impl ScrollableHandle for ListState { } impl ScrollableHandle for ScrollHandle { - fn content_size(&self) -> Size { - self.padded_content_size() + fn max_offset(&self) -> Size { + self.max_offset() } fn set_offset(&self, point: Point) { @@ -91,7 +91,10 @@ impl ScrollableHandle for ScrollHandle { } pub trait ScrollableHandle: Any + Debug { - fn content_size(&self) -> Size; + fn content_size(&self) -> Size { + self.viewport().size + self.max_offset() + } + fn max_offset(&self) -> Size; fn set_offset(&self, point: Point); fn offset(&self) -> Point; fn viewport(&self) -> Bounds; @@ -149,17 +152,17 @@ impl ScrollbarState { fn thumb_range(&self, axis: ScrollbarAxis) -> Option> { const MINIMUM_THUMB_SIZE: Pixels = px(25.); - let content_size = self.scroll_handle.content_size().along(axis); + let max_offset = self.scroll_handle.max_offset().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 { + if max_offset.is_zero() || viewport_size.is_zero() { return None; } + let content_size = viewport_size + max_offset; 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; let current_offset = self .scroll_handle .offset() @@ -307,7 +310,7 @@ impl Element for Scrollbar { let compute_click_offset = move |event_position: Point, - item_size: Size, + max_offset: Size, event_type: ScrollbarMouseEvent| { let viewport_size = padded_bounds.size.along(axis); @@ -323,7 +326,7 @@ impl Element for Scrollbar { - thumb_offset) .clamp(px(0.), viewport_size - thumb_size); - let max_offset = (item_size.along(axis) - viewport_size).max(px(0.)); + let max_offset = max_offset.along(axis); let percentage = if viewport_size > thumb_size { thumb_start / (viewport_size - thumb_size) } else { @@ -347,7 +350,7 @@ impl Element for Scrollbar { } else { let click_offset = compute_click_offset( event.position, - scroll.content_size(), + scroll.max_offset(), ScrollbarMouseEvent::GutterClick, ); scroll.set_offset(scroll.offset().apply_along(axis, |_| click_offset)); @@ -373,7 +376,7 @@ impl Element for Scrollbar { ThumbState::Dragging(drag_state) if event.dragging() => { let drag_offset = compute_click_offset( event.position, - scroll.content_size(), + scroll.max_offset(), ScrollbarMouseEvent::ThumbDrag(drag_state), ); scroll.set_offset(scroll.offset().apply_along(axis, |_| drag_offset)); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 7cc10c27f7..e57b103c61 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -18,7 +18,7 @@ use futures::{StreamExt, stream::FuturesUnordered}; use gpui::{ Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div, DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, - Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, + Focusable, IsZero, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window, actions, anchored, deferred, prelude::*, }; @@ -46,8 +46,8 @@ use theme::ThemeSettings; use ui::{ ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton, IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, - PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip, - prelude::*, right_click_menu, + PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*, + right_click_menu, }; use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front}; @@ -2865,10 +2865,9 @@ impl Pane { } }) .children(pinned_tabs.len().ne(&0).then(|| { - let content_width = self.tab_bar_scroll_handle.content_size().width; - let viewport_width = self.tab_bar_scroll_handle.viewport().size.width; + let max_scroll = self.tab_bar_scroll_handle.max_offset().width; // We need to check both because offset returns delta values even when the scroll handle is not scrollable - let is_scrollable = content_width > viewport_width; + let is_scrollable = !max_scroll.is_zero(); let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.); let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count; h_flex()