diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..06d418a750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17521,6 +17521,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 a49dae25b3..967d977b82 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() @@ -542,22 +500,23 @@ impl Render for AcpThreadHistory { ), ) } else { - view.pr_5() - .child( - uniform_list( - "thread-history", - self.visible_items.len(), - cx.processor(|this, range: Range, window, cx| { - this.render_list_items(range, window, cx) - }), - ) - .p_1() - .track_scroll(self.scroll_handle.clone()) - .flex_grow(), + view.pr_5().child( + uniform_list( + "thread-history", + self.visible_items.len(), + cx.processor(|this, range: Range, window, cx| { + this.render_list_items(range, window, cx) + }), ) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) - }) + .p_1() + .track_scroll(self.scroll_handle.clone()) + .flex_grow() + .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 837ce6f90a..213020b05f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -21,10 +21,10 @@ use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, - ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, - Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, - prelude::*, pulsating_between, + ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, + pulsating_between, }; use language::Buffer; @@ -43,7 +43,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, + SpinnerLabel, Tooltip, WithScrollbar, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -268,7 +268,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)>, @@ -375,8 +374,7 @@ impl AcpThreadView { profile_selector: None, 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(), @@ -4320,39 +4318,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, @@ -4865,23 +4830,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, cx)), + .child(self.render_load_error(e, cx)) + .into_any(), ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { if has_messages { this.child( @@ -4901,9 +4870,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/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2..575d8a3a56 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -22,10 +22,9 @@ use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, Selec use gpui::{ AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry, ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, - ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, - StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, - UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage, - pulsating_between, + ListAlignment, ListOffset, ListState, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, + Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, + WindowHandle, linear_color_stop, linear_gradient, list, percentage, pulsating_between, }; use language::{Buffer, Language, LanguageRegistry}; use language_model::{ @@ -46,8 +45,7 @@ use std::time::Duration; use text::ToPoint; use theme::ThemeSettings; use ui::{ - Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, - Tooltip, prelude::*, + Banner, Disclosure, KeyBinding, PopoverMenuHandle, TextSize, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; use util::markdown::MarkdownCodeBlock; @@ -68,7 +66,6 @@ pub struct ActiveThread { save_thread_task: Option>, messages: Vec, list_state: ListState, - scrollbar_state: ScrollbarState, rendered_messages_by_id: HashMap, rendered_tool_uses: HashMap, editing_message: Option<(MessageId, EditingMessageState)>, @@ -799,8 +796,7 @@ impl ActiveThread { expanded_tool_uses: HashMap::default(), expanded_thinking_segments: HashMap::default(), expanded_code_blocks: HashMap::default(), - list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + list_state, editing_message: None, last_error: None, copied_code_block_ids: HashSet::default(), @@ -3491,39 +3487,6 @@ impl ActiveThread { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("active-thread-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 fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { self.expanded_code_blocks .get(&(message_id, ix)) @@ -3557,13 +3520,13 @@ pub enum ActiveThreadEvent { impl EventEmitter for ActiveThread {} impl Render for ActiveThread { - 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() .size_full() .relative() .bg(cx.theme().colors().panel_background) .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) } } diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 52fb7eed4b..2b2cec5539 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -31,7 +31,7 @@ use project::{ use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, - Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, + Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; use workspace::Workspace; @@ -58,7 +58,6 @@ pub struct AgentConfiguration { tools: Entity, _registry_subscription: Subscription, scroll_handle: ScrollHandle, - scrollbar_state: ScrollbarState, gemini_is_installed: bool, _check_for_gemini: Task<()>, } @@ -102,7 +101,6 @@ impl AgentConfiguration { .detach(); let scroll_handle = ScrollHandle::new(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { fs, @@ -117,7 +115,6 @@ impl AgentConfiguration { tools, _registry_subscription: registry_subscription, scroll_handle, - scrollbar_state, gemini_is_installed: false, _check_for_gemini: Task::ready(()), }; @@ -1209,32 +1206,7 @@ impl Render for AgentConfiguration { .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())), - ) + .vertical_scrollbar(window, cx) } } diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 4ec2078e5d..32c4de1d42 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -4,14 +4,14 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; use std::{fmt::Display, ops::Range, sync::Arc}; use time::{OffsetDateTime, UtcOffset}; use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, - Tooltip, prelude::*, + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, Tooltip, + WithScrollbar, prelude::*, }; use util::ResultExt; @@ -30,8 +30,6 @@ pub struct ThreadHistory { separated_item_indexes: Vec, _separated_items_task: Option>, search_state: SearchState, - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, _subscriptions: Vec, } @@ -90,7 +88,6 @@ impl ThreadHistory { }); let scroll_handle = UniformListScrollHandle::default(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { agent_panel, @@ -103,8 +100,6 @@ impl ThreadHistory { separated_items: Default::default(), separated_item_indexes: Default::default(), search_editor, - scrollbar_visibility: true, - scrollbar_state, _subscriptions: vec![search_editor_subscription, history_store_subscription], _separated_items_task: None, }; @@ -363,43 +358,6 @@ impl ThreadHistory { cx.notify(); } - 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 confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { if let Some(entry) = self.get_match(self.selected_index) { let task_result = match entry { @@ -536,7 +494,7 @@ impl Focusable for ThreadHistory { } impl Render for ThreadHistory { - 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() @@ -601,9 +559,14 @@ impl Render for ThreadHistory { .track_scroll(self.scroll_handle.clone()) .flex_grow(), ) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) - }) + .custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical) + .tracked_scroll_handle(self.scroll_handle.clone()) + .width_sm() + .with_track_along(ScrollAxes::Vertical), + 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 233dba4c52..a9518314e3 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 e7b7963d3f..65dfd5fe0a 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -9,7 +9,7 @@ use std::{ use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable, - MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle, + MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, uniform_list, }; @@ -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; @@ -30,7 +30,6 @@ 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, @@ -121,11 +120,10 @@ impl ViewState { } } -struct ScrollbarDragging; - 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, @@ -139,10 +137,8 @@ impl MemoryView { 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(), @@ -162,43 +158,6 @@ 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 render_memory(&self, cx: &mut Context) -> UniformList { let weak = cx.weak_entity(); let session = self.session.clone(); @@ -233,10 +192,9 @@ impl MemoryView { .track_scroll(self.scroll_handle.clone()) .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| { 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 size = this.scroll_handle.content_size(); + let viewport = this.scroll_handle.viewport(); + let current_offset = this.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() { @@ -245,7 +203,8 @@ impl MemoryView { } 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)); + this.scroll_handle + .set_offset(current_offset + point(px(0.), delta.y)); })) } fn render_query_bar(&self, cx: &Context) -> impl IntoElement { @@ -297,7 +256,7 @@ impl MemoryView { } let row_count = self.view_state.row_count(); debug_assert!(row_count > 1); - let scroll_handle = self.scroll_state.scroll_handle(); + let scroll_handle = &self.scroll_handle; let viewport = scroll_handle.viewport(); if viewport.bottom() < evt.event.position.y { @@ -307,13 +266,15 @@ impl MemoryView { } } - fn handle_scroll_drag(&mut self, evt: &DragMoveEvent) -> bool { - if !self.scroll_state.is_dragging() { - return false; - } + #[allow(unused)] + fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<()>) -> bool { + // todo! + // 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 scroll_handle = &self.scroll_handle; let viewport = scroll_handle.viewport(); if viewport.bottom() < evt.event.position.y { @@ -943,7 +904,7 @@ impl Render for MemoryView { ) .with_priority(1) })) - .child(self.render_vertical_scrollbar(cx)), + .vertical_scrollbar_for(self.scroll_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 a4ea4ab654..b3c500b919 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -5,8 +5,8 @@ use std::time::Duration; use anyhow::{Context as _, Result, anyhow}; use dap::StackFrameId; use gpui::{ - AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton, - Stateful, Subscription, Task, WeakEntity, list, + AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, Subscription, + Task, WeakEntity, list, }; use util::debug_panic; @@ -15,7 +15,7 @@ use language::PointUtf16; use project::debugger::breakpoint_store::ActiveStackFrame; use project::debugger::session::{Session, SessionEvent, StackFrame}; use project::{ProjectItem, ProjectPath}; -use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*}; +use ui::{Tooltip, WithScrollbar, prelude::*}; use workspace::{ItemHandle, Workspace}; use super::RunningState; @@ -35,7 +35,6 @@ pub struct StackFrameList { workspace: WeakEntity, selected_ix: Option, opened_stack_frame_id: Option, - scrollbar_state: ScrollbarState, list_state: ListState, error: Option, _refresh_task: Task<()>, @@ -71,7 +70,6 @@ impl StackFrameList { }); let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); - let scrollbar_state = ScrollbarState::new(list_state.clone()); let mut this = Self { session, @@ -84,7 +82,6 @@ impl StackFrameList { selected_ix: None, opened_stack_frame_id: None, list_state, - scrollbar_state, _refresh_task: Task::ready(()), }; this.schedule_refresh(true, window, cx); @@ -581,39 +578,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(); @@ -740,7 +704,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 29e009fdf8..448852430e 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::{ @@ -165,7 +165,7 @@ use project::{ }; use rand::{seq::SliceRandom, thread_rng}; 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, }; @@ -198,7 +198,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 1d7e04cae0..b52353c86d 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, SettingsSources, VsCodeSettings}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; use util::serde::default_true; /// Imports from the VSCode settings at @@ -196,23 +197,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 @@ -735,6 +719,12 @@ impl EditorSettings { } } +impl ScrollbarVisibilitySetting for EditorSettings { + fn scrollbar_visibility(&self, _cx: &App) -> ShowScrollbar { + self.scrollbar.show + } +} + impl Settings for EditorSettings { const KEY: Option<&'static str> = None; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4f3580da07..70cd413c01 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::{ @@ -84,7 +84,7 @@ use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; 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 fab5345787..e65c6b1807 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 fd504764b6..427e1a02aa 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 958a609a09..ef0ab34394 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::{ @@ -31,7 +28,7 @@ use git::{ UnstageAll, }; use gpui::{ - Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, + Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, @@ -63,9 +60,10 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ - Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar, - ScrollbarState, SplitButton, Tooltip, prelude::*, + Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, + SplitButton, Tooltip, prelude::*, }; +use ui::{ScrollAxes, Scrollbars, WithScrollbar}; use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; @@ -276,61 +274,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, @@ -343,8 +286,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, @@ -429,10 +370,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::(move |this, cx| { @@ -457,24 +394,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| { @@ -555,8 +474,6 @@ impl GitPanel { workspace: workspace.weak_handle(), modal_open: false, entry_count: 0, - horizontal_scrollbar, - vertical_scrollbar, bulk_staging: None, _settings_subscription, }; @@ -566,86 +483,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 @@ -2594,12 +2431,11 @@ impl GitPanel { cx.background_executor().timer(UPDATE_DEBOUNCE).await; if let Some(git_panel) = handle.upgrade() { git_panel - .update_in(cx, |git_panel, window, cx| { + .update(cx, |git_panel, cx| { if clear_pending { git_panel.clear_pending(); } git_panel.update_visible_entries(cx); - git_panel.update_scrollbar_properties(window, cx); }) .ok(); } @@ -3710,110 +3546,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, @@ -3861,33 +3593,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() @@ -3928,15 +3643,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) @@ -3952,72 +3658,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 { @@ -4526,15 +4174,6 @@ impl Render for GitPanel { .when(has_write_access && has_co_authors, |git_panel| { git_panel.on_action(cx.listener(Self::toggle_fill_co_authors)) }) - .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 b6891c7d25..7bb14c92bb 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, SettingsSources}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; use workspace::dock::DockPosition; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -89,6 +90,22 @@ pub struct GitPanelSettings { pub collapse_untracked_diff: bool, } +impl ScrollbarVisibilitySetting for GitPanelSettings { + fn scrollbar_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 { const KEY: Option<&'static str> = Some("git_panel"); diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c9826b704e..de5b720d46 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 5b69ce7fa6..e5cff552ad 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 58386ad1f5..1c67410043 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 0791dcc621..0aa4cf804c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2504,7 +2504,7 @@ impl Window { &mut self, key: impl Into, cx: &mut App, - init: impl FnOnce(&mut Self, &mut App) -> S, + init: impl FnOnce(&mut Self, &mut Context) -> S, ) -> Entity { let current_view = self.current_view(); self.with_global_id(key.into(), |global_id, window| { @@ -2537,7 +2537,7 @@ impl Window { pub fn use_state( &mut self, cx: &mut App, - init: impl FnOnce(&mut Self, &mut App) -> S, + init: impl FnOnce(&mut Self, &mut Context) -> S, ) -> Entity { self.use_keyed_state( ElementId::CodeLocation(*core::panic::Location::caller()), @@ -4838,6 +4838,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/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 10698cead8..d920b935b3 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, ], @@ -4491,150 +4475,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 @@ -4690,7 +4530,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 @@ -4844,17 +4684,20 @@ impl OutlinePanel { }), ) }) + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal) + .notify_content(), + window, + cx, + ) }; v_flex() .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), - ) } .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -5121,15 +4964,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 133d28b748..255c5f3438 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, SettingsSources}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -115,6 +116,14 @@ pub struct OutlinePanelSettingsContent { pub expand_outlines_with_depth: Option, } +impl ScrollbarVisibilitySetting for OutlinePanelSettings { + fn scrollbar_visibility(&self, cx: &App) -> ShowScrollbar { + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for OutlinePanelSettings { const KEY: Option<&'static str> = Some("outline_panel"); diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 34af5fed02..019e48b355 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,12 @@ 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| { + this.custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical).width_sm(), + window, + cx, + ) }), ) }) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c99f5f8172..7c7a3e2dbe 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<()>, @@ -428,7 +424,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(); @@ -619,12 +614,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(()), @@ -4707,103 +4696,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"); @@ -4819,52 +4711,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, @@ -5214,15 +5060,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; @@ -5483,10 +5320,14 @@ impl Render for ProjectPanel { .with_width_from_item(self.max_width_item_index) .track_scroll(self.scroll_handle.clone()), ) - .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 8a243589ed..0101b4333c 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, SettingsSources}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -162,6 +163,14 @@ pub struct ProjectPanelSettingsContent { pub sticky_scroll: Option, } +impl ScrollbarVisibilitySetting for ProjectPanelSettings { + fn scrollbar_visibility(&self, cx: &ui::App) -> ShowScrollbar { + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for ProjectPanelSettings { const KEY: Option<&'static str> = Some("project_panel"); diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index a9c3284d0b..107a857cb0 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,4 +1,3 @@ -use std::any::Any; use std::borrow::Cow; use std::collections::BTreeSet; use std::path::PathBuf; @@ -37,9 +36,10 @@ use settings::watch_config_file; use smol::stream::StreamExt as _; use ui::Navigable; use ui::NavigableEntry; +use ui::WithScrollbar; use ui::{ - IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, - Section, Tooltip, prelude::*, + IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Section, Tooltip, + prelude::*, }; use util::{ ResultExt, @@ -297,7 +297,7 @@ impl RemoteEntry { #[derive(Clone)] struct DefaultState { - scrollbar: ScrollbarState, + scroll_handle: ScrollHandle, add_new_server: NavigableEntry, servers: Vec, } @@ -305,7 +305,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); @@ -346,7 +345,7 @@ impl DefaultState { } Self { - scrollbar, + scroll_handle: handle, add_new_server, servers, } @@ -1449,7 +1448,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 +1478,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 +1578,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.clone(), window, cx), ), ) .into_any_element() diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 9c76725972..7e28977ee6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -11,8 +11,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, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, @@ -426,7 +426,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); @@ -780,9 +780,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 { @@ -811,9 +810,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(); } @@ -1198,9 +1196,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(), )); cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) .detach_and_notify_err(window, cx); @@ -2337,10 +2333,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/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 9d7bb07360..cb0332c868 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/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/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7..36df9d88d9 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, terminals::TerminalKind}; @@ -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, GlobalValue, ScrollbarVisibilitySetting}, }; use util::ResultExt; use workspace::{ @@ -63,7 +64,6 @@ use std::{ }; 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)] @@ -134,10 +134,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>, marked_text: Option, marked_range_utf16: Option>, _subscriptions: Vec, @@ -261,10 +258,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, marked_text: None, marked_range_utf16: None, @@ -833,136 +827,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; @@ -1117,6 +981,29 @@ fn regex_search_for_query(query: &project::search::SearchQuery) -> Option &Self { + &Self + } +} + +impl ScrollbarVisibilitySetting for TerminalScrollbarSettingsWrapper { + fn scrollbar_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); @@ -1148,28 +1035,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() @@ -1206,21 +1096,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, @@ -1231,8 +1112,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..264fee566e 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -1,39 +1,786 @@ -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, UniformList, 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::{ScrollbarVisibilitySetting, 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 { + !matches!(self, Self::Never) + } + + pub(super) fn should_auto_hide(&self, cx: &mut App) -> bool { + matches!(self, Self::System | Self::Auto if cx.default_global::().should_hide()) + } + } + + pub trait GlobalValue { + fn get_value(cx: &App) -> &Self; + } + + impl GlobalValue for T { + fn get_value(cx: &App) -> &T { + T::get_global(cx) + } + } + + impl GlobalValue for ShowScrollbar { + fn get_value(_cx: &App) -> &Self { + &ShowScrollbar::Always + } + } + + pub trait ScrollbarVisibilitySetting: GlobalValue + 'static { + fn scrollbar_visibility(&self, cx: &App) -> ShowScrollbar; + } + + impl ScrollbarVisibilitySetting for ShowScrollbar { + fn scrollbar_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: ScrollbarVisibilitySetting, + 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: ScrollbarVisibilitySetting, + 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: ScrollbarVisibilitySetting, + 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: ScrollbarVisibilitySetting, + 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: ScrollbarVisibilitySetting, + 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) + } +} + +#[derive(PartialEq, Eq)] +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(Debug, Clone, Default, PartialEq)] +enum ReservedSpace { + #[default] + None, + Thumb, + Track, +} + +impl ReservedSpace { + fn is_visible(&self) -> bool { + !matches!(self, ReservedSpace::None) + } + + fn needs_scroll_track(&self) -> bool { + matches!(self, ReservedSpace::Track) + } +} + +#[derive(Debug, Default, Clone)] +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< + S: ScrollbarVisibilitySetting = ShowScrollbar, + T: ScrollableHandle = ScrollHandle, +> { + 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: &Entity) -> Self { + self.tracked_entity = Some(Some(entity.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 { + matches!(self, VisibilityState::Visible) + } + + #[inline] + fn is_disabled(&self) -> bool { + matches!(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(); + + let mut state = 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: ShowScrollbar::Always, + show_state: VisibilityState::Visible, + mouse_in_parent: true, + last_prepaint_state: None, + _auto_hide_task: None, + }; + state.schedule_auto_hide(window, cx); + state + } + + fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { + self.set_show_scrollbar(S::get_value(cx).scrollbar_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); + }) + .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) { + if self.scroll_handle.offset() != offset { + 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, Clone, Copy, 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 +846,112 @@ 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 - } +#[derive(Clone)] +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 { +#[derive(Clone)] +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 +968,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 +985,290 @@ 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); + 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 {