git_ui: Panel Horizontal Scroll (#26402)
Known Issues: - When items can horizontal scroll, the right selected border is hidden TODO: - [ ] Width calculation is off - [ ] When scrollbars should autohide they don't until hovering the panel - [ ] When switching to and from scrollbar track being visible we are missing a notify somewhere. Release Notes: - Git Panel: Added horizontal scrolling in the git panel --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Cole Miller <m@cole-miller.net> Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
5d66c3db85
commit
09a4cfd307
1 changed files with 453 additions and 167 deletions
|
@ -28,9 +28,9 @@ use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
|
||||||
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, deferred, percentage, uniform_list, Action, Animation, AnimationExt as _,
|
actions, anchored, deferred, percentage, uniform_list, Action, Animation, AnimationExt as _,
|
||||||
ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
Axis, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
|
KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
|
||||||
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Stateful, Subscription, Task,
|
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
|
||||||
Transformation, UniformListScrollHandle, WeakEntity,
|
Transformation, UniformListScrollHandle, WeakEntity,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -205,6 +205,21 @@ pub struct GitStatusEntry {
|
||||||
pub(crate) staging: StageStatus,
|
pub(crate) staging: StageStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GitStatusEntry {
|
||||||
|
fn display_name(&self) -> String {
|
||||||
|
self.worktree_path
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| self.worktree_path.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parent_dir(&self) -> Option<String> {
|
||||||
|
self.worktree_path
|
||||||
|
.parent()
|
||||||
|
.map(|parent| parent.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum TargetStatus {
|
enum TargetStatus {
|
||||||
Staged,
|
Staged,
|
||||||
|
@ -222,6 +237,61 @@ struct PendingOperation {
|
||||||
|
|
||||||
type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
|
type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
|
||||||
|
|
||||||
|
// 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<Task<()>>,
|
||||||
|
state: ScrollbarState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollbarProperties {
|
||||||
|
// Shows the scrollbar and cancels any pending hide task
|
||||||
|
fn show(&mut self, cx: &mut Context<GitPanel>) {
|
||||||
|
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<GitPanel>) {
|
||||||
|
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, |panel, mut cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(SCROLLBAR_SHOW_INTERVAL)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(panel) = panel.upgrade() {
|
||||||
|
panel
|
||||||
|
.update(&mut 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 struct GitPanel {
|
||||||
remote_operation_id: u32,
|
remote_operation_id: u32,
|
||||||
pending_remote_operations: RemoteOperations,
|
pending_remote_operations: RemoteOperations,
|
||||||
|
@ -237,7 +307,8 @@ pub struct GitPanel {
|
||||||
single_tracked_entry: Option<GitStatusEntry>,
|
single_tracked_entry: Option<GitStatusEntry>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
hide_scrollbar_task: Option<Task<()>>,
|
horizontal_scrollbar: ScrollbarProperties,
|
||||||
|
vertical_scrollbar: ScrollbarProperties,
|
||||||
new_count: usize,
|
new_count: usize,
|
||||||
entry_count: usize,
|
entry_count: usize,
|
||||||
new_staged_count: usize,
|
new_staged_count: usize,
|
||||||
|
@ -246,10 +317,9 @@ pub struct GitPanel {
|
||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
pub(crate) project: Entity<Project>,
|
pub(crate) project: Entity<Project>,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
scrollbar_state: ScrollbarState,
|
max_width_item_index: Option<usize>,
|
||||||
selected_entry: Option<usize>,
|
selected_entry: Option<usize>,
|
||||||
marked_entries: Vec<usize>,
|
marked_entries: Vec<usize>,
|
||||||
show_scrollbar: bool,
|
|
||||||
tracked_count: usize,
|
tracked_count: usize,
|
||||||
tracked_staged_count: usize,
|
tracked_staged_count: usize,
|
||||||
update_visible_entries_task: Task<()>,
|
update_visible_entries_task: Task<()>,
|
||||||
|
@ -316,7 +386,7 @@ impl GitPanel {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
|
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
|
||||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||||
this.hide_scrollbar(window, cx);
|
this.hide_scrollbars(window, cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -355,8 +425,23 @@ impl GitPanel {
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let scrollbar_state =
|
let vertical_scrollbar = ScrollbarProperties {
|
||||||
ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
|
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 git_panel = Self {
|
let mut git_panel = Self {
|
||||||
pending_remote_operations: Default::default(),
|
pending_remote_operations: Default::default(),
|
||||||
|
@ -371,7 +456,6 @@ impl GitPanel {
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
fs,
|
fs,
|
||||||
hide_scrollbar_task: None,
|
|
||||||
new_count: 0,
|
new_count: 0,
|
||||||
new_staged_count: 0,
|
new_staged_count: 0,
|
||||||
pending: Vec::new(),
|
pending: Vec::new(),
|
||||||
|
@ -381,10 +465,9 @@ impl GitPanel {
|
||||||
single_tracked_entry: None,
|
single_tracked_entry: None,
|
||||||
project,
|
project,
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
scrollbar_state,
|
max_width_item_index: None,
|
||||||
selected_entry: None,
|
selected_entry: None,
|
||||||
marked_entries: Vec::new(),
|
marked_entries: Vec::new(),
|
||||||
show_scrollbar: false,
|
|
||||||
tracked_count: 0,
|
tracked_count: 0,
|
||||||
tracked_staged_count: 0,
|
tracked_staged_count: 0,
|
||||||
update_visible_entries_task: Task::ready(()),
|
update_visible_entries_task: Task::ready(()),
|
||||||
|
@ -393,12 +476,93 @@ impl GitPanel {
|
||||||
workspace,
|
workspace,
|
||||||
modal_open: false,
|
modal_open: false,
|
||||||
entry_count: 0,
|
entry_count: 0,
|
||||||
|
horizontal_scrollbar,
|
||||||
|
vertical_scrollbar,
|
||||||
};
|
};
|
||||||
git_panel.schedule_update(false, window, cx);
|
git_panel.schedule_update(false, window, cx);
|
||||||
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
|
|
||||||
git_panel
|
git_panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.horizontal_scrollbar.hide(window, cx);
|
||||||
|
self.vertical_scrollbar.hide(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
// TODO: This PR should have defined Editor's `scrollbar.axis`
|
||||||
|
// as an Option<ScrollbarAxis>, 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<Self>| match show {
|
||||||
|
ShowScrollbar::Auto => true,
|
||||||
|
ShowScrollbar::System => cx
|
||||||
|
.try_global::<ScrollbarAutoHide>()
|
||||||
|
.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) -> Option<usize> {
|
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
|
||||||
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
|
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
|
||||||
where
|
where
|
||||||
|
@ -555,53 +719,6 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_scrollbar(&self, cx: &mut Context<Self>) -> ShowScrollbar {
|
|
||||||
GitPanelSettings::get_global(cx)
|
|
||||||
.scrollbar
|
|
||||||
.show
|
|
||||||
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_show_scrollbar(&self, cx: &mut Context<Self>) -> bool {
|
|
||||||
let show = self.show_scrollbar(cx);
|
|
||||||
match show {
|
|
||||||
ShowScrollbar::Auto => true,
|
|
||||||
ShowScrollbar::System => true,
|
|
||||||
ShowScrollbar::Always => true,
|
|
||||||
ShowScrollbar::Never => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_autohide_scrollbar(&self, cx: &mut Context<Self>) -> bool {
|
|
||||||
let show = self.show_scrollbar(cx);
|
|
||||||
match show {
|
|
||||||
ShowScrollbar::Auto => true,
|
|
||||||
ShowScrollbar::System => cx
|
|
||||||
.try_global::<ScrollbarAutoHide>()
|
|
||||||
.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<Self>) {
|
|
||||||
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, |panel, mut cx| async move {
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(SCROLLBAR_SHOW_INTERVAL)
|
|
||||||
.await;
|
|
||||||
panel
|
|
||||||
.update(&mut cx, |panel, cx| {
|
|
||||||
panel.show_scrollbar = false;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_modifiers_changed(
|
fn handle_modifiers_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &ModifiersChangedEvent,
|
event: &ModifiersChangedEvent,
|
||||||
|
@ -1972,12 +2089,13 @@ impl GitPanel {
|
||||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||||
if let Some(git_panel) = handle.upgrade() {
|
if let Some(git_panel) = handle.upgrade() {
|
||||||
git_panel
|
git_panel
|
||||||
.update_in(&mut cx, |git_panel, _, cx| {
|
.update_in(&mut cx, |git_panel, window, cx| {
|
||||||
if clear_pending {
|
if clear_pending {
|
||||||
git_panel.clear_pending();
|
git_panel.clear_pending();
|
||||||
}
|
}
|
||||||
git_panel.update_visible_entries(cx);
|
git_panel.update_visible_entries(cx);
|
||||||
git_panel.update_editor_placeholder(cx);
|
git_panel.update_editor_placeholder(cx);
|
||||||
|
git_panel.update_scrollbar_properties(window, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -2038,6 +2156,7 @@ impl GitPanel {
|
||||||
let mut conflict_entries = Vec::new();
|
let mut conflict_entries = Vec::new();
|
||||||
let mut last_staged = None;
|
let mut last_staged = None;
|
||||||
let mut staged_count = 0;
|
let mut staged_count = 0;
|
||||||
|
let mut max_width_item: Option<(RepoPath, usize)> = None;
|
||||||
|
|
||||||
let Some(repo) = self.active_repository.as_ref() else {
|
let Some(repo) = self.active_repository.as_ref() else {
|
||||||
// Just clear entries if no repository is active.
|
// Just clear entries if no repository is active.
|
||||||
|
@ -2083,6 +2202,21 @@ impl GitPanel {
|
||||||
last_staged = Some(entry.clone());
|
last_staged = Some(entry.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let width_estimate = Self::item_width_estimate(
|
||||||
|
entry.parent_dir().map(|s| s.len()).unwrap_or(0),
|
||||||
|
entry.display_name().len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match max_width_item.as_mut() {
|
||||||
|
Some((repo_path, estimate)) => {
|
||||||
|
if width_estimate > *estimate {
|
||||||
|
*repo_path = entry.repo_path.clone();
|
||||||
|
*estimate = width_estimate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => max_width_item = Some((entry.repo_path.clone(), width_estimate)),
|
||||||
|
}
|
||||||
|
|
||||||
if is_conflict {
|
if is_conflict {
|
||||||
conflict_entries.push(entry);
|
conflict_entries.push(entry);
|
||||||
} else if is_new {
|
} else if is_new {
|
||||||
|
@ -2155,6 +2289,15 @@ impl GitPanel {
|
||||||
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
|
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((repo_path, _)) = max_width_item {
|
||||||
|
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
|
||||||
|
GitListEntry::GitStatusEntry(git_status_entry) => {
|
||||||
|
git_status_entry.repo_path == repo_path
|
||||||
|
}
|
||||||
|
GitListEntry::Header(_) => false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self.update_counts(repo);
|
self.update_counts(repo);
|
||||||
|
|
||||||
self.select_first_entry_if_none(cx);
|
self.select_first_entry_if_none(cx);
|
||||||
|
@ -2376,6 +2519,12 @@ impl GitPanel {
|
||||||
self.has_staged_changes()
|
self.has_staged_changes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eventually we'll need to take depth into account here
|
||||||
|
// if we add a tree view
|
||||||
|
fn item_width_estimate(path: usize, file_name: usize) -> usize {
|
||||||
|
path + file_name
|
||||||
|
}
|
||||||
|
|
||||||
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||||
let focus_handle = self.focus_handle.clone();
|
let focus_handle = self.focus_handle.clone();
|
||||||
PopoverMenu::new(id.into())
|
PopoverMenu::new(id.into())
|
||||||
|
@ -2795,58 +2944,108 @@ impl GitPanel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
fn render_vertical_scrollbar(
|
||||||
let scroll_bar_style = self.show_scrollbar(cx);
|
&self,
|
||||||
let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
|
show_horizontal_scrollbar_container: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> 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();
|
||||||
|
}
|
||||||
|
|
||||||
if !self.should_show_scrollbar(cx)
|
|
||||||
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(
|
|
||||||
div()
|
|
||||||
.id("git-panel-vertical-scroll")
|
|
||||||
.occlude()
|
|
||||||
.flex_none()
|
|
||||||
.h_full()
|
|
||||||
.cursor_default()
|
|
||||||
.when(show_container, |this| this.pl_1().px_1p5())
|
|
||||||
.when(!show_container, |this| {
|
|
||||||
this.absolute().right_1().top_1().bottom_1().w(px(12.))
|
|
||||||
})
|
|
||||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
|
||||||
cx.notify();
|
|
||||||
cx.stop_propagation()
|
|
||||||
}))
|
|
||||||
.on_hover(|_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
})
|
}),
|
||||||
.on_any_mouse_down(|_, _, cx| {
|
)
|
||||||
cx.stop_propagation();
|
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||||
})
|
cx.notify();
|
||||||
.on_mouse_up(
|
}))
|
||||||
MouseButton::Left,
|
.children(Scrollbar::vertical(
|
||||||
cx.listener(|this, _, window, cx| {
|
// percentage as f32..end_offset as f32,
|
||||||
if !this.scrollbar_state.is_dragging()
|
self.vertical_scrollbar.state.clone(),
|
||||||
&& !this.focus_handle.contains_focused(window, cx)
|
))
|
||||||
{
|
}
|
||||||
this.hide_scrollbar(window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.stop_propagation();
|
/// Renders the horizontal scrollbar.
|
||||||
}),
|
///
|
||||||
)
|
/// The right offset is used to determine how far to the right the
|
||||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
/// scrollbar should extend to, useful for ensuring it doesn't collide
|
||||||
cx.notify();
|
/// with the vertical scrollbar when visible.
|
||||||
}))
|
fn render_horizontal_scrollbar(
|
||||||
.children(Scrollbar::vertical(
|
&self,
|
||||||
// percentage as f32..end_offset as f32,
|
right_offset: Pixels,
|
||||||
self.scrollbar_state.clone(),
|
cx: &mut Context<Self>,
|
||||||
)),
|
) -> 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(
|
fn render_buffer_header_controls(
|
||||||
|
@ -2900,54 +3099,147 @@ impl GitPanel {
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let entry_count = self.entries.len();
|
let entry_count = self.entries.len();
|
||||||
|
|
||||||
h_flex()
|
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()
|
.size_full()
|
||||||
.flex_grow()
|
|
||||||
.overflow_hidden()
|
.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(
|
.child(
|
||||||
uniform_list(cx.entity().clone(), "entries", entry_count, {
|
h_flex()
|
||||||
move |this, range, window, cx| {
|
.flex_1()
|
||||||
let mut items = Vec::with_capacity(range.end - range.start);
|
.size_full()
|
||||||
|
.relative()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
uniform_list(cx.entity().clone(), "entries", entry_count, {
|
||||||
|
move |this, range, window, cx| {
|
||||||
|
let mut items = Vec::with_capacity(range.end - range.start);
|
||||||
|
|
||||||
for ix in range {
|
for ix in range {
|
||||||
match &this.entries.get(ix) {
|
match &this.entries.get(ix) {
|
||||||
Some(GitListEntry::GitStatusEntry(entry)) => {
|
Some(GitListEntry::GitStatusEntry(entry)) => {
|
||||||
items.push(this.render_entry(
|
items.push(this.render_entry(
|
||||||
ix,
|
ix,
|
||||||
entry,
|
entry,
|
||||||
has_write_access,
|
has_write_access,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
Some(GitListEntry::Header(header)) => {
|
||||||
|
items.push(this.render_list_header(
|
||||||
|
ix,
|
||||||
|
header,
|
||||||
|
has_write_access,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(GitListEntry::Header(header)) => {
|
|
||||||
items.push(this.render_list_header(
|
items
|
||||||
ix,
|
|
||||||
header,
|
|
||||||
has_write_access,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.size_full()
|
||||||
items
|
.flex_grow()
|
||||||
}
|
.with_sizing_behavior(ListSizingBehavior::Auto)
|
||||||
})
|
.with_horizontal_sizing_behavior(
|
||||||
.size_full()
|
ListHorizontalSizingBehavior::Unconstrained,
|
||||||
.with_sizing_behavior(ListSizingBehavior::Auto)
|
)
|
||||||
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
.with_width_from_item(self.max_width_item_index)
|
||||||
.track_scroll(self.scroll_handle.clone()),
|
.track_scroll(self.scroll_handle.clone()),
|
||||||
|
)
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Right,
|
||||||
|
cx.listener(move |this, event: &MouseDownEvent, window, cx| {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.on_mouse_down(
|
.when(self.horizontal_scrollbar.show_track, |this| {
|
||||||
MouseButton::Right,
|
this.child(
|
||||||
cx.listener(move |this, event: &MouseDownEvent, window, cx| {
|
h_flex()
|
||||||
this.deploy_panel_context_menu(event.position, window, cx)
|
.w_full()
|
||||||
}),
|
.h(scroll_track_size)
|
||||||
)
|
.flex_none()
|
||||||
.children(self.render_scrollbar(cx))
|
.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<SharedString>, color: Color) -> Label {
|
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
|
||||||
|
@ -3074,13 +3366,8 @@ impl GitPanel {
|
||||||
window: &Window,
|
window: &Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let display_name = entry
|
let display_name = entry.display_name();
|
||||||
.worktree_path
|
|
||||||
.file_name()
|
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_else(|| entry.worktree_path.to_string_lossy().into_owned());
|
|
||||||
|
|
||||||
let worktree_path = entry.worktree_path.clone();
|
|
||||||
let selected = self.selected_entry == Some(ix);
|
let selected = self.selected_entry == Some(ix);
|
||||||
let marked = self.marked_entries.contains(&ix);
|
let marked = self.marked_entries.contains(&ix);
|
||||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||||
|
@ -3272,12 +3559,12 @@ impl GitPanel {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.overflow_hidden()
|
.flex_1()
|
||||||
.when_some(worktree_path.parent(), |this, parent| {
|
// .overflow_hidden()
|
||||||
let parent_str = parent.to_string_lossy();
|
.when_some(entry.parent_dir(), |this, parent| {
|
||||||
if !parent_str.is_empty() {
|
if !parent.is_empty() {
|
||||||
this.child(
|
this.child(
|
||||||
self.entry_label(format!("{}/", parent_str), path_color)
|
self.entry_label(format!("{}/", parent), path_color)
|
||||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -3351,14 +3638,13 @@ impl Render for GitPanel {
|
||||||
.when(has_write_access && has_co_authors, |git_panel| {
|
.when(has_write_access && has_co_authors, |git_panel| {
|
||||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
||||||
})
|
})
|
||||||
// .on_action(cx.listener(|this, &OpenSelected, cx| this.open_selected(&OpenSelected, cx)))
|
.on_hover(cx.listener(move |this, hovered, window, cx| {
|
||||||
.on_hover(cx.listener(|this, hovered, window, cx| {
|
|
||||||
if *hovered {
|
if *hovered {
|
||||||
this.show_scrollbar = true;
|
this.horizontal_scrollbar.show(cx);
|
||||||
this.hide_scrollbar_task.take();
|
this.vertical_scrollbar.show(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else if !this.focus_handle.contains_focused(window, cx) {
|
} else if !this.focus_handle.contains_focused(window, cx) {
|
||||||
this.hide_scrollbar(window, cx);
|
this.hide_scrollbars(window, cx);
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.size_full()
|
.size_full()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue