From ded646760467d653fd57ee3ef4fc1edcfafba6ae Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 13 Sep 2025 00:43:16 +0200 Subject: [PATCH] Refactor the scrollbar component (#36105) Closes https://github.com/zed-industries/zed/issues/37621 Improves https://github.com/zed-industries/zed/issues/24623 Adding scrollbars withing Zed's UI currently is rather cumbersome, as it requires the copying of a lot of code in order for these to work. Wiring up settings for scrollbar visibilty always has to be done at the call site and the state has to be saved and maintained by the caller as well. Similarly, reserving space has to also be handled by the caller. This PR changes the way scrollbars work in Zed fundamentally by making use of the new `use_keyed_state` APIs: Instead of saving the state at the call site, the window now keeps track of the state corresponding to scrollbars. This enables us to add scrollbars with e.g. one simple call on divs: ```rust div() .vertical_scrollbar(window, cx) ``` will add a scrollbar to the corresponding container. There are some more improvements regarding tracking of scrollbar visibility settings (which is now handled by a trait for each setting that supports this) as well as reserving space. Additionally, all needed stuff for layouting, catching events and reserving space is also now managed by the scrollbar component instead. This drastically reduces the amount of event listeners and makes layouting of two scrollbars easier. Furthermore, this paves the way for more improvements to scrollbars, such as graceful auto-hide. Only downsight here is that we lose some customizability in a few areas. However, once this lands, we gain the ability to quickly follow these up without breaking stuff elsewhere. This also already fixes a few bugs: - Scrollbars no longer flicker on first render. - Auto-hide now properly works for all scrollbars. - If the content size changes, the scrollbar is updated on the same frame. Both of these happened because we were computing the scrollbar sizes too early, causing us to use the sizes from the previous frame or unitialized sizes. - The project panel no longer jumps if scrolled all the way to the bottom and the scrollbar actually auto-hides. Still TODO: - [x] Fix scrolling in the debugger memory view - [x] Clean up some more in the scrollbar component and reduce clones there - [x] Ensure we don't over-notify the entity the scrollbar is rendered within - [x] Make sure auto-hide properly works for all cases - [x] Check whether we want to implement the scrollbar trait for `UniformList`s as well - ~~ [ ] Use for uniformlist where possible~~ Postponed - [x] Improve layout for cases where we render both scrollbars. Release Notes: - N/A --- Cargo.lock | 2 +- crates/agent_ui/src/acp/thread_history.rs | 54 +- crates/agent_ui/src/acp/thread_view.rs | 74 +- crates/agent_ui/src/agent_configuration.rs | 57 +- .../src/session/running/breakpoint_list.rs | 44 +- .../src/session/running/memory_view.rs | 212 ++- .../src/session/running/module_list.rs | 42 +- .../src/session/running/stack_frame_list.rs | 42 +- .../src/session/running/variable_list.rs | 46 +- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_settings.rs | 24 +- crates/editor/src/element.rs | 4 +- crates/editor/src/hover_popover.rs | 92 +- crates/editor/src/scroll.rs | 10 +- crates/editor/src/signature_help.rs | 35 +- crates/extensions_ui/src/extensions_ui.rs | 43 +- crates/git_ui/src/git_panel.rs | 391 +---- crates/git_ui/src/git_panel_settings.rs | 19 +- crates/gpui/src/elements/div.rs | 17 +- crates/gpui/src/elements/uniform_list.rs | 35 +- crates/gpui/src/style.rs | 4 +- crates/gpui/src/taffy.rs | 11 +- crates/gpui/src/window.rs | 6 + crates/keymap_editor/src/keymap_editor.rs | 25 +- .../keymap_editor/src/ui_components/table.rs | 471 +----- crates/outline_panel/src/outline_panel.rs | 201 +-- .../src/outline_panel_settings.rs | 13 +- crates/picker/Cargo.toml | 1 - crates/picker/src/picker.rs | 112 +- crates/project_panel/src/project_panel.rs | 181 +-- .../src/project_panel_settings.rs | 11 +- crates/recent_projects/src/remote_servers.rs | 28 +- crates/terminal_view/src/terminal_view.rs | 216 +-- crates/ui/Cargo.toml | 1 + crates/ui/src/components/scrollbar.rs | 1380 +++++++++++++---- 35 files changed, 1545 insertions(+), 2365 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae72bbdff315dfe1dd7073458afa9260ad4a35c4..8252dc5fe867fc0668ea94b41cee502b47e3aa84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12214,7 +12214,6 @@ dependencies = [ "serde", "serde_json", "ui", - "util", "workspace", "workspace-hack", ] @@ -17632,6 +17631,7 @@ dependencies = [ "icons", "itertools 0.14.0", "menu", + "schemars", "serde", "settings", "smallvec", diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 015a2548d54ac5545f06984ec31bce2d3d58a56e..ed508ea18da7df3426fc13b137b97f37267ed283 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::StringMatchCandidate; use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; use std::{fmt::Display, ops::Range}; use text::Bias; use time::{OffsetDateTime, UtcOffset}; use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, - Tooltip, prelude::*, + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar, + prelude::*, }; pub struct AcpThreadHistory { @@ -26,8 +26,6 @@ pub struct AcpThreadHistory { visible_items: Vec, - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, local_timezone: UtcOffset, _update_task: Task<()>, @@ -90,7 +88,6 @@ impl AcpThreadHistory { }); let scroll_handle = UniformListScrollHandle::default(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { history_store, @@ -99,8 +96,6 @@ impl AcpThreadHistory { hovered_index: None, visible_items: Default::default(), search_editor, - scrollbar_visibility: true, - scrollbar_state, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), ) @@ -339,43 +334,6 @@ impl AcpThreadHistory { task.detach_and_log_err(cx); } - fn render_scrollbar(&self, cx: &mut Context) -> Option> { - if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) { - return None; - } - - Some( - div() - .occlude() - .id("thread-history-scroll") - .h_full() - .bg(cx.theme().colors().panel_background.opacity(0.8)) - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .absolute() - .right_1() - .top_0() - .bottom_0() - .w_4() - .pl_1() - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - fn render_list_items( &mut self, range: Range, @@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory { } impl Render for AcpThreadHistory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("ThreadHistory") .size_full() @@ -555,9 +513,7 @@ impl Render for AcpThreadHistory { .track_scroll(self.scroll_handle.clone()) .flex_grow(), ) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) - }) + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) } }) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index bae2e107eeed39d978edbddada885b55fbf0414c..d3986155d741393a01bce829a886aba4ee497948 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -24,10 +24,9 @@ use futures::FutureExt as _; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, - pulsating_between, + ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, + TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, pulsating_between, }; use language::Buffer; @@ -47,7 +46,7 @@ use text::Anchor; use theme::{AgentFontSize, ThemeSettings}; use ui::{ Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, - PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, + PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -281,7 +280,6 @@ pub struct AcpThreadView { thread_error: Option, thread_feedback: ThreadFeedbackState, list_state: ListState, - scrollbar_state: ScrollbarState, auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, @@ -405,8 +403,7 @@ impl AcpThreadView { notifications: Vec::new(), notification_subscriptions: HashMap::default(), - list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + list_state: list_state, thread_retry_status: None, thread_error: None, thread_feedback: Default::default(), @@ -4764,39 +4761,6 @@ impl AcpThreadView { cx.notify(); } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .id("acp-thread-scrollbar") - .occlude() - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - fn render_token_limit_callout( &self, line_height: Pixels, @@ -5370,23 +5334,27 @@ impl Render for AcpThreadView { configuration_view, pending_auth_method, .. - } => self.render_auth_required_state( - connection, - description.as_ref(), - configuration_view.as_ref(), - pending_auth_method.as_ref(), - window, - cx, - ), + } => self + .render_auth_required_state( + connection, + description.as_ref(), + configuration_view.as_ref(), + pending_auth_method.as_ref(), + window, + cx, + ) + .into_any(), ThreadState::Loading { .. } => v_flex() .flex_1() - .child(self.render_recent_history(window, cx)), + .child(self.render_recent_history(window, cx)) + .into_any(), ThreadState::LoadError(e) => v_flex() .flex_1() .size_full() .items_center() .justify_end() - .child(self.render_load_error(e, window, cx)), + .child(self.render_load_error(e, window, cx)) + .into_any(), ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { if has_messages { this.child( @@ -5406,9 +5374,11 @@ impl Render for AcpThreadView { .flex_grow() .into_any(), ) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) + .into_any() } else { this.child(self.render_recent_history(window, cx)) + .into_any() } }), }) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 4ad5b2d8e84779aeaa928133a11dc1a10d9a02bc..faa6b524ff49fffbd5c8113e164b2c9ab158d71a 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -35,8 +35,7 @@ use project::{ use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, - Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, - prelude::*, + Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; use workspace::{Workspace, create_and_open_local_file}; @@ -64,7 +63,6 @@ pub struct AgentConfiguration { tools: Entity, _registry_subscription: Subscription, scroll_handle: ScrollHandle, - scrollbar_state: ScrollbarState, _check_for_gemini: Task<()>, } @@ -101,9 +99,6 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); - let scroll_handle = ScrollHandle::new(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); - let mut this = Self { fs, language_registry, @@ -116,8 +111,7 @@ impl AgentConfiguration { expanded_provider_configurations: HashMap::default(), tools, _registry_subscription: registry_subscription, - scroll_handle, - scrollbar_state, + scroll_handle: ScrollHandle::new(), _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); @@ -1157,42 +1151,21 @@ impl Render for AgentConfiguration { .size_full() .pb_8() .bg(cx.theme().colors().panel_background) - .child( - v_flex() - .id("assistant-configuration-content") - .track_scroll(&self.scroll_handle) - .size_full() - .overflow_y_scroll() - .child(self.render_general_settings_section(cx)) - .child(self.render_agent_servers_section(cx)) - .child(self.render_context_servers_section(window, cx)) - .child(self.render_provider_configuration_section(cx)), - ) .child( div() - .id("assistant-configuration-scrollbar") - .occlude() - .absolute() - .right(px(3.)) - .top_0() - .bottom_0() - .pb_6() - .w(px(12.)) - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), + .size_full() + .child( + v_flex() + .id("assistant-configuration-content") + .track_scroll(&self.scroll_handle) + .size_full() + .overflow_y_scroll() + .child(self.render_general_settings_section(cx)) + .child(self.render_agent_servers_section(cx)) + .child(self.render_context_servers_section(window, cx)) + .child(self.render_provider_configuration_section(cx)), + ) + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx), ) } } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 9fc952a2ea46ac5e5c58c9ddff1f4860447b77b3..0ede5879aeb6f406191eb6ee1fb83cb6ea67a3f2 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -10,7 +10,7 @@ use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, - Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, + Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, }; use language::Point; use project::{ @@ -23,8 +23,8 @@ use project::{ worktree_store::WorktreeStore, }; use ui::{ - Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar, - ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, + Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, + StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*, }; use workspace::Workspace; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; @@ -49,7 +49,6 @@ pub(crate) struct BreakpointList { breakpoint_store: Entity, dap_store: Entity, worktree_store: Entity, - scrollbar_state: ScrollbarState, breakpoints: Vec, session: Option>, focus_handle: FocusHandle, @@ -87,7 +86,6 @@ impl BreakpointList { let dap_store = project.dap_store(); let focus_handle = cx.focus_handle(); let scroll_handle = UniformListScrollHandle::new(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let adapter_name = session.as_ref().map(|session| session.read(cx).adapter()); cx.new(|cx| { @@ -95,7 +93,6 @@ impl BreakpointList { breakpoint_store, dap_store, worktree_store, - scrollbar_state, breakpoints: Default::default(), workspace, session, @@ -576,39 +573,6 @@ impl BreakpointList { .flex_1() } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("breakpoint-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - pub(crate) fn render_control_strip(&self) -> AnyElement { let selection_kind = self.selection_kind(); let focus_handle = self.focus_handle.clone(); @@ -789,7 +753,7 @@ impl Render for BreakpointList { .size_full() .pt_1() .child(self.render_list(cx)) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) .when_some(self.strip_mode, |this, _| { this.child(Divider::horizontal().color(DividerColor::Border)) .child( diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index a134b916a2200013bcdc9e03e00028a09227e05a..bc6e90ed09a9c6ac519cca8345a0ffbb6459f249 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -1,17 +1,17 @@ use std::{ - cell::LazyCell, + cell::{LazyCell, RefCell, RefMut}, fmt::Write, ops::RangeInclusive, + rc::Rc, sync::{Arc, LazyLock}, time::Duration, }; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ - Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable, - MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle, - UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, - uniform_list, + Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, + Focusable, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle, + UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list, }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session}; @@ -19,7 +19,7 @@ use settings::Settings; use theme::ThemeSettings; use ui::{ ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render, - Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, + ScrollableHandle, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*, }; use workspace::Workspace; @@ -29,11 +29,9 @@ actions!(debugger, [GoToSelectedAddress]); pub(crate) struct MemoryView { workspace: WeakEntity, - scroll_handle: UniformListScrollHandle, - scroll_state: ScrollbarState, stack_frame_list: WeakEntity, focus_handle: FocusHandle, - view_state: ViewState, + view_state_handle: ViewStateHandle, query_editor: Entity, session: Entity, width_picker_handle: PopoverMenuHandle, @@ -90,18 +88,29 @@ impl SelectedMemoryRange { } } +#[derive(Clone)] +struct ViewStateHandle(Rc>); + +impl ViewStateHandle { + fn new(base_row: u64, line_width: ViewWidth) -> Self { + Self(Rc::new(RefCell::new(ViewState::new(base_row, line_width)))) + } +} + #[derive(Clone)] struct ViewState { /// Uppermost row index base_row: u64, /// How many cells per row do we have? line_width: ViewWidth, + scroll_handle: UniformListScrollHandle, selection: Option, } impl ViewState { fn new(base_row: u64, line_width: ViewWidth) -> Self { Self { + scroll_handle: UniformListScrollHandle::new(), base_row, line_width, selection: None, @@ -119,13 +128,39 @@ impl ViewState { fn schedule_scroll_up(&mut self) { self.base_row = self.base_row.saturating_sub(1); } + + fn set_offset(&mut self, point: Point) { + if point.y >= -Pixels::ZERO { + self.schedule_scroll_up(); + } else if point.y <= -self.scroll_handle.max_offset().height { + self.schedule_scroll_down(); + } + self.scroll_handle.set_offset(point); + } } -struct ScrollbarDragging; +impl ScrollableHandle for ViewStateHandle { + fn max_offset(&self) -> gpui::Size { + self.0.borrow().scroll_handle.max_offset() + } + + fn set_offset(&self, point: Point) { + self.0.borrow_mut().set_offset(point); + } + + fn offset(&self) -> Point { + self.0.borrow().scroll_handle.offset() + } + + fn viewport(&self) -> gpui::Bounds { + self.0.borrow().scroll_handle.viewport() + } +} static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> = LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}")))); static UNKNOWN_BYTE: SharedString = SharedString::new_static("??"); + impl MemoryView { pub(crate) fn new( session: Entity, @@ -134,19 +169,15 @@ impl MemoryView { window: &mut Window, cx: &mut Context, ) -> Self { - let view_state = ViewState::new(0, WIDTHS[4].clone()); - let scroll_handle = UniformListScrollHandle::default(); + let view_state_handle = ViewStateHandle::new(0, WIDTHS[4].clone()); let query_editor = cx.new(|cx| Editor::single_line(window, cx)); - let scroll_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { workspace, - scroll_state, - scroll_handle, stack_frame_list, focus_handle: cx.focus_handle(), - view_state, + view_state_handle, query_editor, session, width_picker_handle: Default::default(), @@ -162,50 +193,17 @@ impl MemoryView { this } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("memory-view-vertical-scrollbar") - .on_drag_move(cx.listener(|this, evt, _, cx| { - let did_handle = this.handle_scroll_drag(evt); - cx.notify(); - if did_handle { - cx.stop_propagation() - } - })) - .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx))) + fn view_state(&self) -> RefMut<'_, ViewState> { + self.view_state_handle.0.borrow_mut() } fn render_memory(&self, cx: &mut Context) -> UniformList { let weak = cx.weak_entity(); let session = self.session.clone(); - let view_state = self.view_state.clone(); + let view_state = self.view_state_handle.0.borrow().clone(); uniform_list( "debugger-memory-view", - self.view_state.row_count() as usize, + view_state.row_count() as usize, move |range, _, cx| { let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize); let memory_start = @@ -230,22 +228,13 @@ impl MemoryView { rows }, ) - .track_scroll(self.scroll_handle.clone()) + .track_scroll(view_state.scroll_handle) .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| { + let mut view_state = this.view_state(); let delta = evt.delta.pixel_delta(window.line_height()); - let scroll_handle = this.scroll_state.scroll_handle(); - let size = scroll_handle.content_size(); - let viewport = scroll_handle.viewport(); - let current_offset = scroll_handle.offset(); - let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32; - let last_entry_offset_boundary = size.height - first_entry_offset_boundary; - if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() { - // The topmost entry is visible, hence if we're scrolling up, we need to load extra lines. - this.view_state.schedule_scroll_up(); - } else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height { - this.view_state.schedule_scroll_down(); - } - scroll_handle.set_offset(current_offset + point(px(0.), delta.y)); + let current_offset = view_state.scroll_handle.offset(); + view_state + .set_offset(current_offset.apply_along(Axis::Vertical, |offset| offset + delta.y)); })) } fn render_query_bar(&self, cx: &Context) -> impl IntoElement { @@ -275,7 +264,7 @@ impl MemoryView { cx.spawn(async move |this, cx| { let access_size = access_size.await.unwrap_or(1); this.update(cx, |this, cx| { - this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag { + this.view_state().selection = Some(SelectedMemoryRange::DragComplete(Drag { start_address: as_address, end_address: as_address + access_size - 1, })); @@ -287,43 +276,23 @@ impl MemoryView { } fn handle_memory_drag(&mut self, evt: &DragMoveEvent) { - if !self - .view_state + let mut view_state = self.view_state(); + if !view_state .selection .as_ref() .is_some_and(|selection| selection.is_dragging()) { return; } - let row_count = self.view_state.row_count(); - debug_assert!(row_count > 1); - let scroll_handle = self.scroll_state.scroll_handle(); - let viewport = scroll_handle.viewport(); - - if viewport.bottom() < evt.event.position.y { - self.view_state.schedule_scroll_down(); - } else if viewport.top() > evt.event.position.y { - self.view_state.schedule_scroll_up(); - } - } - - fn handle_scroll_drag(&mut self, evt: &DragMoveEvent) -> bool { - if !self.scroll_state.is_dragging() { - return false; - } - let row_count = self.view_state.row_count(); + let row_count = view_state.row_count(); debug_assert!(row_count > 1); - let scroll_handle = self.scroll_state.scroll_handle(); + let scroll_handle = &view_state.scroll_handle; let viewport = scroll_handle.viewport(); if viewport.bottom() < evt.event.position.y { - self.view_state.schedule_scroll_down(); - true + view_state.schedule_scroll_down(); } else if viewport.top() > evt.event.position.y { - self.view_state.schedule_scroll_up(); - true - } else { - false + view_state.schedule_scroll_up(); } } @@ -354,7 +323,7 @@ impl MemoryView { fn render_width_picker(&self, window: &mut Window, cx: &mut Context) -> DropdownMenu { let weak = cx.weak_entity(); - let selected_width = self.view_state.line_width.clone(); + let selected_width = self.view_state().line_width.clone(); DropdownMenu::new( "memory-view-width-picker", selected_width.label.clone(), @@ -364,24 +333,25 @@ impl MemoryView { let width = width.clone(); this = this.entry(width.label.clone(), None, move |_, cx| { _ = weak.update(cx, |this, _| { + let mut view_state = this.view_state(); // Convert base ix between 2 line widths to keep the shown memory address roughly the same. // All widths are powers of 2, so the conversion should be lossless. - match this.view_state.line_width.width.cmp(&width.width) { + match view_state.line_width.width.cmp(&width.width) { std::cmp::Ordering::Less => { // We're converting up. let shift = width.width.trailing_zeros() - - this.view_state.line_width.width.trailing_zeros(); - this.view_state.base_row >>= shift; + - view_state.line_width.width.trailing_zeros(); + view_state.base_row >>= shift; } std::cmp::Ordering::Greater => { // We're converting down. - let shift = this.view_state.line_width.width.trailing_zeros() + let shift = view_state.line_width.width.trailing_zeros() - width.width.trailing_zeros(); - this.view_state.base_row <<= shift; + view_state.base_row <<= shift; } _ => {} } - this.view_state.line_width = width.clone(); + view_state.line_width = width.clone(); }); }); } @@ -400,18 +370,18 @@ impl MemoryView { } fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context) { - self.view_state.base_row = self - .view_state + let mut view_state = self.view_state(); + view_state.base_row = view_state .base_row - .overflowing_add(self.view_state.row_count()) + .overflowing_add(view_state.row_count()) .0; cx.notify(); } fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context) { - self.view_state.base_row = self - .view_state + let mut view_state = self.view_state(); + view_state.base_row = view_state .base_row - .overflowing_sub(self.view_state.row_count()) + .overflowing_sub(view_state.row_count()) .0; cx.notify(); } @@ -447,7 +417,8 @@ impl MemoryView { _: &mut Window, cx: &mut Context, ) { - let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone() + let Some(SelectedMemoryRange::DragComplete(selection)) = + self.view_state().selection.clone() else { return; }; @@ -484,7 +455,8 @@ impl MemoryView { } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection { + let selection = self.view_state().selection.clone(); + if let Some(SelectedMemoryRange::DragComplete(drag)) = selection { // Go into memory writing mode. if !self.is_writing_memory { let should_return = self.session.update(cx, |session, cx| { @@ -558,9 +530,11 @@ impl MemoryView { } fn jump_to_address(&mut self, address: u64, cx: &mut Context) { - self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64; - let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64; - self.scroll_handle + let mut view_state = self.view_state(); + view_state.base_row = (address & !0xfff) / view_state.line_width.width as u64; + let line_ix = (address & 0xfff) / view_state.line_width.width as u64; + view_state + .scroll_handle .scroll_to_item(line_ix as usize, ScrollStrategy::Center); cx.notify(); } @@ -595,7 +569,7 @@ impl MemoryView { } fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - self.view_state.selection = None; + self.view_state().selection = None; cx.notify(); } @@ -606,7 +580,7 @@ impl MemoryView { window: &mut Window, cx: &mut Context, ) { - let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone() + let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state().selection.clone() else { return; }; @@ -718,7 +692,7 @@ fn render_single_memory_view_line( weak: gpui::WeakEntity, cx: &mut App, ) -> AnyElement { - let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else { + let Ok(view_state) = weak.update(cx, |this, _| this.view_state().clone()) else { return div().into_any(); }; let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64; @@ -799,7 +773,7 @@ fn render_single_memory_view_line( let weak = weak.clone(); move |drag, _, _, cx| { _ = weak.update(cx, |this, _| { - this.view_state.selection = + this.view_state().selection = Some(SelectedMemoryRange::DragUnderway(drag.clone())); }); @@ -811,7 +785,7 @@ fn render_single_memory_view_line( let weak = weak.clone(); move |drag: &Drag, _, cx| { _ = weak.update(cx, |this, _| { - this.view_state.selection = + this.view_state().selection = Some(SelectedMemoryRange::DragComplete(Drag { start_address: drag.start_address, end_address: base_address + cell_ix as u64, @@ -821,7 +795,7 @@ fn render_single_memory_view_line( }) .drag_over(move |style, drag: &Drag, _, cx| { _ = weak.update(cx, |this, _| { - this.view_state.selection = + this.view_state().selection = Some(SelectedMemoryRange::DragUnderway(Drag { start_address: drag.start_address, end_address: base_address + cell_ix as u64, @@ -943,7 +917,7 @@ impl Render for MemoryView { ) .with_priority(1) })) - .child(self.render_vertical_scrollbar(cx)), + .vertical_scrollbar_for(self.view_state_handle.clone(), window, cx), ) } } diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 7743cfbdee7bf200ab25aabad4cfc455dc8b3484..4ea763c92cff18f571f27033174ee0b1163b94f9 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -1,15 +1,15 @@ use anyhow::anyhow; use dap::Module; use gpui::{ - AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, - Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list, + AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task, + UniformListScrollHandle, WeakEntity, uniform_list, }; use project::{ ProjectItem as _, ProjectPath, debugger::session::{Session, SessionEvent}, }; use std::{ops::Range, path::Path, sync::Arc}; -use ui::{Scrollbar, ScrollbarState, prelude::*}; +use ui::{WithScrollbar, prelude::*}; use workspace::Workspace; pub struct ModuleList { @@ -18,7 +18,6 @@ pub struct ModuleList { session: Entity, workspace: WeakEntity, focus_handle: FocusHandle, - scrollbar_state: ScrollbarState, entries: Vec, _rebuild_task: Option>, _subscription: Subscription, @@ -44,7 +43,6 @@ impl ModuleList { let scroll_handle = UniformListScrollHandle::new(); Self { - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, session, workspace, @@ -167,38 +165,6 @@ impl ModuleList { self.session .update(cx, |session, cx| session.modules(cx).to_vec()) } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("module-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { let Some(ix) = self.selected_ix else { return }; @@ -313,6 +279,6 @@ impl Render for ModuleList { .size_full() .p_1() .child(self.render_list(window, cx)) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) } } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index e51b8da362a581c96d2872a213a8be32ff31b097..4271bdcbb83b6a34f3c8e15b7572a9712ffd20fd 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -7,7 +7,7 @@ use dap::StackFrameId; use db::kvp::KEY_VALUE_STORE; use gpui::{ Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, - MouseButton, Stateful, Subscription, Task, WeakEntity, list, + Subscription, Task, WeakEntity, list, }; use util::debug_panic; @@ -16,7 +16,7 @@ use language::PointUtf16; use project::debugger::breakpoint_store::ActiveStackFrame; use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus}; use project::{ProjectItem, ProjectPath}; -use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*}; +use ui::{Tooltip, WithScrollbar, prelude::*}; use workspace::{ItemHandle, Workspace}; use super::RunningState; @@ -64,7 +64,6 @@ pub struct StackFrameList { workspace: WeakEntity, selected_ix: Option, opened_stack_frame_id: Option, - scrollbar_state: ScrollbarState, list_state: ListState, list_filter: StackFrameFilter, filter_entries_indices: Vec, @@ -102,7 +101,6 @@ impl StackFrameList { }); let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); - let scrollbar_state = ScrollbarState::new(list_state.clone()); let list_filter = KEY_VALUE_STORE .read_kvp(&format!( @@ -127,7 +125,6 @@ impl StackFrameList { opened_stack_frame_id: None, list_filter, list_state, - scrollbar_state, _refresh_task: Task::ready(()), }; this.schedule_refresh(true, window, cx); @@ -697,39 +694,6 @@ impl StackFrameList { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("stack-frame-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } - fn select_ix(&mut self, ix: Option, cx: &mut Context) { self.selected_ix = ix; cx.notify(); @@ -941,7 +905,7 @@ impl Render for StackFrameList { ) }) .child(self.render_list(window, cx)) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) } } diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index b396f0921e5fdf58959e82db54bb8d558249891c..494e6c7f86cae48733a569d9245986ec96fea4dc 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -8,9 +8,8 @@ use dap::{ use editor::Editor; use gpui::{ Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity, - FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription, - TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, - uniform_list, + FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement, + UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::debugger::{ @@ -18,7 +17,7 @@ use project::debugger::{ session::{Session, SessionEvent, Watcher}, }; use std::{collections::HashMap, ops::Range, sync::Arc}; -use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*}; +use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*}; use util::{debug_panic, maybe}; actions!( @@ -189,7 +188,6 @@ pub struct VariableList { entry_states: HashMap, selected_stack_frame_id: Option, list_handle: UniformListScrollHandle, - scrollbar_state: ScrollbarState, session: Entity, selection: Option, open_context_menu: Option<(Entity, Point, Subscription)>, @@ -235,7 +233,6 @@ impl VariableList { let list_state = UniformListScrollHandle::default(); Self { - scrollbar_state: ScrollbarState::new(list_state.clone()), list_handle: list_state, session, focus_handle, @@ -1500,39 +1497,6 @@ impl VariableList { ) .into_any() } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("variable-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } impl Focusable for VariableList { @@ -1542,7 +1506,7 @@ impl Focusable for VariableList { } impl Render for VariableList { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .track_focus(&self.focus_handle) .key_context("VariableList") @@ -1587,7 +1551,7 @@ impl Render for VariableList { ) .with_priority(1) })) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_handle.clone(), window, cx) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 007fdd52d799208c1428bcf1faa3bc2196d8d7d2..00ae0bcbac1e49e193739ed8b8af77a8c02dc845 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -55,7 +55,7 @@ pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPla pub use edit_prediction::Direction; pub use editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, - ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, + ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, }; pub use editor_settings_controls::*; pub use element::{ @@ -166,7 +166,7 @@ use project::{ }; use rand::seq::SliceRandom; use rpc::{ErrorCode, ErrorExt, proto::PeerId}; -use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; +use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, }; @@ -196,7 +196,7 @@ use theme::{ }; use ui::{ ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, - IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, + IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, scrollbars::ScrollbarAutoHide, }; use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; use workspace::{ diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 7f4d024e57c4831aa4c512e6dcb3a9ab35d4f610..09f496637498b2535f8836282ffed4ea30950a4f 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -7,6 +7,7 @@ use project::project_settings::DiagnosticSeverity; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings}; +use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; use util::serde::default_true; /// Imports from the VSCode settings at @@ -205,23 +206,6 @@ pub struct Gutter { pub folds: bool, } -/// When to show the scrollbar in the editor. -/// -/// Default: auto -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, -} - /// When to show the minimap in the editor. /// /// Default: never @@ -778,6 +762,12 @@ impl EditorSettings { } } +impl ScrollbarVisibility for EditorSettings { + fn visibility(&self, _cx: &App) -> ShowScrollbar { + self.scrollbar.show + } +} + impl Settings for EditorSettings { type FileContent = EditorSettingsContent; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 673557a2c653311cc5f1c36b21c9937f146d79c1..a9c24a4b3e67de21a72fbe53b4e19fa5c8974d17 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -18,7 +18,7 @@ use crate::{ editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap, MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, - ScrollbarDiagnostics, ShowMinimap, ShowScrollbar, + ScrollbarDiagnostics, ShowMinimap, }, git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer}, hover_popover::{ @@ -85,7 +85,7 @@ use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; use ui::utils::ensure_minimum_contrast; use ui::{ ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, - right_click_menu, + right_click_menu, scrollbars::ShowScrollbar, }; use unicode_segmentation::UnicodeSegmentation; use util::post_inc; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6541f76a56e671fb414e28d83adc6b0459e288a8..51be0d234eca9c2e6b908c0aba6f3746b3eff460 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -9,8 +9,8 @@ use anyhow::Context as _; use gpui::{ AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, - Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, - TextStyleRefinement, Window, div, px, + StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement, + Window, div, px, }; use itertools::Itertools; use language::{DiagnosticEntry, Language, LanguageRegistry}; @@ -23,7 +23,7 @@ use std::{borrow::Cow, cell::RefCell}; use std::{ops::Range, sync::Arc, time::Duration}; use std::{path::PathBuf, rc::Rc}; use theme::ThemeSettings; -use ui::{Scrollbar, ScrollbarState, prelude::*, theme_is_transparent}; +use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent}; use url::Url; use util::TryFutureExt; use workspace::{OpenOptions, OpenVisible, Workspace}; @@ -184,7 +184,6 @@ pub fn hover_at_inlay( let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(false)), anchor: None, @@ -387,7 +386,6 @@ fn show_hover( local_diagnostic, markdown, border_color, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), @@ -457,7 +455,6 @@ fn show_hover( info_popovers.push(InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), @@ -507,7 +504,6 @@ fn show_hover( info_popovers.push(InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), @@ -846,7 +842,6 @@ pub struct InfoPopover { pub symbol_range: RangeInEditor, pub parsed_content: Option>, pub scroll_handle: ScrollHandle, - pub scrollbar_state: ScrollbarState, pub keyboard_grace: Rc>, pub anchor: Option, _subscription: Option, @@ -891,7 +886,12 @@ impl InfoPopover { .on_url_click(open_markdown_url), ), ) - .child(self.render_vertical_scrollbar(cx)) + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()), + window, + cx, + ) }) .into_any_element() } @@ -905,39 +905,6 @@ impl InfoPopover { cx.notify(); self.scroll_handle.set_offset(current); } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("info-popover-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } pub struct DiagnosticPopover { @@ -949,7 +916,6 @@ pub struct DiagnosticPopover { pub anchor: Anchor, _subscription: Subscription, pub scroll_handle: ScrollHandle, - pub scrollbar_state: ScrollbarState, } impl DiagnosticPopover { @@ -1013,43 +979,15 @@ impl DiagnosticPopover { ), ), ) - .child(self.render_vertical_scrollbar(cx)), + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()), + window, + cx, + ), ) .into_any_element() } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("diagnostic-popover-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } #[cfg(test)] diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 82314486187db99c2ba5c104faa42828dad57cdb..828ab0594daf09081bf10af02ec5a4a6a470cd7d 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -12,7 +12,7 @@ use crate::{ }; pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use core::fmt::Debug; -use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px}; +use gpui::{Along, App, Axis, Context, Pixels, Task, Window, point, px}; use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; @@ -21,6 +21,7 @@ use std::{ cmp::Ordering, time::{Duration, Instant}, }; +use ui::scrollbars::ScrollbarAutoHide; use util::ResultExt; use workspace::{ItemId, WorkspaceId}; @@ -29,11 +30,6 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); pub struct WasScrolled(pub(crate) bool); -#[derive(Default)] -pub struct ScrollbarAutoHide(pub bool); - -impl Global for ScrollbarAutoHide {} - #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { pub offset: gpui::Point, @@ -327,7 +323,7 @@ impl ScrollManager { cx.notify(); } - if cx.default_global::().0 { + if cx.default_global::().should_hide() { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor() .timer(SCROLLBAR_SHOW_INTERVAL) diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index cb21f35d7ed7556cf09f9e566286a10f8317ca6c..54d8a50115b7104a2e4469c3619ef366fa229c9d 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -2,8 +2,8 @@ use crate::actions::ShowSignatureHelp; use crate::hover_popover::open_markdown_url; use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style}; use gpui::{ - App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful, - StyledText, Task, TextStyle, Window, combine_highlights, + App, Context, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, StyledText, Task, + TextStyle, Window, combine_highlights, }; use language::BufferSnapshot; use markdown::{Markdown, MarkdownElement}; @@ -15,8 +15,8 @@ use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton, IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, - LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString, - StatefulInteractiveElement, Styled, StyledExt, div, px, relative, + LabelSize, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, StyledExt, + WithScrollbar, div, relative, }; // Language-specific settings may define quotes as "brackets", so filter them out separately. @@ -243,7 +243,6 @@ impl Editor { .min(signatures.len().saturating_sub(1)); let signature_help_popover = SignatureHelpPopover { - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), style, signatures, current_signature, @@ -330,7 +329,6 @@ pub struct SignatureHelpPopover { pub signatures: Vec, pub current_signature: usize, scroll_handle: ScrollHandle, - scrollbar_state: ScrollbarState, } impl SignatureHelpPopover { @@ -391,7 +389,8 @@ impl SignatureHelpPopover { ) }), ) - .child(self.render_vertical_scrollbar(cx)); + .vertical_scrollbar(window, cx); + let controls = if self.signatures.len() > 1 { let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp) .shape(IconButtonShape::Square) @@ -460,26 +459,4 @@ impl SignatureHelpPopover { .child(main_content) .into_any_element() } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("signature_help_scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| cx.stop_propagation()) - .on_any_mouse_down(|_, _, cx| cx.stop_propagation()) - .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify())) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_1() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 82ee54174567987d00478815f6a4eefd94333202..dab238dc8be1ae6f2243e94fc7c00ce363a79469 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -24,8 +24,8 @@ use settings::Settings; use strum::IntoEnumIterator as _; use theme::ThemeSettings; use ui::{ - CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState, - ToggleButton, Tooltip, prelude::*, + CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, ToggleButton, Tooltip, + WithScrollbar, prelude::*, }; use vim_mode_setting::VimModeSetting; use workspace::{ @@ -290,7 +290,6 @@ pub struct ExtensionsPage { _subscriptions: [gpui::Subscription; 2], extension_fetch_task: Option>, upsells: BTreeSet, - scrollbar_state: ScrollbarState, } impl ExtensionsPage { @@ -339,7 +338,7 @@ impl ExtensionsPage { let mut this = Self { workspace: workspace.weak_handle(), - list: scroll_handle.clone(), + list: scroll_handle, is_fetching_extensions: false, filter: ExtensionFilter::All, dev_extension_entries: Vec::new(), @@ -351,7 +350,6 @@ impl ExtensionsPage { _subscriptions: subscriptions, query_editor, upsells: BTreeSet::default(), - scrollbar_state: ScrollbarState::new(scroll_handle), }; this.fetch_extensions( this.search_query(cx), @@ -1375,7 +1373,7 @@ impl ExtensionsPage { } impl Render for ExtensionsPage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .bg(cx.theme().colors().editor_background) @@ -1520,25 +1518,24 @@ impl Render for ExtensionsPage { } if count == 0 { - return this.py_4().child(self.render_empty_state(cx)); - } - - let scroll_handle = self.list.clone(); - this.child( - uniform_list("entries", count, cx.processor(Self::render_extensions)) + this.py_4() + .child(self.render_empty_state(cx)) + .into_any_element() + } else { + let scroll_handle = self.list.clone(); + this.child( + uniform_list( + "entries", + count, + cx.processor(Self::render_extensions), + ) .flex_grow() .pb_4() - .track_scroll(scroll_handle), - ) - .child( - div() - .absolute() - .right_1() - .top_0() - .bottom_0() - .w(px(12.)) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) + .track_scroll(scroll_handle.clone()), + ) + .vertical_scrollbar_for(scroll_handle, window, cx) + .into_any_element() + } }), ) } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index f30b53faee442fdbadea1bec1f0e08148998f74d..3063206723021d00aa21e0f8163b4fe3b941fcb7 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -13,10 +13,7 @@ use agent_settings::AgentSettings; use anyhow::Context as _; use askpass::AskPassDelegate; use db::kvp::KEY_VALUE_STORE; -use editor::{ - Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, - scroll::ScrollbarAutoHide, -}; +use editor::{Editor, EditorElement, EditorMode, MultiBuffer}; use futures::StreamExt as _; use git::blame::ParsedCommitMessage; use git::repository::{ @@ -32,11 +29,10 @@ use git::{ TrashUntrackedFiles, UnstageAll, }; use gpui::{ - Action, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, - ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, - Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, - uniform_list, + Action, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, + MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, + UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; @@ -64,7 +60,7 @@ use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, - PopoverMenu, Scrollbar, ScrollbarState, SplitButton, Tooltip, prelude::*, + PopoverMenu, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*, }; use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; @@ -291,61 +287,6 @@ struct PendingOperation { op_id: usize, } -// computed state related to how to render scrollbars -// one per axis -// on render we just read this off the panel -// we update it when -// - settings change -// - on focus in, on focus out, on hover, etc. -#[derive(Debug)] -struct ScrollbarProperties { - axis: Axis, - show_scrollbar: bool, - show_track: bool, - auto_hide: bool, - hide_task: Option>, - state: ScrollbarState, -} - -impl ScrollbarProperties { - // Shows the scrollbar and cancels any pending hide task - fn show(&mut self, cx: &mut Context) { - if !self.auto_hide { - return; - } - self.show_scrollbar = true; - self.hide_task.take(); - cx.notify(); - } - - fn hide(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - - if !self.auto_hide { - return; - } - - let axis = self.axis; - self.hide_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - - if let Some(panel) = panel.upgrade() { - panel - .update(cx, |panel, cx| { - match axis { - Axis::Vertical => panel.vertical_scrollbar.show_scrollbar = false, - Axis::Horizontal => panel.horizontal_scrollbar.show_scrollbar = false, - } - cx.notify(); - }) - .log_err(); - } - })); - } -} - pub struct GitPanel { pub(crate) active_repository: Option>, pub(crate) commit_editor: Entity, @@ -358,8 +299,6 @@ pub struct GitPanel { single_tracked_entry: Option, focus_handle: FocusHandle, fs: Arc, - horizontal_scrollbar: ScrollbarProperties, - vertical_scrollbar: ScrollbarProperties, new_count: usize, entry_count: usize, new_staged_count: usize, @@ -443,10 +382,6 @@ impl GitPanel { cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); - cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { - this.hide_scrollbars(window, cx); - }) - .detach(); let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; cx.observe_global_in::(window, move |this, window, cx| { @@ -471,24 +406,6 @@ impl GitPanel { let scroll_handle = UniformListScrollHandle::new(); - let vertical_scrollbar = ScrollbarProperties { - axis: Axis::Vertical, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - - let horizontal_scrollbar = ScrollbarProperties { - axis: Axis::Horizontal, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - let mut assistant_enabled = AgentSettings::get_global(cx).enabled; let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; let _settings_subscription = cx.observe_global::(move |_, cx| { @@ -570,8 +487,6 @@ impl GitPanel { workspace: workspace.weak_handle(), modal_open: false, entry_count: 0, - horizontal_scrollbar, - vertical_scrollbar, bulk_staging: None, stash_entries: Default::default(), _settings_subscription, @@ -582,86 +497,6 @@ impl GitPanel { }) } - fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { - self.horizontal_scrollbar.hide(window, cx); - self.vertical_scrollbar.hide(window, cx); - } - - fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context) { - // TODO: This PR should have defined Editor's `scrollbar.axis` - // as an Option, not a ScrollbarAxes as it would allow you to - // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`. - // - // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis` - // so we can show each axis based on the settings. - // - // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495 - - let show_setting = GitPanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or(EditorSettings::get_global(cx).scrollbar.show); - - let scroll_handle = self.scroll_handle.0.borrow(); - - let autohide = |show: ShowScrollbar, cx: &mut Context| match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => false, - }; - - let longest_item_width = scroll_handle.last_item_size.and_then(|size| { - (size.contents.width > size.item.width).then_some(size.contents.width) - }); - - // is there an item long enough that we should show a horizontal scrollbar? - let item_wider_than_container = if let Some(longest_item_width) = longest_item_width { - longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0) - } else { - true - }; - - let show_horizontal = match (show_setting, item_wider_than_container) { - (_, false) => false, - (ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always, true) => true, - (ShowScrollbar::Never, true) => false, - }; - - let show_vertical = match show_setting { - ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - }; - - let show_horizontal_track = - show_horizontal && matches!(show_setting, ShowScrollbar::Always); - - // TODO: we probably should hide the scroll track when the list doesn't need to scroll - let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always); - - self.vertical_scrollbar = ScrollbarProperties { - axis: self.vertical_scrollbar.axis, - state: self.vertical_scrollbar.state.clone(), - show_scrollbar: show_vertical, - show_track: show_vertical_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - self.horizontal_scrollbar = ScrollbarProperties { - axis: self.horizontal_scrollbar.axis, - state: self.horizontal_scrollbar.state.clone(), - show_scrollbar: show_horizontal, - show_track: show_horizontal_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - cx.notify(); - } - pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option { if GitPanelSettings::get_global(cx).sort_by_path { return self @@ -2676,7 +2511,6 @@ impl GitPanel { git_panel.clear_pending(); } git_panel.update_visible_entries(window, cx); - git_panel.update_scrollbar_properties(window, cx); }) .ok(); } @@ -3799,110 +3633,6 @@ impl GitPanel { ) } - fn render_vertical_scrollbar( - &self, - show_horizontal_scrollbar_container: bool, - cx: &mut Context, - ) -> impl IntoElement { - div() - .id("git-panel-vertical-scroll") - .occlude() - .flex_none() - .h_full() - .cursor_default() - .absolute() - .right_0() - .top_0() - .bottom_0() - .w(px(12.)) - .when(show_horizontal_scrollbar_container, |this| { - this.pb_neg_3p5() - }) - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.vertical_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.vertical_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical( - // percentage as f32..end_offset as f32, - self.vertical_scrollbar.state.clone(), - )) - } - - /// Renders the horizontal scrollbar. - /// - /// The right offset is used to determine how far to the right the - /// scrollbar should extend to, useful for ensuring it doesn't collide - /// with the vertical scrollbar when visible. - fn render_horizontal_scrollbar( - &self, - right_offset: Pixels, - cx: &mut Context, - ) -> impl IntoElement { - div() - .id("git-panel-horizontal-scroll") - .occlude() - .flex_none() - .w_full() - .cursor_default() - .absolute() - .bottom_neg_px() - .left_0() - .right_0() - .pr(right_offset) - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.horizontal_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.horizontal_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::horizontal( - // percentage as f32..end_offset as f32, - self.horizontal_scrollbar.state.clone(), - )) - } - fn render_buffer_header_controls( &self, entity: &Entity, @@ -3950,33 +3680,16 @@ impl GitPanel { fn render_entries( &self, has_write_access: bool, - _: &Window, + window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let entry_count = self.entries.len(); - let scroll_track_size = px(16.); - - let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar { - // magic number - px(3.) - } else { - px(0.) - }; - v_flex() .flex_1() .size_full() .overflow_hidden() .relative() - // Show a border on the top and bottom of the container when - // the vertical scrollbar container is visible so we don't have a - // floating left border in the panel. - .when(self.vertical_scrollbar.show_track, |this| { - this.border_t_1() - .border_b_1() - .border_color(cx.theme().colors().border) - }) .child( h_flex() .flex_1() @@ -4017,15 +3730,6 @@ impl GitPanel { items }), ) - .when( - !self.horizontal_scrollbar.show_track - && self.horizontal_scrollbar.show_scrollbar, - |this| { - // when not showing the horizontal scrollbar track, make sure we don't - // obscure the last entry - this.pb(scroll_track_size) - }, - ) .size_full() .flex_grow() .with_sizing_behavior(ListSizingBehavior::Auto) @@ -4041,72 +3745,14 @@ impl GitPanel { this.deploy_panel_context_menu(event.position, window, cx) }), ) - .when(self.vertical_scrollbar.show_track, |this| { - this.child( - v_flex() - .h_full() - .flex_none() - .w(scroll_track_size) - .bg(cx.theme().colors().panel_background) - .child( - div() - .size_full() - .flex_1() - .border_l_1() - .border_color(cx.theme().colors().border), - ), - ) - }) - .when(self.vertical_scrollbar.show_scrollbar, |this| { - this.child( - self.render_vertical_scrollbar( - self.horizontal_scrollbar.show_track, - cx, - ), - ) - }), + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal), + window, + cx, + ), ) - .when(self.horizontal_scrollbar.show_track, |this| { - this.child( - h_flex() - .w_full() - .h(scroll_track_size) - .flex_none() - .relative() - .child( - div() - .w_full() - .flex_1() - // for some reason the horizontal scrollbar is 1px - // taller than the vertical scrollbar?? - .h(scroll_track_size - px(1.)) - .bg(cx.theme().colors().panel_background) - .border_t_1() - .border_color(cx.theme().colors().border), - ) - .when(self.vertical_scrollbar.show_track, |this| { - this.child( - div() - .flex_none() - // -1px prevents a missing pixel between the two container borders - .w(scroll_track_size - px(1.)) - .h_full(), - ) - .child( - // HACK: Fill the missing 1px 🥲 - div() - .absolute() - .right(scroll_track_size - px(1.)) - .bottom(scroll_track_size - px(1.)) - .size_px() - .bg(cx.theme().colors().border), - ) - }), - ) - }) - .when(self.horizontal_scrollbar.show_scrollbar, |this| { - this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx)) - }) } fn entry_label(&self, label: impl Into, color: Color) -> Label { @@ -4620,15 +4266,6 @@ impl Render for GitPanel { git_panel.on_action(cx.listener(Self::toggle_fill_co_authors)) }) .on_action(cx.listener(Self::toggle_sort_by_path)) - .on_hover(cx.listener(move |this, hovered, window, cx| { - if *hovered { - this.horizontal_scrollbar.show(cx); - this.vertical_scrollbar.show(cx); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbars(window, cx); - } - })) .size_full() .overflow_hidden() .bg(cx.theme().colors().panel_background) diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index be207314acd82446566dffd2eb58339974f177ff..74e8e25927f07ec1f159d12023669fab9e518114 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -1,8 +1,9 @@ -use editor::ShowScrollbar; +use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; use workspace::dock::DockPosition; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -90,6 +91,22 @@ pub struct GitPanelSettings { pub collapse_untracked_diff: bool, } +impl ScrollbarVisibility for GitPanelSettings { + fn visibility(&self, cx: &ui::App) -> ShowScrollbar { + // TODO: This PR should have defined Editor's `scrollbar.axis` + // as an Option, not a ScrollbarAxes as it would allow you to + // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`. + // + // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis` + // so we can show each axis based on the settings. + // + // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495 + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for GitPanelSettings { type FileContent = GitPanelSettingsContent; diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 443bcb14bbec7c5fac39fdd0f5e5d621d84df610..bd2eed33973eb520305ab7b185f41569ef778dbf 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -16,10 +16,10 @@ //! constructed by combining these two systems into an all-in-one element. use crate::{ - Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, - Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, - HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, - KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, + AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, + DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, + HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, + KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, size, @@ -1036,6 +1036,15 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Set the space to be reserved for rendering the scrollbar. + /// + /// This will only affect the layout of the element when overflow for this element is set to + /// `Overflow::Scroll`. + fn scrollbar_width(mut self, width: impl Into) -> Self { + self.interactivity().base_style.scrollbar_width = Some(width.into()); + self + } + /// Track the scroll state of this element with the given handle. fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index cdf90d4eb8934de99a21c65b6c9efa2a2fdde258..9c601aac1d72d915b2e8c4b3866de88b966c05f0 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,10 +5,10 @@ //! elements with uniform height. use crate::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId, - Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId, - ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled, - Window, point, size, + AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity, + GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, + IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, + StyleRefinement, Styled, Window, point, size, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -71,7 +71,7 @@ pub struct UniformList { /// Frame state used by the [UniformList]. pub struct UniformListFrameState { items: SmallVec<[AnyElement; 32]>, - decorations: SmallVec<[AnyElement; 1]>, + decorations: SmallVec<[AnyElement; 2]>, } /// A handle for controlling the scroll position of a uniform list. @@ -529,6 +529,31 @@ pub trait UniformListDecoration { ) -> AnyElement; } +impl UniformListDecoration for Entity { + fn compute( + &self, + visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + item_count: usize, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + self.update(cx, |inner, cx| { + inner.compute( + visible_range, + bounds, + scroll_offset, + item_height, + item_count, + window, + cx, + ) + }) + } +} + impl UniformList { /// Selects a specific list item for measurement. pub fn with_width_from_item(mut self, item_index: Option) -> Self { diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 53ca6508bc94e96033d0929a674ce59e8206ba04..78bca5a4993271883c555fe05366a7c9a0c472ac 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -153,7 +153,7 @@ pub struct Style { #[refineable] pub overflow: Point, /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. - pub scrollbar_width: f32, + pub scrollbar_width: AbsoluteLength, /// Whether both x and y axis should be scrollable at the same time. pub allow_concurrent_scroll: bool, /// Whether scrolling should be restricted to the axis indicated by the mouse wheel. @@ -745,7 +745,7 @@ impl Default for Style { }, allow_concurrent_scroll: false, restrict_scroll_to_axis: false, - scrollbar_width: 0.0, + scrollbar_width: AbsoluteLength::default(), position: Position::Relative, inset: Edges::auto(), margin: Edges::::zero(), diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 465928d024172d9cadff1a6c45348f550516120f..288726d379b9fa4c1dbb58f2341b4bc2145a9551 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -277,7 +277,7 @@ impl ToTaffy for Style { taffy::style::Style { display: self.display.into(), overflow: self.overflow.into(), - scrollbar_width: self.scrollbar_width, + scrollbar_width: self.scrollbar_width.to_taffy(rem_size), position: self.position.into(), inset: self.inset.to_taffy(rem_size), size: self.size.to_taffy(rem_size), @@ -314,6 +314,15 @@ impl ToTaffy for Style { } } +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels) -> f32 { + match self { + AbsoluteLength::Pixels(pixels) => pixels.into(), + AbsoluteLength::Rems(rems) => (*rems * rem_size).into(), + } + } +} + impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { match self { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 1c4532fda2adcdc5e995a6486a6366875c1f3b44..307197acca7cd3eba7f69e7731288449a96ad35a 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4960,6 +4960,12 @@ impl> From<(ElementId, T)> for ElementId { } } +impl From<&'static core::panic::Location<'static>> for ElementId { + fn from(location: &'static core::panic::Location<'static>) -> Self { + ElementId::CodeLocation(*location) + } +} + /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`Window::paint_quad`]. #[derive(Clone)] diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 7aa8a0c284576c131e472ea2fb5daba4d6ed9c23..51524f92eacbf6227877f1203050bb6f964033b3 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -13,8 +13,8 @@ use editor::{CompletionProvider, Editor, EditorEvent}; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, + Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, @@ -425,7 +425,7 @@ impl KeymapEditor { fn new(workspace: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { let _keymap_subscription = cx.observe_global_in::(window, Self::on_keymap_changed); - let table_interaction_state = TableInteractionState::new(window, cx); + let table_interaction_state = TableInteractionState::new(cx); let keystroke_editor = cx.new(|cx| { let mut keystroke_editor = KeystrokeInput::new(None, window, cx); @@ -781,9 +781,8 @@ impl KeymapEditor { match previous_edit { // should remove scroll from process_query PreviousEdit::ScrollBarOffset(offset) => { - this.table_interaction_state.update(cx, |table, _| { - table.set_scrollbar_offset(Axis::Vertical, offset) - }) + this.table_interaction_state + .update(cx, |table, _| table.set_scroll_offset(offset)) // set selected index and scroll } PreviousEdit::Keybinding { @@ -812,9 +811,8 @@ impl KeymapEditor { cx, ); } else { - this.table_interaction_state.update(cx, |table, _| { - table.set_scrollbar_offset(Axis::Vertical, fallback) - }); + this.table_interaction_state + .update(cx, |table, _| table.set_scroll_offset(fallback)); } cx.notify(); } @@ -1199,9 +1197,7 @@ impl KeymapEditor { }; let tab_size = cx.global::().json_tab_size(); self.previous_edit = Some(PreviousEdit::ScrollBarOffset( - self.table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), + self.table_interaction_state.read(cx).scroll_offset(), )); let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |_, _| { @@ -2374,10 +2370,7 @@ impl KeybindingEditorModal { keymap.previous_edit = Some(PreviousEdit::Keybinding { action_mapping, action_name, - fallback: keymap - .table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), + fallback: keymap.table_interaction_state.read(cx).scroll_offset(), }); let status_toast = StatusToast::new( format!("Saved edits to the {} action.", humanized_action_name), diff --git a/crates/keymap_editor/src/ui_components/table.rs b/crates/keymap_editor/src/ui_components/table.rs index 9d7bb0736061181eda93d072640f87b5946a2675..cb0332c868032724d555f3fc5c57e33eaeda624b 100644 --- a/crates/keymap_editor/src/ui_components/table.rs +++ b/crates/keymap_editor/src/ui_components/table.rs @@ -1,20 +1,20 @@ -use std::{ops::Range, rc::Rc, time::Duration}; +use std::{ops::Range, rc::Rc}; -use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; +use editor::EditorSettings; use gpui::{ - AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, - FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, - Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, + AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, + FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful, + UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, }; use itertools::intersperse_with; -use settings::Settings as _; use ui::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, - StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, + ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, + StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px, + single_example, v_flex, }; const RESIZE_COLUMN_WIDTH: f32 = 8.0; @@ -56,136 +56,22 @@ impl TableContents { pub struct TableInteractionState { pub focus_handle: FocusHandle, pub scroll_handle: UniformListScrollHandle, - pub horizontal_scrollbar: ScrollbarProperties, - pub vertical_scrollbar: ScrollbarProperties, } impl TableInteractionState { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - - cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| { - this.hide_scrollbars(window, cx); - }) - .detach(); - - let scroll_handle = UniformListScrollHandle::new(); - let vertical_scrollbar = ScrollbarProperties { - axis: Axis::Vertical, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - - let horizontal_scrollbar = ScrollbarProperties { - axis: Axis::Horizontal, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - - let mut this = Self { - focus_handle, - scroll_handle, - horizontal_scrollbar, - vertical_scrollbar, - }; - - this.update_scrollbar_visibility(cx); - this + pub fn new(cx: &mut App) -> Entity { + cx.new(|cx| Self { + focus_handle: cx.focus_handle(), + scroll_handle: UniformListScrollHandle::new(), }) } - pub fn get_scrollbar_offset(&self, axis: Axis) -> Point { - match axis { - Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(), - Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(), - } - } - - pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point) { - match axis { - Axis::Vertical => self - .vertical_scrollbar - .state - .scroll_handle() - .set_offset(offset), - Axis::Horizontal => self - .horizontal_scrollbar - .state - .scroll_handle() - .set_offset(offset), - } - } - - fn update_scrollbar_visibility(&mut self, cx: &mut Context) { - let show_setting = EditorSettings::get_global(cx).scrollbar.show; - - let scroll_handle = self.scroll_handle.0.borrow(); - - let autohide = |show: ShowScrollbar, cx: &mut Context| match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => false, - }; - - let longest_item_width = scroll_handle.last_item_size.and_then(|size| { - (size.contents.width > size.item.width).then_some(size.contents.width) - }); - - // is there an item long enough that we should show a horizontal scrollbar? - let item_wider_than_container = if let Some(longest_item_width) = longest_item_width { - longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0) - } else { - true - }; - - let show_scrollbar = match show_setting { - ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - }; - let show_vertical = show_scrollbar; - - let show_horizontal = item_wider_than_container && show_scrollbar; - - let show_horizontal_track = - show_horizontal && matches!(show_setting, ShowScrollbar::Always); - - // TODO: we probably should hide the scroll track when the list doesn't need to scroll - let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always); - - self.vertical_scrollbar = ScrollbarProperties { - axis: self.vertical_scrollbar.axis, - state: self.vertical_scrollbar.state.clone(), - show_scrollbar: show_vertical, - show_track: show_vertical_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - self.horizontal_scrollbar = ScrollbarProperties { - axis: self.horizontal_scrollbar.axis, - state: self.horizontal_scrollbar.state.clone(), - show_scrollbar: show_horizontal, - show_track: show_horizontal_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - cx.notify(); + pub fn scroll_offset(&self) -> Point { + self.scroll_handle.offset() } - fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { - self.horizontal_scrollbar.hide(window, cx); - self.vertical_scrollbar.hide(window, cx); + pub fn set_scroll_offset(&self, offset: Point) { + self.scroll_handle.set_offset(offset); } pub fn listener( @@ -280,183 +166,6 @@ impl TableInteractionState { .children(dividers) .into_any_element() } - - fn render_vertical_scrollbar_track( - this: &Entity, - parent: Div, - scroll_track_size: Pixels, - cx: &mut App, - ) -> Div { - if !this.read(cx).vertical_scrollbar.show_track { - return parent; - } - let child = v_flex() - .h_full() - .flex_none() - .w(scroll_track_size) - .bg(cx.theme().colors().background) - .child( - div() - .size_full() - .flex_1() - .border_l_1() - .border_color(cx.theme().colors().border), - ); - parent.child(child) - } - - fn render_vertical_scrollbar(this: &Entity, parent: Div, cx: &mut App) -> Div { - if !this.read(cx).vertical_scrollbar.show_scrollbar { - return parent; - } - let child = div() - .id(("table-vertical-scrollbar", this.entity_id())) - .occlude() - .flex_none() - .h_full() - .cursor_default() - .absolute() - .right_0() - .top_0() - .bottom_0() - .w(px(12.)) - .on_mouse_move(Self::listener(this, |_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - Self::listener(this, |this, _, window, cx| { - if !this.vertical_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.vertical_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(Self::listener(this, |_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical( - this.read(cx).vertical_scrollbar.state.clone(), - )); - parent.child(child) - } - - /// Renders the horizontal scrollbar. - /// - /// The right offset is used to determine how far to the right the - /// scrollbar should extend to, useful for ensuring it doesn't collide - /// with the vertical scrollbar when visible. - fn render_horizontal_scrollbar( - this: &Entity, - parent: Div, - right_offset: Pixels, - cx: &mut App, - ) -> Div { - if !this.read(cx).horizontal_scrollbar.show_scrollbar { - return parent; - } - let child = div() - .id(("table-horizontal-scrollbar", this.entity_id())) - .occlude() - .flex_none() - .w_full() - .cursor_default() - .absolute() - .bottom_neg_px() - .left_0() - .right_0() - .pr(right_offset) - .on_mouse_move(Self::listener(this, |_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - Self::listener(this, |this, _, window, cx| { - if !this.horizontal_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.horizontal_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(Self::listener(this, |_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::horizontal( - // percentage as f32..end_offset as f32, - this.read(cx).horizontal_scrollbar.state.clone(), - )); - parent.child(child) - } - - fn render_horizontal_scrollbar_track( - this: &Entity, - parent: Div, - scroll_track_size: Pixels, - cx: &mut App, - ) -> Div { - if !this.read(cx).horizontal_scrollbar.show_track { - return parent; - } - let child = h_flex() - .w_full() - .h(scroll_track_size) - .flex_none() - .relative() - .child( - div() - .w_full() - .flex_1() - // for some reason the horizontal scrollbar is 1px - // taller than the vertical scrollbar?? - .h(scroll_track_size - px(1.)) - .bg(cx.theme().colors().background) - .border_t_1() - .border_color(cx.theme().colors().border), - ) - .when(this.read(cx).vertical_scrollbar.show_track, |parent| { - parent - .child( - div() - .flex_none() - // -1px prevents a missing pixel between the two container borders - .w(scroll_track_size - px(1.)) - .h_full(), - ) - .child( - // HACK: Fill the missing 1px 🥲 - div() - .absolute() - .right(scroll_track_size - px(1.)) - .bottom(scroll_track_size - px(1.)) - .size_px() - .bg(cx.theme().colors().border), - ) - }); - - parent.child(child) - } } #[derive(Debug, Copy, Clone, PartialEq)] @@ -1054,17 +763,6 @@ impl RenderOnce for Table { .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); - let scroll_track_size = px(16.); - let h_scroll_offset = if interaction_state - .as_ref() - .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar) - { - // magic number - px(3.) - } else { - px(0.) - }; - let width = self.width; let no_rows_rendered = self.rows.is_empty(); @@ -1115,8 +813,8 @@ impl RenderOnce for Table { }) } }) - .child( - div() + .child({ + let content = div() .flex_grow() .w_full() .relative() @@ -1187,25 +885,21 @@ impl RenderOnce for Table { ) })) }, - ) - .when_some(interaction_state.as_ref(), |this, interaction_state| { - this.map(|this| { - TableInteractionState::render_vertical_scrollbar_track( - interaction_state, - this, - scroll_track_size, - cx, - ) - }) - .map(|this| { - TableInteractionState::render_vertical_scrollbar( - interaction_state, - this, - cx, - ) - }) - }), - ) + ); + + if let Some(state) = interaction_state.as_ref() { + content + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(state.read(cx).scroll_handle.clone()), + window, + cx, + ) + .into_any_element() + } else { + content.into_any_element() + } + }) .when_some( no_rows_rendered .then_some(self.empty_table_callback) @@ -1220,52 +914,12 @@ impl RenderOnce for Table { .child(callback(window, cx)), ) }, - ) - .when_some( - width.and(interaction_state.as_ref()), - |this, interaction_state| { - this.map(|this| { - TableInteractionState::render_horizontal_scrollbar_track( - interaction_state, - this, - scroll_track_size, - cx, - ) - }) - .map(|this| { - TableInteractionState::render_horizontal_scrollbar( - interaction_state, - this, - h_scroll_offset, - cx, - ) - }) - }, ); if let Some(interaction_state) = interaction_state.as_ref() { table .track_focus(&interaction_state.read(cx).focus_handle) .id(("table", interaction_state.entity_id())) - .on_hover({ - let interaction_state = interaction_state.downgrade(); - move |hovered, window, cx| { - interaction_state - .update(cx, |interaction_state, cx| { - if *hovered { - interaction_state.horizontal_scrollbar.show(cx); - interaction_state.vertical_scrollbar.show(cx); - cx.notify(); - } else if !interaction_state - .focus_handle - .contains_focused(window, cx) - { - interaction_state.hide_scrollbars(window, cx); - } - }) - .ok(); - } - }) .into_any_element() } else { table.into_any_element() @@ -1273,65 +927,6 @@ impl RenderOnce for Table { } } -// computed state related to how to render scrollbars -// one per axis -// on render we just read this off the keymap editor -// we update it when -// - settings change -// - on focus in, on focus out, on hover, etc. -#[derive(Debug)] -pub struct ScrollbarProperties { - axis: Axis, - show_scrollbar: bool, - show_track: bool, - auto_hide: bool, - hide_task: Option>, - state: ScrollbarState, -} - -impl ScrollbarProperties { - // Shows the scrollbar and cancels any pending hide task - fn show(&mut self, cx: &mut Context) { - if !self.auto_hide { - return; - } - self.show_scrollbar = true; - self.hide_task.take(); - cx.notify(); - } - - fn hide(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - - if !self.auto_hide { - return; - } - - let axis = self.axis; - self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - - if let Some(keymap_editor) = keymap_editor.upgrade() { - keymap_editor - .update(cx, |keymap_editor, cx| { - match axis { - Axis::Vertical => { - keymap_editor.vertical_scrollbar.show_scrollbar = false - } - Axis::Horizontal => { - keymap_editor.horizontal_scrollbar.show_scrollbar = false - } - } - cx.notify(); - }) - .ok(); - } - })); - } -} - impl Component for Table<3> { fn scope() -> ComponentScope { ComponentScope::Layout diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 2f34d47667d10e07c468ec53e4d39083a89bd2fb..8f501b2f970019c24c36e65fa94099b80454dfe2 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4,11 +4,11 @@ use anyhow::Context as _; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use db::kvp::KEY_VALUE_STORE; use editor::{ - AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId, - ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, ShowScrollbar, + AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange, + MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, display_map::ToDisplayPoint, items::{entry_git_aware_label_color, entry_label_color}, - scroll::{Autoscroll, ScrollAnchor, ScrollbarAutoHide}, + scroll::{Autoscroll, ScrollAnchor}, }; use file_icons::FileIcons; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; @@ -45,19 +45,18 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use smol::channel; use theme::{SyntaxTheme, ThemeSettings}; -use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout}; +use ui::{ + ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, DynamicSpacing, FluentBuilder, + HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, IndentGuideColors, + IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt, + StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex, +}; use util::{RangeExt, ResultExt, TryFutureExt, debug_panic}; use workspace::{ OpenInTerminal, WeakItemHandle, Workspace, dock::{DockPosition, Panel, PanelEvent}, item::ItemHandle, searchable::{SearchEvent, SearchableItem}, - ui::{ - ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel, - Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem, - Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex, - v_flex, - }, }; use worktree::{Entry, ProjectEntryId, WorktreeId}; @@ -125,10 +124,6 @@ pub struct OutlinePanel { cached_entries: Vec, filter_editor: Entity, mode: ItemsDisplayMode, - show_scrollbar: bool, - vertical_scrollbar_state: ScrollbarState, - horizontal_scrollbar_state: ScrollbarState, - hide_scrollbar_task: Option>, max_width_item_index: Option, preserve_selection_on_buffer_fold_toggles: HashSet, pending_default_expansion_depth: Option, @@ -752,10 +747,6 @@ impl OutlinePanel { let focus_handle = cx.focus_handle(); let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in); - let focus_out_subscription = - cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| { - outline_panel.hide_scrollbar(window, cx); - }); let workspace_subscription = cx.subscribe_in( &workspace .weak_handle() @@ -868,12 +859,6 @@ impl OutlinePanel { workspace: workspace_handle, project, fs: workspace.app_state().fs.clone(), - show_scrollbar: !Self::should_autohide_scrollbar(cx), - hide_scrollbar_task: None, - vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), - horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), max_width_item_index: None, scroll_handle, focus_handle, @@ -903,7 +888,6 @@ impl OutlinePanel { settings_subscription, icons_subscription, focus_subscription, - focus_out_subscription, workspace_subscription, filter_update_subscription, ], @@ -4489,150 +4473,6 @@ impl OutlinePanel { cx.notify(); } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging()) - { - return None; - } - Some( - div() - .occlude() - .id("project-panel-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|outline_panel, _, window, cx| { - if !outline_panel.vertical_scrollbar_state.is_dragging() - && !outline_panel.focus_handle.contains_focused(window, cx) - { - outline_panel.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())), - ) - } - - fn render_horizontal_scrollbar( - &self, - _: &mut Window, - cx: &mut Context, - ) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging()) - { - return None; - } - Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| { - div() - .occlude() - .id("project-panel-horizontal-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|outline_panel, _, window, cx| { - if !outline_panel.horizontal_scrollbar_state.is_dragging() - && !outline_panel.focus_handle.contains_focused(window, cx) - { - outline_panel.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .w_full() - .absolute() - .right_1() - .left_1() - .bottom_0() - .h(px(12.)) - .cursor_default() - .child(scrollbar) - }) - } - - fn should_show_scrollbar(cx: &App) -> bool { - let show = OutlinePanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => true, - ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - } - } - - fn should_autohide_scrollbar(cx: &App) -> bool { - let show = OutlinePanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => true, - } - } - - fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - if !Self::should_autohide_scrollbar(cx) { - return; - } - self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 { let item_text_chars = match entry { PanelEntry::Fs(FsEntry::ExternalFile(external)) => self @@ -4688,7 +4528,7 @@ impl OutlinePanel { indent_size: f32, window: &mut Window, cx: &mut Context, - ) -> Div { + ) -> impl IntoElement { let contents = if self.cached_entries.is_empty() { let header = if self.updating_fs_entries || self.updating_cached_entries { None @@ -4699,6 +4539,7 @@ impl OutlinePanel { }; v_flex() + .id("empty-outline-state") .flex_1() .justify_center() .size_full() @@ -4848,10 +4689,13 @@ impl OutlinePanel { .flex_shrink() .size_full() .child(list_contents.size_full().flex_shrink()) - .children(self.render_vertical_scrollbar(cx)) - .when_some( - self.render_horizontal_scrollbar(window, cx), - |this, scrollbar| this.pb_4().child(scrollbar), + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal) + .tracked_entity(cx.entity_id()), + window, + cx, ) } .children(self.context_menu.as_ref().map(|(menu, position, _)| { @@ -5119,15 +4963,6 @@ impl Render for OutlinePanel { .size_full() .overflow_hidden() .relative() - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(window, cx); - } - })) .key_context(self.dispatch_context(window, cx)) .on_action(cx.listener(Self::open_selected_entry)) .on_action(cx.listener(Self::cancel)) diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index dc123f2ba5fb38dd80b72aee8fc6ad6a000be23d..db34a318188fc10b100b88f959b1c7a77b1161cb 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -1,8 +1,9 @@ -use editor::ShowScrollbar; -use gpui::Pixels; +use editor::EditorSettings; +use gpui::{App, Pixels}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -116,6 +117,14 @@ pub struct OutlinePanelSettingsContent { pub expand_outlines_with_depth: Option, } +impl ScrollbarVisibility for OutlinePanelSettings { + fn visibility(&self, cx: &App) -> ShowScrollbar { + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for OutlinePanelSettings { type FileContent = OutlinePanelSettingsContent; diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index 5f89793d28cb7f6ff896879713e9aa66661d5e7a..d785cb5b3a96502165b10e2bf0def0d8bf66cd67 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -23,7 +23,6 @@ menu.workspace = true schemars.workspace = true serde.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true workspace-hack.workspace = true diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 4bd8ac99cbd9b5fe793e8b0cfe1926732920d0a1..8816fb5424ff25788cec9cb602d2960ab753c135 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -11,17 +11,17 @@ use editor::{ use gpui::{ Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, - ScrollStrategy, Stateful, Task, UniformListScrollHandle, Window, actions, div, list, - prelude::*, uniform_list, + ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*, + uniform_list, }; use head::Head; use schemars::JsonSchema; use serde::Deserialize; use std::{ops::Range, sync::Arc, time::Duration}; use ui::{ - Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex, + Color, Divider, Label, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar, + prelude::*, v_flex, }; -use util::ResultExt; use workspace::ModalView; enum ElementContainer { @@ -65,13 +65,8 @@ pub struct Picker { width: Option, widest_item: Option, max_height: Option, - focus_handle: FocusHandle, /// An external control to display a scrollbar in the `Picker`. show_scrollbar: bool, - /// An internal state that controls whether to show the scrollbar based on the user's focus. - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, - hide_scrollbar_task: Option>, /// Whether the `Picker` is rendered as a self-contained modal. /// /// Set this to `false` when rendering the `Picker` as part of a larger modal. @@ -293,13 +288,6 @@ impl Picker { cx: &mut Context, ) -> Self { let element_container = Self::create_element_container(container); - let scrollbar_state = match &element_container { - ElementContainer::UniformList(scroll_handle) => { - ScrollbarState::new(scroll_handle.clone()) - } - ElementContainer::List(state) => ScrollbarState::new(state.clone()), - }; - let focus_handle = cx.focus_handle(); let mut this = Self { delegate, head, @@ -309,12 +297,8 @@ impl Picker { width: None, widest_item: None, max_height: Some(rems(18.).into()), - focus_handle, show_scrollbar: false, - scrollbar_visibility: true, - scrollbar_state, is_modal: true, - hide_scrollbar_task: None, }; this.update_matches("".to_string(), window, cx); // give the delegate 4ms to render the first set of suggestions. @@ -790,67 +774,6 @@ impl Picker { } } } - - fn hide_scrollbar(&mut self, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.scrollbar_visibility = false; - cx.notify(); - }) - .log_err(); - })) - } - - fn render_scrollbar(&self, cx: &mut Context) -> Option> { - if !self.show_scrollbar - || !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) - { - return None; - } - Some( - div() - .occlude() - .id("picker-scroll") - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|picker, _, window, cx| { - if !picker.scrollbar_state.is_dragging() - && !picker.focus_handle.contains_focused(window, cx) - { - picker.hide_scrollbar(cx); - cx.notify(); - } - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } } impl EventEmitter for Picker {} @@ -900,17 +823,22 @@ impl Render for Picker { .overflow_hidden() .children(self.delegate.render_header(window, cx)) .child(self.render_element_container(cx)) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.scrollbar_visibility = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(cx); - } - })) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) + .when(self.show_scrollbar, |this| { + let base_scrollbar_config = + Scrollbars::new(ScrollAxes::Vertical).width_sm(); + + this.map(|this| match &self.element_container { + ElementContainer::List(state) => this.custom_scrollbars( + base_scrollbar_config.tracked_scroll_handle(state.clone()), + window, + cx, + ), + ElementContainer::UniformList(state) => this.custom_scrollbars( + base_scrollbar_config.tracked_scroll_handle(state.clone()), + window, + cx, + ), + }) }), ) }) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 34471e817db56e9ca9cd693230c06db1fca25a0a..70ceb639acb5d76b35c852654d31093839006e23 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -7,12 +7,11 @@ use collections::{BTreeSet, HashMap, hash_map}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use editor::{ - Editor, EditorEvent, EditorSettings, ShowScrollbar, + Editor, EditorEvent, items::{ entry_diagnostic_aware_icon_decoration_and_color, entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color, }, - scroll::ScrollbarAutoHide, }; use file_icons::FileIcons; use git::status::GitSummary; @@ -59,7 +58,8 @@ use theme::ThemeSettings; use ui::{ Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, - ScrollableHandle, Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, + ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*, + v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -109,10 +109,6 @@ pub struct ProjectPanel { workspace: WeakEntity, width: Option, pending_serialization: Task>, - show_scrollbar: bool, - vertical_scrollbar_state: ScrollbarState, - horizontal_scrollbar_state: ScrollbarState, - hide_scrollbar_task: Option>, diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, max_width_item_index: Option, diagnostic_summary_update: Task<()>, @@ -461,7 +457,6 @@ impl ProjectPanel { cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { this.focus_out(window, cx); - this.hide_scrollbar(window, cx); }) .detach(); @@ -652,12 +647,6 @@ impl ProjectPanel { workspace: workspace.weak_handle(), width: None, pending_serialization: Task::ready(None), - show_scrollbar: !Self::should_autohide_scrollbar(cx), - hide_scrollbar_task: None, - vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), - horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), max_width_item_index: None, diagnostics: Default::default(), diagnostic_summary_update: Task::ready(()), @@ -4847,103 +4836,6 @@ impl ProjectPanel { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging()) - { - return None; - } - Some( - div() - .occlude() - .id("project-panel-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.vertical_scrollbar_state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_1() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical( - // percentage as f32..end_offset as f32, - self.vertical_scrollbar_state.clone(), - )), - ) - } - - fn render_horizontal_scrollbar(&self, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging()) - { - return None; - } - Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| { - div() - .occlude() - .id("project-panel-horizontal-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.horizontal_scrollbar_state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .w_full() - .absolute() - .right_1() - .left_1() - .bottom_1() - .h(px(12.)) - .cursor_default() - .child(scrollbar) - }) - } - fn dispatch_context(&self, window: &Window, cx: &Context) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("ProjectPanel"); @@ -4959,52 +4851,6 @@ impl ProjectPanel { dispatch_context } - fn should_show_scrollbar(cx: &App) -> bool { - let show = ProjectPanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => true, - ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - } - } - - fn should_autohide_scrollbar(cx: &App) -> bool { - let show = ProjectPanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => true, - } - } - - fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - if !Self::should_autohide_scrollbar(cx) { - return; - } - self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - fn reveal_entry( &mut self, project: Entity, @@ -5357,15 +5203,6 @@ impl Render for ProjectPanel { this.refresh_drag_cursor_style(&event.modifiers, window, cx); }, )) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(window, cx); - } - })) .on_click(cx.listener(|this, event, _, cx| { if matches!(event, gpui::ClickEvent::Keyboard(_)) { return; @@ -5755,10 +5592,14 @@ impl Render for ProjectPanel { ) .size_full(), ) - .children(self.render_vertical_scrollbar(cx)) - .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| { - this.pb_4().child(scrollbar) - }) + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal) + .notify_content(), + window, + cx, + ) .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( anchored() diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 6c812c294663d1d6fe7915d201f9e8925fa943ab..09e450c149ab5580e6c86241f8c3e30515b93a4f 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -1,8 +1,9 @@ -use editor::ShowScrollbar; +use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -168,6 +169,14 @@ pub struct ProjectPanelSettingsContent { pub drag_and_drop: Option, } +impl ScrollbarVisibility for ProjectPanelSettings { + fn visibility(&self, cx: &ui::App) -> ShowScrollbar { + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for ProjectPanelSettings { type FileContent = ProjectPanelSettingsContent; diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 99a1695a3f859dd48ebcb2ddf88093f1d1e474e1..39032642b887350730c16a12d696253c256cfd72 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -23,7 +23,6 @@ use remote::{ use settings::{Settings, SettingsStore, update_settings_file, watch_config_file}; use smol::stream::StreamExt as _; use std::{ - any::Any, borrow::Cow, collections::BTreeSet, path::PathBuf, @@ -35,7 +34,7 @@ use std::{ }; use ui::{ IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Navigable, NavigableEntry, - Scrollbar, ScrollbarState, Section, Tooltip, prelude::*, + Section, Tooltip, WithScrollbar, prelude::*, }; use util::{ ResultExt, @@ -284,7 +283,7 @@ impl RemoteEntry { #[derive(Clone)] struct DefaultState { - scrollbar: ScrollbarState, + scroll_handle: ScrollHandle, add_new_server: NavigableEntry, servers: Vec, } @@ -292,7 +291,6 @@ struct DefaultState { impl DefaultState { fn new(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { let handle = ScrollHandle::new(); - let scrollbar = ScrollbarState::new(handle.clone()); let add_new_server = NavigableEntry::new(&handle, cx); let ssh_settings = SshSettings::get_global(cx); @@ -333,7 +331,7 @@ impl DefaultState { } Self { - scrollbar, + scroll_handle: handle, add_new_server, servers, } @@ -1449,7 +1447,6 @@ impl RemoteServerProjects { } } - let scroll_state = state.scrollbar.parent_entity(&cx.entity()); let connect_button = div() .id("ssh-connect-new-server-container") .track_focus(&state.add_new_server.focus_handle) @@ -1480,17 +1477,12 @@ impl RemoteServerProjects { cx.notify(); })); - let handle = &**scroll_state.scroll_handle() as &dyn Any; - let Some(scroll_handle) = handle.downcast_ref::() else { - unreachable!() - }; - let mut modal_section = Navigable::new( v_flex() .track_focus(&self.focus_handle(cx)) .id("ssh-server-list") .overflow_y_scroll() - .track_scroll(scroll_handle) + .track_scroll(&state.scroll_handle) .size_full() .child(connect_button) .child( @@ -1585,17 +1577,7 @@ impl RemoteServerProjects { ) .size_full(), ) - .child( - div() - .occlude() - .h_full() - .absolute() - .top_1() - .bottom_1() - .right_1() - .w(px(8.)) - .children(Scrollbar::vertical(scroll_state)), - ), + .vertical_scrollbar_for(state.scroll_handle, window, cx), ), ) .into_any_element() diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 08caf9a4ef1c0b49dbfa8f8f2578f00ddb130ee0..5fdefc6a66224587bc746d21ecddb1a66a2bbe4f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -7,12 +7,11 @@ mod terminal_slash_command; pub mod terminal_tab_tooltip; use assistant_slash_command::SlashCommandRegistry; -use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; +use editor::{EditorSettings, actions::SelectAll}; use gpui::{ Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, - ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, - deferred, div, + ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; use persistence::TERMINAL_DB; use project::{Project, search::SearchQuery}; @@ -35,7 +34,9 @@ use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; use terminal_tab_tooltip::TerminalTooltip; use ui::{ - ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*, + ContextMenu, Icon, IconName, Label, ScrollAxes, Scrollbars, Tooltip, WithScrollbar, h_flex, + prelude::*, + scrollbars::{self, GlobalSetting, ScrollbarVisibility}, }; use util::ResultExt; use workspace::{ @@ -68,7 +69,6 @@ struct ImeState { } const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] @@ -139,10 +139,7 @@ pub struct TerminalView { show_breadcrumbs: bool, block_below_cursor: Option>, scroll_top: Pixels, - scrollbar_state: ScrollbarState, scroll_handle: TerminalScrollHandle, - show_scrollbar: bool, - hide_scrollbar_task: Option>, ime_state: Option, _subscriptions: Vec, _terminal_subscriptions: Vec, @@ -262,10 +259,7 @@ impl TerminalView { show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs, block_below_cursor: None, scroll_top: Pixels::ZERO, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, - show_scrollbar: !Self::should_autohide_scrollbar(cx), - hide_scrollbar_task: None, cwd_serialized: false, ime_state: None, _subscriptions: vec![ @@ -836,136 +830,6 @@ impl TerminalView { self.terminal = terminal; } - // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor. - fn map_show_scrollbar_from_editor_to_terminal( - show_scrollbar: editor::ShowScrollbar, - ) -> terminal_settings::ShowScrollbar { - match show_scrollbar { - editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto, - editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System, - editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always, - editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never, - } - } - - fn should_show_scrollbar(cx: &App) -> bool { - let show = TerminalSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| { - Self::map_show_scrollbar_from_editor_to_terminal( - EditorSettings::get_global(cx).scrollbar.show, - ) - }); - match show { - terminal_settings::ShowScrollbar::Auto => true, - terminal_settings::ShowScrollbar::System => true, - terminal_settings::ShowScrollbar::Always => true, - terminal_settings::ShowScrollbar::Never => false, - } - } - - fn should_autohide_scrollbar(cx: &App) -> bool { - let show = TerminalSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| { - Self::map_show_scrollbar_from_editor_to_terminal( - EditorSettings::get_global(cx).scrollbar.show, - ) - }); - match show { - terminal_settings::ShowScrollbar::Auto => true, - terminal_settings::ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - terminal_settings::ShowScrollbar::Always => false, - terminal_settings::ShowScrollbar::Never => true, - } - } - - fn hide_scrollbar(&mut self, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - if !Self::should_autohide_scrollbar(cx) { - return; - } - self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - - fn render_scrollbar(&self, window: &Window, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.scrollbar_state.is_dragging()) - || !self.content_mode(window, cx).is_scrollable() - { - return None; - } - - if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() { - return None; - } - - self.scroll_handle.update(self.terminal.read(cx)); - - if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() { - self.terminal.update(cx, |term, _| { - let delta = new_display_offset as i32 - term.last_content.display_offset as i32; - match delta.cmp(&0) { - std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize), - std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize), - std::cmp::Ordering::Equal => {} - } - }); - } - - Some( - div() - .occlude() - .id("terminal-view-scroll") - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|terminal_view, _, window, cx| { - if !terminal_view.scrollbar_state.is_dragging() - && !terminal_view.focus_handle.contains_focused(window, cx) - { - terminal_view.hide_scrollbar(cx); - cx.notify(); - } - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .absolute() - .top_0() - .bottom_0() - .right_0() - .h_full() - .w(TERMINAL_SCROLLBAR_WIDTH) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - fn rerun_button(task: &TaskState) -> Option { if !task.show_rerun { return None; @@ -1120,6 +984,29 @@ fn regex_search_for_query(query: &project::search::SearchQuery) -> Option &Self { + &Self + } +} + +impl ScrollbarVisibility for TerminalScrollbarSettingsWrapper { + fn visibility(&self, cx: &App) -> scrollbars::ShowScrollbar { + TerminalSettings::get_global(cx) + .scrollbar + .show + .map(|value| match value { + terminal_settings::ShowScrollbar::Auto => scrollbars::ShowScrollbar::Auto, + terminal_settings::ShowScrollbar::System => scrollbars::ShowScrollbar::System, + terminal_settings::ShowScrollbar::Always => scrollbars::ShowScrollbar::Always, + terminal_settings::ShowScrollbar::Never => scrollbars::ShowScrollbar::Never, + }) + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl TerminalView { fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { self.clear_bell(cx); @@ -1151,28 +1038,31 @@ impl TerminalView { terminal.focus_out(); terminal.set_cursor_shape(CursorShape::Hollow); }); - self.hide_scrollbar(cx); cx.notify(); } } impl Render for TerminalView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // TODO: this should be moved out of render + self.scroll_handle.update(self.terminal.read(cx)); + + if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() { + self.terminal.update(cx, |term, _| { + let delta = new_display_offset as i32 - term.last_content.display_offset as i32; + match delta.cmp(&0) { + std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize), + std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize), + std::cmp::Ordering::Equal => {} + } + }); + } + let terminal_handle = self.terminal.clone(); let terminal_view_handle = cx.entity(); let focused = self.focus_handle.is_focused(window); - // Always calculate scrollbar width to prevent layout shift - let scrollbar_width = if Self::should_show_scrollbar(cx) - && self.content_mode(window, cx).is_scrollable() - && self.terminal.read(cx).total_lines() > self.terminal.read(cx).viewport_lines() - { - TERMINAL_SCROLLBAR_WIDTH - } else { - px(0.) - }; - div() .id("terminal-view") .size_full() @@ -1209,21 +1099,12 @@ impl Render for TerminalView { } }), ) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(cx); - } - })) .child( // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu div() + .id("terminal-view-container") .size_full() .bg(cx.theme().colors().editor_background) - .when(scrollbar_width > px(0.), |div| div.pr(scrollbar_width)) .child(TerminalElement::new( terminal_handle, terminal_view_handle, @@ -1234,8 +1115,15 @@ impl Render for TerminalView { self.block_below_cursor.clone(), self.mode.clone(), )) - .when_some(self.render_scrollbar(window, cx), |div, scrollbar| { - div.child(scrollbar) + .when(self.content_mode(window, cx).is_scrollable(), |div| { + div.custom_scrollbars( + Scrollbars::for_settings::() + .show_along(ScrollAxes::Vertical) + .with_track_along(ScrollAxes::Vertical) + .tracked_scroll_handle(self.scroll_handle.clone()), + window, + cx, + ) }), ) .children(self.context_menu.as_ref().map(|(menu, position, _)| { diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index c0472917721eaca37214cc21505c94fab54d21cd..985a2bcdc7dadf3b28241b3d59744e48c654e76e 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -21,6 +21,7 @@ gpui_macros.workspace = true icons.workspace = true itertools.workspace = true menu.workspace = true +schemars.workspace = true serde.workspace = true settings.workspace = true smallvec.workspace = true diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 605028202fffa37d67bbdb4a9f33a97459390dfa..35dd52e357480d0a82c1c474b609c49bf2c53db0 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -1,39 +1,783 @@ -use std::{ - any::Any, - cell::{Cell, RefCell}, - fmt::Debug, - ops::Range, - rc::Rc, - sync::Arc, - time::Duration, -}; +use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Not, time::Duration}; -use crate::{IntoElement, prelude::*, px, relative}; use gpui::{ - Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle, - Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, - IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Point, ScrollHandle, ScrollWheelEvent, Size, Style, Task, UniformListScrollHandle, Window, - quad, + Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context, + Corner, Corners, CursorStyle, Div, Edges, Element, ElementId, Entity, EntityId, + GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, + LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate, + ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful, + StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration, + UniformListScrollHandle, Window, prelude::FluentBuilder as _, px, quad, relative, size, }; +use settings::SettingsStore; +use smallvec::SmallVec; +use theme::ActiveTheme as _; +use util::ResultExt; + +use std::ops::Range; + +use crate::scrollbars::{ScrollbarVisibility, ShowScrollbar}; + +const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_millis(1500); +const SCROLLBAR_PADDING: Pixels = px(4.); + +pub mod scrollbars { + use gpui::{App, Global}; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use settings::Settings; + + /// When to show the scrollbar in the editor. + /// + /// Default: auto + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "snake_case")] + pub enum ShowScrollbar { + /// Show the scrollbar if there's important information or + /// follow the system's configured behavior. + Auto, + /// Match the system's configured behavior. + System, + /// Always show the scrollbar. + Always, + /// Never show the scrollbar. + Never, + } + + impl ShowScrollbar { + pub(super) fn show(&self) -> bool { + *self != Self::Never + } + + pub(super) fn should_auto_hide(&self, cx: &mut App) -> bool { + match self { + Self::Auto => true, + Self::System => cx.default_global::().should_hide(), + _ => false, + } + } + } + + pub trait GlobalSetting { + fn get_value(cx: &App) -> &Self; + } + + impl GlobalSetting for T { + fn get_value(cx: &App) -> &T { + T::get_global(cx) + } + } + + impl GlobalSetting for ShowScrollbar { + fn get_value(_cx: &App) -> &Self { + &ShowScrollbar::Always + } + } + + pub trait ScrollbarVisibility: GlobalSetting + 'static { + fn visibility(&self, cx: &App) -> ShowScrollbar; + } + + impl ScrollbarVisibility for ShowScrollbar { + fn visibility(&self, cx: &App) -> ShowScrollbar { + *ShowScrollbar::get_value(cx) + } + } + + #[derive(Default)] + pub struct ScrollbarAutoHide(pub bool); + + impl ScrollbarAutoHide { + pub fn should_hide(&self) -> bool { + self.0 + } + } + + impl Global for ScrollbarAutoHide {} +} + +fn get_scrollbar_state( + mut config: Scrollbars, + caller_location: &'static std::panic::Location, + window: &mut Window, + cx: &mut App, +) -> Entity> +where + S: ScrollbarVisibility, + T: ScrollableHandle, +{ + let element_id = config.id.take().unwrap_or_else(|| caller_location.into()); + + window.use_keyed_state(element_id, cx, |window, cx| { + let parent_id = cx.entity_id(); + ScrollbarStateWrapper( + cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, window, cx)), + ) + }) +} + +pub trait WithScrollbar: Sized { + type Output; + + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibility, + T: ScrollableHandle; + + #[track_caller] + fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { + self.custom_scrollbars( + Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()), + window, + cx, + ) + } + + #[track_caller] + fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { + self.custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()), + window, + cx, + ) + } + + #[track_caller] + fn vertical_scrollbar_for( + self, + scroll_handle: ScrollHandle, + window: &mut Window, + cx: &mut App, + ) -> Self::Output { + self.custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical) + .tracked_scroll_handle(scroll_handle) + .ensure_id(core::panic::Location::caller()), + window, + cx, + ) + } +} + +impl WithScrollbar for Stateful
{ + type Output = Self; + + #[track_caller] + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibility, + T: ScrollableHandle, + { + render_scrollbar( + get_scrollbar_state(config, std::panic::Location::caller(), window, cx), + self, + cx, + ) + } +} + +impl WithScrollbar for Div { + type Output = Stateful
; + + #[track_caller] + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibility, + T: ScrollableHandle, + { + let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); + // We know this ID stays consistent as long as the element is rendered for + // consecutive frames, which is sufficient for our use case here + let scrollbar_entity_id = scrollbar.entity_id(); + + render_scrollbar( + scrollbar, + self.id(("track-scroll", scrollbar_entity_id)), + cx, + ) + } +} + +fn render_scrollbar( + scrollbar: Entity>, + div: Stateful
, + cx: &App, +) -> Stateful
+where + S: ScrollbarVisibility, + T: ScrollableHandle, +{ + let state = &scrollbar.read(cx).0; + + div.when_some(state.read(cx).handle_to_track(), |this, handle| { + this.track_scroll(handle).when_some( + state.read(cx).visible_axes(), + |this, axes| match axes { + ScrollAxes::Horizontal => this.overflow_x_scroll(), + ScrollAxes::Vertical => this.overflow_y_scroll(), + ScrollAxes::Both => this.overflow_scroll(), + }, + ) + }) + .when_some( + state + .read(cx) + .space_to_reserve_for(ScrollbarAxis::Horizontal), + |this, space| this.pb(space), + ) + .when_some( + state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical), + |this, space| this.pr(space), + ) + .child(state.clone()) +} + +impl UniformListDecoration + for ScrollbarStateWrapper +{ + fn compute( + &self, + _visible_range: Range, + _bounds: Bounds, + scroll_offset: Point, + _item_height: Pixels, + _item_count: usize, + _window: &mut Window, + _cx: &mut App, + ) -> gpui::AnyElement { + ScrollbarElement { + origin: scroll_offset.negate(), + state: self.0.clone(), + } + .into_any() + } +} + +// impl WithScrollbar for UniformList { +// type Output = Self; + +// #[track_caller] +// fn custom_scrollbars( +// self, +// config: Scrollbars, +// window: &mut Window, +// cx: &mut App, +// ) -> Self::Output +// where +// S: ScrollbarVisibilitySetting, +// T: ScrollableHandle, +// { +// let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); +// self.when_some( +// scrollbar.read_with(cx, |wrapper, cx| { +// wrapper +// .0 +// .read(cx) +// .handle_to_track::() +// .cloned() +// }), +// |this, handle| this.track_scroll(handle), +// ) +// .with_decoration(scrollbar) +// } +// } + +pub enum ScrollAxes { + Horizontal, + Vertical, + Both, +} + +impl ScrollAxes { + fn apply_to(self, point: Point, value: T) -> Point + where + T: Debug + Default + PartialEq + Clone, + { + match self { + Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value), + Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value), + Self::Both => Point::new(value.clone(), value), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +enum ReservedSpace { + #[default] + None, + Thumb, + Track, +} + +impl ReservedSpace { + fn is_visible(&self) -> bool { + *self != ReservedSpace::None + } + + fn needs_scroll_track(&self) -> bool { + *self == ReservedSpace::Track + } +} + +#[derive(Debug, Default)] +enum ScrollbarWidth { + #[default] + Normal, + Small, + XSmall, +} + +impl ScrollbarWidth { + fn to_pixels(&self) -> Pixels { + match self { + ScrollbarWidth::Normal => px(8.), + ScrollbarWidth::Small => px(6.), + ScrollbarWidth::XSmall => px(4.), + } + } +} + +pub struct Scrollbars { + id: Option, + tracked_setting: PhantomData, + tracked_entity: Option>, + scrollable_handle: Box T>, + handle_was_added: bool, + visibility: Point, + scrollbar_width: ScrollbarWidth, +} + +impl Scrollbars { + pub fn new(show_along: ScrollAxes) -> Self { + Self::new_with_setting(show_along) + } + + pub fn for_settings() -> Scrollbars { + Scrollbars::::new_with_setting(ScrollAxes::Both) + } +} + +impl Scrollbars { + fn new_with_setting(show_along: ScrollAxes) -> Self { + Self { + id: None, + tracked_setting: PhantomData, + handle_was_added: false, + scrollable_handle: Box::new(ScrollHandle::new), + tracked_entity: None, + visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb), + scrollbar_width: ScrollbarWidth::Normal, + } + } +} + +impl + Scrollbars +{ + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + fn ensure_id(mut self, id: impl Into) -> Self { + if self.id.is_none() { + self.id = Some(id.into()); + } + self + } + + /// Notify the current context whenever this scrollbar gets a scroll event + pub fn notify_content(mut self) -> Self { + self.tracked_entity = Some(None); + self + } + + /// Set a parent model which should be notified whenever this scrollbar gets a scroll event. + pub fn tracked_entity(mut self, entity_id: EntityId) -> Self { + self.tracked_entity = Some(Some(entity_id)); + self + } + + pub fn tracked_scroll_handle( + self, + tracked_scroll_handle: TrackedHandle, + ) -> Scrollbars { + let Self { + id, + tracked_setting, + tracked_entity: tracked_entity_id, + scrollbar_width, + visibility, + .. + } = self; + + Scrollbars { + handle_was_added: true, + scrollable_handle: Box::new(|| tracked_scroll_handle), + id, + tracked_setting, + tracked_entity: tracked_entity_id, + visibility, + scrollbar_width, + } + } + + pub fn show_along(mut self, along: ScrollAxes) -> Self { + self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb); + self + } + + pub fn with_track_along(mut self, along: ScrollAxes) -> Self { + self.visibility = along.apply_to(self.visibility, ReservedSpace::Track); + self + } + + pub fn width_sm(mut self) -> Self { + self.scrollbar_width = ScrollbarWidth::Small; + self + } + + pub fn width_xs(mut self) -> Self { + self.scrollbar_width = ScrollbarWidth::XSmall; + self + } +} + +#[derive(PartialEq, Eq)] +enum VisibilityState { + Visible, + Hidden, + Disabled, +} + +impl VisibilityState { + fn from_show_setting(show_setting: ShowScrollbar) -> Self { + if show_setting.show() { + Self::Visible + } else { + Self::Disabled + } + } + + fn is_visible(&self) -> bool { + *self == VisibilityState::Visible + } + + #[inline] + fn is_disabled(&self) -> bool { + *self == VisibilityState::Disabled + } +} + +enum ParentHovered { + Yes(bool), + No(bool), +} + +/// This is used to ensure notifies within the state do not notify the parent +/// unintentionally. +struct ScrollbarStateWrapper( + Entity>, +); + +/// A scrollbar state that should be persisted across frames. +struct ScrollbarState { + thumb_state: ThumbState, + notify_id: Option, + manually_added: bool, + scroll_handle: T, + width: ScrollbarWidth, + tracked_setting: PhantomData, + show_setting: ShowScrollbar, + visibility: Point, + show_state: VisibilityState, + mouse_in_parent: bool, + last_prepaint_state: Option, + _auto_hide_task: Option>, +} + +impl ScrollbarState { + fn new_from_config( + config: Scrollbars, + parent_id: EntityId, + window: &mut Window, + cx: &mut Context, + ) -> Self { + cx.observe_global_in::(window, Self::settings_changed) + .detach(); + + ScrollbarState { + thumb_state: Default::default(), + notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)), + manually_added: config.handle_was_added, + scroll_handle: (config.scrollable_handle)(), + width: config.scrollbar_width, + visibility: config.visibility, + tracked_setting: PhantomData, + show_setting: S::get_value(cx).visibility(cx), + show_state: VisibilityState::Visible, + mouse_in_parent: true, + last_prepaint_state: None, + _auto_hide_task: None, + } + } + + fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { + self.set_show_scrollbar(S::get_value(cx).visibility(cx), window, cx); + } + + /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet. + fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context) { + if self._auto_hide_task.is_none() { + self._auto_hide_task = + (self.visible() && self.show_setting.should_auto_hide(cx)).then(|| { + cx.spawn_in(window, async move |scrollbar_state, cx| { + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; + scrollbar_state + .update(cx, |state, cx| { + state.set_visibility(VisibilityState::Hidden, cx); + state._auto_hide_task.take() + }) + .log_err(); + }) + }); + } + } + + fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { + self.set_visibility(VisibilityState::Visible, cx); + self._auto_hide_task.take(); + self.schedule_auto_hide(window, cx); + } + + fn set_show_scrollbar( + &mut self, + show: ShowScrollbar, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_setting != show { + self.show_setting = show; + self.set_visibility(VisibilityState::from_show_setting(show), cx); + self.schedule_auto_hide(window, cx); + cx.notify(); + } + } + + fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context) { + if self.show_state != visibility { + self.show_state = visibility; + cx.notify(); + } + } + + #[inline] + fn visible_axes(&self) -> Option { + match (&self.visibility.x, &self.visibility.y) { + (ReservedSpace::None, ReservedSpace::None) => None, + (ReservedSpace::None, _) => Some(ScrollAxes::Vertical), + (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal), + _ => Some(ScrollAxes::Both), + } + } + + fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option { + (self.show_state.is_disabled().not() && self.visibility.along(axis).needs_scroll_track()) + .then(|| self.space_to_reserve()) + } + + fn space_to_reserve(&self) -> Pixels { + self.width.to_pixels() + 2 * SCROLLBAR_PADDING + } + + fn handle_to_track(&self) -> Option<&Handle> { + (!self.manually_added) + .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::()) + .flatten() + } + + fn scroll_handle(&self) -> &T { + &self.scroll_handle + } + + fn set_offset(&mut self, offset: Point, cx: &mut Context) { + self.scroll_handle.set_offset(offset); + self.notify_parent(cx); + cx.notify(); + } + + fn is_dragging(&self) -> bool { + self.thumb_state.is_dragging() + } + + fn set_dragging( + &mut self, + axis: ScrollbarAxis, + drag_offset: Pixels, + window: &mut Window, + cx: &mut Context, + ) { + self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx); + self.scroll_handle().drag_started(); + } + + fn update_hovered_thumb( + &mut self, + position: &Point, + window: &mut Window, + cx: &mut Context, + ) { + self.set_thumb_state( + if let Some(&ScrollbarLayout { axis, .. }) = self + .last_prepaint_state + .as_ref() + .and_then(|state| state.thumb_for_position(position)) + { + ThumbState::Hover(axis) + } else { + ThumbState::Inactive + }, + window, + cx, + ); + } + + fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context) { + if self.thumb_state != state { + if state == ThumbState::Inactive { + self.schedule_auto_hide(window, cx); + } else { + self.show_scrollbars(window, cx); + } + self.thumb_state = state; + cx.notify(); + } + } + + fn update_parent_hovered(&mut self, position: &Point) -> ParentHovered { + let last_parent_hovered = self.mouse_in_parent; + self.mouse_in_parent = self.parent_hovered(position); + let state_changed = self.mouse_in_parent != last_parent_hovered; + match self.mouse_in_parent { + true => ParentHovered::Yes(state_changed), + false => ParentHovered::No(state_changed), + } + } + + fn parent_hovered(&self, position: &Point) -> bool { + self.last_prepaint_state + .as_ref() + .is_some_and(|state| state.parent_bounds.contains(position)) + } + + fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { + self.last_prepaint_state + .as_ref() + .and_then(|state| state.hit_for_position(position)) + } + + fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> { + self.last_prepaint_state + .as_ref() + .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis)) + } + + fn thumb_ranges( + &self, + ) -> impl Iterator, ReservedSpace)> + '_ { + const MINIMUM_THUMB_SIZE: Pixels = px(25.); + let max_offset = self.scroll_handle().max_offset(); + let viewport_size = self.scroll_handle().viewport().size; + let current_offset = self.scroll_handle().offset(); + + [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical] + .into_iter() + .filter(|&axis| self.visibility.along(axis).is_visible()) + .flat_map(move |axis| { + let max_offset = max_offset.along(axis); + let viewport_size = viewport_size.along(axis); + 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 current_offset = current_offset + .along(axis) + .clamp(-max_offset, Pixels::ZERO) + .abs(); + 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; + Some(( + axis, + thumb_percentage_start..thumb_percentage_end, + self.visibility.along(axis), + )) + }) + } + + fn visible(&self) -> bool { + self.show_state.is_visible() + } -pub struct Scrollbar { - thumb: Range, - state: ScrollbarState, - kind: ScrollbarAxis, + #[inline] + fn disabled(&self) -> bool { + self.show_state.is_disabled() + } + + fn notify_parent(&self, cx: &mut App) { + if let Some(entity_id) = self.notify_id { + cx.notify(entity_id); + } + } } -#[derive(Default, Debug, Clone, Copy)] +impl Render for ScrollbarState { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + ScrollbarElement { + state: cx.entity(), + origin: Default::default(), + } + } +} + +struct ScrollbarElement { + origin: Point, + state: Entity>, +} + +#[derive(Default, Debug, PartialEq, Eq)] enum ThumbState { #[default] Inactive, - Hover, - Dragging(Pixels), + Hover(ScrollbarAxis), + Dragging(ScrollbarAxis, Pixels), } impl ThumbState { fn is_dragging(&self) -> bool { - matches!(*self, ThumbState::Dragging(_)) + matches!(*self, ThumbState::Dragging(..)) } } @@ -99,170 +843,110 @@ impl ScrollableHandle for ScrollHandle { } } -pub trait ScrollableHandle: Any + Debug { - fn content_size(&self) -> Size { - self.viewport().size + self.max_offset() - } +pub trait ScrollableHandle: 'static + Any + Sized { fn max_offset(&self) -> Size; fn set_offset(&self, point: Point); fn offset(&self) -> Point; fn viewport(&self) -> Bounds; fn drag_started(&self) {} fn drag_ended(&self) {} -} -/// A scrollbar state that should be persisted across frames. -#[derive(Clone, Debug)] -pub struct ScrollbarState { - thumb_state: Rc>, - parent_id: Option, - scroll_handle: Arc, - auto_hide: Rc>, + fn scrollable_along(&self, axis: ScrollbarAxis) -> bool { + self.max_offset().along(axis) > Pixels::ZERO + } + fn content_size(&self) -> Size { + self.viewport().size + self.max_offset() + } } -#[derive(Debug)] -enum AutoHide { - Disabled, - Hidden { - parent_id: EntityId, - }, - Visible { - parent_id: EntityId, - _task: Task<()>, - }, +enum ScrollbarMouseEvent { + TrackClick, + ThumbDrag(Pixels), } -impl AutoHide { - fn is_hidden(&self) -> bool { - matches!(self, AutoHide::Hidden { .. }) - } +struct ScrollbarLayout { + thumb_bounds: Bounds, + track_bounds: Bounds, + cursor_hitbox: Hitbox, + reserved_space: ReservedSpace, + axis: ScrollbarAxis, } -impl ScrollbarState { - pub fn new(scroll: impl ScrollableHandle) -> Self { - Self { - thumb_state: Default::default(), - parent_id: None, - scroll_handle: Arc::new(scroll), - auto_hide: Rc::new(RefCell::new(AutoHide::Disabled)), - } - } - - /// Set a parent model which should be notified whenever this Scrollbar gets a scroll event. - pub fn parent_entity(mut self, v: &Entity) -> Self { - self.parent_id = Some(v.entity_id()); - self - } - - pub fn scroll_handle(&self) -> &Arc { - &self.scroll_handle - } - - pub fn is_dragging(&self) -> bool { - matches!(self.thumb_state.get(), ThumbState::Dragging(_)) - } +impl ScrollbarLayout { + fn compute_click_offset( + &self, + event_position: Point, + max_offset: Size, + event_type: ScrollbarMouseEvent, + ) -> Pixels { + let Self { + track_bounds, + thumb_bounds, + axis, + .. + } = self; + let axis = *axis; + + let viewport_size = track_bounds.size.along(axis); + let thumb_size = thumb_bounds.size.along(axis); + let thumb_offset = match event_type { + ScrollbarMouseEvent::TrackClick => thumb_size / 2., + ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset, + }; - fn set_dragging(&self, drag_offset: Pixels) { - self.set_thumb_state(ThumbState::Dragging(drag_offset)); - self.scroll_handle.drag_started(); - } + let thumb_start = + (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset) + .clamp(px(0.), viewport_size - thumb_size); - fn set_thumb_hovered(&self, hovered: bool) { - self.set_thumb_state(if hovered { - ThumbState::Hover + let max_offset = max_offset.along(axis); + let percentage = if viewport_size > thumb_size { + thumb_start / (viewport_size - thumb_size) } else { - ThumbState::Inactive - }); - } - - fn set_thumb_state(&self, state: ThumbState) { - self.thumb_state.set(state); - } - - fn thumb_range(&self, axis: ScrollbarAxis) -> Option> { - const MINIMUM_THUMB_SIZE: Pixels = px(25.); - let max_offset = self.scroll_handle.max_offset().along(axis); - let viewport_size = self.scroll_handle.viewport().size.along(axis); - 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 current_offset = self - .scroll_handle - .offset() - .along(axis) - .clamp(-max_offset, Pixels::ZERO) - .abs(); - 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; - Some(thumb_percentage_start..thumb_percentage_end) - } - - fn show_temporarily(&self, parent_id: EntityId, cx: &mut App) { - const SHOW_INTERVAL: Duration = Duration::from_secs(1); - - let auto_hide = self.auto_hide.clone(); - auto_hide.replace(AutoHide::Visible { - parent_id, - _task: cx.spawn({ - let this = auto_hide.clone(); - async move |cx| { - cx.background_executor().timer(SHOW_INTERVAL).await; - this.replace(AutoHide::Hidden { parent_id }); - cx.update(|cx| { - cx.notify(parent_id); - }) - .ok(); - } - }), - }); - } - - fn unhide(&self, position: &Point, cx: &mut App) { - let parent_id = match &*self.auto_hide.borrow() { - AutoHide::Disabled => return, - AutoHide::Hidden { parent_id } => *parent_id, - AutoHide::Visible { parent_id, _task } => *parent_id, + 0. }; - if self.scroll_handle().viewport().contains(position) { - self.show_temporarily(parent_id, cx); - } + -max_offset * percentage } } -impl Scrollbar { - pub fn vertical(state: ScrollbarState) -> Option { - Self::new(state, ScrollbarAxis::Vertical) +impl PartialEq for ScrollbarLayout { + fn eq(&self, other: &Self) -> bool { + self.axis == other.axis && self.thumb_bounds == other.thumb_bounds } +} - pub fn horizontal(state: ScrollbarState) -> Option { - Self::new(state, ScrollbarAxis::Horizontal) +pub struct ScrollbarPrepaintState { + parent_bounds: Bounds, + thumbs: SmallVec<[ScrollbarLayout; 2]>, +} + +impl ScrollbarPrepaintState { + fn thumb_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { + self.thumbs + .iter() + .find(|info| info.thumb_bounds.contains(position)) } - fn new(state: ScrollbarState, kind: ScrollbarAxis) -> Option { - let thumb = state.thumb_range(kind)?; - Some(Self { thumb, state, kind }) + fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { + self.thumbs.iter().find(|info| { + if info.reserved_space.needs_scroll_track() { + info.track_bounds.contains(position) + } else { + info.thumb_bounds.contains(position) + } + }) } +} - /// Automatically hide the scrollbar when idle - pub fn auto_hide(self, cx: &mut Context) -> Self { - if matches!(*self.state.auto_hide.borrow(), AutoHide::Disabled) { - self.state.show_temporarily(cx.entity_id(), cx); - } - self +impl PartialEq for ScrollbarPrepaintState { + fn eq(&self, other: &Self) -> bool { + self.thumbs == other.thumbs } } -impl Element for Scrollbar { +impl Element for ScrollbarElement { type RequestLayoutState = (); - type PrepaintState = Hitbox; + type PrepaintState = Option; fn id(&self) -> Option { None @@ -279,19 +963,14 @@ impl Element for Scrollbar { window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.flex_grow = 1.; - style.flex_shrink = 1.; - - if self.kind == ScrollbarAxis::Vertical { - style.size.width = px(12.).into(); - style.size.height = relative(1.).into(); - } else { - style.size.width = relative(1.).into(); - style.size.height = px(12.).into(); - } + let scrollbar_style = Style { + position: Position::Absolute, + inset: Edges::default(), + size: size(relative(1.), relative(1.)).map(Into::into), + ..Default::default() + }; - (window.request_layout(style, None, cx), ()) + (window.request_layout(scrollbar_style, None, cx), ()) } fn prepaint( @@ -301,203 +980,292 @@ impl Element for Scrollbar { bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, window: &mut Window, - _: &mut App, + cx: &mut App, ) -> Self::PrepaintState { - window.with_content_mask(Some(ContentMask { bounds }), |window| { - window.insert_hitbox(bounds, HitboxBehavior::Normal) - }) + let prepaint_state = self + .state + .read(cx) + .disabled() + .not() + .then(|| ScrollbarPrepaintState { + parent_bounds: bounds, + thumbs: { + let thumb_ranges = self.state.read(cx).thumb_ranges().collect::>(); + let width = self.state.read(cx).width.to_pixels(); + + let additional_padding = if thumb_ranges.len() == 2 { + width + } else { + Pixels::ZERO + }; + + thumb_ranges + .into_iter() + .map(|(axis, thumb_range, reserved_space)| { + let track_anchor = match axis { + ScrollbarAxis::Horizontal => Corner::BottomLeft, + ScrollbarAxis::Vertical => Corner::TopRight, + }; + let Bounds { origin, size } = Bounds::from_corner_and_size( + track_anchor, + bounds + .corner(track_anchor) + .apply_along(axis.invert(), |corner| { + corner - SCROLLBAR_PADDING + }), + bounds.size.apply_along(axis.invert(), |_| width), + ); + let scroll_track_bounds = Bounds::new(self.origin + origin, size); + + let padded_bounds = scroll_track_bounds.extend(match axis { + ScrollbarAxis::Horizontal => Edges { + right: -SCROLLBAR_PADDING, + left: -SCROLLBAR_PADDING, + ..Default::default() + }, + ScrollbarAxis::Vertical => Edges { + top: -SCROLLBAR_PADDING, + bottom: -SCROLLBAR_PADDING, + ..Default::default() + }, + }); + + let available_space = + padded_bounds.size.along(axis) - additional_padding; + + let thumb_offset = thumb_range.start * available_space; + let thumb_end = thumb_range.end * available_space; + 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), + ); + + ScrollbarLayout { + thumb_bounds, + track_bounds: padded_bounds, + axis, + cursor_hitbox: window.insert_hitbox( + if reserved_space.needs_scroll_track() { + padded_bounds + } else { + thumb_bounds + }, + HitboxBehavior::BlockMouseExceptScroll, + ), + reserved_space, + } + }) + .collect() + }, + }); + if prepaint_state + .as_ref() + .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref()) + { + self.state + .update(cx, |state, cx| state.show_scrollbars(window, cx)); + } + + prepaint_state } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, + Bounds { origin, size }: Bounds, _request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Self::PrepaintState, + prepaint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { - const EXTRA_PADDING: Pixels = px(5.0); + let Some(prepaint_state) = prepaint_state.take() else { + return; + }; + + let bounds = Bounds::new(self.origin + origin, size); window.with_content_mask(Some(ContentMask { bounds }), |window| { - let axis = self.kind; let colors = cx.theme().colors(); - let thumb_state = self.state.thumb_state.get(); - let thumb_base_color = match thumb_state { - ThumbState::Dragging(_) => colors.scrollbar_thumb_active_background, - ThumbState::Hover => colors.scrollbar_thumb_hover_background, - ThumbState::Inactive => colors.scrollbar_thumb_background, - }; - - let thumb_background = colors.surface_background.blend(thumb_base_color); - - 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), - ); - - if thumb_state.is_dragging() || !self.state.auto_hide.borrow().is_hidden() { - let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0); - - window.paint_quad(quad( - thumb_bounds, - corners, - thumb_background, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); - } - - if thumb_state.is_dragging() { - window.set_window_cursor_style(CursorStyle::Arrow); - } else { - window.set_cursor_style(CursorStyle::Arrow, hitbox); - } - - enum ScrollbarMouseEvent { - GutterClick, - ThumbDrag(Pixels), - } - - let compute_click_offset = - move |event_position: Point, - max_offset: Size, - 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, + if self.state.read(cx).visible() { + for ScrollbarLayout { + thumb_bounds, + cursor_hitbox, + axis, + reserved_space, + .. + } in &prepaint_state.thumbs + { + const MAXIMUM_OPACITY: f32 = 0.7; + let thumb_state = &self.state.read(cx).thumb_state; + let (thumb_base_color, hovered) = match thumb_state { + ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => { + (colors.scrollbar_thumb_active_background, false) + } + ThumbState::Hover(hovered_axis) if hovered_axis == axis => { + (colors.scrollbar_thumb_hover_background, true) + } + _ => (colors.scrollbar_thumb_background, false), }; - let thumb_start = (event_position.along(axis) - - padded_bounds.origin.along(axis) - - thumb_offset) - .clamp(px(0.), viewport_size - thumb_size); - - let max_offset = max_offset.along(axis); - let percentage = if viewport_size > thumb_size { - thumb_start / (viewport_size - thumb_size) + let blending_color = if hovered || reserved_space.needs_scroll_track() { + colors.surface_background } else { - 0. + let blend_color = colors.surface_background; + blend_color.min(blend_color.alpha(MAXIMUM_OPACITY)) }; - -max_offset * percentage - }; + let thumb_background = blending_color.blend(thumb_base_color); - window.on_mouse_event({ - let state = self.state.clone(); - move |event: &MouseDownEvent, phase, _, _| { - if !phase.bubble() - || event.button != MouseButton::Left - || !bounds.contains(&event.position) - { - return; - } + window.paint_quad(quad( + *thumb_bounds, + Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size), + thumb_background, + Edges::default(), + Hsla::transparent_black(), + BorderStyle::default(), + )); - if thumb_bounds.contains(&event.position) { - let offset = event.position.along(axis) - thumb_bounds.origin.along(axis); - state.set_dragging(offset); + if thumb_state.is_dragging() { + window.set_window_cursor_style(CursorStyle::Arrow); } else { - let scroll_handle = state.scroll_handle(); - let click_offset = compute_click_offset( - event.position, - scroll_handle.max_offset(), - ScrollbarMouseEvent::GutterClick, - ); - scroll_handle - .set_offset(scroll_handle.offset().apply_along(axis, |_| click_offset)); + window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox); } } + } + + self.state.update(cx, |state, _| { + state.last_prepaint_state = Some(prepaint_state) }); window.on_mouse_event({ let state = self.state.clone(); - let scroll_handle = self.state.scroll_handle().clone(); - move |event: &ScrollWheelEvent, phase, window, cx| { - if phase.bubble() { - state.unhide(&event.position, cx); - if bounds.contains(&event.position) { - let current_offset = scroll_handle.offset(); - scroll_handle.set_offset( - current_offset + event.delta.pixel_delta(window.line_height()), + move |event: &MouseDownEvent, phase, window, cx| { + state.update(cx, |state, cx| { + let Some(scrollbar_layout) = (phase.capture() + && event.button == MouseButton::Left) + .then(|| state.hit_for_position(&event.position)) + .flatten() + else { + return; + }; + + let ScrollbarLayout { + thumb_bounds, axis, .. + } = scrollbar_layout; + + if thumb_bounds.contains(&event.position) { + let offset = + event.position.along(*axis) - thumb_bounds.origin.along(*axis); + state.set_dragging(*axis, offset, window, cx); + } else { + let scroll_handle = state.scroll_handle(); + let click_offset = scrollbar_layout.compute_click_offset( + event.position, + scroll_handle.max_offset(), + ScrollbarMouseEvent::TrackClick, ); - } + state.set_offset( + scroll_handle.offset().apply_along(*axis, |_| click_offset), + cx, + ); + }; + + cx.stop_propagation(); + }); + } + }); + + window.on_mouse_event({ + let state = self.state.clone(); + + move |event: &ScrollWheelEvent, phase, window, cx| { + if phase.capture() { + state.update(cx, |state, cx| { + state.update_hovered_thumb(&event.position, window, cx) + }); } } }); window.on_mouse_event({ let state = self.state.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { - if phase.bubble() { - state.unhide(&event.position, cx); + if !phase.capture() { + return; + } - match state.thumb_state.get() { - ThumbState::Dragging(drag_state) if event.dragging() => { - let scroll_handle = state.scroll_handle(); - let drag_offset = compute_click_offset( + match state.read(cx).thumb_state { + ThumbState::Dragging(axis, drag_state) if event.dragging() => { + if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) { + let scroll_handle = state.read(cx).scroll_handle(); + let drag_offset = scrollbar_layout.compute_click_offset( event.position, scroll_handle.max_offset(), ScrollbarMouseEvent::ThumbDrag(drag_state), ); - scroll_handle.set_offset( - scroll_handle.offset().apply_along(axis, |_| drag_offset), - ); - window.refresh(); - if let Some(id) = state.parent_id { - cx.notify(id); - } - } - _ if event.pressed_button.is_none() => { - state.set_thumb_hovered(thumb_bounds.contains(&event.position)) + let new_offset = + scroll_handle.offset().apply_along(axis, |_| drag_offset); + + state.update(cx, |state, cx| state.set_offset(new_offset, cx)); + cx.stop_propagation(); } - _ => {} } + _ => state.update(cx, |state, cx| { + match state.update_parent_hovered(&event.position) { + ParentHovered::Yes(state_changed) + if event.pressed_button.is_none() => + { + if state_changed { + state.show_scrollbars(window, cx); + } + state.update_hovered_thumb(&event.position, window, cx); + if state.thumb_state != ThumbState::Inactive { + cx.stop_propagation(); + } + } + ParentHovered::No(state_changed) if state_changed => { + state.set_thumb_state(ThumbState::Inactive, window, cx); + } + _ => {} + } + }), } } }); window.on_mouse_event({ let state = self.state.clone(); - move |event: &MouseUpEvent, phase, _, cx| { - if phase.bubble() { + move |event: &MouseUpEvent, phase, window, cx| { + if !phase.capture() { + return; + } + + state.update(cx, |state, cx| { if state.is_dragging() { state.scroll_handle().drag_ended(); - if let Some(id) = state.parent_id { - cx.notify(id); - } } - state.set_thumb_hovered(thumb_bounds.contains(&event.position)); - } + + if !state.parent_hovered(&event.position) { + state.schedule_auto_hide(window, cx); + return; + } + + state.update_hovered_thumb(&event.position, window, cx); + }); } }); }) } } -impl IntoElement for Scrollbar { +impl IntoElement for ScrollbarElement { type Element = Self; fn into_element(self) -> Self::Element {