project panel: Add vertical scrollbar (#13358)
Fixes #4865 Release Notes: - Added vertical scrollbar to project panel
This commit is contained in:
parent
5c93506e9f
commit
d272e402ea
6 changed files with 357 additions and 37 deletions
|
@ -312,7 +312,14 @@
|
||||||
"auto_reveal_entries": true,
|
"auto_reveal_entries": true,
|
||||||
/// Whether to fold directories automatically
|
/// Whether to fold directories automatically
|
||||||
/// when a directory has only one directory inside.
|
/// when a directory has only one directory inside.
|
||||||
"auto_fold_dirs": false
|
"auto_fold_dirs": false,
|
||||||
|
/// Scrollbar-related settings
|
||||||
|
"scrollbar": {
|
||||||
|
/// When to show the scrollbar in the project panel.
|
||||||
|
///
|
||||||
|
/// Default: always
|
||||||
|
"show": "always"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"outline_panel": {
|
"outline_panel": {
|
||||||
// Whether to show the outline panel button in the status bar
|
// Whether to show the outline panel button in the status bar
|
||||||
|
|
|
@ -2493,6 +2493,11 @@ impl ScrollHandle {
|
||||||
self.0.borrow().bounds
|
self.0.borrow().bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the bounds into which this child is painted
|
||||||
|
pub(super) fn set_bounds(&self, bounds: Bounds<Pixels>) {
|
||||||
|
self.0.borrow_mut().bounds = bounds;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the bounds for a specific child.
|
/// Get the bounds for a specific child.
|
||||||
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
||||||
self.0.borrow().child_bounds.get(ix).cloned()
|
self.0.borrow().child_bounds.get(ix).cloned()
|
||||||
|
|
|
@ -79,31 +79,37 @@ pub struct UniformListFrameState {
|
||||||
|
|
||||||
/// A handle for controlling the scroll position of a uniform list.
|
/// A handle for controlling the scroll position of a uniform list.
|
||||||
/// This should be stored in your view and passed to the uniform_list on each frame.
|
/// This should be stored in your view and passed to the uniform_list on each frame.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct UniformListScrollHandle {
|
pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
|
||||||
base_handle: ScrollHandle,
|
|
||||||
deferred_scroll_to_item: Rc<RefCell<Option<usize>>>,
|
#[derive(Clone, Debug, Default)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub struct UniformListScrollState {
|
||||||
|
pub base_handle: ScrollHandle,
|
||||||
|
pub deferred_scroll_to_item: Option<usize>,
|
||||||
|
pub last_item_height: Option<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UniformListScrollHandle {
|
impl UniformListScrollHandle {
|
||||||
/// Create a new scroll handle to bind to a uniform list.
|
/// Create a new scroll handle to bind to a uniform list.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self(Rc::new(RefCell::new(UniformListScrollState {
|
||||||
base_handle: ScrollHandle::new(),
|
base_handle: ScrollHandle::new(),
|
||||||
deferred_scroll_to_item: Rc::new(RefCell::new(None)),
|
deferred_scroll_to_item: None,
|
||||||
}
|
last_item_height: None,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the list to the given item index.
|
/// Scroll the list to the given item index.
|
||||||
pub fn scroll_to_item(&mut self, ix: usize) {
|
pub fn scroll_to_item(&mut self, ix: usize) {
|
||||||
self.deferred_scroll_to_item.replace(Some(ix));
|
self.0.borrow_mut().deferred_scroll_to_item = Some(ix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the index of the topmost visible child.
|
/// Get the index of the topmost visible child.
|
||||||
pub fn logical_scroll_top_index(&self) -> usize {
|
pub fn logical_scroll_top_index(&self) -> usize {
|
||||||
self.deferred_scroll_to_item
|
let this = self.0.borrow();
|
||||||
.borrow()
|
this.deferred_scroll_to_item
|
||||||
.unwrap_or_else(|| self.base_handle.logical_scroll_top().0)
|
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,10 +201,11 @@ impl Element for UniformList {
|
||||||
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
|
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
|
||||||
|
|
||||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||||
let shared_scroll_to_item = self
|
let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
|
||||||
.scroll_handle
|
let mut handle = handle.0.borrow_mut();
|
||||||
.as_mut()
|
handle.last_item_height = Some(item_height);
|
||||||
.and_then(|handle| handle.deferred_scroll_to_item.take());
|
handle.deferred_scroll_to_item.take()
|
||||||
|
});
|
||||||
|
|
||||||
self.interactivity.prepaint(
|
self.interactivity.prepaint(
|
||||||
global_id,
|
global_id,
|
||||||
|
@ -214,6 +221,10 @@ impl Element for UniformList {
|
||||||
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(handle) = self.scroll_handle.as_mut() {
|
||||||
|
handle.0.borrow_mut().base_handle.set_bounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
if self.item_count > 0 {
|
if self.item_count > 0 {
|
||||||
let content_height =
|
let content_height =
|
||||||
item_height * self.item_count + padding.top + padding.bottom;
|
item_height * self.item_count + padding.top + padding.bottom;
|
||||||
|
@ -326,7 +337,7 @@ impl UniformList {
|
||||||
|
|
||||||
/// Track and render scroll state of this list with reference to the given scroll handle.
|
/// Track and render scroll state of this list with reference to the given scroll handle.
|
||||||
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
|
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
|
||||||
self.interactivity.tracked_scroll_handle = Some(handle.base_handle.clone());
|
self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
|
||||||
self.scroll_handle = Some(handle);
|
self.scroll_handle = Some(handle);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
mod project_panel_settings;
|
mod project_panel_settings;
|
||||||
|
mod scrollbar;
|
||||||
use client::{ErrorCode, ErrorExt};
|
use client::{ErrorCode, ErrorExt};
|
||||||
|
use scrollbar::ProjectPanelScrollbar;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{items::entry_git_aware_label_color, scroll::Autoscroll, Editor};
|
use editor::{
|
||||||
|
items::entry_git_aware_label_color,
|
||||||
|
scroll::{Autoscroll, ScrollbarAutoHide},
|
||||||
|
Editor,
|
||||||
|
};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
@ -19,7 +25,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||||
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
|
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings, ShowScrollbar};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
cell::OnceCell,
|
cell::OnceCell,
|
||||||
|
@ -28,6 +34,7 @@ use std::{
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, v_flex, ContextMenu, Icon, KeyBinding, Label, ListItem, Tooltip};
|
use ui::{prelude::*, v_flex, ContextMenu, Icon, KeyBinding, Label, ListItem, Tooltip};
|
||||||
|
@ -63,6 +70,8 @@ pub struct ProjectPanel {
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
width: Option<Pixels>,
|
width: Option<Pixels>,
|
||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
|
show_scrollbar: bool,
|
||||||
|
hide_scrollbar_task: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -188,7 +197,10 @@ impl ProjectPanel {
|
||||||
let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
|
let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
cx.on_focus(&focus_handle, Self::focus_in).detach();
|
cx.on_focus(&focus_handle, Self::focus_in).detach();
|
||||||
|
cx.on_focus_out(&focus_handle, |this, _, cx| {
|
||||||
|
this.hide_scrollbar(cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||||
if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
|
if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
|
||||||
|
@ -273,6 +285,8 @@ impl ProjectPanel {
|
||||||
workspace: workspace.weak_handle(),
|
workspace: workspace.weak_handle(),
|
||||||
width: None,
|
width: None,
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
|
show_scrollbar: !Self::should_autohide_scrollbar(cx),
|
||||||
|
hide_scrollbar_task: None,
|
||||||
};
|
};
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
|
|
||||||
|
@ -2201,6 +2215,73 @@ impl ProjectPanel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_scrollbar(
|
||||||
|
&self,
|
||||||
|
items_count: usize,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Stateful<Div>> {
|
||||||
|
let settings = ProjectPanelSettings::get_global(cx);
|
||||||
|
if settings.scrollbar.show == ShowScrollbar::Never {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let scroll_handle = self.scroll_handle.0.borrow();
|
||||||
|
|
||||||
|
let height = scroll_handle
|
||||||
|
.last_item_height
|
||||||
|
.filter(|_| self.show_scrollbar)?;
|
||||||
|
|
||||||
|
let total_list_length = height.0 as f64 * items_count as f64;
|
||||||
|
let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
|
||||||
|
let mut percentage = current_offset / total_list_length;
|
||||||
|
let mut end_offset = (current_offset
|
||||||
|
+ scroll_handle.base_handle.bounds().size.height.0 as f64)
|
||||||
|
/ total_list_length;
|
||||||
|
// Uniform scroll handle might briefly report an offset greater than the length of a list;
|
||||||
|
// in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
|
||||||
|
let overshoot = (end_offset - 1.).clamp(0., 1.);
|
||||||
|
if overshoot > 0. {
|
||||||
|
percentage -= overshoot;
|
||||||
|
}
|
||||||
|
if percentage + 0.005 > 1.0 || end_offset > total_list_length {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if total_list_length < scroll_handle.base_handle.bounds().size.height.0 as f64 {
|
||||||
|
percentage = 0.;
|
||||||
|
end_offset = 1.;
|
||||||
|
}
|
||||||
|
let end_offset = end_offset.clamp(percentage + 0.005, 1.);
|
||||||
|
Some(
|
||||||
|
div()
|
||||||
|
.occlude()
|
||||||
|
.id("project-panel-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_scroll_wheel(cx.listener(|_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
.h_full()
|
||||||
|
.absolute()
|
||||||
|
.right_0()
|
||||||
|
.top_0()
|
||||||
|
.bottom_0()
|
||||||
|
.w_3()
|
||||||
|
.cursor_default()
|
||||||
|
.child(ProjectPanelScrollbar::new(
|
||||||
|
percentage as f32..end_offset as f32,
|
||||||
|
self.scroll_handle.clone(),
|
||||||
|
items_count,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
||||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||||
dispatch_context.add("ProjectPanel");
|
dispatch_context.add("ProjectPanel");
|
||||||
|
@ -2216,6 +2297,29 @@ impl ProjectPanel {
|
||||||
dispatch_context
|
dispatch_context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_autohide_scrollbar(cx: &AppContext) -> bool {
|
||||||
|
cx.try_global::<ScrollbarAutoHide>()
|
||||||
|
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
if !Self::should_autohide_scrollbar(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(SCROLLBAR_SHOW_INTERVAL)
|
||||||
|
.await;
|
||||||
|
panel
|
||||||
|
.update(&mut cx, |editor, cx| {
|
||||||
|
editor.show_scrollbar = false;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn reveal_entry(
|
fn reveal_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
|
@ -2249,10 +2353,26 @@ impl Render for ProjectPanel {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
|
|
||||||
if has_worktree {
|
if has_worktree {
|
||||||
|
let items_count = self
|
||||||
|
.visible_entries
|
||||||
|
.iter()
|
||||||
|
.map(|(_, worktree_entries, _)| worktree_entries.len())
|
||||||
|
.sum();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("project-panel")
|
.id("project-panel")
|
||||||
|
.group("project-panel")
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
|
.on_hover(cx.listener(|this, hovered, cx| {
|
||||||
|
if *hovered {
|
||||||
|
this.show_scrollbar = true;
|
||||||
|
this.hide_scrollbar_task.take();
|
||||||
|
cx.notify();
|
||||||
|
} else if !this.focus_handle.contains_focused(cx) {
|
||||||
|
this.hide_scrollbar(cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
.key_context(self.dispatch_context(cx))
|
.key_context(self.dispatch_context(cx))
|
||||||
.on_action(cx.listener(Self::select_next))
|
.on_action(cx.listener(Self::select_next))
|
||||||
.on_action(cx.listener(Self::select_prev))
|
.on_action(cx.listener(Self::select_prev))
|
||||||
|
@ -2298,14 +2418,7 @@ impl Render for ProjectPanel {
|
||||||
)
|
)
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(cx.view().clone(), "entries", items_count, {
|
||||||
cx.view().clone(),
|
|
||||||
"entries",
|
|
||||||
self.visible_entries
|
|
||||||
.iter()
|
|
||||||
.map(|(_, worktree_entries, _)| worktree_entries.len())
|
|
||||||
.sum(),
|
|
||||||
{
|
|
||||||
|this, range, cx| {
|
|this, range, cx| {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||||
|
@ -2313,12 +2426,12 @@ impl Render for ProjectPanel {
|
||||||
});
|
});
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||||
.track_scroll(self.scroll_handle.clone()),
|
.track_scroll(self.scroll_handle.clone()),
|
||||||
)
|
)
|
||||||
|
.children(self.render_scrollbar(items_count, cx))
|
||||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||||
deferred(
|
deferred(
|
||||||
anchored()
|
anchored()
|
||||||
|
|
|
@ -22,6 +22,36 @@ pub struct ProjectPanelSettings {
|
||||||
pub indent_size: f32,
|
pub indent_size: f32,
|
||||||
pub auto_reveal_entries: bool,
|
pub auto_reveal_entries: bool,
|
||||||
pub auto_fold_dirs: bool,
|
pub auto_fold_dirs: bool,
|
||||||
|
pub scrollbar: ScrollbarSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When to show the scrollbar in the project panel.
|
||||||
|
///
|
||||||
|
/// Default: always
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ShowScrollbar {
|
||||||
|
#[default]
|
||||||
|
/// Always show the scrollbar.
|
||||||
|
Always,
|
||||||
|
/// Never show the scrollbar.
|
||||||
|
Never,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||||
|
pub struct ScrollbarSettings {
|
||||||
|
/// When to show the scrollbar in the project panel.
|
||||||
|
///
|
||||||
|
/// Default: always
|
||||||
|
pub show: ShowScrollbar,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||||
|
pub struct ScrollbarSettingsContent {
|
||||||
|
/// When to show the scrollbar in the project panel.
|
||||||
|
///
|
||||||
|
/// Default: always
|
||||||
|
pub show: Option<ShowScrollbar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
@ -65,6 +95,8 @@ pub struct ProjectPanelSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: false
|
/// Default: false
|
||||||
pub auto_fold_dirs: Option<bool>,
|
pub auto_fold_dirs: Option<bool>,
|
||||||
|
/// Scrollbar-related settings
|
||||||
|
pub scrollbar: Option<ScrollbarSettingsContent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings for ProjectPanelSettings {
|
impl Settings for ProjectPanelSettings {
|
||||||
|
|
152
crates/project_panel/src/scrollbar.rs
Normal file
152
crates/project_panel/src/scrollbar.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
point, Bounds, ContentMask, Hitbox, MouseDownEvent, MouseMoveEvent, ScrollWheelEvent, Style,
|
||||||
|
UniformListScrollHandle,
|
||||||
|
};
|
||||||
|
use ui::{prelude::*, px, relative, IntoElement};
|
||||||
|
|
||||||
|
pub(crate) struct ProjectPanelScrollbar {
|
||||||
|
thumb: Range<f32>,
|
||||||
|
scroll: UniformListScrollHandle,
|
||||||
|
item_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectPanelScrollbar {
|
||||||
|
pub(crate) fn new(
|
||||||
|
thumb: Range<f32>,
|
||||||
|
scroll: UniformListScrollHandle,
|
||||||
|
item_count: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
thumb,
|
||||||
|
scroll,
|
||||||
|
item_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Element for ProjectPanelScrollbar {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
|
||||||
|
type PrepaintState = Hitbox;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ui::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&gpui::GlobalElementId>,
|
||||||
|
cx: &mut ui::WindowContext,
|
||||||
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.flex_grow = 1.;
|
||||||
|
style.flex_shrink = 1.;
|
||||||
|
style.size.width = px(12.).into();
|
||||||
|
style.size.height = relative(1.).into();
|
||||||
|
(cx.request_layout(style, None), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&gpui::GlobalElementId>,
|
||||||
|
bounds: Bounds<ui::Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut ui::WindowContext,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
|
cx.insert_hitbox(bounds, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&gpui::GlobalElementId>,
|
||||||
|
bounds: Bounds<ui::Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
_prepaint: &mut Self::PrepaintState,
|
||||||
|
cx: &mut ui::WindowContext,
|
||||||
|
) {
|
||||||
|
let hitbox_id = _prepaint.id;
|
||||||
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
|
let colors = cx.theme().colors();
|
||||||
|
let scrollbar_background = colors.scrollbar_track_border;
|
||||||
|
let thumb_background = colors.scrollbar_thumb_background;
|
||||||
|
cx.paint_quad(gpui::fill(bounds, scrollbar_background));
|
||||||
|
|
||||||
|
let thumb_offset = self.thumb.start * bounds.size.height;
|
||||||
|
let thumb_end = self.thumb.end * bounds.size.height;
|
||||||
|
let thumb_upper_left = point(bounds.origin.x, bounds.origin.y + thumb_offset);
|
||||||
|
let thumb_lower_right = point(
|
||||||
|
bounds.origin.x + bounds.size.width,
|
||||||
|
bounds.origin.y + thumb_end,
|
||||||
|
);
|
||||||
|
let thumb_percentage_size = self.thumb.end - self.thumb.start;
|
||||||
|
cx.paint_quad(gpui::fill(
|
||||||
|
Bounds::from_corners(thumb_upper_left, thumb_lower_right),
|
||||||
|
thumb_background,
|
||||||
|
));
|
||||||
|
let scroll = self.scroll.clone();
|
||||||
|
let item_count = self.item_count;
|
||||||
|
cx.on_mouse_event({
|
||||||
|
let scroll = self.scroll.clone();
|
||||||
|
move |event: &MouseDownEvent, phase, _cx| {
|
||||||
|
if phase.bubble() && bounds.contains(&event.position) {
|
||||||
|
let scroll = scroll.0.borrow();
|
||||||
|
if let Some(last_height) = scroll.last_item_height {
|
||||||
|
let max_offset = item_count as f32 * last_height;
|
||||||
|
let percentage =
|
||||||
|
(event.position.y - bounds.origin.y) / bounds.size.height;
|
||||||
|
|
||||||
|
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||||
|
scroll
|
||||||
|
.base_handle
|
||||||
|
.set_offset(point(px(0.), -max_offset * percentage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.on_mouse_event({
|
||||||
|
let scroll = self.scroll.clone();
|
||||||
|
move |event: &ScrollWheelEvent, phase, cx| {
|
||||||
|
if phase.bubble() && bounds.contains(&event.position) {
|
||||||
|
let scroll = scroll.0.borrow_mut();
|
||||||
|
let current_offset = scroll.base_handle.offset();
|
||||||
|
scroll
|
||||||
|
.base_handle
|
||||||
|
.set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||||
|
if phase.bubble() && bounds.contains(&event.position) && hitbox_id.is_hovered(cx) {
|
||||||
|
if event.dragging() {
|
||||||
|
let scroll = scroll.0.borrow();
|
||||||
|
if let Some(last_height) = scroll.last_item_height {
|
||||||
|
let max_offset = item_count as f32 * last_height;
|
||||||
|
let percentage =
|
||||||
|
(event.position.y - bounds.origin.y) / bounds.size.height;
|
||||||
|
|
||||||
|
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||||
|
scroll
|
||||||
|
.base_handle
|
||||||
|
.set_offset(point(px(0.), -max_offset * percentage));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for ProjectPanelScrollbar {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue