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 | 1422 +++++++++++++---- 35 files changed, 1566 insertions(+), 2386 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae72bbdff3..8252dc5fe8 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 015a2548d5..ed508ea18d 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 bae2e107ee..d3986155d7 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 4ad5b2d8e8..faa6b524ff 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 9fc952a2ea..0ede5879ae 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 a134b916a2..bc6e90ed09 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(); + 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(); + 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(); - 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(); - true - } 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 7743cfbdee..4ea763c92c 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 e51b8da362..4271bdcbb8 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 b396f0921e..494e6c7f86 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 007fdd52d7..00ae0bcbac 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 7f4d024e57..09f4966374 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 673557a2c6..a9c24a4b3e 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 6541f76a56..51be0d234e 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 8231448618..828ab0594d 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 cb21f35d7e..54d8a50115 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 82ee541745..dab238dc8b 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 f30b53faee..3063206723 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 be207314ac..74e8e25927 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 443bcb14bb..bd2eed3397 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 cdf90d4eb8..9c601aac1d 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 53ca6508bc..78bca5a499 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 465928d024..288726d379 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 1c4532fda2..307197acca 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 7aa8a0c284..51524f92ea 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 9d7bb07360..cb0332c868 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 scroll_offset(&self) -> Point { + self.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(); - } - - 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 2f34d47667..8f501b2f97 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 dc123f2ba5..db34a31818 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 5f89793d28..d785cb5b3a 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 4bd8ac99cb..8816fb5424 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 34471e817d..70ceb639ac 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 6c812c2946..09e450c149 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 99a1695a3f..39032642b8 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 08caf9a4ef..5fdefc6a66 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 c047291772..985a2bcdc7 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 605028202f..35dd52e357 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; -pub struct Scrollbar { - thumb: Range, - state: ScrollbarState, - kind: ScrollbarAxis, +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 {} } -#[derive(Default, Debug, Clone, Copy)] +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() + } + + #[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); + } + } +} + +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>, -} - -#[derive(Debug)] -enum AutoHide { - Disabled, - Hidden { - parent_id: EntityId, - }, - Visible { - parent_id: EntityId, - _task: Task<()>, - }, -} - -impl AutoHide { - fn is_hidden(&self) -> bool { - matches!(self, AutoHide::Hidden { .. }) + 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() } } -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)), - } - } +enum ScrollbarMouseEvent { + TrackClick, + ThumbDrag(Pixels), +} - /// 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 - } +struct ScrollbarLayout { + thumb_bounds: Bounds, + track_bounds: Bounds, + cursor_hitbox: Hitbox, + reserved_space: ReservedSpace, + axis: ScrollbarAxis, +} - pub fn scroll_handle(&self) -> &Arc { - &self.scroll_handle - } +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; - pub fn is_dragging(&self) -> bool { - matches!(self.thumb_state.get(), ThumbState::Dragging(_)) - } - - fn set_dragging(&self, drag_offset: Pixels) { - self.set_thumb_state(ThumbState::Dragging(drag_offset)); - self.scroll_handle.drag_started(); - } - - fn set_thumb_hovered(&self, hovered: bool) { - self.set_thumb_state(if hovered { - ThumbState::Hover - } 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, + 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, }; - if self.scroll_handle().viewport().contains(position) { - self.show_temporarily(parent_id, cx); - } + let thumb_start = + (event_position.along(axis) - track_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) + } else { + 0. + }; + + -max_offset * percentage } } -impl Scrollbar { - pub fn vertical(state: ScrollbarState) -> Option { - Self::new(state, ScrollbarAxis::Vertical) - } - - pub fn horizontal(state: ScrollbarState) -> Option { - Self::new(state, ScrollbarAxis::Horizontal) - } - - fn new(state: ScrollbarState, kind: ScrollbarAxis) -> Option { - let thumb = state.thumb_range(kind)?; - Some(Self { thumb, state, kind }) - } - - /// 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 ScrollbarLayout { + fn eq(&self, other: &Self) -> bool { + self.axis == other.axis && self.thumb_bounds == other.thumb_bounds } } -impl Element for Scrollbar { +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 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) + } + }) + } +} + +impl PartialEq for ScrollbarPrepaintState { + fn eq(&self, other: &Self) -> bool { + self.thumbs == other.thumbs + } +} + +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.; + let scrollbar_style = Style { + position: Position::Absolute, + inset: Edges::default(), + size: size(relative(1.), relative(1.)).map(Into::into), + ..Default::default() + }; - 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(); - } - - (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( + if self.state.read(cx).visible() { + for ScrollbarLayout { 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, + 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.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_state.is_dragging() { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + 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(); - move |event: &MouseDownEvent, phase, _, _| { - if !phase.bubble() - || event.button != MouseButton::Left - || !bounds.contains(&event.position) - { + + 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.capture() { return; } - if thumb_bounds.contains(&event.position) { - let offset = event.position.along(axis) - thumb_bounds.origin.along(axis); - state.set_dragging(offset); - } 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.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()), - ); - } - } - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase.bubble() { - state.unhide(&event.position, cx); - - 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); - } + 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(); } - _ if event.pressed_button.is_none() => { - state.set_thumb_hovered(thumb_bounds.contains(&event.position)) - } - _ => {} } + _ => 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 {