From 69477dfd8cd3a4ba0e9bb433b5907566ec58d781 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:16:48 -0300 Subject: [PATCH] ui: Add `show_scrollbar` method to Picker (#25025) Now, you can pass `show_scrollbar` to Picker that implement a `uniform_list`. If that's on, the scrollbar should auto-hide if you move your focus elsewhere. By default, this method is turned off. Release Notes: - N/A --------- Co-authored-by: smit <0xtimsb@gmail.com> --- Cargo.lock | 1 + crates/picker/Cargo.toml | 1 + crates/picker/src/picker.rs | 120 +++++++++++++++++++++++++++++++++--- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b809d43c5..60086189e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9776,6 +9776,7 @@ dependencies = [ "serde", "serde_json", "ui", + "util", "workspace", ] diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index 3caf152b24..e53705b4e2 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -23,6 +23,7 @@ menu.workspace = true schemars.workspace = true serde.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index f004e50787..a82b46012d 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -3,14 +3,17 @@ use editor::{scroll::Autoscroll, Editor}; use gpui::{ actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Length, - ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, ScrollStrategy, Task, - UniformListScrollHandle, Window, + ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, ScrollHandle, ScrollStrategy, + Stateful, Task, UniformListScrollHandle, Window, }; use head::Head; use schemars::JsonSchema; use serde::Deserialize; use std::{sync::Arc, time::Duration}; -use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing}; +use ui::{ + prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, +}; +use util::ResultExt; use workspace::ModalView; mod head; @@ -46,7 +49,13 @@ pub struct Picker { confirm_on_update: Option, width: 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. @@ -256,15 +265,31 @@ impl Picker { window: &mut Window, cx: &mut Context, ) -> Self { + let element_container = Self::create_element_container(container, cx); + let scrollbar_state = match &element_container { + ElementContainer::UniformList(scroll_handle) => { + ScrollbarState::new(scroll_handle.clone()) + } + ElementContainer::List(_) => { + // todo smit: implement for list + ScrollbarState::new(ScrollHandle::new()) + } + }; + let focus_handle = cx.focus_handle(); let mut this = Self { delegate, head, - element_container: Self::create_element_container(container, cx), + element_container, pending_update_matches: None, confirm_on_update: None, width: 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. @@ -312,6 +337,11 @@ impl Picker { self } + pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self { + self.show_scrollbar = show_scrollbar; + self + } + pub fn modal(mut self, modal: bool) -> Self { self.is_modal = modal; self @@ -642,6 +672,7 @@ impl Picker { } else { ListSizingBehavior::Auto }; + match &self.element_container { ElementContainer::UniformList(scroll_handle) => uniform_list( cx.entity().clone(), @@ -675,6 +706,67 @@ 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(|panel, mut cx| async move { + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; + panel + .update(&mut 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 {} @@ -683,7 +775,6 @@ impl ModalView for Picker {} impl Render for Picker { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let editor_position = self.delegate.editor_position(); - v_flex() .key_context("Picker") .size_full() @@ -716,11 +807,26 @@ impl Render for Picker { .when(self.delegate.match_count() > 0, |el| { el.child( v_flex() + .id("element-container") + .relative() .flex_grow() .when_some(self.max_height, |div, max_h| div.max_h(max_h)) .overflow_hidden() .children(self.delegate.render_header(window, cx)) - .child(self.render_element_container(cx)), + .child(self.render_element_container(cx)) + .track_focus(&self.focus_handle(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.delegate.match_count() == 0, |el| {