From b110fd5fb74021e311c220a52205e4131504d257 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 May 2022 18:30:04 -0600 Subject: [PATCH 01/54] Render a context menu when right-clicking in project panel It doesn't currently do anything, but I managed to get it rendering in an absolutely positioned way. --- crates/auto_update/src/auto_update.rs | 2 +- crates/chat_panel/src/chat_panel.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 14 +-- crates/contacts_panel/src/notifications.rs | 15 +-- crates/diagnostics/src/items.rs | 4 +- crates/editor/src/editor.rs | 6 +- crates/gpui/src/elements/list.rs | 24 ++-- .../gpui/src/elements/mouse_event_handler.rs | 71 ++++++++++- crates/gpui/src/elements/overlay.rs | 30 ++++- crates/gpui/src/platform/event.rs | 3 +- crates/gpui/src/platform/mac/event.rs | 1 + crates/gpui/src/presenter.rs | 9 +- crates/gpui/src/views/select.rs | 6 +- crates/picker/src/picker.rs | 2 +- crates/project_panel/src/project_panel.rs | 118 +++++++++++++----- crates/search/src/buffer_search.rs | 4 +- crates/search/src/project_search.rs | 4 +- crates/theme/src/theme.rs | 9 ++ crates/workspace/src/lsp_status.rs | 3 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/sidebar.rs | 2 +- crates/workspace/src/workspace.rs | 4 +- styles/src/styleTree/projectPanel.ts | 16 ++- 23 files changed, 260 insertions(+), 91 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 499b3ed99d579af99ec02dddadad9a6aa77dd0d6..234319bdd66090cd5dd6bb22ccc82962a23fefd8 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator { ) .boxed() }) - .on_click(|_, cx| cx.dispatch_action(DismissErrorMessage)) + .on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage)) .boxed() } AutoUpdateStatus::Idle => Empty::new().boxed(), diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 460e01c527bddca2145f5f0fbf284403fdeb952a..29c64128d1ffb5fde745666aa2acab1ee1a8e6f2 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -320,7 +320,7 @@ impl ChatPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { let rpc = rpc.clone(); let this = this.clone(); cx.spawn(|mut cx| async move { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 44aa0626c59cea9a6704e1c8bef7e28920d80c4c..763772b89e60591d4894eef3258727ee92e34359 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -302,7 +302,7 @@ impl ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(ToggleExpanded(section))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleExpanded(section))) .boxed() } @@ -445,7 +445,7 @@ impl ContactsPanel { } else { CursorStyle::Arrow }) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { if !is_host { cx.dispatch_global_action(JoinProject { contact: contact.clone(), @@ -507,7 +507,7 @@ impl ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.dispatch_action(RespondToContactRequest { user_id, accept: false, @@ -529,7 +529,7 @@ impl ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.dispatch_action(RespondToContactRequest { user_id, accept: true, @@ -552,7 +552,7 @@ impl ContactsPanel { }) .with_padding(Padding::uniform(2.)) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) + .on_click(move |_, _, cx| cx.dispatch_action(RemoveContact(user_id))) .flex_float() .boxed(), ); @@ -865,7 +865,7 @@ impl View for ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle)) + .on_click(|_, _, cx| cx.dispatch_action(contact_finder::Toggle)) .boxed(), ) .constrained() @@ -913,7 +913,7 @@ impl View for ContactsPanel { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new( info.url.to_string(), )); diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs index 555d8962d3d8003eeaa11ad078625f51844b6dbb..c02fd73b8f7143bf8b45f83ace3bedff4fc799db 100644 --- a/crates/contacts_panel/src/notifications.rs +++ b/crates/contacts_panel/src/notifications.rs @@ -61,7 +61,7 @@ pub fn render_user_notification( }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(5.)) - .on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) + .on_click(move |_, _, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) .aligned() .constrained() .with_height( @@ -76,13 +76,10 @@ pub fn render_user_notification( .named("contact notification header"), ) .with_children(body.map(|body| { - Label::new( - body.to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed() + Label::new(body.to_string(), theme.body_message.text.clone()) + .contained() + .with_style(theme.body_message.container) + .boxed() })) .with_children(if buttons.is_empty() { None @@ -99,7 +96,7 @@ pub fn render_user_notification( .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone())) + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) .boxed() }, )) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 426f25629dd139d21e8ad10e6abb4d33e29d4063..224e5e94a781471ce71389151cc119b3c09f537e 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -159,7 +159,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(crate::Deploy)) + .on_click(|_, _, cx| cx.dispatch_action(crate::Deploy)) .aligned() .boxed(), ); @@ -192,7 +192,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic)) + .on_click(|_, _, cx| cx.dispatch_action(GoToNextDiagnostic)) .boxed(), ); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e5a80e44f4014127cb5cb76090bfd04a1092e65b..556b7a9091228c17650c0b26fd4b55155d74f6db 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -672,7 +672,7 @@ impl CompletionsMenu { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { + .on_mouse_down(move |_, cx| { cx.dispatch_action(ConfirmCompletion { item_ix: Some(item_ix), }); @@ -800,7 +800,7 @@ impl CodeActionsMenu { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { + .on_mouse_down(move |_, cx| { cx.dispatch_action(ConfirmCodeAction { item_ix: Some(item_ix), }); @@ -2590,7 +2590,7 @@ impl Editor { }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(3.)) - .on_mouse_down(|cx| { + .on_mouse_down(|_, cx| { cx.dispatch_action(ToggleCodeActions { deployed_from_indicator: true, }); diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 77d37bc3bf3b2dc7e879fc3ddcec8ae64bff283d..969fe3cb842ee04e3f5cf127136831c341933dcb 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -612,7 +612,10 @@ mod tests { }); let mut list = List::new(state.clone()); - let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx)); + let (size, _) = list.layout( + constraint, + &mut presenter.build_layout_context(vec2f(100., 40.), false, cx), + ); assert_eq!(size, vec2f(100., 40.)); assert_eq!( state.0.borrow().items.summary().clone(), @@ -634,8 +637,10 @@ mod tests { true, &mut presenter.build_event_context(cx), ); - let (_, logical_scroll_top) = - list.layout(constraint, &mut presenter.build_layout_context(false, cx)); + let (_, logical_scroll_top) = list.layout( + constraint, + &mut presenter.build_layout_context(vec2f(100., 40.), false, cx), + ); assert_eq!( logical_scroll_top, ListOffset { @@ -659,8 +664,10 @@ mod tests { } ); - let (size, logical_scroll_top) = - list.layout(constraint, &mut presenter.build_layout_context(false, cx)); + let (size, logical_scroll_top) = list.layout( + constraint, + &mut presenter.build_layout_context(vec2f(100., 40.), false, cx), + ); assert_eq!(size, vec2f(100., 40.)); assert_eq!( state.0.borrow().items.summary().clone(), @@ -770,11 +777,12 @@ mod tests { } let mut list = List::new(state.clone()); + let window_size = vec2f(width, height); let (size, logical_scroll_top) = list.layout( - SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)), - &mut presenter.build_layout_context(false, cx), + SizeConstraint::new(vec2f(0., 0.), window_size), + &mut presenter.build_layout_context(window_size, false, cx), ); - assert_eq!(size, vec2f(width, height)); + assert_eq!(size, window_size); last_logical_scroll_top = Some(logical_scroll_top); let state = state.0.borrow(); diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 1ee7c6cbb5e57bf2108d36f48424fd3c9db7e058..65cb6ed61d8717ed69e636d5e17e1ab1e77b9509 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -14,9 +14,11 @@ pub struct MouseEventHandler { state: ElementStateHandle, child: ElementBox, cursor_style: Option, - mouse_down_handler: Option>, - click_handler: Option>, + mouse_down_handler: Option>, + click_handler: Option>, drag_handler: Option>, + right_mouse_down_handler: Option>, + right_click_handler: Option>, padding: Padding, } @@ -24,6 +26,7 @@ pub struct MouseEventHandler { pub struct MouseState { pub hovered: bool, pub clicked: bool, + pub right_clicked: bool, prev_drag_position: Option, } @@ -43,6 +46,8 @@ impl MouseEventHandler { mouse_down_handler: None, click_handler: None, drag_handler: None, + right_mouse_down_handler: None, + right_click_handler: None, padding: Default::default(), } } @@ -52,12 +57,18 @@ impl MouseEventHandler { self } - pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { + pub fn on_mouse_down( + mut self, + handler: impl FnMut(Vector2F, &mut EventContext) + 'static, + ) -> Self { self.mouse_down_handler = Some(Box::new(handler)); self } - pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self { + pub fn on_click( + mut self, + handler: impl FnMut(Vector2F, usize, &mut EventContext) + 'static, + ) -> Self { self.click_handler = Some(Box::new(handler)); self } @@ -67,6 +78,22 @@ impl MouseEventHandler { self } + pub fn on_right_mouse_down( + mut self, + handler: impl FnMut(Vector2F, &mut EventContext) + 'static, + ) -> Self { + self.right_mouse_down_handler = Some(Box::new(handler)); + self + } + + pub fn on_right_click( + mut self, + handler: impl FnMut(Vector2F, usize, &mut EventContext) + 'static, + ) -> Self { + self.right_click_handler = Some(Box::new(handler)); + self + } + pub fn with_padding(mut self, padding: Padding) -> Self { self.padding = padding; self @@ -120,6 +147,8 @@ impl Element for MouseEventHandler { let mouse_down_handler = self.mouse_down_handler.as_mut(); let click_handler = self.click_handler.as_mut(); let drag_handler = self.drag_handler.as_mut(); + let right_mouse_down_handler = self.right_mouse_down_handler.as_mut(); + let right_click_handler = self.right_click_handler.as_mut(); let handled_in_child = self.child.dispatch_event(event, cx); @@ -144,7 +173,7 @@ impl Element for MouseEventHandler { state.prev_drag_position = Some(*position); cx.notify(); if let Some(handler) = mouse_down_handler { - handler(cx); + handler(*position, cx); } true } else { @@ -162,7 +191,7 @@ impl Element for MouseEventHandler { cx.notify(); if let Some(handler) = click_handler { if hit_bounds.contains_point(*position) { - handler(*click_count, cx); + handler(*position, *click_count, cx); } } true @@ -184,6 +213,36 @@ impl Element for MouseEventHandler { handled_in_child } } + Event::RightMouseDown { position, .. } => { + if !handled_in_child && hit_bounds.contains_point(*position) { + state.right_clicked = true; + cx.notify(); + if let Some(handler) = right_mouse_down_handler { + handler(*position, cx); + } + true + } else { + handled_in_child + } + } + Event::RightMouseUp { + position, + click_count, + .. + } => { + if !handled_in_child && state.right_clicked { + state.right_clicked = false; + cx.notify(); + if let Some(handler) = right_click_handler { + if hit_bounds.contains_point(*position) { + handler(*position, *click_count, cx); + } + } + true + } else { + handled_in_child + } + } _ => handled_in_child, }) } diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 0cac2ed86302c8e7c88ca58fa811ee116b8deff0..3d90a7554cd15a641217173974295c5b61e6e086 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -1,16 +1,28 @@ +use serde_json::json; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, + json::ToJson, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; pub struct Overlay { child: ElementBox, + abs_position: Option, } impl Overlay { pub fn new(child: ElementBox) -> Self { - Self { child } + Self { + child, + abs_position: None, + } + } + + pub fn with_abs_position(mut self, position: Vector2F) -> Self { + self.abs_position = Some(position); + self } } @@ -23,6 +35,11 @@ impl Element for Overlay { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { + let constraint = if self.abs_position.is_some() { + SizeConstraint::new(Vector2F::zero(), cx.window_size) + } else { + constraint + }; let size = self.child.layout(constraint, cx); (Vector2F::zero(), size) } @@ -34,9 +51,10 @@ impl Element for Overlay { size: &mut Self::LayoutState, cx: &mut PaintContext, ) { - let bounds = RectF::new(bounds.origin(), *size); + let origin = self.abs_position.unwrap_or(bounds.origin()); + let visible_bounds = RectF::new(origin, *size); cx.scene.push_stacking_context(None); - self.child.paint(bounds.origin(), bounds, cx); + self.child.paint(origin, visible_bounds, cx); cx.scene.pop_stacking_context(); } @@ -59,6 +77,10 @@ impl Element for Overlay { _: &Self::PaintState, cx: &DebugContext, ) -> serde_json::Value { - self.child.debug(cx) + json!({ + "type": "Overlay", + "abs_position": self.abs_position.to_json(), + "child": self.child.debug(cx), + }) } } diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index b32ab952c79dfbef1b1a539bec117463954c25bf..61cfa99bfe0a8b6cc032baf0788eb0b3541f36ba 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -43,6 +43,7 @@ pub enum Event { }, RightMouseUp { position: Vector2F, + click_count: usize, }, NavigateMouseDown { position: Vector2F, @@ -72,7 +73,7 @@ impl Event { | Event::LeftMouseUp { position, .. } | Event::LeftMouseDragged { position } | Event::RightMouseDown { position, .. } - | Event::RightMouseUp { position } + | Event::RightMouseUp { position, .. } | Event::NavigateMouseDown { position, .. } | Event::NavigateMouseUp { position, .. } | Event::MouseMoved { position, .. } => Some(*position), diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 9d07177b16fd322c92973745983396a6c0abf5ea..4d3aa6cf9ac8ff65c2ef92d4acb22ce70a585fe9 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -178,6 +178,7 @@ impl Event { native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), + click_count: native_event.clickCount() as usize, }), NSEventType::NSOtherMouseDown => { let direction = match native_event.buttonNumber() { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index fbdd6963e372f711d193c9bde16b34156d6118d5..2a2da34d631101d3fef73db3469fcdca6eaa6598 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -134,15 +134,16 @@ impl Presenter { scene } - fn layout(&mut self, size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) { + fn layout(&mut self, window_size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - self.build_layout_context(refreshing, cx) - .layout(root_view_id, SizeConstraint::strict(size)); + self.build_layout_context(window_size, refreshing, cx) + .layout(root_view_id, SizeConstraint::strict(window_size)); } } pub fn build_layout_context<'a>( &'a mut self, + window_size: Vector2F, refreshing: bool, cx: &'a mut MutableAppContext, ) -> LayoutContext<'a> { @@ -150,6 +151,7 @@ impl Presenter { rendered_views: &mut self.rendered_views, parents: &mut self.parents, refreshing, + window_size, font_cache: &self.font_cache, font_system: cx.platform().fonts(), text_layout_cache: &self.text_layout_cache, @@ -259,6 +261,7 @@ pub struct LayoutContext<'a> { parents: &'a mut HashMap, view_stack: Vec, pub refreshing: bool, + pub window_size: Vector2F, pub font_cache: &'a Arc, pub font_system: Arc, pub text_layout_cache: &'a TextLayoutCache, diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index d5d2105c3f34e6b74126080771118f07df8903af..a58be77c03299c195948c5ca0791179182d4e2b5 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -119,7 +119,7 @@ impl View for Select { .with_style(style.header) .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(ToggleSelect)) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleSelect)) .boxed(), ); if self.is_open { @@ -153,7 +153,9 @@ impl View for Select { ) }, ) - .on_click(move |_, cx| cx.dispatch_action(SelectItem(ix))) + .on_click(move |_, _, cx| { + cx.dispatch_action(SelectItem(ix)) + }) .boxed() })) }, diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 67db36208b0b114fabb2457b9388809a1a80cee2..8fd662978b4eb3b0c334aa04a9a40ffc76b1dc21 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -90,7 +90,7 @@ impl View for Picker { .read(cx) .render_match(ix, state, ix == selected_ix, cx) }) - .on_mouse_down(move |cx| cx.dispatch_action(SelectIndex(ix))) + .on_mouse_down(move |_, cx| cx.dispatch_action(SelectIndex(ix))) .with_cursor_style(CursorStyle::PointingHand) .boxed() })); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7056eb9ceb61649c91282885b30b5372f5948031..f3506e2c94bb754efa5d45a958f4344af300ae12 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4,13 +4,14 @@ use gpui::{ actions, anyhow::{anyhow, Result}, elements::{ - ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, - ScrollTarget, Svg, UniformList, UniformListState, + ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, Overlay, ParentElement, + ScrollTarget, Stack, Svg, UniformList, UniformListState, }, + geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, + RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -36,6 +37,7 @@ pub struct ProjectPanel { selection: Option, edit_state: Option, filename_editor: ViewHandle, + context_menu: Option, handle: WeakViewHandle, } @@ -75,6 +77,17 @@ pub struct Open { pub change_focus: bool, } +#[derive(Clone)] +pub struct DeployContextMenu { + pub position: Vector2F, + pub entry_id: Option, +} + +pub struct ContextMenu { + pub position: Vector2F, + pub entry_id: Option, +} + actions!( project_panel, [ @@ -86,9 +99,10 @@ actions!( Rename ] ); -impl_internal_actions!(project_panel, [Open, ToggleExpanded]); +impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]); pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ProjectPanel::deploy_context_menu); cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry); cx.add_action(ProjectPanel::toggle_expanded); @@ -156,6 +170,7 @@ impl ProjectPanel { selection: None, edit_state: None, filename_editor, + context_menu: None, handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); @@ -195,6 +210,14 @@ impl ProjectPanel { project_panel } + fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { + self.context_menu = Some(ContextMenu { + position: action.position, + entry_id: action.entry_id, + }); + cx.notify(); + } + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { if let Some((worktree, entry)) = self.selected_entry(cx) { let expanded_dir_ids = @@ -841,7 +864,7 @@ impl ProjectPanel { .with_padding_left(padding) .boxed() }) - .on_click(move |click_count, cx| { + .on_click(move |_, click_count, cx| { if kind == EntryKind::Dir { cx.dispatch_action(ToggleExpanded(entry_id)) } else { @@ -851,9 +874,33 @@ impl ProjectPanel { }) } }) + .on_right_mouse_down(move |position, cx| { + cx.dispatch_action(DeployContextMenu { + entry_id: Some(entry_id), + position, + }) + }) .with_cursor_style(CursorStyle::PointingHand) .boxed() } + + fn render_context_menu(&self, cx: &mut RenderContext) -> Option { + self.context_menu.as_ref().map(|menu| { + let style = &cx.global::().theme.project_panel.context_menu; + + Overlay::new( + Flex::column() + .with_child(Label::new("Add File".to_string(), style.label.clone()).boxed()) + .contained() + .with_style(style.container) + // .constrained() + // .with_width(style.width) + .boxed(), + ) + .with_abs_position(menu.position) + .named("Project Panel Context Menu") + }) + } } impl View for ProjectPanel { @@ -866,33 +913,38 @@ impl View for ProjectPanel { let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); let handle = self.handle.clone(); - UniformList::new( - self.list.clone(), - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - move |range, items, cx| { - let theme = cx.global::().theme.clone(); - let this = handle.upgrade(cx).unwrap(); - this.update(cx.app, |this, cx| { - this.for_each_visible_entry(range.clone(), cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - &theme.project_panel, - cx, - )); - }); - }) - }, - ) - .with_padding_top(padding.top) - .with_padding_bottom(padding.bottom) - .contained() - .with_style(container_style) - .boxed() + Stack::new() + .with_child( + UniformList::new( + self.list.clone(), + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + move |range, items, cx| { + let theme = cx.global::().theme.clone(); + let this = handle.upgrade(cx).unwrap(); + this.update(cx.app, |this, cx| { + this.for_each_visible_entry(range.clone(), cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + &theme.project_panel, + cx, + )); + }); + }) + }, + ) + .with_padding_top(padding.top) + .with_padding_bottom(padding.bottom) + .contained() + .with_style(container_style) + .boxed(), + ) + .with_children(self.render_context_menu(cx)) + .boxed() } fn keymap_context(&self, _: &AppContext) -> keymap::Context { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5581cbd60870f5cec82dad5af5e634ffa342c2f6..94b6261a0f11c3b4ba56333a22365fdf6fb3a79a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -290,7 +290,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -314,7 +314,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| match direction { + .on_click(move |_, _, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4549aa4f90388bb441bcbbf6cad428136061eccd..e3834f6f45d8e8caaee731445074875753d09a0d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -672,7 +672,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| match direction { + .on_click(move |_, _, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) @@ -699,7 +699,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc9e93025d1d1a13c4d0235382a3864f91f33729..28e31ad3bbd5abf2ed06c7b5a26b441faaa6fe8c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -226,6 +226,7 @@ pub struct ProjectPanel { pub ignored_entry_fade: f32, pub filename_editor: FieldEditor, pub indent_width: f32, + pub context_menu: ContextMenu, } #[derive(Clone, Debug, Deserialize, Default)] @@ -239,6 +240,14 @@ pub struct ProjectPanelEntry { pub icon_spacing: f32, } +#[derive(Clone, Debug, Deserialize, Default)] +pub struct ContextMenu { + pub width: f32, + #[serde(flatten)] + pub container: ContainerStyle, + pub label: TextStyle, +} + #[derive(Debug, Deserialize, Default)] pub struct CommandPalette { pub key: Interactive, diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index f58e0b973e05e27db44359b95c11524010e14ea2..ab1ae4931fecd0ebf8f09a574f29755859d53992 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -168,7 +168,8 @@ impl View for LspStatus { self.failed.join(", "), if self.failed.len() > 1 { "s" } else { "" } ); - handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); + handler = + Some(|_, _, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); } else { return Empty::new().boxed(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8b97ef1a80476d5c33b4ff8dc18aaaa1ecf9bd88..ba2a21bce4cad4e0ada71ef312366afe7e0f555e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -788,7 +788,7 @@ impl Pane { .with_cursor_style(CursorStyle::PointingHand) .on_click({ let pane = pane.clone(); - move |_, cx| { + move |_, _, cx| { cx.dispatch_action(CloseItem { item_id, pane: pane.clone(), diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index afdacc2a31a86f297c70e9b2a4168d01720a4da8..5aec332913420cc8beeab4a071b7a95468919d6b 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -293,7 +293,7 @@ impl View for SidebarButtons { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.dispatch_action(ToggleSidebarItem { side, item_index: ix, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e9f0efa31115dac5d98eb13826526f4dc96994ec..f4197e72962e6536afbefaf48d08be23f3338e8d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1730,7 +1730,7 @@ impl Workspace { .with_style(style.container) .boxed() }) - .on_click(|_, cx| cx.dispatch_action(Authenticate)) + .on_click(|_, _, cx| cx.dispatch_action(Authenticate)) .with_cursor_style(CursorStyle::PointingHand) .aligned() .boxed(), @@ -1781,7 +1781,7 @@ impl Workspace { if let Some(peer_id) = peer_id { MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id))) .boxed() } else { content diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 2f3e3eea72195e415a2a536bc959414f857b30b9..66aaa85952ce26010e76c9a8d0721c7fcfaaf51e 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,6 +1,6 @@ import Theme from "../themes/common/theme"; import { panel } from "./app"; -import { backgroundColor, iconColor, player, text } from "./components"; +import { backgroundColor, iconColor, player, shadow, text } from "./components"; export default function projectPanel(theme: Theme) { return { @@ -32,5 +32,19 @@ export default function projectPanel(theme: Theme) { text: text(theme, "mono", "primary", { size: "sm" }), selection: player(theme, 1).selection, }, + contextMenu: { + width: 100, + // background: "#ff0000", + background: backgroundColor(theme, 300, "base"), + cornerRadius: 6, + padding: { + bottom: 2, + left: 6, + right: 6, + top: 2, + }, + label: text(theme, "sans", "secondary", { size: "sm" }), + shadow: shadow(theme), + } }; } From 5b7825d5de0e4e517daa63e6db26b6b62dc20058 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 May 2022 19:46:50 -0600 Subject: [PATCH 02/54] Add MutableAppContext::keystrokes_for_action This can be used to lookup keystrokes that will dispatch an action based on the currently focused view. There might be multiple, but we return the first found, meaning the most recently added bindings matching that action for the closest view to the focused view in the hierarchy. --- crates/gpui/src/app.rs | 22 ++++++++++++++++++++++ crates/gpui/src/keymap.rs | 30 ++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index eb4b9650a67dbc0568f754abb72322df659cc06b..2d93e46c05d6f18459cf6d7acecc2f77289bcc6a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1413,6 +1413,28 @@ impl MutableAppContext { self.global_actions.contains_key(&action_type) } + /// Return keystrokes that would dispatch the given action closest to the focused view, if there are any. + pub fn keystrokes_for_action(&self, action: &dyn Action) -> Option> { + let window_id = self.cx.platform.key_window_id()?; + let (presenter, _) = self.presenters_and_platform_windows.get(&window_id)?; + let dispatch_path = presenter.borrow().dispatch_path(&self.cx); + + for view_id in dispatch_path.iter().rev() { + let view = self + .cx + .views + .get(&(window_id, *view_id)) + .expect("view in responder chain does not exist"); + let cx = view.keymap_context(self.as_ref()); + let keystrokes = self.keystroke_matcher.keystrokes_for_action(action, &cx); + if keystrokes.is_some() { + return keystrokes; + } + } + + None + } + pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) { let presenter = self .presenters_and_platform_windows diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index bd156ed66149007068b2b8fc4f1b9be58c479390..dca752ed6f8c4b59e3427fcbee2f80d8eb3475ea 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -30,9 +30,9 @@ pub struct Keymap { } pub struct Binding { - keystrokes: Vec, + keystrokes: SmallVec<[Keystroke; 2]>, action: Box, - context: Option, + context_predicate: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -146,7 +146,11 @@ impl Matcher { let mut retain_pending = false; for binding in self.keymap.bindings.iter().rev() { if binding.keystrokes.starts_with(&pending.keystrokes) - && binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true) + && binding + .context_predicate + .as_ref() + .map(|c| c.eval(cx)) + .unwrap_or(true) { if binding.keystrokes.len() == pending.keystrokes.len() { self.pending.remove(&view_id); @@ -165,6 +169,24 @@ impl Matcher { MatchResult::None } } + + pub fn keystrokes_for_action( + &self, + action: &dyn Action, + cx: &Context, + ) -> Option> { + for binding in self.keymap.bindings.iter().rev() { + if binding.action.id() == action.id() + && binding + .context_predicate + .as_ref() + .map_or(true, |predicate| predicate.eval(cx)) + { + return Some(binding.keystrokes.clone()); + } + } + todo!() + } } impl Default for Matcher { @@ -236,7 +258,7 @@ impl Binding { Ok(Self { keystrokes, action, - context, + context_predicate: context, }) } From 6b96822c1a1d53bd405e10ef8dd7efd3a9b597d8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 May 2022 19:58:41 -0600 Subject: [PATCH 03/54] Fix editor tests --- crates/editor/src/element.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 355d1f44337c9c855625ecc0255e51b57ee5db4b..f71bc09d23c31af5fca5088ac07aabcd760dec52 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1528,7 +1528,7 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); let mut presenter = cx.build_presenter(window_id, 30.); - let mut layout_cx = presenter.build_layout_context(false, cx); + let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx); element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx) }); assert_eq!(layouts.len(), 6); @@ -1566,7 +1566,7 @@ mod tests { let mut scene = Scene::new(1.0); let mut presenter = cx.build_presenter(window_id, 30.); - let mut layout_cx = presenter.build_layout_context(false, cx); + let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx); let (size, mut state) = element.layout( SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), &mut layout_cx, From b428d0de38263d539a18aa829502370ed9d96457 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 May 2022 19:59:08 -0600 Subject: [PATCH 04/54] Break context menu items out in theme --- crates/project_panel/src/project_panel.rs | 6 +++--- crates/theme/src/theme.rs | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f3506e2c94bb754efa5d45a958f4344af300ae12..d3d4856f4d35fece2df1faba6cca3f19c68c6e95 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -890,11 +890,11 @@ impl ProjectPanel { Overlay::new( Flex::column() - .with_child(Label::new("Add File".to_string(), style.label.clone()).boxed()) + .with_child( + Label::new("Add File".to_string(), style.item.label.clone()).boxed(), + ) .contained() .with_style(style.container) - // .constrained() - // .with_width(style.width) .boxed(), ) .with_abs_position(menu.position) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 28e31ad3bbd5abf2ed06c7b5a26b441faaa6fe8c..c69a8d389831b50591cc5b7257604f36f33b79b5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -242,7 +242,13 @@ pub struct ProjectPanelEntry { #[derive(Clone, Debug, Deserialize, Default)] pub struct ContextMenu { - pub width: f32, + #[serde(flatten)] + pub container: ContainerStyle, + pub item: ContextMenuItem, +} + +#[derive(Clone, Debug, Deserialize, Default)] +pub struct ContextMenuItem { #[serde(flatten)] pub container: ContainerStyle, pub label: TextStyle, From dcee8439b690e48254c068c4f1eb9bd995dd1628 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 May 2022 19:59:43 -0600 Subject: [PATCH 05/54] Start on context_menu crate --- Cargo.lock | 8 +++++++ crates/context_menu/Cargo.toml | 12 +++++++++++ crates/context_menu/src/context_menu.rs | 28 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 crates/context_menu/Cargo.toml create mode 100644 crates/context_menu/src/context_menu.rs diff --git a/Cargo.lock b/Cargo.lock index 2be38304384bc0d5a581b6c8be59f8c428564657..b0233d6827363b1bafae9e1f9f3c404e1900c55f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -974,6 +974,14 @@ dependencies = [ "workspace", ] +[[package]] +name = "context_menu" +version = "0.1.0" +dependencies = [ + "gpui", + "theme", +] + [[package]] name = "core-foundation" version = "0.9.3" diff --git a/crates/context_menu/Cargo.toml b/crates/context_menu/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..3392d68579b788c41eb32775f3035853144da138 --- /dev/null +++ b/crates/context_menu/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "context_menu" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/context_menu.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +theme = { path = "../theme" } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..8d698ce9be56b67f5a7bc89da283dac207ae792a --- /dev/null +++ b/crates/context_menu/src/context_menu.rs @@ -0,0 +1,28 @@ +use gpui::{Entity, View}; + +enum ContextMenuItem { + Item { + label: String, + action: Box, + }, + Separator, +} + +pub struct ContextMenu { + position: Vector2F, + items: Vec, +} + +impl Entity for ContextMenu { + type Event = (); +} + +impl View for ContextMenu { + fn ui_name() -> &'static str { + "ContextMenu" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + Overlay::new().with_abs_position(self.position).boxed() + } +} From f403d87eff0f96ca4d05664816ba3ca9354ff447 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 May 2022 10:23:43 +0200 Subject: [PATCH 06/54] WIP --- Cargo.lock | 2 + crates/collab/src/main.rs | 1 + crates/context_menu/Cargo.toml | 1 + crates/context_menu/src/context_menu.rs | 115 ++++++++++++++++++++-- crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 55 +++++------ crates/theme/src/theme.rs | 3 +- styles/src/styleTree/projectPanel.ts | 4 +- 8 files changed, 140 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0233d6827363b1bafae9e1f9f3c404e1900c55f..e8b571add83f19c7e8437c66059c98f516c2dfc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -979,6 +979,7 @@ name = "context_menu" version = "0.1.0" dependencies = [ "gpui", + "settings", "theme", ] @@ -3457,6 +3458,7 @@ dependencies = [ name = "project_panel" version = "0.1.0" dependencies = [ + "context_menu", "editor", "futures", "gpui", diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 74401699ca62afbc23273f667494258d955af9e9..784987534da5deed1cf21c2355e6a19c8f773b8b 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -149,6 +149,7 @@ pub fn init_tracing(config: &Config) -> Option<()> { use tracing_subscriber::layer::SubscriberExt; let rust_log = config.rust_log.clone()?; + println!("HEY!"); LogTracer::init().log_err()?; let open_telemetry_layer = config diff --git a/crates/context_menu/Cargo.toml b/crates/context_menu/Cargo.toml index 3392d68579b788c41eb32775f3035853144da138..c33b935c45327a87d9917f89c4c0cec8ac9ffaa7 100644 --- a/crates/context_menu/Cargo.toml +++ b/crates/context_menu/Cargo.toml @@ -9,4 +9,5 @@ doctest = false [dependencies] gpui = { path = "../gpui" } +settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 8d698ce9be56b67f5a7bc89da283dac207ae792a..c3f00ac5b6fea33a1a0dcb9ddc9db487ae59fd9c 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,6 +1,10 @@ -use gpui::{Entity, View}; +use gpui::{ + elements::*, geometry::vector::Vector2F, Action, Entity, RenderContext, View, ViewContext, +}; +use settings::Settings; +use std::{marker::PhantomData, sync::Arc}; -enum ContextMenuItem { +pub enum ContextMenuItem { Item { label: String, action: Box, @@ -8,21 +12,116 @@ enum ContextMenuItem { Separator, } -pub struct ContextMenu { +pub struct ContextMenu { position: Vector2F, - items: Vec, + items: Arc<[ContextMenuItem]>, + state: UniformListState, + selected_index: Option, + widest_item_index: Option, + visible: bool, + _phantom: PhantomData, } -impl Entity for ContextMenu { +impl Entity for ContextMenu { type Event = (); } -impl View for ContextMenu { +impl View for ContextMenu { fn ui_name() -> &'static str { "ContextMenu" } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - Overlay::new().with_abs_position(self.position).boxed() + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + if !self.visible { + return Empty::new().boxed(); + } + + let theme = &cx.global::().theme; + let menu_style = &theme.project_panel.context_menu; + let separator_style = menu_style.separator; + let item_style = menu_style.item.clone(); + let items = self.items.clone(); + let selected_ix = self.selected_index; + Overlay::new( + UniformList::new( + self.state.clone(), + self.items.len(), + move |range, elements, cx| { + let start = range.start; + elements.extend(items[range].iter().enumerate().map(|(ix, item)| { + let item_ix = start + ix; + match item { + ContextMenuItem::Item { label, action } => { + let action = action.boxed_clone(); + MouseEventHandler::new::(item_ix, cx, |state, _| { + let style = + item_style.style_for(state, Some(item_ix) == selected_ix); + Flex::row() + .with_child( + Label::new(label.to_string(), style.label.clone()) + .boxed(), + ) + .boxed() + }) + .on_click(move |_, _, cx| { + cx.dispatch_any_action(action.boxed_clone()) + }) + .boxed() + } + ContextMenuItem::Separator => { + Empty::new().contained().with_style(separator_style).boxed() + } + } + })) + }, + ) + .with_width_from_item(self.widest_item_index) + .boxed(), + ) + .with_abs_position(self.position) + .contained() + .with_style(menu_style.container) + .boxed() + } + + fn on_blur(&mut self, cx: &mut ViewContext) { + self.visible = false; + cx.notify(); + } +} + +impl ContextMenu { + pub fn new() -> Self { + Self { + position: Default::default(), + items: Arc::from([]), + state: Default::default(), + selected_index: Default::default(), + widest_item_index: Default::default(), + visible: false, + _phantom: PhantomData, + } + } + + pub fn show( + &mut self, + position: Vector2F, + items: impl IntoIterator, + cx: &mut ViewContext, + ) { + self.items = items.into_iter().collect(); + self.widest_item_index = self + .items + .iter() + .enumerate() + .max_by_key(|(_, item)| match item { + ContextMenuItem::Item { label, .. } => label.chars().count(), + ContextMenuItem::Separator => 0, + }) + .map(|(ix, _)| ix); + self.position = position; + self.visible = true; + cx.focus_self(); + cx.notify(); } } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 257bac21d9c2c43f05378dd3556d0808a63289fc..7eb0282660883bdcf58aeea38f1350ed66f7ff19 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -8,6 +8,7 @@ path = "src/project_panel.rs" doctest = false [dependencies] +context_menu = { path = "../context_menu" } editor = { path = "../editor" } gpui = { path = "../gpui" } project = { path = "../project" } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d3d4856f4d35fece2df1faba6cca3f19c68c6e95..85d1ed7407d88a51748f99acfd6bec574222e42c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,17 +1,18 @@ +use context_menu::{ContextMenu, ContextMenuItem}; use editor::{Cancel, Editor}; use futures::stream::StreamExt; use gpui::{ actions, anyhow::{anyhow, Result}, elements::{ - ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, Overlay, ParentElement, + ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, }, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -37,7 +38,7 @@ pub struct ProjectPanel { selection: Option, edit_state: Option, filename_editor: ViewHandle, - context_menu: Option, + context_menu: ViewHandle>, handle: WeakViewHandle, } @@ -83,11 +84,6 @@ pub struct DeployContextMenu { pub entry_id: Option, } -pub struct ContextMenu { - pub position: Vector2F, - pub entry_id: Option, -} - actions!( project_panel, [ @@ -170,7 +166,7 @@ impl ProjectPanel { selection: None, edit_state: None, filename_editor, - context_menu: None, + context_menu: cx.add_view(|_| ContextMenu::new()), handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); @@ -211,9 +207,22 @@ impl ProjectPanel { } fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { - self.context_menu = Some(ContextMenu { - position: action.position, - entry_id: action.entry_id, + self.context_menu.update(cx, |menu, cx| { + menu.show( + action.position, + [ + ContextMenuItem::Item { + label: "New File".to_string(), + action: Box::new(AddFile), + }, + ContextMenuItem::Item { + label: "New Directory".to_string(), + action: Box::new(AddDirectory), + }, + ContextMenuItem::Separator, + ], + cx, + ); }); cx.notify(); } @@ -883,24 +892,6 @@ impl ProjectPanel { .with_cursor_style(CursorStyle::PointingHand) .boxed() } - - fn render_context_menu(&self, cx: &mut RenderContext) -> Option { - self.context_menu.as_ref().map(|menu| { - let style = &cx.global::().theme.project_panel.context_menu; - - Overlay::new( - Flex::column() - .with_child( - Label::new("Add File".to_string(), style.item.label.clone()).boxed(), - ) - .contained() - .with_style(style.container) - .boxed(), - ) - .with_abs_position(menu.position) - .named("Project Panel Context Menu") - }) - } } impl View for ProjectPanel { @@ -943,7 +934,7 @@ impl View for ProjectPanel { .with_style(container_style) .boxed(), ) - .with_children(self.render_context_menu(cx)) + .with_child(ChildView::new(&self.context_menu).boxed()) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c69a8d389831b50591cc5b7257604f36f33b79b5..941ae03d4d87bc45494c8d7b00832f03c7a87269 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -244,7 +244,8 @@ pub struct ProjectPanelEntry { pub struct ContextMenu { #[serde(flatten)] pub container: ContainerStyle, - pub item: ContextMenuItem, + pub item: Interactive, + pub separator: ContainerStyle, } #[derive(Clone, Debug, Deserialize, Default)] diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 66aaa85952ce26010e76c9a8d0721c7fcfaaf51e..b2d8b9d4acd613088eaefbbba37725e21ecd36d1 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -43,7 +43,9 @@ export default function projectPanel(theme: Theme) { right: 6, top: 2, }, - label: text(theme, "sans", "secondary", { size: "sm" }), + item: { + label: text(theme, "sans", "secondary", { size: "sm" }), + }, shadow: shadow(theme), } }; From 3b2f1644fb4245bac941218a0c413040e6bde534 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 May 2022 14:24:53 +0200 Subject: [PATCH 07/54] Constrain context menu to the width of the widest item Co-Authored-By: Nathan Sobo --- crates/context_menu/src/context_menu.rs | 119 ++++++++++---------- crates/gpui/src/elements/constrained_box.rs | 112 +++++++++++++++--- crates/gpui/src/presenter.rs | 9 ++ crates/project_panel/src/project_panel.rs | 10 +- crates/theme/src/theme.rs | 2 +- styles/src/styleTree/app.ts | 2 + styles/src/styleTree/contextMenu.ts | 23 ++++ styles/src/styleTree/projectPanel.ts | 16 --- 8 files changed, 200 insertions(+), 93 deletions(-) create mode 100644 styles/src/styleTree/contextMenu.ts diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index c3f00ac5b6fea33a1a0dcb9ddc9db487ae59fd9c..0bcc97a25dac5f802f5685ccaf0a8caf737b4b19 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,8 +1,8 @@ use gpui::{ - elements::*, geometry::vector::Vector2F, Action, Entity, RenderContext, View, ViewContext, + elements::*, geometry::vector::Vector2F, Action, Axis, Entity, RenderContext, SizeConstraint, + View, ViewContext, }; use settings::Settings; -use std::{marker::PhantomData, sync::Arc}; pub enum ContextMenuItem { Item { @@ -12,75 +12,51 @@ pub enum ContextMenuItem { Separator, } -pub struct ContextMenu { +pub struct ContextMenu { position: Vector2F, - items: Arc<[ContextMenuItem]>, - state: UniformListState, + items: Vec, + widest_item_index: usize, selected_index: Option, - widest_item_index: Option, visible: bool, - _phantom: PhantomData, } -impl Entity for ContextMenu { +impl Entity for ContextMenu { type Event = (); } -impl View for ContextMenu { +impl View for ContextMenu { fn ui_name() -> &'static str { "ContextMenu" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum Tag {} + if !self.visible { return Empty::new().boxed(); } - let theme = &cx.global::().theme; - let menu_style = &theme.project_panel.context_menu; - let separator_style = menu_style.separator; - let item_style = menu_style.item.clone(); - let items = self.items.clone(); - let selected_ix = self.selected_index; + let style = cx.global::().theme.context_menu.clone(); + + let mut widest_item = self.render_menu_item::<()>(self.widest_item_index, cx, &style); + Overlay::new( - UniformList::new( - self.state.clone(), - self.items.len(), - move |range, elements, cx| { - let start = range.start; - elements.extend(items[range].iter().enumerate().map(|(ix, item)| { - let item_ix = start + ix; - match item { - ContextMenuItem::Item { label, action } => { - let action = action.boxed_clone(); - MouseEventHandler::new::(item_ix, cx, |state, _| { - let style = - item_style.style_for(state, Some(item_ix) == selected_ix); - Flex::row() - .with_child( - Label::new(label.to_string(), style.label.clone()) - .boxed(), - ) - .boxed() - }) - .on_click(move |_, _, cx| { - cx.dispatch_any_action(action.boxed_clone()) - }) - .boxed() - } - ContextMenuItem::Separator => { - Empty::new().contained().with_style(separator_style).boxed() - } - } - })) - }, - ) - .with_width_from_item(self.widest_item_index) - .boxed(), + Flex::column() + .with_children( + (0..self.items.len()).map(|ix| self.render_menu_item::(ix, cx, &style)), + ) + .constrained() + .dynamically(move |constraint, cx| { + SizeConstraint::strict_along( + Axis::Horizontal, + widest_item.layout(constraint, cx).x(), + ) + }) + .contained() + .with_style(style.container) + .boxed(), ) .with_abs_position(self.position) - .contained() - .with_style(menu_style.container) .boxed() } @@ -90,16 +66,14 @@ impl View for ContextMenu { } } -impl ContextMenu { +impl ContextMenu { pub fn new() -> Self { Self { position: Default::default(), - items: Arc::from([]), - state: Default::default(), + items: Default::default(), selected_index: Default::default(), widest_item_index: Default::default(), visible: false, - _phantom: PhantomData, } } @@ -109,7 +83,9 @@ impl ContextMenu { items: impl IntoIterator, cx: &mut ViewContext, ) { - self.items = items.into_iter().collect(); + let mut items = items.into_iter().peekable(); + assert!(items.peek().is_some(), "must have at least one item"); + self.items = items.collect(); self.widest_item_index = self .items .iter() @@ -118,10 +94,39 @@ impl ContextMenu { ContextMenuItem::Item { label, .. } => label.chars().count(), ContextMenuItem::Separator => 0, }) - .map(|(ix, _)| ix); + .unwrap() + .0; self.position = position; self.visible = true; cx.focus_self(); cx.notify(); } + + fn render_menu_item( + &self, + ix: usize, + cx: &mut RenderContext, + style: &theme::ContextMenu, + ) -> ElementBox { + match &self.items[ix] { + ContextMenuItem::Item { label, action } => { + let action = action.boxed_clone(); + MouseEventHandler::new::(ix, cx, |state, _| { + let style = style.item.style_for(state, Some(ix) == self.selected_index); + Flex::row() + .with_child(Label::new(label.to_string(), style.label.clone()).boxed()) + .boxed() + }) + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) + .boxed() + } + ContextMenuItem::Separator => Empty::new() + .contained() + .with_style(style.separator) + .constrained() + .with_height(1.) + .flex(1., false) + .boxed(), + } + } } diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index f12ed6900abc03a50d435fbb5eba859ec02c0e89..5ab01df1e1d7547ba1d565f001c177c0edcfb863 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -9,46 +9,121 @@ use crate::{ pub struct ConstrainedBox { child: ElementBox, - constraint: SizeConstraint, + constraint: Constraint, +} + +pub enum Constraint { + Static(SizeConstraint), + Dynamic(Box SizeConstraint>), +} + +impl ToJson for Constraint { + fn to_json(&self) -> serde_json::Value { + match self { + Constraint::Static(constraint) => constraint.to_json(), + Constraint::Dynamic(_) => "dynamic".into(), + } + } } impl ConstrainedBox { pub fn new(child: ElementBox) -> Self { Self { child, - constraint: SizeConstraint { - min: Vector2F::zero(), - max: Vector2F::splat(f32::INFINITY), - }, + constraint: Constraint::Static(Default::default()), } } + pub fn dynamically( + mut self, + constraint: impl 'static + FnMut(SizeConstraint, &mut LayoutContext) -> SizeConstraint, + ) -> Self { + self.constraint = Constraint::Dynamic(Box::new(constraint)); + self + } + pub fn with_min_width(mut self, min_width: f32) -> Self { - self.constraint.min.set_x(min_width); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.min.set_x(min_width); + } else { + unreachable!() + } + self } pub fn with_max_width(mut self, max_width: f32) -> Self { - self.constraint.max.set_x(max_width); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.max.set_x(max_width); + } else { + unreachable!() + } + self } pub fn with_max_height(mut self, max_height: f32) -> Self { - self.constraint.max.set_y(max_height); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.max.set_y(max_height); + } else { + unreachable!() + } + self } pub fn with_width(mut self, width: f32) -> Self { - self.constraint.min.set_x(width); - self.constraint.max.set_x(width); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.min.set_x(width); + constraint.max.set_x(width); + } else { + unreachable!() + } + self } pub fn with_height(mut self, height: f32) -> Self { - self.constraint.min.set_y(height); - self.constraint.max.set_y(height); + if let Constraint::Dynamic(_) = self.constraint { + self.constraint = Constraint::Static(Default::default()); + } + + if let Constraint::Static(constraint) = &mut self.constraint { + constraint.min.set_y(height); + constraint.max.set_y(height); + } else { + unreachable!() + } + self } + + fn constraint( + &mut self, + input_constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> SizeConstraint { + match &mut self.constraint { + Constraint::Static(constraint) => *constraint, + Constraint::Dynamic(compute_constraint) => compute_constraint(input_constraint, cx), + } + } } impl Element for ConstrainedBox { @@ -57,13 +132,14 @@ impl Element for ConstrainedBox { fn layout( &mut self, - mut constraint: SizeConstraint, + mut parent_constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - constraint.min = constraint.min.max(self.constraint.min); - constraint.max = constraint.max.min(self.constraint.max); - constraint.max = constraint.max.max(constraint.min); - let size = self.child.layout(constraint, cx); + let constraint = self.constraint(parent_constraint, cx); + parent_constraint.min = parent_constraint.min.max(constraint.min); + parent_constraint.max = parent_constraint.max.min(constraint.max); + parent_constraint.max = parent_constraint.max.max(parent_constraint.min); + let size = self.child.layout(parent_constraint, cx); (size, ()) } @@ -96,6 +172,6 @@ impl Element for ConstrainedBox { _: &Self::PaintState, cx: &DebugContext, ) -> json::Value { - json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(cx)}) + json!({"type": "ConstrainedBox", "assigned_constraint": self.constraint.to_json(), "child": self.child.debug(cx)}) } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 2a2da34d631101d3fef73db3469fcdca6eaa6598..053b69269c9335662c1215fa0e725d9910260eb7 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -524,6 +524,15 @@ impl SizeConstraint { } } +impl Default for SizeConstraint { + fn default() -> Self { + SizeConstraint { + min: Vector2F::zero(), + max: Vector2F::splat(f32::INFINITY), + } + } +} + impl ToJson for SizeConstraint { fn to_json(&self) -> serde_json::Value { json!({ diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 85d1ed7407d88a51748f99acfd6bec574222e42c..4b060b4e7033967d2e7ed73c99e9e6c4e260927c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -38,7 +38,7 @@ pub struct ProjectPanel { selection: Option, edit_state: Option, filename_editor: ViewHandle, - context_menu: ViewHandle>, + context_menu: ViewHandle, handle: WeakViewHandle, } @@ -220,6 +220,14 @@ impl ProjectPanel { action: Box::new(AddDirectory), }, ContextMenuItem::Separator, + ContextMenuItem::Item { + label: "Rename".to_string(), + action: Box::new(Rename), + }, + ContextMenuItem::Item { + label: "Delete".to_string(), + action: Box::new(Delete), + }, ], cx, ); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 941ae03d4d87bc45494c8d7b00832f03c7a87269..4cbe60db3c0d6998be6e317db27467bd3e85c066 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -19,6 +19,7 @@ pub struct Theme { #[serde(default)] pub name: String, pub workspace: Workspace, + pub context_menu: ContextMenu, pub chat_panel: ChatPanel, pub contacts_panel: ContactsPanel, pub contact_finder: ContactFinder, @@ -226,7 +227,6 @@ pub struct ProjectPanel { pub ignored_entry_fade: f32, pub filename_editor: FieldEditor, pub indent_width: f32, - pub context_menu: ContextMenu, } #[derive(Clone, Debug, Deserialize, Default)] diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 230bd3e57f8eb31b5b602785026c6abe927d1ffe..41266ff5f7d775551ce7cec8d7cff91e604d8658 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -9,6 +9,7 @@ import projectPanel from "./projectPanel"; import search from "./search"; import picker from "./picker"; import workspace from "./workspace"; +import contextMenu from "./contextMenu"; import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; @@ -20,6 +21,7 @@ export default function app(theme: Theme): Object { return { picker: picker(theme), workspace: workspace(theme), + contextMenu: contextMenu(theme), editor: editor(theme), projectDiagnostics: projectDiagnostics(theme), commandPalette: commandPalette(theme), diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts new file mode 100644 index 0000000000000000000000000000000000000000..5458ceda6940043fdb4f9cf1496c8e202e2b3600 --- /dev/null +++ b/styles/src/styleTree/contextMenu.ts @@ -0,0 +1,23 @@ +import Theme from "../themes/common/theme"; +import { shadow, text } from "./components"; + +export default function contextMenu(theme: Theme) { + return { + background: "#ff0000", + // background: backgroundColor(theme, 300, "base"), + cornerRadius: 6, + padding: { + bottom: 2, + left: 6, + right: 6, + top: 2, + }, + shadow: shadow(theme), + item: { + label: text(theme, "sans", "secondary", { size: "sm" }), + }, + separator: { + background: "#00ff00" + } + } +} \ No newline at end of file diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index b2d8b9d4acd613088eaefbbba37725e21ecd36d1..547414afc885188ebac64ca7c1cacd2a14e88ebf 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -32,21 +32,5 @@ export default function projectPanel(theme: Theme) { text: text(theme, "mono", "primary", { size: "sm" }), selection: player(theme, 1).selection, }, - contextMenu: { - width: 100, - // background: "#ff0000", - background: backgroundColor(theme, 300, "base"), - cornerRadius: 6, - padding: { - bottom: 2, - left: 6, - right: 6, - top: 2, - }, - item: { - label: text(theme, "sans", "secondary", { size: "sm" }), - }, - shadow: shadow(theme), - } }; } From 85ed7b41f1f42b13bb4b3907eaa6ef94728c1262 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 May 2022 14:42:45 +0200 Subject: [PATCH 08/54] Select right-clicked entry before deploying context menu Co-Authored-By: Nathan Sobo --- crates/project_panel/src/project_panel.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4b060b4e7033967d2e7ed73c99e9e6c4e260927c..e310459cf75c6890b3c7050310f06733f6d94f3b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -207,6 +207,15 @@ impl ProjectPanel { } fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { + if let Some(entry_id) = action.entry_id { + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + } + } + self.context_menu.update(cx, |menu, cx| { menu.show( action.position, @@ -232,6 +241,7 @@ impl ProjectPanel { cx, ); }); + cx.notify(); } From a8483ba4580ed76680199377c92ca5a77b5544e1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 May 2022 15:24:44 +0200 Subject: [PATCH 09/54] WIP --- Cargo.lock | 1 + crates/context_menu/Cargo.toml | 1 + crates/context_menu/src/context_menu.rs | 108 ++++++++++++-------- crates/gpui/src/app.rs | 11 +- crates/gpui/src/elements.rs | 5 +- crates/gpui/src/elements/keystroke_label.rs | 92 +++++++++++++++++ crates/gpui/src/keymap.rs | 30 +++++- crates/gpui/src/presenter.rs | 12 +++ crates/project_panel/src/project_panel.rs | 20 +--- crates/theme/src/theme.rs | 1 + styles/src/styleTree/contextMenu.ts | 3 +- 11 files changed, 219 insertions(+), 65 deletions(-) create mode 100644 crates/gpui/src/elements/keystroke_label.rs diff --git a/Cargo.lock b/Cargo.lock index e8b571add83f19c7e8437c66059c98f516c2dfc2..a103488028b87c6118bfc0132687c080a7c2f7cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -980,6 +980,7 @@ version = "0.1.0" dependencies = [ "gpui", "settings", + "smallvec", "theme", ] diff --git a/crates/context_menu/Cargo.toml b/crates/context_menu/Cargo.toml index c33b935c45327a87d9917f89c4c0cec8ac9ffaa7..65f7f59a14c590fada55a8a29841a206fe3844d1 100644 --- a/crates/context_menu/Cargo.toml +++ b/crates/context_menu/Cargo.toml @@ -11,3 +11,4 @@ doctest = false gpui = { path = "../gpui" } settings = { path = "../settings" } theme = { path = "../theme" } +smallvec = "1.6" diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 0bcc97a25dac5f802f5685ccaf0a8caf737b4b19..4e5f15aae5a3c56e56fc478f2966798a47422064 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -12,10 +12,22 @@ pub enum ContextMenuItem { Separator, } +impl ContextMenuItem { + pub fn item(label: String, action: impl 'static + Action) -> Self { + Self::Item { + label, + action: Box::new(action), + } + } + + pub fn separator() -> Self { + Self::Separator + } +} + pub struct ContextMenu { position: Vector2F, items: Vec, - widest_item_index: usize, selected_index: Option, visible: bool, } @@ -36,28 +48,22 @@ impl View for ContextMenu { return Empty::new().boxed(); } - let style = cx.global::().theme.context_menu.clone(); - - let mut widest_item = self.render_menu_item::<()>(self.widest_item_index, cx, &style); - - Overlay::new( - Flex::column() - .with_children( - (0..self.items.len()).map(|ix| self.render_menu_item::(ix, cx, &style)), + // Render the menu once at minimum width. + let mut collapsed_menu = self.render_menu::<()>(false, cx).boxed(); + let expanded_menu = self + .render_menu::(true, cx) + .constrained() + .dynamically(move |constraint, cx| { + SizeConstraint::strict_along( + Axis::Horizontal, + collapsed_menu.layout(constraint, cx).x(), ) - .constrained() - .dynamically(move |constraint, cx| { - SizeConstraint::strict_along( - Axis::Horizontal, - widest_item.layout(constraint, cx).x(), - ) - }) - .contained() - .with_style(style.container) - .boxed(), - ) - .with_abs_position(self.position) - .boxed() + }) + .boxed(); + + Overlay::new(expanded_menu) + .with_abs_position(self.position) + .boxed() } fn on_blur(&mut self, cx: &mut ViewContext) { @@ -72,7 +78,6 @@ impl ContextMenu { position: Default::default(), items: Default::default(), selected_index: Default::default(), - widest_item_index: Default::default(), visible: false, } } @@ -86,25 +91,31 @@ impl ContextMenu { let mut items = items.into_iter().peekable(); assert!(items.peek().is_some(), "must have at least one item"); self.items = items.collect(); - self.widest_item_index = self - .items - .iter() - .enumerate() - .max_by_key(|(_, item)| match item { - ContextMenuItem::Item { label, .. } => label.chars().count(), - ContextMenuItem::Separator => 0, - }) - .unwrap() - .0; self.position = position; self.visible = true; cx.focus_self(); cx.notify(); } + fn render_menu( + &mut self, + expanded: bool, + cx: &mut RenderContext, + ) -> impl Element { + let style = cx.global::().theme.context_menu.clone(); + Flex::column() + .with_children( + (0..self.items.len()) + .map(|ix| self.render_menu_item::(ix, expanded, cx, &style)), + ) + .contained() + .with_style(style.container) + } + fn render_menu_item( &self, ix: usize, + expanded: bool, cx: &mut RenderContext, style: &theme::ContextMenu, ) -> ElementBox { @@ -115,18 +126,35 @@ impl ContextMenu { let style = style.item.style_for(state, Some(ix) == self.selected_index); Flex::row() .with_child(Label::new(label.to_string(), style.label.clone()).boxed()) + .with_child({ + let label = KeystrokeLabel::new( + action.boxed_clone(), + style.keystroke.container, + style.keystroke.text.clone(), + ); + if expanded { + label.flex_float().boxed() + } else { + label.boxed() + } + }) .boxed() }) .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) .boxed() } - ContextMenuItem::Separator => Empty::new() - .contained() - .with_style(style.separator) - .constrained() - .with_height(1.) - .flex(1., false) - .boxed(), + ContextMenuItem::Separator => { + let mut separator = Empty::new(); + if !expanded { + separator = separator.collapsed(); + } + separator + .contained() + .with_style(style.separator) + .constrained() + .with_height(1.) + .boxed() + } } } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2d93e46c05d6f18459cf6d7acecc2f77289bcc6a..d11940b2c6d13776c424d94339b96f9459d66208 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1414,11 +1414,12 @@ impl MutableAppContext { } /// Return keystrokes that would dispatch the given action closest to the focused view, if there are any. - pub fn keystrokes_for_action(&self, action: &dyn Action) -> Option> { - let window_id = self.cx.platform.key_window_id()?; - let (presenter, _) = self.presenters_and_platform_windows.get(&window_id)?; - let dispatch_path = presenter.borrow().dispatch_path(&self.cx); - + pub(crate) fn keystrokes_for_action( + &self, + window_id: usize, + dispatch_path: &[usize], + action: &dyn Action, + ) -> Option> { for view_id in dispatch_path.iter().rev() { let view = self .cx diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 55c7bf22fe460979f70ff943cf75fc368148d86b..231339d9e07ed232bb61b1ac7c0ba38c2685c949 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -8,6 +8,7 @@ mod expanded; mod flex; mod hook; mod image; +mod keystroke_label; mod label; mod list; mod mouse_event_handler; @@ -20,8 +21,8 @@ mod uniform_list; use self::expanded::Expanded; pub use self::{ align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*, - hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*, - text::*, uniform_list::*, + hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, + stack::*, svg::*, text::*, uniform_list::*, }; pub use crate::presenter::ChildView; use crate::{ diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs new file mode 100644 index 0000000000000000000000000000000000000000..0112b548463b450c0b49aa5e1ad4c93f633a5d10 --- /dev/null +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -0,0 +1,92 @@ +use crate::{ + elements::*, + fonts::TextStyle, + geometry::{rect::RectF, vector::Vector2F}, + Action, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, +}; +use serde_json::json; + +use super::ContainerStyle; + +pub struct KeystrokeLabel { + action: Box, + container_style: ContainerStyle, + text_style: TextStyle, +} + +impl KeystrokeLabel { + pub fn new( + action: Box, + container_style: ContainerStyle, + text_style: TextStyle, + ) -> Self { + Self { + action, + container_style, + text_style, + } + } +} + +impl Element for KeystrokeLabel { + type LayoutState = ElementBox; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, ElementBox) { + let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) { + Flex::row() + .with_children(keystrokes.iter().map(|keystroke| { + Label::new(keystroke.to_string(), self.text_style.clone()) + .contained() + .with_style(self.container_style) + .boxed() + })) + .boxed() + } else { + Empty::new().collapsed().boxed() + }; + + let size = element.layout(constraint, cx); + (size, element) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + element: &mut ElementBox, + cx: &mut PaintContext, + ) { + element.paint(bounds.origin(), visible_bounds, cx); + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: RectF, + element: &mut ElementBox, + _: &mut (), + cx: &mut EventContext, + ) -> bool { + element.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + element: &ElementBox, + _: &(), + cx: &crate::DebugContext, + ) -> serde_json::Value { + json!({ + "type": "KeystrokeLabel", + "action": self.action.name(), + "child": element.debug(cx) + }) + } +} diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index dca752ed6f8c4b59e3427fcbee2f80d8eb3475ea..87b0287dc48fce0b55d48346ef462297f9150bc6 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -185,7 +185,7 @@ impl Matcher { return Some(binding.keystrokes.clone()); } } - todo!() + None } } @@ -311,6 +311,34 @@ impl Keystroke { } } +impl std::fmt::Display for Keystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.ctrl { + write!(f, "{}", "^")?; + } + if self.alt { + write!(f, "{}", "⎇")?; + } + if self.cmd { + write!(f, "{}", "⌘")?; + } + if self.shift { + write!(f, "{}", "⇧")?; + } + let key = match self.key.as_str() { + "backspace" => "⌫", + "up" => "↑", + "down" => "↓", + "left" => "←", + "right" => "→", + "tab" => "⇥", + "escape" => "⎋", + key => key, + }; + write!(f, "{}", key) + } +} + impl Context { pub fn extend(&mut self, other: &Context) { for v in &other.set { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 053b69269c9335662c1215fa0e725d9910260eb7..2c9c4719d6ecbdd9dee153726c115fed34ffadbd 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -4,6 +4,7 @@ use crate::{ font_cache::FontCache, geometry::rect::RectF, json::{self, ToJson}, + keymap::Keystroke, platform::{CursorStyle, Event}, text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, @@ -12,6 +13,7 @@ use crate::{ }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; +use smallvec::SmallVec; use std::{ collections::{HashMap, HashSet}, ops::{Deref, DerefMut}, @@ -148,6 +150,7 @@ impl Presenter { cx: &'a mut MutableAppContext, ) -> LayoutContext<'a> { LayoutContext { + window_id: self.window_id, rendered_views: &mut self.rendered_views, parents: &mut self.parents, refreshing, @@ -257,6 +260,7 @@ pub struct DispatchDirective { } pub struct LayoutContext<'a> { + window_id: usize, rendered_views: &'a mut HashMap, parents: &'a mut HashMap, view_stack: Vec, @@ -281,6 +285,14 @@ impl<'a> LayoutContext<'a> { self.view_stack.pop(); size } + + pub(crate) fn keystrokes_for_action( + &self, + action: &dyn Action, + ) -> Option> { + self.app + .keystrokes_for_action(self.window_id, &self.view_stack, action) + } } impl<'a> Deref for LayoutContext<'a> { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e310459cf75c6890b3c7050310f06733f6d94f3b..7d49a2d07dfc439f2407753a98f567e3bca15c45 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -220,23 +220,11 @@ impl ProjectPanel { menu.show( action.position, [ - ContextMenuItem::Item { - label: "New File".to_string(), - action: Box::new(AddFile), - }, - ContextMenuItem::Item { - label: "New Directory".to_string(), - action: Box::new(AddDirectory), - }, + ContextMenuItem::item("New File".to_string(), AddFile), + ContextMenuItem::item("New Directory".to_string(), AddDirectory), ContextMenuItem::Separator, - ContextMenuItem::Item { - label: "Rename".to_string(), - action: Box::new(Rename), - }, - ContextMenuItem::Item { - label: "Delete".to_string(), - action: Box::new(Delete), - }, + ContextMenuItem::item("Rename".to_string(), Rename), + ContextMenuItem::item("Delete".to_string(), Delete), ], cx, ); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4cbe60db3c0d6998be6e317db27467bd3e85c066..2edd3cef45eae765962b13b8c8c0320ee928ac60 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -253,6 +253,7 @@ pub struct ContextMenuItem { #[serde(flatten)] pub container: ContainerStyle, pub label: TextStyle, + pub keystroke: ContainedText, } #[derive(Debug, Deserialize, Default)] diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index 5458ceda6940043fdb4f9cf1496c8e202e2b3600..e2b60f2685c12e1d3caa6eb7fd28c7ea5bff200c 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -15,9 +15,10 @@ export default function contextMenu(theme: Theme) { shadow: shadow(theme), item: { label: text(theme, "sans", "secondary", { size: "sm" }), + keystroke: text(theme, "sans", "muted", { size: "sm", weight: "bold" }), }, separator: { background: "#00ff00" - } + }, } } \ No newline at end of file From c0aafac387f72c7d95bb43f63a107e7b2bc5bb85 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 May 2022 10:20:56 -0600 Subject: [PATCH 10/54] Put keystrokes in their own column This requires rendering the menu for measurement in a totally different way, where the top level is a flex row. We don't want to render the menu like this for presentation because of hovers / highlights on individual items needing to include the keystrokes. Co-Authored-By: Antonio Scandurra --- crates/context_menu/src/context_menu.rs | 142 +++++++++++++++--------- styles/src/styleTree/contextMenu.ts | 10 +- 2 files changed, 94 insertions(+), 58 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 4e5f15aae5a3c56e56fc478f2966798a47422064..d12d8c0315655b0dd85da73157c9f5b0bcfb61a6 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -42,16 +42,14 @@ impl View for ContextMenu { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum Tag {} - if !self.visible { return Empty::new().boxed(); } // Render the menu once at minimum width. - let mut collapsed_menu = self.render_menu::<()>(false, cx).boxed(); + let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed(); let expanded_menu = self - .render_menu::(true, cx) + .render_menu(cx) .constrained() .dynamically(move |constraint, cx| { SizeConstraint::strict_along( @@ -97,64 +95,100 @@ impl ContextMenu { cx.notify(); } - fn render_menu( - &mut self, - expanded: bool, - cx: &mut RenderContext, - ) -> impl Element { + fn render_menu_for_measurement(&self, cx: &mut RenderContext) -> impl Element { let style = cx.global::().theme.context_menu.clone(); - Flex::column() - .with_children( - (0..self.items.len()) - .map(|ix| self.render_menu_item::(ix, expanded, cx, &style)), + Flex::row() + .with_child( + Flex::column() + .with_children(self.items.iter().enumerate().map(|(ix, item)| { + match item { + ContextMenuItem::Item { label, .. } => { + let style = style.item.style_for( + &Default::default(), + Some(ix) == self.selected_index, + ); + Label::new(label.to_string(), style.label.clone()).boxed() + } + ContextMenuItem::Separator => Empty::new() + .collapsed() + .contained() + .with_style(style.separator) + .constrained() + .with_height(1.) + .boxed(), + } + })) + .boxed(), + ) + .with_child( + Flex::column() + .with_children(self.items.iter().enumerate().map(|(ix, item)| { + match item { + ContextMenuItem::Item { action, .. } => { + let style = style.item.style_for( + &Default::default(), + Some(ix) == self.selected_index, + ); + KeystrokeLabel::new( + action.boxed_clone(), + style.keystroke.container, + style.keystroke.text.clone(), + ) + .boxed() + } + ContextMenuItem::Separator => Empty::new() + .collapsed() + .contained() + .with_style(style.separator) + .constrained() + .with_height(1.) + .boxed(), + } + })) + .boxed(), ) .contained() .with_style(style.container) } - fn render_menu_item( - &self, - ix: usize, - expanded: bool, - cx: &mut RenderContext, - style: &theme::ContextMenu, - ) -> ElementBox { - match &self.items[ix] { - ContextMenuItem::Item { label, action } => { - let action = action.boxed_clone(); - MouseEventHandler::new::(ix, cx, |state, _| { - let style = style.item.style_for(state, Some(ix) == self.selected_index); - Flex::row() - .with_child(Label::new(label.to_string(), style.label.clone()).boxed()) - .with_child({ - let label = KeystrokeLabel::new( - action.boxed_clone(), - style.keystroke.container, - style.keystroke.text.clone(), - ); - if expanded { - label.flex_float().boxed() - } else { - label.boxed() - } + fn render_menu(&self, cx: &mut RenderContext) -> impl Element { + enum Tag {} + let style = cx.global::().theme.context_menu.clone(); + Flex::column() + .with_children(self.items.iter().enumerate().map(|(ix, item)| { + match item { + ContextMenuItem::Item { label, action } => { + let action = action.boxed_clone(); + MouseEventHandler::new::(ix, cx, |state, _| { + let style = + style.item.style_for(state, Some(ix) == self.selected_index); + Flex::row() + .with_child( + Label::new(label.to_string(), style.label.clone()).boxed(), + ) + .with_child({ + KeystrokeLabel::new( + action.boxed_clone(), + style.keystroke.container, + style.keystroke.text.clone(), + ) + .flex_float() + .boxed() + }) + .boxed() }) + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) .boxed() - }) - .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) - .boxed() - } - ContextMenuItem::Separator => { - let mut separator = Empty::new(); - if !expanded { - separator = separator.collapsed(); + } + ContextMenuItem::Separator => Empty::new() + .contained() + .with_style(style.separator) + .constrained() + .with_height(1.) + .boxed(), } - separator - .contained() - .with_style(style.separator) - .constrained() - .with_height(1.) - .boxed() - } - } + })) + .contained() + .with_style(style.container) } } diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index e2b60f2685c12e1d3caa6eb7fd28c7ea5bff200c..e0a78cafe684b4e0bee893b645a4fd741547c3b6 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -1,10 +1,9 @@ import Theme from "../themes/common/theme"; -import { shadow, text } from "./components"; +import { backgroundColor, shadow, text } from "./components"; export default function contextMenu(theme: Theme) { return { - background: "#ff0000", - // background: backgroundColor(theme, 300, "base"), + background: backgroundColor(theme, 300, "base"), cornerRadius: 6, padding: { bottom: 2, @@ -15,7 +14,10 @@ export default function contextMenu(theme: Theme) { shadow: shadow(theme), item: { label: text(theme, "sans", "secondary", { size: "sm" }), - keystroke: text(theme, "sans", "muted", { size: "sm", weight: "bold" }), + keystroke: { + margin: { left: 60 }, + ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }) + }, }, separator: { background: "#00ff00" From 580f1a4125cbaa99cf6865f395e72586835e9729 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 May 2022 10:40:53 +0200 Subject: [PATCH 11/54] Style context menu --- crates/context_menu/src/context_menu.rs | 15 ++++++----- .../gpui/src/elements/mouse_event_handler.rs | 2 +- styles/src/styleTree/contextMenu.ts | 26 +++++++++++++------ styles/src/styleTree/projectPanel.ts | 2 +- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index d12d8c0315655b0dd85da73157c9f5b0bcfb61a6..5a740a91b930904b098d002d5bc39d697d85188a 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,6 +1,6 @@ use gpui::{ - elements::*, geometry::vector::Vector2F, Action, Axis, Entity, RenderContext, SizeConstraint, - View, ViewContext, + elements::*, geometry::vector::Vector2F, platform::CursorStyle, Action, Axis, Entity, + RenderContext, SizeConstraint, View, ViewContext, }; use settings::Settings; @@ -138,10 +138,10 @@ impl ContextMenu { } ContextMenuItem::Separator => Empty::new() .collapsed() - .contained() - .with_style(style.separator) .constrained() .with_height(1.) + .contained() + .with_style(style.separator) .boxed(), } })) @@ -175,16 +175,19 @@ impl ContextMenu { .flex_float() .boxed() }) + .contained() + .with_style(style.container) .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) .boxed() } ContextMenuItem::Separator => Empty::new() - .contained() - .with_style(style.separator) .constrained() .with_height(1.) + .contained() + .with_style(style.separator) .boxed(), } })) diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 65cb6ed61d8717ed69e636d5e17e1ab1e77b9509..7e0281bffe553c60bc9cf02f0ca39b7c617c8953 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -22,7 +22,7 @@ pub struct MouseEventHandler { padding: Padding, } -#[derive(Default)] +#[derive(Default, Debug)] pub struct MouseState { pub hovered: bool, pub clicked: bool, diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index e0a78cafe684b4e0bee893b645a4fd741547c3b6..e1677b066647c143e55a7876ea8610cf05ab08dd 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -1,26 +1,36 @@ import Theme from "../themes/common/theme"; -import { backgroundColor, shadow, text } from "./components"; +import { backgroundColor, borderColor, shadow, text } from "./components"; export default function contextMenu(theme: Theme) { return { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, - padding: { - bottom: 2, - left: 6, - right: 6, - top: 2, - }, + padding: 6, shadow: shadow(theme), item: { + padding: { left: 4, right: 4, top: 2, bottom: 2 }, + cornerRadius: 6, label: text(theme, "sans", "secondary", { size: "sm" }), keystroke: { margin: { left: 60 }, ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }) }, + hover: { + background: backgroundColor(theme, 300, "hovered"), + text: text(theme, "sans", "primary", { size: "sm" }), + }, + active: { + background: backgroundColor(theme, 300, "active"), + text: text(theme, "sans", "primary", { size: "sm" }), + }, + activeHover: { + background: backgroundColor(theme, 300, "hovered"), + text: text(theme, "sans", "active", { size: "sm" }), + } }, separator: { - background: "#00ff00" + background: borderColor(theme, "primary"), + margin: { top: 2, bottom: 2 } }, } } \ No newline at end of file diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 547414afc885188ebac64ca7c1cacd2a14e88ebf..2f3e3eea72195e415a2a536bc959414f857b30b9 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,6 +1,6 @@ import Theme from "../themes/common/theme"; import { panel } from "./app"; -import { backgroundColor, iconColor, player, shadow, text } from "./components"; +import { backgroundColor, iconColor, player, text } from "./components"; export default function projectPanel(theme: Theme) { return { From a5044ccbba54bef855edab5acf2a8f2b7f6390bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 May 2022 11:17:10 +0200 Subject: [PATCH 12/54] WIP --- crates/context_menu/src/context_menu.rs | 4 +- crates/project_panel/src/project_panel.rs | 91 ++++++++++++++--------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 5a740a91b930904b098d002d5bc39d697d85188a..ff1c3dda7281752d49560251b188c7599c16b4a4 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -13,9 +13,9 @@ pub enum ContextMenuItem { } impl ContextMenuItem { - pub fn item(label: String, action: impl 'static + Action) -> Self { + pub fn item(label: impl ToString, action: impl 'static + Action) -> Self { Self::Item { - label, + label: label.to_string(), action: Box::new(action), } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7d49a2d07dfc439f2407753a98f567e3bca15c45..d4c3410b1b44f705bbd5a8756f817ef24cf65d30 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -91,6 +91,10 @@ actions!( CollapseSelectedEntry, AddDirectory, AddFile, + Copy, + CopyPath, + Cut, + Paste, Delete, Rename ] @@ -207,27 +211,25 @@ impl ProjectPanel { } fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { + let mut menu_entries = Vec::new(); + menu_entries.push(ContextMenuItem::item("New File", AddFile)); + menu_entries.push(ContextMenuItem::item("New Directory", AddDirectory)); if let Some(entry_id) = action.entry_id { if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { self.selection = Some(Selection { worktree_id, entry_id, }); + menu_entries.push(ContextMenuItem::Separator); + menu_entries.push(ContextMenuItem::item("Copy", Copy)); + menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath)); + menu_entries.push(ContextMenuItem::item("Cut", Cut)); + menu_entries.push(ContextMenuItem::item("Rename", Rename)); + menu_entries.push(ContextMenuItem::item("Delete", Delete)); } } - self.context_menu.update(cx, |menu, cx| { - menu.show( - action.position, - [ - ContextMenuItem::item("New File".to_string(), AddFile), - ContextMenuItem::item("New Directory".to_string(), AddDirectory), - ContextMenuItem::Separator, - ContextMenuItem::item("Rename".to_string(), Rename), - ContextMenuItem::item("Delete".to_string(), Delete), - ], - cx, - ); + menu.show(action.position, menu_entries, cx); }); cx.notify(); @@ -906,38 +908,53 @@ impl View for ProjectPanel { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + enum Tag {} let theme = &cx.global::().theme.project_panel; let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); let handle = self.handle.clone(); Stack::new() .with_child( - UniformList::new( - self.list.clone(), - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - move |range, items, cx| { - let theme = cx.global::().theme.clone(); - let this = handle.upgrade(cx).unwrap(); - this.update(cx.app, |this, cx| { - this.for_each_visible_entry(range.clone(), cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - &theme.project_panel, + MouseEventHandler::new::(0, cx, |_, _| { + UniformList::new( + self.list.clone(), + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + move |range, items, cx| { + let theme = cx.global::().theme.clone(); + let this = handle.upgrade(cx).unwrap(); + this.update(cx.app, |this, cx| { + this.for_each_visible_entry( + range.clone(), cx, - )); - }); - }) - }, - ) - .with_padding_top(padding.top) - .with_padding_bottom(padding.bottom) - .contained() - .with_style(container_style) + |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + &theme.project_panel, + cx, + )); + }, + ); + }) + }, + ) + .with_padding_top(padding.top) + .with_padding_bottom(padding.bottom) + .contained() + .with_style(container_style) + .expanded() + .boxed() + }) + .on_right_mouse_down(move |position, cx| { + cx.dispatch_action(DeployContextMenu { + entry_id: None, + position, + }) + }) .boxed(), ) .with_child(ChildView::new(&self.context_menu).boxed()) From 82ddac8e7eeb019491a446d4372e185c97ef9338 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 May 2022 15:21:02 +0200 Subject: [PATCH 13/54] Restore focus when closing context menu --- Cargo.lock | 1 + crates/context_menu/src/context_menu.rs | 51 ++++++++++++++++++------- crates/gpui/src/app.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a103488028b87c6118bfc0132687c080a7c2f7cd..b557431d80f54dd206c69e7181b3a9129cdedccb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6088,6 +6088,7 @@ dependencies = [ "collections", "command_palette", "contacts_panel", + "context_menu", "ctor", "diagnostics", "dirs 3.0.1", diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index ff1c3dda7281752d49560251b188c7599c16b4a4..de4f05cade95bb26308d1952eeeab93e88c564dc 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,9 +1,18 @@ use gpui::{ - elements::*, geometry::vector::Vector2F, platform::CursorStyle, Action, Axis, Entity, - RenderContext, SizeConstraint, View, ViewContext, + elements::*, geometry::vector::Vector2F, impl_internal_actions, platform::CursorStyle, Action, + Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, View, ViewContext, }; use settings::Settings; +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContextMenu::dismiss); +} + +#[derive(Clone)] +struct Dismiss; + +impl_internal_actions!(context_menu, [Dismiss]); + pub enum ContextMenuItem { Item { label: String, @@ -25,11 +34,13 @@ impl ContextMenuItem { } } +#[derive(Default)] pub struct ContextMenu { position: Vector2F, items: Vec, selected_index: Option, visible: bool, + previously_focused_view_id: Option, } impl Entity for ContextMenu { @@ -72,11 +83,13 @@ impl View for ContextMenu { impl ContextMenu { pub fn new() -> Self { - Self { - position: Default::default(), - items: Default::default(), - selected_index: Default::default(), - visible: false, + Default::default() + } + + fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + if cx.handle().is_focused(cx) { + let window_id = cx.window_id(); + (**cx).focus(window_id, self.previously_focused_view_id.take()); } } @@ -87,11 +100,15 @@ impl ContextMenu { cx: &mut ViewContext, ) { let mut items = items.into_iter().peekable(); - assert!(items.peek().is_some(), "must have at least one item"); - self.items = items.collect(); - self.position = position; - self.visible = true; - cx.focus_self(); + if items.peek().is_some() { + self.items = items.collect(); + self.position = position; + self.visible = true; + self.previously_focused_view_id = cx.focused_view_id(cx.window_id()); + cx.focus_self(); + } else { + self.visible = false; + } cx.notify(); } @@ -107,7 +124,10 @@ impl ContextMenu { &Default::default(), Some(ix) == self.selected_index, ); - Label::new(label.to_string(), style.label.clone()).boxed() + Label::new(label.to_string(), style.label.clone()) + .contained() + .with_style(style.container) + .boxed() } ContextMenuItem::Separator => Empty::new() .collapsed() @@ -180,7 +200,10 @@ impl ContextMenu { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) + .on_click(move |_, _, cx| { + cx.dispatch_any_action(action.boxed_clone()); + cx.dispatch_action(Dismiss); + }) .boxed() } ContextMenuItem::Separator => Empty::new() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d11940b2c6d13776c424d94339b96f9459d66208..6c667710828dc8cc40d74a38188a4259c9348857 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2407,7 +2407,7 @@ impl MutableAppContext { }) } - fn focus(&mut self, window_id: usize, view_id: Option) { + pub fn focus(&mut self, window_id: usize, view_id: Option) { if let Some(pending_focus_index) = self.pending_focus_index { self.pending_effects.remove(pending_focus_index); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 525569b86914716cb0467bbcdee08f2adc0c7c53..97a50e78d2897b636b751d079917473e438367be 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -22,6 +22,7 @@ chat_panel = { path = "../chat_panel" } cli = { path = "../cli" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } +context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } contacts_panel = { path = "../contacts_panel" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3e21e454f2f9f08f93fec6886938e98eb03d8647..04a4f9aada48921935339bc744ee6b52c3090be1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -134,6 +134,7 @@ fn main() { let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); + context_menu::init(cx); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); project::Project::init(&client); client::Channel::init(&client); From 991eb742b0d653f54e74be40bc33a6ccf27fe66b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 May 2022 15:23:40 +0200 Subject: [PATCH 14/54] Start adding project panel context menu actions --- crates/project_panel/src/project_panel.rs | 61 ++++++++++++++++++++--- crates/workspace/src/workspace.rs | 25 +++++++++- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d4c3410b1b44f705bbd5a8756f817ef24cf65d30..13b27d62dd9e3cfe5787150ce47553a3aeead579 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -115,6 +115,10 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(ProjectPanel::delete); cx.add_async_action(ProjectPanel::confirm); cx.add_action(ProjectPanel::cancel); + cx.add_action(ProjectPanel::copy); + cx.add_action(ProjectPanel::copy_path); + cx.add_action(ProjectPanel::cut); + cx.add_action(ProjectPanel::paste); } pub enum Event { @@ -212,22 +216,47 @@ impl ProjectPanel { fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext) { let mut menu_entries = Vec::new(); - menu_entries.push(ContextMenuItem::item("New File", AddFile)); - menu_entries.push(ContextMenuItem::item("New Directory", AddDirectory)); + if let Some(entry_id) = action.entry_id { if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { self.selection = Some(Selection { worktree_id, entry_id, }); - menu_entries.push(ContextMenuItem::Separator); - menu_entries.push(ContextMenuItem::item("Copy", Copy)); - menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath)); - menu_entries.push(ContextMenuItem::item("Cut", Cut)); - menu_entries.push(ContextMenuItem::item("Rename", Rename)); - menu_entries.push(ContextMenuItem::item("Delete", Delete)); + + if let Some((worktree, entry)) = self.selected_entry(cx) { + let is_root = Some(entry) == worktree.root_entry(); + menu_entries.push(ContextMenuItem::item( + "Add Folder to Project", + workspace::AddFolderToProject, + )); + if is_root { + menu_entries.push(ContextMenuItem::item( + "Remove Folder from Project", + workspace::RemoveFolderFromProject(worktree_id), + )); + } + menu_entries.push(ContextMenuItem::item("New File", AddFile)); + menu_entries.push(ContextMenuItem::item("New Folder", AddDirectory)); + menu_entries.push(ContextMenuItem::Separator); + menu_entries.push(ContextMenuItem::item("Copy", Copy)); + menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath)); + menu_entries.push(ContextMenuItem::item("Cut", Cut)); + menu_entries.push(ContextMenuItem::Separator); + menu_entries.push(ContextMenuItem::item("Rename", Rename)); + if !is_root { + menu_entries.push(ContextMenuItem::item("Delete", Delete)); + } + } } + } else { + self.selection.take(); + menu_entries.push(ContextMenuItem::item( + "Add Folder to Project", + workspace::AddFolderToProject, + )); } + self.context_menu.update(cx, |menu, cx| { menu.show(action.position, menu_entries, cx); }); @@ -581,6 +610,22 @@ impl ProjectPanel { } } + fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + todo!() + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + todo!() + } + + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + todo!() + } + + fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + todo!() + } + fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { let mut worktree_index = 0; let mut entry_index = 0; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f4197e72962e6536afbefaf48d08be23f3338e8d..6fa4c9a6b20974acaa976abdce5ed66e227c8243 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -30,7 +30,7 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; -use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree}; +use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus}; use smallvec::SmallVec; @@ -72,6 +72,9 @@ type FollowableItemBuilders = HashMap< ), >; +#[derive(Clone)] +pub struct RemoveFolderFromProject(pub WorktreeId); + actions!( workspace, [ @@ -104,7 +107,15 @@ pub struct JoinProject { pub project_index: usize, } -impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]); +impl_internal_actions!( + workspace, + [ + OpenPaths, + ToggleFollow, + JoinProject, + RemoveFolderFromProject + ] +); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -148,6 +159,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_async_action(Workspace::close); cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::add_folder_to_project); + cx.add_action(Workspace::remove_folder_from_project); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -1028,6 +1040,15 @@ impl Workspace { .detach(); } + fn remove_folder_from_project( + &mut self, + RemoveFolderFromProject(worktree_id): &RemoveFolderFromProject, + cx: &mut ViewContext, + ) { + self.project + .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx)); + } + fn project_path_for_path( &self, abs_path: &Path, From 5b2d6e41f3f9c62c1c72d936546f3092c5c9c952 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 May 2022 16:36:30 +0200 Subject: [PATCH 15/54] Introduce keyboard navigation in context menus --- Cargo.lock | 15 ++++ crates/chat_panel/Cargo.toml | 1 + crates/chat_panel/src/chat_panel.rs | 2 +- crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contacts_panel.rs | 7 +- crates/context_menu/Cargo.toml | 1 + crates/context_menu/src/context_menu.rs | 83 ++++++++++++++++++--- crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 6 +- crates/go_to_line/Cargo.toml | 3 +- crates/go_to_line/src/go_to_line.rs | 6 +- crates/menu/Cargo.toml | 11 +++ crates/{workspace => menu}/src/menu.rs | 0 crates/picker/Cargo.toml | 1 + crates/picker/src/picker.rs | 4 +- crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 6 +- crates/search/Cargo.toml | 1 + crates/search/src/project_search.rs | 4 +- crates/workspace/src/workspace.rs | 1 - 20 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 crates/menu/Cargo.toml rename crates/{workspace => menu}/src/menu.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index b557431d80f54dd206c69e7181b3a9129cdedccb..f39f473cccbe339ac794fc8478789798c8473283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,6 +665,7 @@ dependencies = [ "client", "editor", "gpui", + "menu", "postage", "settings", "theme", @@ -964,6 +965,7 @@ dependencies = [ "gpui", "language", "log", + "menu", "picker", "postage", "project", @@ -979,6 +981,7 @@ name = "context_menu" version = "0.1.0" dependencies = [ "gpui", + "menu", "settings", "smallvec", "theme", @@ -1526,6 +1529,7 @@ dependencies = [ "env_logger", "fuzzy", "gpui", + "menu", "picker", "postage", "project", @@ -1904,6 +1908,7 @@ version = "0.1.0" dependencies = [ "editor", "gpui", + "menu", "postage", "settings", "text", @@ -2698,6 +2703,13 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "menu" +version = "0.1.0" +dependencies = [ + "gpui", +] + [[package]] name = "metal" version = "0.21.0" @@ -3252,6 +3264,7 @@ dependencies = [ "editor", "env_logger", "gpui", + "menu", "serde_json", "settings", "theme", @@ -3463,6 +3476,7 @@ dependencies = [ "editor", "futures", "gpui", + "menu", "postage", "project", "serde_json", @@ -4176,6 +4190,7 @@ dependencies = [ "gpui", "language", "log", + "menu", "postage", "project", "serde", diff --git a/crates/chat_panel/Cargo.toml b/crates/chat_panel/Cargo.toml index 95426517d7f7cd4f216ef806aeab151dba2bf68d..e54245502ffa91843b4b7510066c41275cee096b 100644 --- a/crates/chat_panel/Cargo.toml +++ b/crates/chat_panel/Cargo.toml @@ -11,6 +11,7 @@ doctest = false client = { path = "../client" } editor = { path = "../editor" } gpui = { path = "../gpui" } +menu = { path = "../menu" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 29c64128d1ffb5fde745666aa2acab1ee1a8e6f2..8bce551a8c3030f922fe0544653895a4a43b10ad 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -11,12 +11,12 @@ use gpui::{ AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; +use menu::Confirm; use postage::prelude::Stream; use settings::{Settings, SoftWrap}; use std::sync::Arc; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; -use workspace::menu::Confirm; const MESSAGE_LOADING_THRESHOLD: usize = 50; diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 800bad497d67a1c40d111a7d33778211f96a55ff..ab05a56ce773c3aec06eff673eaf810cf09de714 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -12,6 +12,7 @@ client = { path = "../client" } editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 763772b89e60591d4894eef3258727ee92e34359..4b965d3c1d7dc53ae612de7c51e1aa553aa49b4d 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -16,15 +16,12 @@ use gpui::{ MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use join_project_notification::JoinProjectNotification; +use menu::{Confirm, SelectNext, SelectPrev}; use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::IconButton; -use workspace::{ - menu::{Confirm, SelectNext, SelectPrev}, - sidebar::SidebarItem, - JoinProject, Workspace, -}; +use workspace::{sidebar::SidebarItem, JoinProject, Workspace}; impl_actions!( contacts_panel, diff --git a/crates/context_menu/Cargo.toml b/crates/context_menu/Cargo.toml index 65f7f59a14c590fada55a8a29841a206fe3844d1..817893f43eba786bdb429e4e5ccdae66c54c472f 100644 --- a/crates/context_menu/Cargo.toml +++ b/crates/context_menu/Cargo.toml @@ -9,6 +9,7 @@ doctest = false [dependencies] gpui = { path = "../gpui" } +menu = { path = "../menu" } settings = { path = "../settings" } theme = { path = "../theme" } smallvec = "1.6" diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index de4f05cade95bb26308d1952eeeab93e88c564dc..7e42c2596e951a1c81546f6367b7dd635c6ab7e9 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,18 +1,19 @@ use gpui::{ - elements::*, geometry::vector::Vector2F, impl_internal_actions, platform::CursorStyle, Action, + elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext, Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, View, ViewContext, }; +use menu::*; use settings::Settings; pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ContextMenu::dismiss); + cx.add_action(ContextMenu::select_first); + cx.add_action(ContextMenu::select_last); + cx.add_action(ContextMenu::select_next); + cx.add_action(ContextMenu::select_prev); + cx.add_action(ContextMenu::confirm); + cx.add_action(ContextMenu::cancel); } -#[derive(Clone)] -struct Dismiss; - -impl_internal_actions!(context_menu, [Dismiss]); - pub enum ContextMenuItem { Item { label: String, @@ -32,6 +33,10 @@ impl ContextMenuItem { pub fn separator() -> Self { Self::Separator } + + fn is_separator(&self) -> bool { + matches!(self, Self::Separator) + } } #[derive(Default)] @@ -52,6 +57,12 @@ impl View for ContextMenu { "ContextMenu" } + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if !self.visible { return Empty::new().boxed(); @@ -77,6 +88,7 @@ impl View for ContextMenu { fn on_blur(&mut self, cx: &mut ViewContext) { self.visible = false; + self.selected_index.take(); cx.notify(); } } @@ -86,13 +98,66 @@ impl ContextMenu { Default::default() } - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(ix) = self.selected_index { + if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { + let window_id = cx.window_id(); + let view_id = cx.view_id(); + cx.dispatch_action_at(window_id, view_id, action.as_ref()); + } + } + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { if cx.handle().is_focused(cx) { let window_id = cx.window_id(); (**cx).focus(window_id, self.previously_focused_view_id.take()); } } + fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + self.selected_index = self.items.iter().position(|item| !item.is_separator()); + cx.notify(); + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + for (ix, item) in self.items.iter().enumerate().rev() { + if !item.is_separator() { + self.selected_index = Some(ix); + cx.notify(); + break; + } + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(ix) = self.selected_index { + for (ix, item) in self.items.iter().enumerate().skip(ix + 1) { + if !item.is_separator() { + self.selected_index = Some(ix); + cx.notify(); + break; + } + } + } else { + self.select_first(&Default::default(), cx); + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(ix) = self.selected_index { + for (ix, item) in self.items.iter().enumerate().take(ix).rev() { + if !item.is_separator() { + self.selected_index = Some(ix); + cx.notify(); + break; + } + } + } else { + self.select_last(&Default::default(), cx); + } + } + pub fn show( &mut self, position: Vector2F, @@ -202,7 +267,7 @@ impl ContextMenu { .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| { cx.dispatch_any_action(action.boxed_clone()); - cx.dispatch_action(Dismiss); + cx.dispatch_action(Cancel); }) .boxed() } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index ca3eb6b429140e5ccf555f6c08ef2ee2625eeee9..554cf433a2d7f0fe38077d44cc7834fd22605987 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -11,6 +11,7 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f58c733cc7aa8bd44e3643ddef4a78ee9861f2c4..84b69735336b97511d7c295e063d3f63e470be75 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -257,11 +257,9 @@ impl PickerDelegate for FileFinder { mod tests { use super::*; use editor::{Editor, Input}; + use menu::{Confirm, SelectNext}; use serde_json::json; - use workspace::{ - menu::{Confirm, SelectNext}, - AppState, Workspace, - }; + use workspace::{AppState, Workspace}; #[ctor::ctor] fn init_logger() { diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 76744274c7ed704d3af71498fe1aa1869ab8823e..93ae96f93e261813e21c8bf1065145b15e9f262c 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -8,9 +8,10 @@ path = "src/go_to_line.rs" doctest = false [dependencies] -text = { path = "../text" } editor = { path = "../editor" } gpui = { path = "../gpui" } +menu = { path = "../menu" } settings = { path = "../settings" } +text = { path = "../text" } workspace = { path = "../workspace" } postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index bae5ffc46c35a2b47aff376d3f2f54780e8f399c..f2df235a7bd0b2ded5e6ad928c9f910ae582223d 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -3,12 +3,10 @@ use gpui::{ actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; +use menu::{Cancel, Confirm}; use settings::Settings; use text::{Bias, Point}; -use workspace::{ - menu::{Cancel, Confirm}, - Workspace, -}; +use workspace::Workspace; actions!(go_to_line, [Toggle]); diff --git a/crates/menu/Cargo.toml b/crates/menu/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..cdcacd44163661557dc6401f4aa614caf5a01d74 --- /dev/null +++ b/crates/menu/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "menu" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/menu.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } diff --git a/crates/workspace/src/menu.rs b/crates/menu/src/menu.rs similarity index 100% rename from crates/workspace/src/menu.rs rename to crates/menu/src/menu.rs diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index 4528f006877f8ff3a8581b8c123e693f743a5484..c74b6927ae5104784235c5de22c2ebdd3f750e6a 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] editor = { path = "../editor" } gpui = { path = "../gpui" } +menu = { path = "../menu" } settings = { path = "../settings" } util = { path = "../util" } theme = { path = "../theme" } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 8fd662978b4eb3b0c334aa04a9a40ffc76b1dc21..19dc3054b7c4fbe0e97ebc6616d89560a10f95dc 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -10,11 +10,9 @@ use gpui::{ AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev}; use settings::Settings; use std::cmp; -use workspace::menu::{ - Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev, -}; pub struct Picker { delegate: WeakViewHandle, diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 7eb0282660883bdcf58aeea38f1350ed66f7ff19..6d566699fa0ee77946e84a3cd9fb4718510dcc89 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -11,6 +11,7 @@ doctest = false context_menu = { path = "../context_menu" } editor = { path = "../editor" } gpui = { path = "../gpui" } +menu = { path = "../menu" } project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 13b27d62dd9e3cfe5787150ce47553a3aeead579..5fda6f04fae0f679fd10f3dd249365ea21ba2ce3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -14,6 +14,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use menu::{Confirm, SelectNext, SelectPrev}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; use std::{ @@ -23,10 +24,7 @@ use std::{ ops::Range, }; use unicase::UniCase; -use workspace::{ - menu::{Confirm, SelectNext, SelectPrev}, - Workspace, -}; +use workspace::Workspace; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 56c4fff6519405ecb0dcadeed8177c79213f9058..3e80b5979e4bcf8f0d2d15767366e4fda7e3dc2f 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -12,6 +12,7 @@ collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +menu = { path = "../menu" } project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e3834f6f45d8e8caaee731445074875753d09a0d..9943ce5dedbc073776dfe96b21cfa8821050e17a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -9,6 +9,7 @@ use gpui::{ ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; +use menu::Confirm; use project::{search::SearchQuery, Project}; use settings::Settings; use smallvec::SmallVec; @@ -19,8 +20,7 @@ use std::{ }; use util::ResultExt as _; use workspace::{ - menu::Confirm, Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, - Workspace, + Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(project_search, [Deploy, SearchInNew, ToggleFocus]); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6fa4c9a6b20974acaa976abdce5ed66e227c8243..21361b80816dbd0b809bedae2e13d0a6d98b43b7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,5 +1,4 @@ pub mod lsp_status; -pub mod menu; pub mod pane; pub mod pane_group; pub mod sidebar; From eedb29963c6b887c78438c84254ceea9e32891d8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 May 2022 16:45:41 +0200 Subject: [PATCH 16/54] Implement `CopyPath` --- assets/keymaps/default.json | 1 + crates/project_panel/src/project_panel.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1049e216f3d6452641da021f23e6b7efc9084e43..f21138e541cf879511017ab5b1caa3217843d550 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -352,6 +352,7 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", + "cmd-alt-c": "project_panel::CopyPath", "f2": "project_panel::Rename", "backspace": "project_panel::Delete" } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5fda6f04fae0f679fd10f3dd249365ea21ba2ce3..f1dac49f814d362083ffbb92deeb7da2759cd478 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -11,8 +11,8 @@ use gpui::{ geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext, + PromptLevel, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; @@ -22,6 +22,7 @@ use std::{ collections::{hash_map, HashMap}, ffi::OsStr, ops::Range, + path::PathBuf, }; use unicase::UniCase; use workspace::Workspace; @@ -621,7 +622,12 @@ impl ProjectPanel { } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { - todo!() + if let Some((worktree, entry)) = self.selected_entry(cx) { + let mut path = PathBuf::new(); + path.push(worktree.root_name()); + path.push(&entry.path); + cx.write_to_clipboard(ClipboardItem::new(path.to_string_lossy().to_string())); + } } fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { From 0866f0ed55f5189005decf8ef9b4248dca7caa14 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 11:00:10 -0600 Subject: [PATCH 17/54] Introduce CursorRegion struct This will blend in with an upcoming MouseRegion struct that sits next to it in the scene. Co-Authored-By: Antonio Scandurra --- crates/editor/src/element.rs | 10 ++++--- crates/gpui/src/elements/container.rs | 7 +++-- .../gpui/src/elements/mouse_event_handler.rs | 9 ++++--- crates/gpui/src/gpui.rs | 2 +- crates/gpui/src/presenter.rs | 13 ++++----- crates/gpui/src/scene.rs | 27 ++++++++++++------- 6 files changed, 44 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 355d1f44337c9c855625ecc0255e51b57ee5db4b..319ed212528d7d9a73ada985702441ad093f54c5 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -18,8 +18,9 @@ use gpui::{ json::{self, ToJson}, platform::CursorStyle, text_layout::{self, Line, RunStyle, TextLayoutCache}, - AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext, - MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, + AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, + LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, + WeakViewHandle, }; use json::json; use language::{Bias, DiagnosticSeverity}; @@ -330,7 +331,10 @@ impl EditorElement { let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); cx.scene.push_layer(Some(bounds)); - cx.scene.push_cursor_style(bounds, CursorStyle::IBeam); + cx.scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::IBeam, + }); for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 62f19636b7c5dd95a02d7a6f4d200b4343ddec9c..004052b9baec4e310dcfd879f6950d264a2749e2 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -7,7 +7,7 @@ use crate::{ }, json::ToJson, platform::CursorStyle, - scene::{self, Border, Quad}, + scene::{self, Border, CursorRegion, Quad}, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde::Deserialize; @@ -213,7 +213,10 @@ impl Element for Container { } if let Some(style) = self.style.cursor { - cx.scene.push_cursor_style(quad_bounds, style); + cx.scene.push_cursor_region(CursorRegion { + bounds: quad_bounds, + style, + }); } let child_origin = diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 1ee7c6cbb5e57bf2108d36f48424fd3c9db7e058..975a47a1efe44708da7d5a562ac77c4e9d6cc697 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -5,6 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, platform::CursorStyle, + scene::CursorRegion, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -100,9 +101,11 @@ impl Element for MouseEventHandler { _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - if let Some(cursor_style) = self.cursor_style { - cx.scene - .push_cursor_style(self.hit_bounds(bounds), cursor_style); + if let Some(style) = self.cursor_style { + cx.scene.push_cursor_region(CursorRegion { + bounds: self.hit_bounds(bounds), + style, + }); } self.child.paint(bounds.origin(), visible_bounds, cx); } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index e58bbec1c60f485ae5a24986de99841f00d2a705..085ec867918692dea99c65cc4db1c1936a81d1fe 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -16,7 +16,7 @@ pub mod fonts; pub mod geometry; mod presenter; mod scene; -pub use scene::{Border, Quad, Scene}; +pub use scene::{Border, CursorRegion, Quad, Scene}; pub mod text_layout; pub use text_layout::TextLayoutCache; mod util; diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index fbdd6963e372f711d193c9bde16b34156d6118d5..2bc241daee2dae65b862d594bd5899487886e1f9 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -5,6 +5,7 @@ use crate::{ geometry::rect::RectF, json::{self, ToJson}, platform::{CursorStyle, Event}, + scene::CursorRegion, text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene, @@ -22,7 +23,7 @@ pub struct Presenter { window_id: usize, pub(crate) rendered_views: HashMap, parents: HashMap, - cursor_styles: Vec<(RectF, CursorStyle)>, + cursor_regions: Vec, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, @@ -43,7 +44,7 @@ impl Presenter { window_id, rendered_views: cx.render_views(window_id, titlebar_height), parents: HashMap::new(), - cursor_styles: Default::default(), + cursor_regions: Default::default(), font_cache, text_layout_cache, asset_cache, @@ -120,7 +121,7 @@ impl Presenter { RectF::new(Vector2F::zero(), window_size), ); self.text_layout_cache.finish_frame(); - self.cursor_styles = scene.cursor_styles(); + self.cursor_regions = scene.cursor_regions(); if cx.window_is_active(self.window_id) { if let Some(event) = self.last_mouse_moved_event.clone() { @@ -184,9 +185,9 @@ impl Presenter { if !left_mouse_down { let mut style_to_assign = CursorStyle::Arrow; - for (bounds, style) in self.cursor_styles.iter().rev() { - if bounds.contains_point(position) { - style_to_assign = *style; + for region in self.cursor_regions.iter().rev() { + if region.bounds.contains_point(position) { + style_to_assign = region.style; break; } } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 7c358b85a04991272b05a4fb4ececeec07b51ef0..0f10256cb0dfcf3d14ee8aa5e6c2174551a24b76 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -33,7 +33,13 @@ pub struct Layer { image_glyphs: Vec, icons: Vec, paths: Vec, - cursor_styles: Vec<(RectF, CursorStyle)>, + cursor_regions: Vec, +} + +#[derive(Copy, Clone)] +pub struct CursorRegion { + pub bounds: RectF, + pub style: CursorStyle, } #[derive(Default, Debug)] @@ -175,9 +181,9 @@ impl Scene { self.stacking_contexts.iter().flat_map(|s| &s.layers) } - pub fn cursor_styles(&self) -> Vec<(RectF, CursorStyle)> { + pub fn cursor_regions(&self) -> Vec { self.layers() - .flat_map(|layer| &layer.cursor_styles) + .flat_map(|layer| &layer.cursor_regions) .copied() .collect() } @@ -206,8 +212,8 @@ impl Scene { self.active_layer().push_quad(quad) } - pub fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) { - self.active_layer().push_cursor_style(bounds, style); + pub fn push_cursor_region(&mut self, region: CursorRegion) { + self.active_layer().push_cursor_region(region); } pub fn push_image(&mut self, image: Image) { @@ -298,7 +304,7 @@ impl Layer { glyphs: Default::default(), icons: Default::default(), paths: Default::default(), - cursor_styles: Default::default(), + cursor_regions: Default::default(), } } @@ -316,10 +322,13 @@ impl Layer { self.quads.as_slice() } - fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) { - if let Some(bounds) = bounds.intersection(self.clip_bounds.unwrap_or(bounds)) { + fn push_cursor_region(&mut self, region: CursorRegion) { + if let Some(bounds) = region + .bounds + .intersection(self.clip_bounds.unwrap_or(region.bounds)) + { if can_draw(bounds) { - self.cursor_styles.push((bounds, style)); + self.cursor_regions.push(region); } } } From 3a59d2a3314382c80835ccd38ec4a7f1a76b0c9c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 12:44:52 -0600 Subject: [PATCH 18/54] Allow hovered and clicked mouse regions to be tracked in the presenter --- crates/gpui/src/gpui.rs | 2 +- crates/gpui/src/presenter.rs | 88 ++++++++++++++++++++++++++++++++++-- crates/gpui/src/scene.rs | 51 ++++++++++++++++++++- 3 files changed, 133 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 085ec867918692dea99c65cc4db1c1936a81d1fe..c7d619fd7d37c1122b1439f57737a2bc54994138 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -16,7 +16,7 @@ pub mod fonts; pub mod geometry; mod presenter; mod scene; -pub use scene::{Border, CursorRegion, Quad, Scene}; +pub use scene::{Border, CursorRegion, MouseRegion, MouseRegionId, Quad, Scene}; pub mod text_layout; pub use text_layout::TextLayoutCache; mod util; diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 2bc241daee2dae65b862d594bd5899487886e1f9..cbd7b2cb82f54b77ade8b87f4aa03b905c93e9b3 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -8,8 +8,9 @@ use crate::{ scene::CursorRegion, text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, - ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene, - UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle, + ElementStateContext, Entity, FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, + ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, + WeakViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -24,10 +25,13 @@ pub struct Presenter { pub(crate) rendered_views: HashMap, parents: HashMap, cursor_regions: Vec, + mouse_regions: Vec, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, last_mouse_moved_event: Option, + hovered_region_id: Option, + clicked_region: Option, titlebar_height: f32, } @@ -45,10 +49,13 @@ impl Presenter { rendered_views: cx.render_views(window_id, titlebar_height), parents: HashMap::new(), cursor_regions: Default::default(), + mouse_regions: Default::default(), font_cache, text_layout_cache, asset_cache, last_mouse_moved_event: None, + hovered_region_id: None, + clicked_region: None, titlebar_height, } } @@ -122,6 +129,7 @@ impl Presenter { ); self.text_layout_cache.finish_frame(); self.cursor_regions = scene.cursor_regions(); + self.mouse_regions = scene.mouse_regions(); if cx.window_is_active(self.window_id) { if let Some(event) = self.last_mouse_moved_event.clone() { @@ -176,7 +184,30 @@ impl Presenter { pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { + let mut unhovered_region = None; + let mut hovered_region = None; + let mut clicked_region = None; + match event { + Event::LeftMouseDown { position, .. } => { + for region in self.mouse_regions.iter().rev() { + if region.bounds.contains_point(position) { + self.clicked_region = Some(region.clone()); + break; + } + } + } + Event::LeftMouseUp { + position, + click_count, + .. + } => { + if let Some(region) = self.clicked_region.take() { + if region.bounds.contains_point(position) { + clicked_region = Some((region, position, click_count)); + } + } + } Event::MouseMoved { position, left_mouse_down, @@ -192,6 +223,18 @@ impl Presenter { } } cx.platform().set_cursor_style(style_to_assign); + + for region in self.mouse_regions.iter().rev() { + if region.bounds.contains_point(position) { + if hovered_region.is_none() { + hovered_region = Some(region.clone()); + } + } else { + if self.hovered_region_id == Some(region.id()) { + unhovered_region = Some(region.clone()) + } + } + } } } Event::LeftMouseDragged { position } => { @@ -203,7 +246,33 @@ impl Presenter { _ => {} } + self.hovered_region_id = hovered_region.as_ref().map(MouseRegion::id); + let mut event_cx = self.build_event_context(cx); + if let Some(unhovered_region) = unhovered_region { + if let Some(hover_callback) = unhovered_region.hover { + event_cx.with_current_view(unhovered_region.view_id, |event_cx| { + hover_callback(false, event_cx) + }) + } + } + + if let Some(hovered_region) = hovered_region { + if let Some(hover_callback) = hovered_region.hover { + event_cx.with_current_view(hovered_region.view_id, |event_cx| { + hover_callback(true, event_cx) + }) + } + } + + if let Some((clicked_region, position, click_count)) = clicked_region { + if let Some(click_callback) = clicked_region.click { + event_cx.with_current_view(clicked_region.view_id, |event_cx| { + click_callback(position, click_count, event_cx) + }) + } + } + event_cx.dispatch_event(root_view_id, &event); let invalidated_views = event_cx.invalidated_views; @@ -379,9 +448,8 @@ pub struct EventContext<'a> { impl<'a> EventContext<'a> { fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool { if let Some(mut element) = self.rendered_views.remove(&view_id) { - self.view_stack.push(view_id); - let result = element.dispatch_event(event, self); - self.view_stack.pop(); + let result = + self.with_current_view(view_id, |this| element.dispatch_event(event, this)); self.rendered_views.insert(view_id, element); result } else { @@ -389,6 +457,16 @@ impl<'a> EventContext<'a> { } } + fn with_current_view(&mut self, view_id: usize, f: F) -> T + where + F: FnOnce(&mut Self) -> T, + { + self.view_stack.push(view_id); + let result = f(self); + self.view_stack.pop(); + result + } + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 0f10256cb0dfcf3d14ee8aa5e6c2174551a24b76..003d9b066b17dc7a7542ab7f08b2c3717c7b9144 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1,6 +1,6 @@ use serde::Deserialize; use serde_json::json; -use std::{borrow::Cow, sync::Arc}; +use std::{any::TypeId, borrow::Cow, rc::Rc, sync::Arc}; use crate::{ color::Color, @@ -8,7 +8,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, platform::CursorStyle, - ImageData, + EventContext, ImageData, }; pub struct Scene { @@ -34,6 +34,7 @@ pub struct Layer { icons: Vec, paths: Vec, cursor_regions: Vec, + mouse_regions: Vec, } #[derive(Copy, Clone)] @@ -42,6 +43,23 @@ pub struct CursorRegion { pub style: CursorStyle, } +#[derive(Clone)] +pub struct MouseRegion { + pub view_id: usize, + pub tag: TypeId, + pub region_id: usize, + pub bounds: RectF, + pub hover: Option>, + pub click: Option>, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct MouseRegionId { + pub view_id: usize, + pub tag: TypeId, + pub region_id: usize, +} + #[derive(Default, Debug)] pub struct Quad { pub bounds: RectF, @@ -188,6 +206,13 @@ impl Scene { .collect() } + pub fn mouse_regions(&self) -> Vec { + self.layers() + .flat_map(|layer| &layer.mouse_regions) + .cloned() + .collect() + } + pub fn push_stacking_context(&mut self, clip_bounds: Option) { self.active_stacking_context_stack .push(self.stacking_contexts.len()); @@ -305,6 +330,7 @@ impl Layer { icons: Default::default(), paths: Default::default(), cursor_regions: Default::default(), + mouse_regions: Default::default(), } } @@ -333,6 +359,17 @@ impl Layer { } } + fn push_mouse_region(&mut self, region: MouseRegion) { + if let Some(bounds) = region + .bounds + .intersection(self.clip_bounds.unwrap_or(region.bounds)) + { + if can_draw(bounds) { + self.mouse_regions.push(region); + } + } + } + fn push_underline(&mut self, underline: Underline) { if underline.width > 0. { self.underlines.push(underline); @@ -493,6 +530,16 @@ impl ToJson for Border { } } +impl MouseRegion { + pub fn id(&self) -> MouseRegionId { + MouseRegionId { + view_id: self.view_id, + tag: self.tag, + region_id: self.region_id, + } + } +} + fn can_draw(bounds: RectF) -> bool { let size = bounds.size(); size.x() > 0. && size.y() > 0. From d69776585d2b43306b909eb7aecff103d35bde26 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 13:22:23 -0600 Subject: [PATCH 19/54] Add mouse_state method to RenderContext We can use this to determine if a region is hovered or clicked. --- crates/gpui/src/app.rs | 112 +++++++++++++++++++++-------------- crates/gpui/src/presenter.rs | 24 ++++++-- 2 files changed, 88 insertions(+), 48 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index eb4b9650a67dbc0568f754abb72322df659cc06b..ab4de50564f0a1313eda7a131856566231f1485c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -7,7 +7,8 @@ use crate::{ platform::{self, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, - AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions, + TextLayoutCache, }; pub use action::*; use anyhow::{anyhow, Context, Result}; @@ -1040,19 +1041,15 @@ impl MutableAppContext { .map_or(false, |window| window.is_active) } - pub fn render_view( - &mut self, - window_id: usize, - view_id: usize, - titlebar_height: f32, - refreshing: bool, - ) -> Result { + pub fn render_view(&mut self, params: RenderParams) -> Result { + let window_id = params.window_id; + let view_id = params.view_id; let mut view = self .cx .views - .remove(&(window_id, view_id)) + .remove(&(params.window_id, params.view_id)) .ok_or(anyhow!("view not found"))?; - let element = view.render(window_id, view_id, titlebar_height, refreshing, self); + let element = view.render(params, self); self.cx.views.insert((window_id, view_id), view); Ok(element) } @@ -1079,8 +1076,15 @@ impl MutableAppContext { .map(|view_id| { ( view_id, - self.render_view(window_id, view_id, titlebar_height, false) - .unwrap(), + self.render_view(RenderParams { + window_id, + view_id, + titlebar_height, + hovered_region_id: None, + clicked_region_id: None, + refreshing: false, + }) + .unwrap(), ) }) .collect() @@ -1757,15 +1761,19 @@ impl MutableAppContext { window_id: usize, view_id: usize, titlebar_height: f32, + hovered_region_id: Option, + clicked_region_id: Option, refreshing: bool, ) -> RenderContext { RenderContext { app: self, - titlebar_height, - refreshing, window_id, view_id, view_type: PhantomData, + titlebar_height, + hovered_region_id, + clicked_region_id, + refreshing, } } @@ -2894,14 +2902,7 @@ pub trait AnyView { cx: &mut MutableAppContext, ) -> Option>>>; fn ui_name(&self) -> &'static str; - fn render<'a>( - &mut self, - window_id: usize, - view_id: usize, - titlebar_height: f32, - refreshing: bool, - cx: &mut MutableAppContext, - ) -> ElementBox; + fn render<'a>(&mut self, params: RenderParams, cx: &mut MutableAppContext) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; @@ -2935,25 +2936,8 @@ where T::ui_name() } - fn render<'a>( - &mut self, - window_id: usize, - view_id: usize, - titlebar_height: f32, - refreshing: bool, - cx: &mut MutableAppContext, - ) -> ElementBox { - View::render( - self, - &mut RenderContext { - window_id, - view_id, - app: cx, - view_type: PhantomData::, - titlebar_height, - refreshing, - }, - ) + fn render<'a>(&mut self, params: RenderParams, cx: &mut MutableAppContext) -> ElementBox { + View::render(self, &mut RenderContext::new(params, cx)) } fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) { @@ -3435,16 +3419,46 @@ impl<'a, T: View> ViewContext<'a, T> { } } -pub struct RenderContext<'a, T: View> { - pub app: &'a mut MutableAppContext, +pub struct RenderParams { + pub window_id: usize, + pub view_id: usize, pub titlebar_height: f32, + pub hovered_region_id: Option, + pub clicked_region_id: Option, pub refreshing: bool, +} + +pub struct RenderContext<'a, T: View> { + pub app: &'a mut MutableAppContext, window_id: usize, view_id: usize, view_type: PhantomData, + pub titlebar_height: f32, + hovered_region_id: Option, + clicked_region_id: Option, + pub refreshing: bool, +} + +#[derive(Clone, Copy)] +pub struct MouseState { + pub hovered: bool, + pub clicked: bool, } impl<'a, T: View> RenderContext<'a, T> { + fn new(params: RenderParams, app: &'a mut MutableAppContext) -> Self { + Self { + app, + window_id: params.window_id, + view_id: params.view_id, + view_type: PhantomData, + titlebar_height: params.titlebar_height, + hovered_region_id: params.hovered_region_id, + clicked_region_id: params.clicked_region_id, + refreshing: params.refreshing, + } + } + pub fn handle(&self) -> WeakViewHandle { WeakViewHandle::new(self.window_id, self.view_id) } @@ -3452,6 +3466,18 @@ impl<'a, T: View> RenderContext<'a, T> { pub fn view_id(&self) -> usize { self.view_id } + + pub fn mouse_state(&self, region_id: usize) -> MouseState { + let region_id = Some(MouseRegionId { + view_id: self.view_id, + tag: TypeId::of::(), + region_id, + }); + MouseState { + hovered: self.hovered_region_id == region_id, + clicked: self.clicked_region_id == region_id, + } + } } impl AsRef for &AppContext { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index cbd7b2cb82f54b77ade8b87f4aa03b905c93e9b3..899aa59309cb2bd266698f03c6daf4d8b37b53a3 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -9,8 +9,8 @@ use crate::{ text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, ElementStateContext, Entity, FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, - ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, - WeakViewHandle, + ReadView, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, + WeakModelHandle, WeakViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -93,8 +93,15 @@ impl Presenter { for view_id in &invalidation.updated { self.rendered_views.insert( *view_id, - cx.render_view(self.window_id, *view_id, self.titlebar_height, false) - .unwrap(), + cx.render_view(RenderParams { + window_id: self.window_id, + view_id: *view_id, + titlebar_height: self.titlebar_height, + hovered_region_id: self.hovered_region_id, + clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + refreshing: false, + }) + .unwrap(), ); } } @@ -104,7 +111,14 @@ impl Presenter { for (view_id, view) in &mut self.rendered_views { if !invalidation.updated.contains(view_id) { *view = cx - .render_view(self.window_id, *view_id, self.titlebar_height, true) + .render_view(RenderParams { + window_id: self.window_id, + view_id: *view_id, + titlebar_height: self.titlebar_height, + hovered_region_id: self.hovered_region_id, + clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + refreshing: true, + }) .unwrap(); } } From 2ea085b1780125c0cee77de562bc03c43a523ffa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 18:03:34 -0600 Subject: [PATCH 20/54] Pass a RenderContext to UniformList In some cases, we need to render during layout. Previously, we were rendering with a LayoutContext in some cases, but this commit adds the ability to retrieve a render context with a given handle and we use that feature in UniformList. Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 131 ++++++++++++---------- crates/editor/src/element.rs | 6 +- crates/gpui/src/app.rs | 61 +++++----- crates/gpui/src/elements/uniform_list.rs | 42 ++++--- crates/gpui/src/presenter.rs | 35 +++++- crates/gpui/src/views/select.rs | 8 +- crates/picker/src/picker.rs | 8 +- crates/project_panel/src/project_panel.rs | 39 +++---- 8 files changed, 194 insertions(+), 136 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e5a80e44f4014127cb5cb76090bfd04a1092e65b..ae8335f2420f5715c31a2fd9395c73a92d051360 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -592,11 +592,11 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: EditorStyle, - cx: &AppContext, + cx: &mut RenderContext, ) -> (DisplayPoint, ElementBox) { match self { ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), - ContextMenu::CodeActions(menu) => menu.render(cursor_position, style), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } } @@ -633,54 +633,62 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox { + fn render(&self, style: EditorStyle, cx: &mut RenderContext) -> ElementBox { enum CompletionTag {} let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; let container_style = style.autocomplete.container; - UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| { - let start_ix = range.start; - for (ix, mat) in matches[range].iter().enumerate() { - let completion = &completions[mat.candidate_id]; - let item_ix = start_ix + ix; - items.push( - MouseEventHandler::new::( - mat.candidate_id, - cx, - |state, _| { - let item_style = if item_ix == selected_item { - style.autocomplete.selected_item - } else if state.hovered { - style.autocomplete.hovered_item - } else { - style.autocomplete.item - }; - - Text::new(completion.label.text.clone(), style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - style.text.color.into(), - styled_runs_for_code_label(&completion.label, &style.syntax), - &mat.positions, - )) - .contained() - .with_style(item_style) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { - cx.dispatch_action(ConfirmCompletion { - item_ix: Some(item_ix), - }); - }) - .boxed(), - ); - } - }) + UniformList::new( + self.list.clone(), + matches.len(), + cx, + move |_, range, items, cx| { + let start_ix = range.start; + for (ix, mat) in matches[range].iter().enumerate() { + let completion = &completions[mat.candidate_id]; + let item_ix = start_ix + ix; + items.push( + MouseEventHandler::new::( + mat.candidate_id, + cx, + |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + &style.syntax, + ), + &mat.positions, + )) + .contained() + .with_style(item_style) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ConfirmCompletion { + item_ix: Some(item_ix), + }); + }) + .boxed(), + ); + } + }, + ) .with_width_from_item( self.matches .iter() @@ -772,14 +780,18 @@ impl CodeActionsMenu { &self, mut cursor_position: DisplayPoint, style: EditorStyle, + cx: &mut RenderContext, ) -> (DisplayPoint, ElementBox) { enum ActionTag {} let container_style = style.autocomplete.container; let actions = self.actions.clone(); let selected_item = self.selected_item; - let element = - UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| { + let element = UniformList::new( + self.list.clone(), + actions.len(), + cx, + move |_, range, items, cx| { let start_ix = range.start; for (ix, action) in actions[range].iter().enumerate() { let item_ix = start_ix + ix; @@ -808,17 +820,18 @@ impl CodeActionsMenu { .boxed(), ); } - }) - .with_width_from_item( - self.actions - .iter() - .enumerate() - .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) - .map(|(ix, _)| ix), - ) - .contained() - .with_style(container_style) - .boxed(); + }, + ) + .with_width_from_item( + self.actions + .iter() + .enumerate() + .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(container_style) + .boxed(); if self.deployed_from_indicator { *cursor_position.column_mut() = 0; @@ -2578,7 +2591,7 @@ impl Editor { pub fn render_code_actions_indicator( &self, style: &EditorStyle, - cx: &mut ViewContext, + cx: &mut RenderContext, ) -> Option { if self.available_code_actions.is_some() { enum Tag {} @@ -2612,7 +2625,7 @@ impl Editor { &self, cursor_position: DisplayPoint, style: EditorStyle, - cx: &AppContext, + cx: &mut RenderContext, ) -> Option<(DisplayPoint, ElementBox)> { self.context_menu .as_ref() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 319ed212528d7d9a73ada985702441ad093f54c5..68751b000b5907a01217742c474830cbb1941c84 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1024,8 +1024,6 @@ impl Element for EditorElement { max_row.saturating_sub(1) as f32, ); - let mut context_menu = None; - let mut code_actions_indicator = None; self.update_view(cx.app, |view, cx| { let clamped = view.clamp_scroll_left(scroll_max.x()); let autoscrolled; @@ -1045,7 +1043,11 @@ impl Element for EditorElement { if clamped || autoscrolled { snapshot = view.snapshot(cx); } + }); + let mut context_menu = None; + let mut code_actions_indicator = None; + cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| { let newest_selection_head = view .selections .newest::(cx) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ab4de50564f0a1313eda7a131856566231f1485c..a0fca6aeb235946dfaa1998b4af83a43d2807c7c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -468,6 +468,26 @@ impl TestAppContext { result } + pub fn render(&mut self, handle: &ViewHandle, f: F) -> T + where + F: FnOnce(&mut V, &mut RenderContext) -> T, + V: View, + { + handle.update(&mut *self.cx.borrow_mut(), |view, cx| { + let mut render_cx = RenderContext { + app: cx, + window_id: handle.window_id(), + view_id: handle.id(), + view_type: PhantomData, + titlebar_height: 0., + hovered_region_id: None, + clicked_region_id: None, + refreshing: false, + }; + f(view, &mut render_cx) + }) + } + pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext(self.cx.clone()) } @@ -1756,27 +1776,6 @@ impl MutableAppContext { ) } - pub fn build_render_context( - &mut self, - window_id: usize, - view_id: usize, - titlebar_height: f32, - hovered_region_id: Option, - clicked_region_id: Option, - refreshing: bool, - ) -> RenderContext { - RenderContext { - app: self, - window_id, - view_id, - view_type: PhantomData, - titlebar_height, - hovered_region_id, - clicked_region_id, - refreshing, - } - } - pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle where T: View, @@ -3429,13 +3428,13 @@ pub struct RenderParams { } pub struct RenderContext<'a, T: View> { + pub(crate) window_id: usize, + pub(crate) view_id: usize, + pub(crate) view_type: PhantomData, + pub(crate) hovered_region_id: Option, + pub(crate) clicked_region_id: Option, pub app: &'a mut MutableAppContext, - window_id: usize, - view_id: usize, - view_type: PhantomData, pub titlebar_height: f32, - hovered_region_id: Option, - clicked_region_id: Option, pub refreshing: bool, } @@ -3587,6 +3586,16 @@ impl UpgradeViewHandle for ViewContext<'_, V> { } } +impl UpgradeViewHandle for RenderContext<'_, V> { + fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { + self.cx.upgrade_view_handle(handle) + } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } +} + impl UpdateModel for ViewContext<'_, V> { fn update_model( &mut self, diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 3f384b5ea526f6ead4fc9eaaca8b5f5867d65911..c320f2662e4eb7e436118922a0d81f7c620809d3 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, - ElementBox, + ElementBox, RenderContext, View, }; use json::ToJson; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -41,27 +41,40 @@ pub struct LayoutState { items: Vec, } -pub struct UniformList -where - F: Fn(Range, &mut Vec, &mut LayoutContext), -{ +pub struct UniformList { state: UniformListState, item_count: usize, - append_items: F, + append_items: Box, &mut Vec, &mut LayoutContext) -> bool>, padding_top: f32, padding_bottom: f32, get_width_from_item: Option, } -impl UniformList -where - F: Fn(Range, &mut Vec, &mut LayoutContext), -{ - pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { +impl UniformList { + pub fn new( + state: UniformListState, + item_count: usize, + cx: &mut RenderContext, + append_items: F, + ) -> Self + where + V: View, + F: 'static + Fn(&mut V, Range, &mut Vec, &mut RenderContext), + { + let handle = cx.handle(); Self { state, item_count, - append_items, + append_items: Box::new(move |range, items, cx| { + if let Some(handle) = handle.upgrade(cx) { + cx.render(&handle, |view, cx| { + append_items(view, range, items, cx); + }); + true + } else { + false + } + }), padding_top: 0., padding_bottom: 0., get_width_from_item: None, @@ -144,10 +157,7 @@ where } } -impl Element for UniformList -where - F: Fn(Range, &mut Vec, &mut LayoutContext), -{ +impl Element for UniformList { type LayoutState = LayoutState; type PaintState = (); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 899aa59309cb2bd266698f03c6daf4d8b37b53a3..a5b874188a5c0abc00d5192923be6fb218657cd1 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -9,13 +9,14 @@ use crate::{ text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, ElementStateContext, Entity, FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, - ReadView, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, - WeakModelHandle, WeakViewHandle, + ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, + ViewHandle, WeakModelHandle, WeakViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use std::{ collections::{HashMap, HashSet}, + marker::PhantomData, ops::{Deref, DerefMut}, sync::Arc, }; @@ -172,12 +173,15 @@ impl Presenter { LayoutContext { rendered_views: &mut self.rendered_views, parents: &mut self.parents, - refreshing, font_cache: &self.font_cache, font_system: cx.platform().fonts(), text_layout_cache: &self.text_layout_cache, asset_cache: &self.asset_cache, view_stack: Vec::new(), + refreshing, + hovered_region_id: self.hovered_region_id, + clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + titlebar_height: self.titlebar_height, app: cx, } } @@ -342,12 +346,15 @@ pub struct LayoutContext<'a> { rendered_views: &'a mut HashMap, parents: &'a mut HashMap, view_stack: Vec, - pub refreshing: bool, pub font_cache: &'a Arc, pub font_system: Arc, pub text_layout_cache: &'a TextLayoutCache, pub asset_cache: &'a AssetCache, pub app: &'a mut MutableAppContext, + pub refreshing: bool, + titlebar_height: f32, + hovered_region_id: Option, + clicked_region_id: Option, } impl<'a> LayoutContext<'a> { @@ -362,6 +369,26 @@ impl<'a> LayoutContext<'a> { self.view_stack.pop(); size } + + pub fn render(&mut self, handle: &ViewHandle, f: F) -> T + where + F: FnOnce(&mut V, &mut RenderContext) -> T, + V: View, + { + handle.update(self.app, |view, cx| { + let mut render_cx = RenderContext { + app: cx, + window_id: handle.window_id(), + view_id: handle.id(), + view_type: PhantomData, + titlebar_height: self.titlebar_height, + hovered_region_id: self.hovered_region_id, + clicked_region_id: self.clicked_region_id, + refreshing: self.refreshing, + }; + f(view, &mut render_cx) + }) + } } impl<'a> Deref for LayoutContext<'a> { diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index d5d2105c3f34e6b74126080771118f07df8903af..44576a0d954bb2691be9101965d1c335d9342c23 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -123,7 +123,6 @@ impl View for Select { .boxed(), ); if self.is_open { - let handle = self.handle.clone(); result.add_child( Overlay::new( Container::new( @@ -131,9 +130,8 @@ impl View for Select { UniformList::new( self.list_state.clone(), self.item_count, - move |mut range, items, cx| { - let handle = handle.upgrade(cx).unwrap(); - let this = handle.read(cx); + cx, + move |this, mut range, items, cx| { let selected_item_ix = this.selected_item_ix; range.end = range.end.min(this.item_count); items.extend(range.map(|ix| { @@ -141,7 +139,7 @@ impl View for Select { ix, cx, |mouse_state, cx| { - (handle.read(cx).render_item)( + (this.render_item)( ix, if ix == selected_item_ix { ItemType::Selected diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 67db36208b0b114fabb2457b9388809a1a80cee2..0dfd7c0a49f89af2109757e7137eebdfc01e112f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -54,6 +54,7 @@ impl View for Picker { fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { let settings = cx.global::(); + let container_style = settings.theme.picker.container; let delegate = self.delegate.clone(); let match_count = if let Some(delegate) = delegate.upgrade(cx.app) { delegate.read(cx).match_count() @@ -80,8 +81,9 @@ impl View for Picker { UniformList::new( self.list_state.clone(), match_count, - move |mut range, items, cx| { - let delegate = delegate.upgrade(cx).unwrap(); + cx, + move |this, mut range, items, cx| { + let delegate = this.delegate.upgrade(cx).unwrap(); let selected_ix = delegate.read(cx).selected_index(); range.end = cmp::min(range.end, delegate.read(cx).match_count()); items.extend(range.map(move |ix| { @@ -103,7 +105,7 @@ impl View for Picker { .boxed(), ) .contained() - .with_style(settings.theme.picker.container) + .with_style(container_style) .constrained() .with_max_width(self.max_size.x()) .with_max_height(self.max_size.y()) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7056eb9ceb61649c91282885b30b5372f5948031..1be1f1e9405bb9b3864eb684f671e51a2d6e2f08 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -9,8 +9,8 @@ use gpui::{ }, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, + RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -706,8 +706,8 @@ impl ProjectPanel { fn for_each_visible_entry( &self, range: Range, - cx: &mut ViewContext, - mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), + cx: &mut RenderContext, + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut RenderContext), ) { let mut ix = 0; for (worktree_id, visible_worktree_entries) in &self.visible_entries { @@ -780,7 +780,7 @@ impl ProjectPanel { details: EntryDetails, editor: &ViewHandle, theme: &theme::ProjectPanel, - cx: &mut ViewContext, + cx: &mut RenderContext, ) -> ElementBox { let kind = details.kind; let show_editor = details.is_editing && !details.is_processing; @@ -861,31 +861,28 @@ impl View for ProjectPanel { "ProjectPanel" } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> gpui::ElementBox { let theme = &cx.global::().theme.project_panel; let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); - let handle = self.handle.clone(); UniformList::new( self.list.clone(), self.visible_entries .iter() .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), - move |range, items, cx| { + cx, + move |this, range, items, cx| { let theme = cx.global::().theme.clone(); - let this = handle.upgrade(cx).unwrap(); - this.update(cx.app, |this, cx| { - this.for_each_visible_entry(range.clone(), cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - &theme.project_panel, - cx, - )); - }); - }) + this.for_each_visible_entry(range.clone(), cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + &theme.project_panel, + cx, + )); + }); }, ) .with_padding_top(padding.top) @@ -1343,7 +1340,7 @@ mod tests { let mut result = Vec::new(); let mut project_entries = HashSet::new(); let mut has_editor = false; - panel.update(cx, |panel, cx| { + cx.render(panel, |panel, cx| { panel.for_each_visible_entry(range, cx, |project_entry, details, _| { if details.is_editing { assert!(!has_editor, "duplicate editor entry"); From 8dd82fdce131a905e1f00ab2759edb3ad12cec05 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 18:23:44 -0600 Subject: [PATCH 21/54] Take a RenderContext in ListState's build item callback Co-Authored-By: Max Brunsfeld --- crates/chat_panel/src/chat_panel.rs | 4 +- crates/contacts_panel/src/contacts_panel.rs | 17 ++-- crates/gpui/src/elements/list.rs | 102 ++++++++++++++------ crates/project_panel/src/project_panel.rs | 4 +- 4 files changed, 80 insertions(+), 47 deletions(-) diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 460e01c527bddca2145f5f0fbf284403fdeb952a..2240bbf9c627188a44c886171c5b769eb1fda6f3 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -75,9 +75,9 @@ impl ChatPanel { }) }); - let mut message_list = ListState::new(0, Orientation::Bottom, 1000., { + let mut message_list = ListState::new(0, Orientation::Bottom, 1000., cx, { let this = cx.weak_handle(); - move |ix, cx| { + move |_, ix, cx| { let this = this.upgrade(cx).unwrap().read(cx); let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix); this.render_message(message, cx) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 44aa0626c59cea9a6704e1c8bef7e28920d80c4c..888a0a28fb15ca9596a5e085228c4935cdc2a000 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -12,8 +12,8 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, impl_actions, impl_internal_actions, platform::CursorStyle, - AppContext, ClipboardItem, Element, ElementBox, Entity, LayoutContext, ModelHandle, - MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext, + RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use join_project_notification::JoinProjectNotification; use serde::Deserialize; @@ -184,11 +184,8 @@ impl ContactsPanel { .detach(); let mut this = Self { - list_state: ListState::new(0, Orientation::Top, 1000., { - let this = cx.weak_handle(); - move |ix, cx| { - let this = this.upgrade(cx).unwrap(); - let this = this.read(cx); + list_state: ListState::new(0, Orientation::Top, 1000., cx, { + move |this, ix, cx| { let theme = cx.global::().theme.clone(); let theme = &theme.contacts_panel; let current_user_id = @@ -258,7 +255,7 @@ impl ContactsPanel { theme: &theme::ContactsPanel, is_selected: bool, is_collapsed: bool, - cx: &mut LayoutContext, + cx: &mut RenderContext, ) -> ElementBox { enum Header {} @@ -349,7 +346,7 @@ impl ContactsPanel { theme: &theme::ContactsPanel, is_last_project: bool, is_selected: bool, - cx: &mut LayoutContext, + cx: &mut RenderContext, ) -> ElementBox { let project = &contact.projects[project_index]; let project_id = project.id; @@ -462,7 +459,7 @@ impl ContactsPanel { theme: &theme::ContactsPanel, is_incoming: bool, is_selected: bool, - cx: &mut LayoutContext, + cx: &mut RenderContext, ) -> ElementBox { enum Decline {} enum Accept {} diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 77d37bc3bf3b2dc7e879fc3ddcec8ae64bff283d..c6d3096a8b9905e934ea80b57a70bf4fa55fd271 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -5,7 +5,7 @@ use crate::{ }, json::json, DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + RenderContext, SizeConstraint, View, ViewContext, }; use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; @@ -26,7 +26,7 @@ pub enum Orientation { struct StateInner { last_layout_width: Option, - render_item: Box ElementBox>, + render_item: Box Option>, rendered_range: Range, items: SumTree, logical_scroll_top: Option, @@ -135,9 +135,12 @@ impl Element for List { break; } - let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx); - rendered_height += element.size().y(); - rendered_items.push_back(ListItem::Rendered(element)); + if let Some(element) = + state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx) + { + rendered_height += element.size().y(); + rendered_items.push_back(ListItem::Rendered(element)); + } } // Prepare to start walking upward from the item at the scroll top. @@ -149,9 +152,12 @@ impl Element for List { while rendered_height < size.y() { cursor.prev(&()); if let Some(item) = cursor.item() { - let element = state.render_item(cursor.start().0, item, item_constraint, cx); - rendered_height += element.size().y(); - rendered_items.push_front(ListItem::Rendered(element)); + if let Some(element) = + state.render_item(cursor.start().0, item, item_constraint, cx) + { + rendered_height += element.size().y(); + rendered_items.push_front(ListItem::Rendered(element)); + } } else { break; } @@ -182,9 +188,12 @@ impl Element for List { while leading_overdraw < state.overdraw { cursor.prev(&()); if let Some(item) = cursor.item() { - let element = state.render_item(cursor.start().0, item, item_constraint, cx); - leading_overdraw += element.size().y(); - rendered_items.push_front(ListItem::Rendered(element)); + if let Some(element) = + state.render_item(cursor.start().0, item, item_constraint, cx) + { + leading_overdraw += element.size().y(); + rendered_items.push_front(ListItem::Rendered(element)); + } } else { break; } @@ -330,20 +339,25 @@ impl Element for List { } impl ListState { - pub fn new( + pub fn new( element_count: usize, orientation: Orientation, overdraw: f32, - render_item: F, + cx: &mut ViewContext, + mut render_item: F, ) -> Self where - F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox, + V: View, + F: 'static + FnMut(&mut V, usize, &mut RenderContext) -> ElementBox, { let mut items = SumTree::new(); items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); + let handle = cx.handle(); Self(Rc::new(RefCell::new(StateInner { last_layout_width: None, - render_item: Box::new(render_item), + render_item: Box::new(move |ix, cx| { + Some(cx.render(&handle, |view, cx| render_item(view, ix, cx))) + }), rendered_range: 0..0, items, logical_scroll_top: None, @@ -414,13 +428,13 @@ impl StateInner { existing_item: &ListItem, constraint: SizeConstraint, cx: &mut LayoutContext, - ) -> ElementRc { + ) -> Option { if let ListItem::Rendered(element) = existing_item { - element.clone() + Some(element.clone()) } else { - let mut element = (self.render_item)(ix, cx); + let mut element = (self.render_item)(ix, cx)?; element.layout(constraint, cx); - element.into() + Some(element.into()) } } @@ -593,22 +607,26 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height { #[cfg(test)] mod tests { use super::*; - use crate::geometry::vector::vec2f; + use crate::{elements::Empty, geometry::vector::vec2f, Entity}; use rand::prelude::*; use std::env; #[crate::test(self)] fn test_layout(cx: &mut crate::MutableAppContext) { let mut presenter = cx.build_presenter(0, 0.); + let (_, view) = cx.add_window(Default::default(), |_| TestView); let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)); let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)])); - let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, { - let elements = elements.clone(); - move |ix, _| { - let (id, height) = elements.borrow()[ix]; - TestElement::new(id, height).boxed() - } + + let state = view.update(cx, |_, cx| { + ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, cx, { + let elements = elements.clone(); + move |_, ix, _| { + let (id, height) = elements.borrow()[ix]; + TestElement::new(id, height).boxed() + } + }) }); let mut list = List::new(state.clone()); @@ -687,6 +705,7 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); + let (_, view) = cx.add_window(Default::default(), |_| TestView); let mut presenter = cx.build_presenter(0, 0.); let mut next_id = 0; let elements = Rc::new(RefCell::new( @@ -702,12 +721,15 @@ mod tests { .choose(&mut rng) .unwrap(); let overdraw = rng.gen_range(1..=100) as f32; - let state = ListState::new(elements.borrow().len(), orientation, overdraw, { - let elements = elements.clone(); - move |ix, _| { - let (id, height) = elements.borrow()[ix]; - TestElement::new(id, height).boxed() - } + + let state = view.update(cx, |_, cx| { + ListState::new(elements.borrow().len(), orientation, overdraw, cx, { + let elements = elements.clone(); + move |_, ix, _| { + let (id, height) = elements.borrow()[ix]; + TestElement::new(id, height).boxed() + } + }) }); let mut width = rng.gen_range(0..=2000) as f32 / 2.; @@ -843,6 +865,22 @@ mod tests { } } + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut RenderContext<'_, Self>) -> ElementBox { + Empty::new().boxed() + } + } + struct TestElement { id: usize, size: Vector2F, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 1be1f1e9405bb9b3864eb684f671e51a2d6e2f08..e128efddbec9ea34ad49c248b20b4001d86e0b4d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -10,7 +10,7 @@ use gpui::{ impl_internal_actions, keymap, platform::CursorStyle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + RenderContext, Task, View, ViewContext, ViewHandle, }; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -36,7 +36,6 @@ pub struct ProjectPanel { selection: Option, edit_state: Option, filename_editor: ViewHandle, - handle: WeakViewHandle, } #[derive(Copy, Clone)] @@ -156,7 +155,6 @@ impl ProjectPanel { selection: None, edit_state: None, filename_editor, - handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); this From b6b16fc9c391f5553ae70b3f3ce6819152dae8ec Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 18:30:28 -0600 Subject: [PATCH 22/54] In UniformList, guard against misbehavior of append_items If for some reason the handle got dropped and we call it, we'll deal with it somewhat gracefully. Co-Authored-By: Max Brunsfeld --- crates/gpui/src/elements/uniform_list.rs | 58 ++++++++++++++---------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index c320f2662e4eb7e436118922a0d81f7c620809d3..de217a017c7699bddad0d991fadca9cf72ba02a9 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -44,7 +44,7 @@ pub struct LayoutState { pub struct UniformList { state: UniformListState, item_count: usize, - append_items: Box, &mut Vec, &mut LayoutContext) -> bool>, + append_items: Box, &mut Vec, &mut LayoutContext)>, padding_top: f32, padding_bottom: f32, get_width_from_item: Option, @@ -70,9 +70,6 @@ impl UniformList { cx.render(&handle, |view, cx| { append_items(view, range, items, cx); }); - true - } else { - false } }), padding_top: 0., @@ -172,40 +169,51 @@ impl Element for UniformList { ); } + let no_items = ( + constraint.min, + LayoutState { + item_height: 0., + scroll_max: 0., + items: Default::default(), + }, + ); + if self.item_count == 0 { - return ( - constraint.min, - LayoutState { - item_height: 0., - scroll_max: 0., - items: Default::default(), - }, - ); + return no_items; } let mut items = Vec::new(); let mut size = constraint.max; let mut item_size; let sample_item_ix; - let mut sample_item; + let sample_item; if let Some(sample_ix) = self.get_width_from_item { (self.append_items)(sample_ix..sample_ix + 1, &mut items, cx); sample_item_ix = sample_ix; - sample_item = items.pop().unwrap(); - item_size = sample_item.layout(constraint, cx); - size.set_x(item_size.x()); + + if let Some(mut item) = items.pop() { + item_size = item.layout(constraint, cx); + size.set_x(item_size.x()); + sample_item = item; + } else { + return no_items; + } } else { (self.append_items)(0..1, &mut items, cx); sample_item_ix = 0; - sample_item = items.pop().unwrap(); - item_size = sample_item.layout( - SizeConstraint::new( - vec2f(constraint.max.x(), 0.0), - vec2f(constraint.max.x(), f32::INFINITY), - ), - cx, - ); - item_size.set_x(size.x()); + if let Some(mut item) = items.pop() { + item_size = item.layout( + SizeConstraint::new( + vec2f(constraint.max.x(), 0.0), + vec2f(constraint.max.x(), f32::INFINITY), + ), + cx, + ); + item_size.set_x(size.x()); + sample_item = item + } else { + return no_items; + } } let item_constraint = SizeConstraint { From bd62a68234df951795a081aca230cdb54bc54200 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 18:37:28 -0600 Subject: [PATCH 23/54] Eliminate ElementStateContext trait We now always have a RenderContext when rendering MouseEventHandlers or scrollable Flex columns/rows. Co-Authored-By: Max Brunsfeld --- crates/gpui/src/app.rs | 52 +++++++------------ crates/gpui/src/elements/flex.rs | 10 ++-- .../gpui/src/elements/mouse_event_handler.rs | 10 ++-- crates/gpui/src/presenter.rs | 14 ++--- 4 files changed, 32 insertions(+), 54 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a0fca6aeb235946dfaa1998b4af83a43d2807c7c..e75461953f88aa8fc67326b44aed0a2dcd213acb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -127,26 +127,6 @@ pub trait UpdateView { T: View; } -pub trait ElementStateContext: DerefMut { - fn current_view_id(&self) -> usize; - - fn element_state( - &mut self, - element_id: usize, - ) -> ElementStateHandle { - let id = ElementStateId { - view_id: self.current_view_id(), - element_id, - tag: TypeId::of::(), - }; - self.cx - .element_states - .entry(id) - .or_insert_with(|| Box::new(T::default())); - ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts) - } -} - pub struct Menu<'a> { pub name: &'a str, pub items: Vec>, @@ -3444,7 +3424,7 @@ pub struct MouseState { pub clicked: bool, } -impl<'a, T: View> RenderContext<'a, T> { +impl<'a, V: View> RenderContext<'a, V> { fn new(params: RenderParams, app: &'a mut MutableAppContext) -> Self { Self { app, @@ -3458,7 +3438,7 @@ impl<'a, T: View> RenderContext<'a, T> { } } - pub fn handle(&self) -> WeakViewHandle { + pub fn handle(&self) -> WeakViewHandle { WeakViewHandle::new(self.window_id, self.view_id) } @@ -3477,6 +3457,22 @@ impl<'a, T: View> RenderContext<'a, T> { clicked: self.clicked_region_id == region_id, } } + + pub fn element_state( + &mut self, + element_id: usize, + ) -> ElementStateHandle { + let id = ElementStateId { + view_id: self.view_id(), + element_id, + tag: TypeId::of::(), + }; + self.cx + .element_states + .entry(id) + .or_insert_with(|| Box::new(T::default())); + ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts) + } } impl AsRef for &AppContext { @@ -3521,12 +3517,6 @@ impl ReadView for RenderContext<'_, V> { } } -impl ElementStateContext for RenderContext<'_, V> { - fn current_view_id(&self) -> usize { - self.view_id - } -} - impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx @@ -3625,12 +3615,6 @@ impl UpdateView for ViewContext<'_, V> { } } -impl ElementStateContext for ViewContext<'_, V> { - fn current_view_id(&self) -> usize { - self.view_id - } -} - pub trait Handle { type Weak: 'static; fn id(&self) -> usize; diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 3f42f984075a16589de9256aa5f414a97bf7bf05..8d1fb37a08b0afafdb3ea2671bb14d9bdb7e4317 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY}; use crate::{ json::{self, ToJson, Value}, - Axis, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event, - EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, + Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, + LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View, }; use pathfinder_geometry::{ rect::RectF, @@ -40,15 +40,15 @@ impl Flex { Self::new(Axis::Vertical) } - pub fn scrollable( + pub fn scrollable( mut self, element_id: usize, scroll_to: Option, - cx: &mut C, + cx: &mut RenderContext, ) -> Self where Tag: 'static, - C: ElementStateContext, + V: View, { let scroll_state = cx.element_state::(element_id); scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to); diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 975a47a1efe44708da7d5a562ac77c4e9d6cc697..7440be8c56442aca86551d1894964ba1a308375b 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -6,8 +6,8 @@ use crate::{ }, platform::CursorStyle, scene::CursorRegion, - DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event, - EventContext, LayoutContext, PaintContext, SizeConstraint, + DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext, + PaintContext, RenderContext, SizeConstraint, View, }; use serde_json::json; @@ -29,11 +29,11 @@ pub struct MouseState { } impl MouseEventHandler { - pub fn new(id: usize, cx: &mut C, render_child: F) -> Self + pub fn new(id: usize, cx: &mut RenderContext, render_child: F) -> Self where Tag: 'static, - C: ElementStateContext, - F: FnOnce(&MouseState, &mut C) -> ElementBox, + V: View, + F: FnOnce(&MouseState, &mut RenderContext) -> ElementBox, { let state_handle = cx.element_state::(id); let child = state_handle.update(cx, |state, cx| render_child(state, cx)); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index a5b874188a5c0abc00d5192923be6fb218657cd1..bba95f33d45ecf547880fed40fa356a141ca54d7 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -7,10 +7,10 @@ use crate::{ platform::{CursorStyle, Event}, scene::CursorRegion, text_layout::TextLayoutCache, - Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, - ElementStateContext, Entity, FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, - ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, - ViewHandle, WeakModelHandle, WeakViewHandle, + Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity, + FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, ReadView, RenderContext, + RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, + WeakViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -444,12 +444,6 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> { } } -impl<'a> ElementStateContext for LayoutContext<'a> { - fn current_view_id(&self) -> usize { - *self.view_stack.last().unwrap() - } -} - pub struct PaintContext<'a> { rendered_views: &'a mut HashMap, pub scene: &'a mut Scene, From 50edcb06dd1cb37a24d8fac9c97642a358edd10e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 18:59:38 -0600 Subject: [PATCH 24/54] Add drag callbacks to mouse regions Co-Authored-By: Max Brunsfeld --- crates/gpui/src/presenter.rs | 29 ++++++++++++++++++++++++++--- crates/gpui/src/scene.rs | 1 + 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index bba95f33d45ecf547880fed40fa356a141ca54d7..349f8a1a6fb3c666b9d92dc130504c019f8aa4b0 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -33,6 +33,7 @@ pub struct Presenter { last_mouse_moved_event: Option, hovered_region_id: Option, clicked_region: Option, + prev_drag_position: Option, titlebar_height: f32, } @@ -57,6 +58,7 @@ impl Presenter { last_mouse_moved_event: None, hovered_region_id: None, clicked_region: None, + prev_drag_position: None, titlebar_height, } } @@ -205,12 +207,14 @@ impl Presenter { let mut unhovered_region = None; let mut hovered_region = None; let mut clicked_region = None; + let mut dragged_region = None; match event { Event::LeftMouseDown { position, .. } => { for region in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { self.clicked_region = Some(region.clone()); + self.prev_drag_position = Some(position); break; } } @@ -220,6 +224,7 @@ impl Presenter { click_count, .. } => { + self.prev_drag_position.take(); if let Some(region) = self.clicked_region.take() { if region.bounds.contains_point(position) { clicked_region = Some((region, position, click_count)); @@ -256,6 +261,16 @@ impl Presenter { } } Event::LeftMouseDragged { position } => { + if let Some((clicked_region, prev_drag_position)) = self + .clicked_region + .as_ref() + .zip(self.prev_drag_position.as_mut()) + { + dragged_region = + Some((clicked_region.clone(), position - *prev_drag_position)); + *prev_drag_position = position; + } + self.last_mouse_moved_event = Some(Event::MouseMoved { position, left_mouse_down: true, @@ -270,7 +285,7 @@ impl Presenter { if let Some(unhovered_region) = unhovered_region { if let Some(hover_callback) = unhovered_region.hover { event_cx.with_current_view(unhovered_region.view_id, |event_cx| { - hover_callback(false, event_cx) + hover_callback(false, event_cx); }) } } @@ -278,7 +293,7 @@ impl Presenter { if let Some(hovered_region) = hovered_region { if let Some(hover_callback) = hovered_region.hover { event_cx.with_current_view(hovered_region.view_id, |event_cx| { - hover_callback(true, event_cx) + hover_callback(true, event_cx); }) } } @@ -286,7 +301,15 @@ impl Presenter { if let Some((clicked_region, position, click_count)) = clicked_region { if let Some(click_callback) = clicked_region.click { event_cx.with_current_view(clicked_region.view_id, |event_cx| { - click_callback(position, click_count, event_cx) + click_callback(position, click_count, event_cx); + }) + } + } + + if let Some((dragged_region, delta)) = dragged_region { + if let Some(drag_callback) = dragged_region.drag { + event_cx.with_current_view(dragged_region.view_id, |event_cx| { + drag_callback(delta, event_cx); }) } } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 003d9b066b17dc7a7542ab7f08b2c3717c7b9144..6b9ef962cfe1c486f55d244abe9188801e978dd5 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -51,6 +51,7 @@ pub struct MouseRegion { pub bounds: RectF, pub hover: Option>, pub click: Option>, + pub drag: Option>, } #[derive(Copy, Clone, Eq, PartialEq)] From 893f15ddaba31ae7e6cc4e12fcf9fbdf92f3a151 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 20:00:01 -0600 Subject: [PATCH 25/54] Switch MouseEventHandler to use MouseRegions Co-Authored-By: Max Brunsfeld --- crates/auto_update/src/auto_update.rs | 2 +- crates/chat_panel/src/chat_panel.rs | 2 +- crates/command_palette/src/command_palette.rs | 6 +- crates/contacts_panel/src/contact_finder.rs | 6 +- crates/contacts_panel/src/contacts_panel.rs | 28 ++-- crates/contacts_panel/src/notifications.rs | 15 +- crates/diagnostics/src/items.rs | 4 +- crates/editor/src/editor.rs | 6 +- crates/file_finder/src/file_finder.rs | 6 +- crates/gpui/src/app.rs | 20 +-- .../gpui/src/elements/mouse_event_handler.rs | 132 +++++------------- crates/gpui/src/presenter.rs | 50 ++++--- crates/gpui/src/scene.rs | 7 +- crates/gpui/src/views/select.rs | 6 +- crates/outline/src/outline.rs | 6 +- crates/picker/src/picker.rs | 12 +- crates/project_panel/src/project_panel.rs | 2 +- crates/project_symbols/src/project_symbols.rs | 6 +- crates/search/src/buffer_search.rs | 4 +- crates/search/src/project_search.rs | 4 +- crates/theme/src/theme.rs | 6 +- crates/theme_selector/src/theme_selector.rs | 4 +- crates/workspace/src/lsp_status.rs | 3 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/sidebar.rs | 2 +- crates/workspace/src/workspace.rs | 4 +- 26 files changed, 150 insertions(+), 195 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 499b3ed99d579af99ec02dddadad9a6aa77dd0d6..234319bdd66090cd5dd6bb22ccc82962a23fefd8 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator { ) .boxed() }) - .on_click(|_, cx| cx.dispatch_action(DismissErrorMessage)) + .on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage)) .boxed() } AutoUpdateStatus::Idle => Empty::new().boxed(), diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 2240bbf9c627188a44c886171c5b769eb1fda6f3..d3c5ef55922f58553754104b68c616eb0994e46e 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -320,7 +320,7 @@ impl ChatPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { let rpc = rpc.clone(); let this = this.clone(); cx.spawn(|mut cx| async move { diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 9f0f396d8530b4d074a4763772c0584a9d5f263b..0708826ea704a3ca3a30725185b0beef87ca14e5 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,9 +1,9 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, - elements::{ChildView, Flex, Label, MouseState, ParentElement}, + elements::{ChildView, Flex, Label, ParentElement}, keymap::Keystroke, - Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, + Action, Element, Entity, MouseState, MutableAppContext, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use settings::Settings; @@ -203,7 +203,7 @@ impl PickerDelegate for CommandPalette { fn render_match( &self, ix: usize, - mouse_state: &MouseState, + mouse_state: MouseState, selected: bool, cx: &gpui::AppContext, ) -> gpui::ElementBox { diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index 18e17a93d9ba448f533d83a0a09701b5d3ae84cc..244cfcad4a5af9bc11964da485e76041d56f9c69 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -1,7 +1,7 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, - ViewContext, ViewHandle, + actions, elements::*, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use settings::Settings; @@ -105,7 +105,7 @@ impl PickerDelegate for ContactFinder { fn render_match( &self, ix: usize, - mouse_state: &MouseState, + mouse_state: MouseState, selected: bool, cx: &gpui::AppContext, ) -> ElementBox { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 888a0a28fb15ca9596a5e085228c4935cdc2a000..6d2885e86709202c7ae0aa7048bded892e75179c 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -259,7 +259,7 @@ impl ContactsPanel { ) -> ElementBox { enum Header {} - let header_style = theme.header_row.style_for(&Default::default(), is_selected); + let header_style = theme.header_row.style_for(Default::default(), is_selected); let text = match section { Section::Requests => "Requests", Section::Online => "Online", @@ -299,7 +299,7 @@ impl ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(ToggleExpanded(section))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleExpanded(section))) .boxed() } @@ -331,11 +331,7 @@ impl ContactsPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style( - *theme - .contact_row - .style_for(&Default::default(), is_selected), - ) + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) .boxed() } @@ -442,7 +438,7 @@ impl ContactsPanel { } else { CursorStyle::Arrow }) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { if !is_host { cx.dispatch_global_action(JoinProject { contact: contact.clone(), @@ -504,7 +500,7 @@ impl ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.dispatch_action(RespondToContactRequest { user_id, accept: false, @@ -526,7 +522,7 @@ impl ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.dispatch_action(RespondToContactRequest { user_id, accept: true, @@ -549,7 +545,7 @@ impl ContactsPanel { }) .with_padding(Padding::uniform(2.)) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) + .on_click(move |_, _, cx| cx.dispatch_action(RemoveContact(user_id))) .flex_float() .boxed(), ); @@ -558,11 +554,7 @@ impl ContactsPanel { row.constrained() .with_height(theme.row_height) .contained() - .with_style( - *theme - .contact_row - .style_for(&Default::default(), is_selected), - ) + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) .boxed() } @@ -862,7 +854,7 @@ impl View for ContactsPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle)) + .on_click(|_, _, cx| cx.dispatch_action(contact_finder::Toggle)) .boxed(), ) .constrained() @@ -910,7 +902,7 @@ impl View for ContactsPanel { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new( info.url.to_string(), )); diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs index 555d8962d3d8003eeaa11ad078625f51844b6dbb..c02fd73b8f7143bf8b45f83ace3bedff4fc799db 100644 --- a/crates/contacts_panel/src/notifications.rs +++ b/crates/contacts_panel/src/notifications.rs @@ -61,7 +61,7 @@ pub fn render_user_notification( }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(5.)) - .on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) + .on_click(move |_, _, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) .aligned() .constrained() .with_height( @@ -76,13 +76,10 @@ pub fn render_user_notification( .named("contact notification header"), ) .with_children(body.map(|body| { - Label::new( - body.to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed() + Label::new(body.to_string(), theme.body_message.text.clone()) + .contained() + .with_style(theme.body_message.container) + .boxed() })) .with_children(if buttons.is_empty() { None @@ -99,7 +96,7 @@ pub fn render_user_notification( .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone())) + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) .boxed() }, )) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 426f25629dd139d21e8ad10e6abb4d33e29d4063..224e5e94a781471ce71389151cc119b3c09f537e 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -159,7 +159,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(crate::Deploy)) + .on_click(|_, _, cx| cx.dispatch_action(crate::Deploy)) .aligned() .boxed(), ); @@ -192,7 +192,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic)) + .on_click(|_, _, cx| cx.dispatch_action(GoToNextDiagnostic)) .boxed(), ); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ae8335f2420f5715c31a2fd9395c73a92d051360..56e88add5401197d5c469099b3cfdfc8cd0e0773 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -679,7 +679,7 @@ impl CompletionsMenu { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { + .on_mouse_down(move |_, cx| { cx.dispatch_action(ConfirmCompletion { item_ix: Some(item_ix), }); @@ -812,7 +812,7 @@ impl CodeActionsMenu { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { + .on_mouse_down(move |_, cx| { cx.dispatch_action(ConfirmCodeAction { item_ix: Some(item_ix), }); @@ -2603,7 +2603,7 @@ impl Editor { }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(3.)) - .on_mouse_down(|cx| { + .on_mouse_down(|_, cx| { cx.dispatch_action(ToggleCodeActions { deployed_from_indicator: true, }); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f58c733cc7aa8bd44e3643ddef4a78ee9861f2c4..e19c1de3a86e7f66d1ce4a43014394ea19652e12 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,7 +1,7 @@ use fuzzy::PathMatch; use gpui::{ - actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, - View, ViewContext, ViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MouseState, MutableAppContext, + RenderContext, Task, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use project::{Project, ProjectPath, WorktreeId}; @@ -226,7 +226,7 @@ impl PickerDelegate for FileFinder { fn render_match( &self, ix: usize, - mouse_state: &MouseState, + mouse_state: MouseState, selected: bool, cx: &AppContext, ) -> ElementBox { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e75461953f88aa8fc67326b44aed0a2dcd213acb..63f7a673273df919e2f7b9b8039a369f9ccddbbb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -460,7 +460,7 @@ impl TestAppContext { view_id: handle.id(), view_type: PhantomData, titlebar_height: 0., - hovered_region_id: None, + hovered_region_ids: Default::default(), clicked_region_id: None, refreshing: false, }; @@ -1080,7 +1080,7 @@ impl MutableAppContext { window_id, view_id, titlebar_height, - hovered_region_id: None, + hovered_region_ids: Default::default(), clicked_region_id: None, refreshing: false, }) @@ -3402,7 +3402,7 @@ pub struct RenderParams { pub window_id: usize, pub view_id: usize, pub titlebar_height: f32, - pub hovered_region_id: Option, + pub hovered_region_ids: HashSet, pub clicked_region_id: Option, pub refreshing: bool, } @@ -3411,14 +3411,14 @@ pub struct RenderContext<'a, T: View> { pub(crate) window_id: usize, pub(crate) view_id: usize, pub(crate) view_type: PhantomData, - pub(crate) hovered_region_id: Option, + pub(crate) hovered_region_ids: HashSet, pub(crate) clicked_region_id: Option, pub app: &'a mut MutableAppContext, pub titlebar_height: f32, pub refreshing: bool, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct MouseState { pub hovered: bool, pub clicked: bool, @@ -3432,7 +3432,7 @@ impl<'a, V: View> RenderContext<'a, V> { view_id: params.view_id, view_type: PhantomData, titlebar_height: params.titlebar_height, - hovered_region_id: params.hovered_region_id, + hovered_region_ids: params.hovered_region_ids.clone(), clicked_region_id: params.clicked_region_id, refreshing: params.refreshing, } @@ -3447,14 +3447,14 @@ impl<'a, V: View> RenderContext<'a, V> { } pub fn mouse_state(&self, region_id: usize) -> MouseState { - let region_id = Some(MouseRegionId { + let region_id = MouseRegionId { view_id: self.view_id, tag: TypeId::of::(), region_id, - }); + }; MouseState { - hovered: self.hovered_region_id == region_id, - clicked: self.clicked_region_id == region_id, + hovered: self.hovered_region_ids.contains(®ion_id), + clicked: self.clicked_region_id == Some(region_id), } } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 7440be8c56442aca86551d1894964ba1a308375b..ffe70caa12c233c94b04442ceccfc01583dbc45c 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -1,3 +1,5 @@ +use std::{any::TypeId, rc::Rc}; + use super::Padding; use crate::{ geometry::{ @@ -6,40 +8,33 @@ use crate::{ }, platform::CursorStyle, scene::CursorRegion, - DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View, }; use serde_json::json; pub struct MouseEventHandler { - state: ElementStateHandle, child: ElementBox, + tag: TypeId, + id: usize, cursor_style: Option, - mouse_down_handler: Option>, - click_handler: Option>, - drag_handler: Option>, + mouse_down_handler: Option>, + click_handler: Option>, + drag_handler: Option>, padding: Padding, } -#[derive(Default)] -pub struct MouseState { - pub hovered: bool, - pub clicked: bool, - prev_drag_position: Option, -} - impl MouseEventHandler { pub fn new(id: usize, cx: &mut RenderContext, render_child: F) -> Self where Tag: 'static, V: View, - F: FnOnce(&MouseState, &mut RenderContext) -> ElementBox, + F: FnOnce(MouseState, &mut RenderContext) -> ElementBox, { - let state_handle = cx.element_state::(id); - let child = state_handle.update(cx, |state, cx| render_child(state, cx)); Self { - state: state_handle, - child, + id, + tag: TypeId::of::(), + child: render_child(cx.mouse_state::(id), cx), cursor_style: None, mouse_down_handler: None, click_handler: None, @@ -53,18 +48,24 @@ impl MouseEventHandler { self } - pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { - self.mouse_down_handler = Some(Box::new(handler)); + pub fn on_mouse_down( + mut self, + handler: impl Fn(Vector2F, &mut EventContext) + 'static, + ) -> Self { + self.mouse_down_handler = Some(Rc::new(handler)); self } - pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self { - self.click_handler = Some(Box::new(handler)); + pub fn on_click( + mut self, + handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static, + ) -> Self { + self.click_handler = Some(Rc::new(handler)); self } - pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self { - self.drag_handler = Some(Box::new(handler)); + pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self { + self.drag_handler = Some(Rc::new(handler)); self } @@ -107,6 +108,18 @@ impl Element for MouseEventHandler { style, }); } + + cx.scene.push_mouse_region(MouseRegion { + view_id: cx.current_view_id(), + tag: self.tag, + region_id: self.id, + bounds, + hover: None, + click: self.click_handler.clone(), + mouse_down: self.mouse_down_handler.clone(), + drag: self.drag_handler.clone(), + }); + self.child.paint(bounds.origin(), visible_bounds, cx); } @@ -114,81 +127,12 @@ impl Element for MouseEventHandler { &mut self, event: &Event, _: RectF, - visible_bounds: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { - let hit_bounds = self.hit_bounds(visible_bounds); - let mouse_down_handler = self.mouse_down_handler.as_mut(); - let click_handler = self.click_handler.as_mut(); - let drag_handler = self.drag_handler.as_mut(); - - let handled_in_child = self.child.dispatch_event(event, cx); - - self.state.update(cx, |state, cx| match event { - Event::MouseMoved { - position, - left_mouse_down, - } => { - if !left_mouse_down { - let mouse_in = hit_bounds.contains_point(*position); - if state.hovered != mouse_in { - state.hovered = mouse_in; - cx.notify(); - return true; - } - } - handled_in_child - } - Event::LeftMouseDown { position, .. } => { - if !handled_in_child && hit_bounds.contains_point(*position) { - state.clicked = true; - state.prev_drag_position = Some(*position); - cx.notify(); - if let Some(handler) = mouse_down_handler { - handler(cx); - } - true - } else { - handled_in_child - } - } - Event::LeftMouseUp { - position, - click_count, - .. - } => { - state.prev_drag_position = None; - if !handled_in_child && state.clicked { - state.clicked = false; - cx.notify(); - if let Some(handler) = click_handler { - if hit_bounds.contains_point(*position) { - handler(*click_count, cx); - } - } - true - } else { - handled_in_child - } - } - Event::LeftMouseDragged { position, .. } => { - if !handled_in_child && state.clicked { - let prev_drag_position = state.prev_drag_position.replace(*position); - if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) { - let delta = *position - prev_position; - if !delta.is_zero() { - (handler)(delta, cx); - } - } - true - } else { - handled_in_child - } - } - _ => handled_in_child, - }) + self.child.dispatch_event(event, cx) } fn debug( diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 349f8a1a6fb3c666b9d92dc130504c019f8aa4b0..1c22ceb1b65b65020cf22c24adefea348fc17ebf 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -31,7 +31,7 @@ pub struct Presenter { text_layout_cache: TextLayoutCache, asset_cache: Arc, last_mouse_moved_event: Option, - hovered_region_id: Option, + hovered_region_ids: HashSet, clicked_region: Option, prev_drag_position: Option, titlebar_height: f32, @@ -56,7 +56,7 @@ impl Presenter { text_layout_cache, asset_cache, last_mouse_moved_event: None, - hovered_region_id: None, + hovered_region_ids: Default::default(), clicked_region: None, prev_drag_position: None, titlebar_height, @@ -100,7 +100,7 @@ impl Presenter { window_id: self.window_id, view_id: *view_id, titlebar_height: self.titlebar_height, - hovered_region_id: self.hovered_region_id, + hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), refreshing: false, }) @@ -118,7 +118,7 @@ impl Presenter { window_id: self.window_id, view_id: *view_id, titlebar_height: self.titlebar_height, - hovered_region_id: self.hovered_region_id, + hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), refreshing: true, }) @@ -181,7 +181,7 @@ impl Presenter { asset_cache: &self.asset_cache, view_stack: Vec::new(), refreshing, - hovered_region_id: self.hovered_region_id, + hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), titlebar_height: self.titlebar_height, app: cx, @@ -198,14 +198,16 @@ impl Presenter { font_cache: &self.font_cache, text_layout_cache: &self.text_layout_cache, rendered_views: &mut self.rendered_views, + view_stack: Vec::new(), app: cx, } } pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - let mut unhovered_region = None; - let mut hovered_region = None; + let mut invalidated_views = Vec::new(); + let mut hovered_regions = Vec::new(); + let mut unhovered_regions = Vec::new(); let mut clicked_region = None; let mut dragged_region = None; @@ -213,6 +215,7 @@ impl Presenter { Event::LeftMouseDown { position, .. } => { for region in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { + invalidated_views.push(region.view_id); self.clicked_region = Some(region.clone()); self.prev_drag_position = Some(position); break; @@ -226,6 +229,7 @@ impl Presenter { } => { self.prev_drag_position.take(); if let Some(region) = self.clicked_region.take() { + invalidated_views.push(region.view_id); if region.bounds.contains_point(position) { clicked_region = Some((region, position, click_count)); } @@ -248,13 +252,18 @@ impl Presenter { cx.platform().set_cursor_style(style_to_assign); for region in self.mouse_regions.iter().rev() { + let region_id = region.id(); if region.bounds.contains_point(position) { - if hovered_region.is_none() { - hovered_region = Some(region.clone()); + if !self.hovered_region_ids.contains(®ion_id) { + invalidated_views.push(region.view_id); + hovered_regions.push(region.clone()); + self.hovered_region_ids.insert(region_id); } } else { - if self.hovered_region_id == Some(region.id()) { - unhovered_region = Some(region.clone()) + if self.hovered_region_ids.contains(®ion_id) { + invalidated_views.push(region.view_id); + unhovered_regions.push(region.clone()); + self.hovered_region_ids.remove(®ion_id); } } } @@ -279,10 +288,8 @@ impl Presenter { _ => {} } - self.hovered_region_id = hovered_region.as_ref().map(MouseRegion::id); - let mut event_cx = self.build_event_context(cx); - if let Some(unhovered_region) = unhovered_region { + for unhovered_region in unhovered_regions { if let Some(hover_callback) = unhovered_region.hover { event_cx.with_current_view(unhovered_region.view_id, |event_cx| { hover_callback(false, event_cx); @@ -290,7 +297,7 @@ impl Presenter { } } - if let Some(hovered_region) = hovered_region { + for hovered_region in hovered_regions { if let Some(hover_callback) = hovered_region.hover { event_cx.with_current_view(hovered_region.view_id, |event_cx| { hover_callback(true, event_cx); @@ -316,7 +323,7 @@ impl Presenter { event_cx.dispatch_event(root_view_id, &event); - let invalidated_views = event_cx.invalidated_views; + invalidated_views.extend(event_cx.invalidated_views); let dispatch_directives = event_cx.dispatched_actions; for view_id in invalidated_views { @@ -376,7 +383,7 @@ pub struct LayoutContext<'a> { pub app: &'a mut MutableAppContext, pub refreshing: bool, titlebar_height: f32, - hovered_region_id: Option, + hovered_region_ids: HashSet, clicked_region_id: Option, } @@ -405,7 +412,7 @@ impl<'a> LayoutContext<'a> { view_id: handle.id(), view_type: PhantomData, titlebar_height: self.titlebar_height, - hovered_region_id: self.hovered_region_id, + hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region_id, refreshing: self.refreshing, }; @@ -469,6 +476,7 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> { pub struct PaintContext<'a> { rendered_views: &'a mut HashMap, + view_stack: Vec, pub scene: &'a mut Scene, pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, @@ -478,10 +486,16 @@ pub struct PaintContext<'a> { impl<'a> PaintContext<'a> { fn paint(&mut self, view_id: usize, origin: Vector2F, visible_bounds: RectF) { if let Some(mut tree) = self.rendered_views.remove(&view_id) { + self.view_stack.push(view_id); tree.paint(origin, visible_bounds, self); self.rendered_views.insert(view_id, tree); + self.view_stack.pop(); } } + + pub fn current_view_id(&self) -> usize { + *self.view_stack.last().unwrap() + } } impl<'a> Deref for PaintContext<'a> { diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 6b9ef962cfe1c486f55d244abe9188801e978dd5..caeae3c89fbb0add583ee1f19ad662ee6dcba85d 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -50,11 +50,12 @@ pub struct MouseRegion { pub region_id: usize, pub bounds: RectF, pub hover: Option>, + pub mouse_down: Option>, pub click: Option>, pub drag: Option>, } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct MouseRegionId { pub view_id: usize, pub tag: TypeId, @@ -242,6 +243,10 @@ impl Scene { self.active_layer().push_cursor_region(region); } + pub fn push_mouse_region(&mut self, region: MouseRegion) { + self.active_layer().push_mouse_region(region); + } + pub fn push_image(&mut self, image: Image) { self.active_layer().push_image(image) } diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index 44576a0d954bb2691be9101965d1c335d9342c23..80c3ba28847e616e5727a3fd040af50976420568 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -119,7 +119,7 @@ impl View for Select { .with_style(style.header) .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(ToggleSelect)) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleSelect)) .boxed(), ); if self.is_open { @@ -151,7 +151,9 @@ impl View for Select { ) }, ) - .on_click(move |_, cx| cx.dispatch_action(SelectItem(ix))) + .on_click(move |_, _, cx| { + cx.dispatch_action(SelectItem(ix)) + }) .boxed() })) }, diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 57c7441bfe761e0d16d559abbade8a024be8f2d6..19b309116a45f7486d07c235c23212d66b77762a 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -4,8 +4,8 @@ use editor::{ }; use fuzzy::StringMatch; use gpui::{ - actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, + actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MouseState, + MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, }; use language::Outline; use ordered_float::OrderedFloat; @@ -231,7 +231,7 @@ impl PickerDelegate for OutlineView { fn render_match( &self, ix: usize, - mouse_state: &MouseState, + mouse_state: MouseState, selected: bool, cx: &AppContext, ) -> ElementBox { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 0dfd7c0a49f89af2109757e7137eebdfc01e112f..383d45ff3f2964bf32d1eb83d82a96aa939b5776 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -1,14 +1,14 @@ use editor::Editor; use gpui::{ elements::{ - ChildView, Flex, Label, MouseEventHandler, MouseState, ParentElement, ScrollTarget, - UniformList, UniformListState, + ChildView, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, UniformList, + UniformListState, }, geometry::vector::{vec2f, Vector2F}, keymap, platform::CursorStyle, - AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AppContext, Axis, Element, ElementBox, Entity, MouseState, MutableAppContext, RenderContext, + Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; use std::cmp; @@ -34,7 +34,7 @@ pub trait PickerDelegate: View { fn render_match( &self, ix: usize, - state: &MouseState, + state: MouseState, selected: bool, cx: &AppContext, ) -> ElementBox; @@ -92,7 +92,7 @@ impl View for Picker { .read(cx) .render_match(ix, state, ix == selected_ix, cx) }) - .on_mouse_down(move |cx| cx.dispatch_action(SelectIndex(ix))) + .on_mouse_down(move |_, cx| cx.dispatch_action(SelectIndex(ix))) .with_cursor_style(CursorStyle::PointingHand) .boxed() })); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e128efddbec9ea34ad49c248b20b4001d86e0b4d..17eca4d99f33b286b19d96b55e286d7cfc92c037 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -839,7 +839,7 @@ impl ProjectPanel { .with_padding_left(padding) .boxed() }) - .on_click(move |click_count, cx| { + .on_click(move |_, click_count, cx| { if kind == EntryKind::Dir { cx.dispatch_action(ToggleExpanded(entry_id)) } else { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 20da49b1dfdc93d846f98b8a2fa2a33980b6c87d..ea99767e0a15b63f57cab7e163bbfc10bfa641c3 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -3,8 +3,8 @@ use editor::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, - View, ViewContext, ViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MouseState, MutableAppContext, + RenderContext, Task, View, ViewContext, ViewHandle, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -221,7 +221,7 @@ impl PickerDelegate for ProjectSymbolsView { fn render_match( &self, ix: usize, - mouse_state: &MouseState, + mouse_state: MouseState, selected: bool, cx: &AppContext, ) -> ElementBox { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5581cbd60870f5cec82dad5af5e634ffa342c2f6..94b6261a0f11c3b4ba56333a22365fdf6fb3a79a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -290,7 +290,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -314,7 +314,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| match direction { + .on_click(move |_, _, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4549aa4f90388bb441bcbbf6cad428136061eccd..e3834f6f45d8e8caaee731445074875753d09a0d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -672,7 +672,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| match direction { + .on_click(move |_, _, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) @@ -699,7 +699,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc9e93025d1d1a13c4d0235382a3864f91f33729..d654363648677274eeb23febeed0f799ca760060 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -2,9 +2,9 @@ mod theme_registry; use gpui::{ color::Color, - elements::{ContainerStyle, ImageStyle, LabelStyle, MouseState}, + elements::{ContainerStyle, ImageStyle, LabelStyle}, fonts::{HighlightStyle, TextStyle}, - Border, + Border, MouseState, }; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; @@ -488,7 +488,7 @@ pub struct Interactive { } impl Interactive { - pub fn style_for(&self, state: &MouseState, active: bool) -> &T { + pub fn style_for(&self, state: MouseState, active: bool) -> &T { if active { if state.hovered { self.active_hover diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 9f445c633af0ff2c8e9715ab5fe107790f240db3..106e6ad42921fabc425e65dea1a68f634e730ab6 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,6 +1,6 @@ use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext, + actions, elements::*, AppContext, Element, ElementBox, Entity, MouseState, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; @@ -213,7 +213,7 @@ impl PickerDelegate for ThemeSelector { fn render_match( &self, ix: usize, - mouse_state: &MouseState, + mouse_state: MouseState, selected: bool, cx: &AppContext, ) -> ElementBox { diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index f58e0b973e05e27db44359b95c11524010e14ea2..ab1ae4931fecd0ebf8f09a574f29755859d53992 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -168,7 +168,8 @@ impl View for LspStatus { self.failed.join(", "), if self.failed.len() > 1 { "s" } else { "" } ); - handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); + handler = + Some(|_, _, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); } else { return Empty::new().boxed(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8b97ef1a80476d5c33b4ff8dc18aaaa1ecf9bd88..ba2a21bce4cad4e0ada71ef312366afe7e0f555e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -788,7 +788,7 @@ impl Pane { .with_cursor_style(CursorStyle::PointingHand) .on_click({ let pane = pane.clone(); - move |_, cx| { + move |_, _, cx| { cx.dispatch_action(CloseItem { item_id, pane: pane.clone(), diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index afdacc2a31a86f297c70e9b2a4168d01720a4da8..5aec332913420cc8beeab4a071b7a95468919d6b 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -293,7 +293,7 @@ impl View for SidebarButtons { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.dispatch_action(ToggleSidebarItem { side, item_index: ix, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e9f0efa31115dac5d98eb13826526f4dc96994ec..f4197e72962e6536afbefaf48d08be23f3338e8d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1730,7 +1730,7 @@ impl Workspace { .with_style(style.container) .boxed() }) - .on_click(|_, cx| cx.dispatch_action(Authenticate)) + .on_click(|_, _, cx| cx.dispatch_action(Authenticate)) .with_cursor_style(CursorStyle::PointingHand) .aligned() .boxed(), @@ -1781,7 +1781,7 @@ impl Workspace { if let Some(peer_id) = peer_id { MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id))) + .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id))) .boxed() } else { content From aedfd74d30273751f0ebe75b67d8079374692c23 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 May 2022 20:05:20 -0600 Subject: [PATCH 26/54] Use the hit bounds when painting mouse regions --- crates/gpui/src/elements/mouse_event_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index ffe70caa12c233c94b04442ceccfc01583dbc45c..a9535667fd20c826c193fa22febd7f3643836aea 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -113,7 +113,7 @@ impl Element for MouseEventHandler { view_id: cx.current_view_id(), tag: self.tag, region_id: self.id, - bounds, + bounds: self.hit_bounds(bounds), hover: None, click: self.click_handler.clone(), mouse_down: self.mouse_down_handler.clone(), From 307eb1726c3d47a0b27d3ebfcd295ac8dde0b47c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 May 2022 09:59:24 +0200 Subject: [PATCH 27/54] Compute dispatch path based on the view id that dispatched the action --- crates/gpui/src/app.rs | 10 ++++++++-- crates/gpui/src/presenter.rs | 22 +++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 63f7a673273df919e2f7b9b8039a369f9ccddbbb..37bb86570b75358ddfc2c30d1fe33b58916391c9 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1370,7 +1370,10 @@ impl MutableAppContext { .unwrap() .0 .clone(); - let dispatch_path = presenter.borrow().dispatch_path_from(view_id); + let mut dispatch_path = Vec::new(); + presenter + .borrow() + .compute_dispatch_path_from(view_id, &mut dispatch_path); for view_id in dispatch_path { if let Some(view) = self.views.get(&(window_id, view_id)) { let view_type = view.as_any().type_id(); @@ -1424,7 +1427,10 @@ impl MutableAppContext { .unwrap() .0 .clone(); - let dispatch_path = presenter.borrow().dispatch_path_from(view_id); + let mut dispatch_path = Vec::new(); + presenter + .borrow() + .compute_dispatch_path_from(view_id, &mut dispatch_path); self.dispatch_action_any(window_id, &dispatch_path, action); } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 1c22ceb1b65b65020cf22c24adefea348fc17ebf..42149feca85278b0f9854b1f6d6db2b37c6536ad 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -64,22 +64,20 @@ impl Presenter { } pub fn dispatch_path(&self, app: &AppContext) -> Vec { + let mut path = Vec::new(); if let Some(view_id) = app.focused_view_id(self.window_id) { - self.dispatch_path_from(view_id) - } else { - Vec::new() + self.compute_dispatch_path_from(view_id, &mut path) } + path } - pub(crate) fn dispatch_path_from(&self, mut view_id: usize) -> Vec { - let mut path = Vec::new(); + pub(crate) fn compute_dispatch_path_from(&self, mut view_id: usize, path: &mut Vec) { path.push(view_id); while let Some(parent_id) = self.parents.get(&view_id).copied() { path.push(parent_id); view_id = parent_id; } path.reverse(); - path } pub fn invalidate( @@ -329,8 +327,14 @@ impl Presenter { for view_id in invalidated_views { cx.notify_view(self.window_id, view_id); } + + let mut dispatch_path = Vec::new(); for directive in dispatch_directives { - cx.dispatch_action_any(self.window_id, &directive.path, directive.action.as_ref()); + dispatch_path.clear(); + if let Some(view_id) = directive.dispatcher_view_id { + self.compute_dispatch_path_from(view_id, &mut dispatch_path); + } + cx.dispatch_action_any(self.window_id, &dispatch_path, directive.action.as_ref()); } } } @@ -368,7 +372,7 @@ impl Presenter { } pub struct DispatchDirective { - pub path: Vec, + pub dispatcher_view_id: Option, pub action: Box, } @@ -541,7 +545,7 @@ impl<'a> EventContext<'a> { pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { - path: self.view_stack.clone(), + dispatcher_view_id: self.view_stack.last().copied(), action, }); } From 1d7fc122297f6848f2fac27c95e4d2a923ab37d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 May 2022 10:47:54 +0200 Subject: [PATCH 28/54] Add right-click support to `MouseEventHandler` --- crates/gpui/src/app.rs | 7 +++ .../gpui/src/elements/mouse_event_handler.rs | 22 ++++++++++ crates/gpui/src/platform/event.rs | 3 +- crates/gpui/src/platform/mac/event.rs | 1 + crates/gpui/src/presenter.rs | 43 +++++++++++++++++++ crates/gpui/src/scene.rs | 2 + 6 files changed, 77 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 37bb86570b75358ddfc2c30d1fe33b58916391c9..5c8f49ac1818faca5f048d27bc5d21aefb587fcd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -462,6 +462,7 @@ impl TestAppContext { titlebar_height: 0., hovered_region_ids: Default::default(), clicked_region_id: None, + right_clicked_region_id: None, refreshing: false, }; f(view, &mut render_cx) @@ -1082,6 +1083,7 @@ impl MutableAppContext { titlebar_height, hovered_region_ids: Default::default(), clicked_region_id: None, + right_clicked_region_id: None, refreshing: false, }) .unwrap(), @@ -3410,6 +3412,7 @@ pub struct RenderParams { pub titlebar_height: f32, pub hovered_region_ids: HashSet, pub clicked_region_id: Option, + pub right_clicked_region_id: Option, pub refreshing: bool, } @@ -3419,6 +3422,7 @@ pub struct RenderContext<'a, T: View> { pub(crate) view_type: PhantomData, pub(crate) hovered_region_ids: HashSet, pub(crate) clicked_region_id: Option, + pub(crate) right_clicked_region_id: Option, pub app: &'a mut MutableAppContext, pub titlebar_height: f32, pub refreshing: bool, @@ -3428,6 +3432,7 @@ pub struct RenderContext<'a, T: View> { pub struct MouseState { pub hovered: bool, pub clicked: bool, + pub right_clicked: bool, } impl<'a, V: View> RenderContext<'a, V> { @@ -3440,6 +3445,7 @@ impl<'a, V: View> RenderContext<'a, V> { titlebar_height: params.titlebar_height, hovered_region_ids: params.hovered_region_ids.clone(), clicked_region_id: params.clicked_region_id, + right_clicked_region_id: params.right_clicked_region_id, refreshing: params.refreshing, } } @@ -3461,6 +3467,7 @@ impl<'a, V: View> RenderContext<'a, V> { MouseState { hovered: self.hovered_region_ids.contains(®ion_id), clicked: self.clicked_region_id == Some(region_id), + right_clicked: self.right_clicked_region_id == Some(region_id), } } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index a9535667fd20c826c193fa22febd7f3643836aea..1ca4c13fc856e21f886883aa1101dca27ae879d6 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -20,6 +20,8 @@ pub struct MouseEventHandler { cursor_style: Option, mouse_down_handler: Option>, click_handler: Option>, + right_mouse_down_handler: Option>, + right_click_handler: Option>, drag_handler: Option>, padding: Padding, } @@ -38,6 +40,8 @@ impl MouseEventHandler { cursor_style: None, mouse_down_handler: None, click_handler: None, + right_mouse_down_handler: None, + right_click_handler: None, drag_handler: None, padding: Default::default(), } @@ -64,6 +68,22 @@ impl MouseEventHandler { self } + pub fn on_right_mouse_down( + mut self, + handler: impl Fn(Vector2F, &mut EventContext) + 'static, + ) -> Self { + self.right_mouse_down_handler = Some(Rc::new(handler)); + self + } + + pub fn on_right_click( + mut self, + handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static, + ) -> Self { + self.right_click_handler = Some(Rc::new(handler)); + self + } + pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self { self.drag_handler = Some(Rc::new(handler)); self @@ -117,6 +137,8 @@ impl Element for MouseEventHandler { hover: None, click: self.click_handler.clone(), mouse_down: self.mouse_down_handler.clone(), + right_click: self.right_click_handler.clone(), + right_mouse_down: self.right_mouse_down_handler.clone(), drag: self.drag_handler.clone(), }); diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index b32ab952c79dfbef1b1a539bec117463954c25bf..61cfa99bfe0a8b6cc032baf0788eb0b3541f36ba 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -43,6 +43,7 @@ pub enum Event { }, RightMouseUp { position: Vector2F, + click_count: usize, }, NavigateMouseDown { position: Vector2F, @@ -72,7 +73,7 @@ impl Event { | Event::LeftMouseUp { position, .. } | Event::LeftMouseDragged { position } | Event::RightMouseDown { position, .. } - | Event::RightMouseUp { position } + | Event::RightMouseUp { position, .. } | Event::NavigateMouseDown { position, .. } | Event::NavigateMouseUp { position, .. } | Event::MouseMoved { position, .. } => Some(*position), diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 9d07177b16fd322c92973745983396a6c0abf5ea..4d3aa6cf9ac8ff65c2ef92d4acb22ce70a585fe9 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -178,6 +178,7 @@ impl Event { native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), + click_count: native_event.clickCount() as usize, }), NSEventType::NSOtherMouseDown => { let direction = match native_event.buttonNumber() { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 42149feca85278b0f9854b1f6d6db2b37c6536ad..9a8a77258eb87f81f303ae101659fbe50b4fac68 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -33,6 +33,7 @@ pub struct Presenter { last_mouse_moved_event: Option, hovered_region_ids: HashSet, clicked_region: Option, + right_clicked_region: Option, prev_drag_position: Option, titlebar_height: f32, } @@ -58,6 +59,7 @@ impl Presenter { last_mouse_moved_event: None, hovered_region_ids: Default::default(), clicked_region: None, + right_clicked_region: None, prev_drag_position: None, titlebar_height, } @@ -100,6 +102,10 @@ impl Presenter { titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + right_clicked_region_id: self + .right_clicked_region + .as_ref() + .map(MouseRegion::id), refreshing: false, }) .unwrap(), @@ -118,6 +124,10 @@ impl Presenter { titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + right_clicked_region_id: self + .right_clicked_region + .as_ref() + .map(MouseRegion::id), refreshing: true, }) .unwrap(); @@ -181,6 +191,7 @@ impl Presenter { refreshing, hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + right_clicked_region_id: self.right_clicked_region.as_ref().map(MouseRegion::id), titlebar_height: self.titlebar_height, app: cx, } @@ -207,6 +218,7 @@ impl Presenter { let mut hovered_regions = Vec::new(); let mut unhovered_regions = Vec::new(); let mut clicked_region = None; + let mut right_clicked_region = None; let mut dragged_region = None; match event { @@ -233,6 +245,27 @@ impl Presenter { } } } + Event::RightMouseDown { position, .. } => { + for region in self.mouse_regions.iter().rev() { + if region.bounds.contains_point(position) { + invalidated_views.push(region.view_id); + self.right_clicked_region = Some(region.clone()); + break; + } + } + } + Event::RightMouseUp { + position, + click_count, + .. + } => { + if let Some(region) = self.right_clicked_region.take() { + invalidated_views.push(region.view_id); + if region.bounds.contains_point(position) { + right_clicked_region = Some((region, position, click_count)); + } + } + } Event::MouseMoved { position, left_mouse_down, @@ -311,6 +344,14 @@ impl Presenter { } } + if let Some((right_clicked_region, position, click_count)) = right_clicked_region { + if let Some(right_click_callback) = right_clicked_region.right_click { + event_cx.with_current_view(right_clicked_region.view_id, |event_cx| { + right_click_callback(position, click_count, event_cx); + }) + } + } + if let Some((dragged_region, delta)) = dragged_region { if let Some(drag_callback) = dragged_region.drag { event_cx.with_current_view(dragged_region.view_id, |event_cx| { @@ -389,6 +430,7 @@ pub struct LayoutContext<'a> { titlebar_height: f32, hovered_region_ids: HashSet, clicked_region_id: Option, + right_clicked_region_id: Option, } impl<'a> LayoutContext<'a> { @@ -418,6 +460,7 @@ impl<'a> LayoutContext<'a> { titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), clicked_region_id: self.clicked_region_id, + right_clicked_region_id: self.right_clicked_region_id, refreshing: self.refreshing, }; f(view, &mut render_cx) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index caeae3c89fbb0add583ee1f19ad662ee6dcba85d..843a3b62d6309b0a0293a86040763ba6833865e6 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -52,6 +52,8 @@ pub struct MouseRegion { pub hover: Option>, pub mouse_down: Option>, pub click: Option>, + pub right_mouse_down: Option>, + pub right_click: Option>, pub drag: Option>, } From 7c7917494c8e34d9e4bc803722ce9ee402f0c695 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 May 2022 11:20:32 +0200 Subject: [PATCH 29/54] Don't dispatch events down the tree if they were handled by mouse region --- crates/gpui/src/presenter.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 9a8a77258eb87f81f303ae101659fbe50b4fac68..58f6f127af1cd7b3543d41d94b86220c79f0c249 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -320,8 +320,10 @@ impl Presenter { } let mut event_cx = self.build_event_context(cx); + let mut handled = false; for unhovered_region in unhovered_regions { if let Some(hover_callback) = unhovered_region.hover { + handled = true; event_cx.with_current_view(unhovered_region.view_id, |event_cx| { hover_callback(false, event_cx); }) @@ -330,6 +332,7 @@ impl Presenter { for hovered_region in hovered_regions { if let Some(hover_callback) = hovered_region.hover { + handled = true; event_cx.with_current_view(hovered_region.view_id, |event_cx| { hover_callback(true, event_cx); }) @@ -338,6 +341,7 @@ impl Presenter { if let Some((clicked_region, position, click_count)) = clicked_region { if let Some(click_callback) = clicked_region.click { + handled = true; event_cx.with_current_view(clicked_region.view_id, |event_cx| { click_callback(position, click_count, event_cx); }) @@ -346,6 +350,7 @@ impl Presenter { if let Some((right_clicked_region, position, click_count)) = right_clicked_region { if let Some(right_click_callback) = right_clicked_region.right_click { + handled = true; event_cx.with_current_view(right_clicked_region.view_id, |event_cx| { right_click_callback(position, click_count, event_cx); }) @@ -354,13 +359,16 @@ impl Presenter { if let Some((dragged_region, delta)) = dragged_region { if let Some(drag_callback) = dragged_region.drag { + handled = true; event_cx.with_current_view(dragged_region.view_id, |event_cx| { drag_callback(delta, event_cx); }) } } - event_cx.dispatch_event(root_view_id, &event); + if !handled { + event_cx.dispatch_event(root_view_id, &event); + } invalidated_views.extend(event_cx.invalidated_views); let dispatch_directives = event_cx.dispatched_actions; From be0e66ef21fbc95bc318bd730750363e8f166679 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 May 2022 11:20:39 +0200 Subject: [PATCH 30/54] Invoke `mouse_down` and `right_mouse_down` callbacks --- crates/gpui/src/presenter.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 58f6f127af1cd7b3543d41d94b86220c79f0c249..68e624c0ce7acd21b4054a791a701de79f372991 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -217,7 +217,9 @@ impl Presenter { let mut invalidated_views = Vec::new(); let mut hovered_regions = Vec::new(); let mut unhovered_regions = Vec::new(); + let mut mouse_down_region = None; let mut clicked_region = None; + let mut right_mouse_down_region = None; let mut right_clicked_region = None; let mut dragged_region = None; @@ -226,6 +228,7 @@ impl Presenter { for region in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { invalidated_views.push(region.view_id); + mouse_down_region = Some((region.clone(), position)); self.clicked_region = Some(region.clone()); self.prev_drag_position = Some(position); break; @@ -249,6 +252,7 @@ impl Presenter { for region in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { invalidated_views.push(region.view_id); + right_mouse_down_region = Some((region.clone(), position)); self.right_clicked_region = Some(region.clone()); break; } @@ -339,6 +343,15 @@ impl Presenter { } } + if let Some((mouse_down_region, position)) = mouse_down_region { + if let Some(mouse_down_callback) = mouse_down_region.mouse_down { + handled = true; + event_cx.with_current_view(mouse_down_region.view_id, |event_cx| { + mouse_down_callback(position, event_cx); + }) + } + } + if let Some((clicked_region, position, click_count)) = clicked_region { if let Some(click_callback) = clicked_region.click { handled = true; @@ -348,6 +361,15 @@ impl Presenter { } } + if let Some((right_mouse_down_region, position)) = right_mouse_down_region { + if let Some(right_mouse_down_callback) = right_mouse_down_region.right_mouse_down { + handled = true; + event_cx.with_current_view(right_mouse_down_region.view_id, |event_cx| { + right_mouse_down_callback(position, event_cx); + }) + } + } + if let Some((right_clicked_region, position, click_count)) = right_clicked_region { if let Some(right_click_callback) = right_clicked_region.right_click { handled = true; From 98de269b4ac3151f1fd95ad1b0cff557402aab16 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 May 2022 11:36:37 +0200 Subject: [PATCH 31/54] Don't focus editor when clicking on sidebar resize handle --- crates/workspace/src/sidebar.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 5aec332913420cc8beeab4a071b7a95468919d6b..9aaf2b832ab49d6f44ca5fd3293c15f12574aa7f 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -165,6 +165,7 @@ impl Sidebar { ..Default::default() }) .with_cursor_style(CursorStyle::ResizeLeftRight) + .on_mouse_down(|_, _| {}) // This prevents the mouse down event from being propagated elsewhere .on_drag(move |delta, cx| { let prev_width = *actual_width.borrow(); *custom_width.borrow_mut() = 0f32 From 82d6e606fc35e4bc0b50afcb774c8cbd55faf5aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 May 2022 11:43:58 +0200 Subject: [PATCH 32/54] Use a `MouseEventHandler` for activating tabs on mouse down Previously, we were using an `EventHandler` which doesn't take into account other mouse regions floating above the rendered element. This was problematic because, when clicking the `x` icon on a tab that was not active, we were first activating it and then closing it. --- crates/workspace/src/pane.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ba2a21bce4cad4e0ada71ef312366afe7e0f555e..db157ed8503ef619b202c21de86f12e52da13047 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -689,6 +689,7 @@ impl Pane { let theme = cx.global::().theme.clone(); enum Tabs {} + enum Tab {} let pane = cx.handle(); let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { let autoscroll = if mem::take(&mut self.autoscroll) { @@ -717,7 +718,7 @@ impl Pane { style.container.border.left = false; } - EventHandler::new( + MouseEventHandler::new::(ix, cx, |_, cx| { Container::new( Flex::row() .with_child( @@ -807,11 +808,10 @@ impl Pane { .boxed(), ) .with_style(style.container) - .boxed(), - ) - .on_mouse_down(move |cx| { + .boxed() + }) + .on_mouse_down(move |_, cx| { cx.dispatch_action(ActivateItem(ix)); - true }) .boxed() }) From 5413a97c7efa86eb5260f97cd559d0c45d551f90 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 May 2022 11:09:07 -0600 Subject: [PATCH 33/54] Restrict multiple hovered regions to a single stacking context We won't hover regions from stacking contexts that are below the one being hovered. --- crates/gpui/src/presenter.rs | 14 +++++++++----- crates/gpui/src/scene.rs | 15 ++++++++++----- crates/project_panel/src/project_panel.rs | 4 +--- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 87e40db1ea8b347c0fd8d66dde7a06a69d98d4e0..54d5d90d49f91574324153df18ce2538ed5db9d7 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -28,7 +28,7 @@ pub struct Presenter { pub(crate) rendered_views: HashMap, parents: HashMap, cursor_regions: Vec, - mouse_regions: Vec, + mouse_regions: Vec<(MouseRegion, usize)>, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, @@ -230,7 +230,7 @@ impl Presenter { match event { Event::LeftMouseDown { position, .. } => { - for region in self.mouse_regions.iter().rev() { + for (region, _) in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { invalidated_views.push(region.view_id); mouse_down_region = Some((region.clone(), position)); @@ -254,7 +254,7 @@ impl Presenter { } } Event::RightMouseDown { position, .. } => { - for region in self.mouse_regions.iter().rev() { + for (region, _) in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { invalidated_views.push(region.view_id); right_mouse_down_region = Some((region.clone(), position)); @@ -291,9 +291,13 @@ impl Presenter { } cx.platform().set_cursor_style(style_to_assign); - for region in self.mouse_regions.iter().rev() { + let mut hover_depth = None; + for (region, depth) in self.mouse_regions.iter().rev() { let region_id = region.id(); - if region.bounds.contains_point(position) { + if region.bounds.contains_point(position) + && hover_depth.map_or(true, |hover_depth| hover_depth == *depth) + { + hover_depth = Some(*depth); if !self.hovered_region_ids.contains(®ion_id) { invalidated_views.push(region.view_id); hovered_regions.push(region.clone()); diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 843a3b62d6309b0a0293a86040763ba6833865e6..c9891177476ee3e2f40d490f0a9263a6cef8954f 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -210,11 +210,16 @@ impl Scene { .collect() } - pub fn mouse_regions(&self) -> Vec { - self.layers() - .flat_map(|layer| &layer.mouse_regions) - .cloned() - .collect() + pub fn mouse_regions(&self) -> Vec<(MouseRegion, usize)> { + let mut regions = Vec::new(); + for (stacking_depth, stacking_context) in self.stacking_contexts.iter().enumerate() { + for layer in &stacking_context.layers { + for mouse_region in &layer.mouse_regions { + regions.push((mouse_region.clone(), stacking_depth)); + } + } + } + regions } pub fn push_stacking_context(&mut self, clip_bounds: Option) { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2ce233fa21ee79b1390a94dfd50cb9aa2fc4e718..9046917e390d74d6973dcb7c3e8a136112d8ecd2 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -12,7 +12,7 @@ use gpui::{ impl_internal_actions, keymap, platform::CursorStyle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext, - PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; @@ -38,7 +38,6 @@ pub struct ProjectPanel { edit_state: Option, filename_editor: ViewHandle, context_menu: ViewHandle, - handle: WeakViewHandle, } #[derive(Copy, Clone)] @@ -174,7 +173,6 @@ impl ProjectPanel { edit_state: None, filename_editor, context_menu: cx.add_view(|_| ContextMenu::new()), - handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); this From c3baf2748f140670565908fba25735c19ac0e439 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 May 2022 11:54:51 -0600 Subject: [PATCH 34/54] Block hovering behind overlays --- crates/gpui/src/app.rs | 3 +- .../gpui/src/elements/mouse_event_handler.rs | 3 +- crates/gpui/src/elements/overlay.rs | 9 +++-- crates/gpui/src/presenter.rs | 33 ++++++++++--------- crates/gpui/src/scene.rs | 17 ++++------ 5 files changed, 34 insertions(+), 31 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 91c357ddee8eb6fd09238ebdb940e365c77afa82..3575accbeb1b79e5dcbfb5f9aad2c976b597dfb7 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3484,8 +3484,7 @@ impl<'a, V: View> RenderContext<'a, V> { pub fn mouse_state(&self, region_id: usize) -> MouseState { let region_id = MouseRegionId { view_id: self.view_id, - tag: TypeId::of::(), - region_id, + discriminant: (TypeId::of::(), region_id), }; MouseState { hovered: self.hovered_region_ids.contains(®ion_id), diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 1ca4c13fc856e21f886883aa1101dca27ae879d6..ee809746ae79adf586e0f51a61ddc516133d7bad 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -131,8 +131,7 @@ impl Element for MouseEventHandler { cx.scene.push_mouse_region(MouseRegion { view_id: cx.current_view_id(), - tag: self.tag, - region_id: self.id, + discriminant: Some((self.tag, self.id)), bounds: self.hit_bounds(bounds), hover: None, click: self.click_handler.clone(), diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 3d90a7554cd15a641217173974295c5b61e6e086..d841bcbc04ab7d6ab063511408e521024e3bdf8d 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -3,8 +3,8 @@ use serde_json::json; use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, - DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, + PaintContext, SizeConstraint, }; pub struct Overlay { @@ -54,6 +54,11 @@ impl Element for Overlay { let origin = self.abs_position.unwrap_or(bounds.origin()); let visible_bounds = RectF::new(origin, *size); cx.scene.push_stacking_context(None); + cx.scene.push_mouse_region(MouseRegion { + view_id: cx.current_view_id(), + bounds: visible_bounds, + ..Default::default() + }); self.child.paint(origin, visible_bounds, cx); cx.scene.pop_stacking_context(); } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 54d5d90d49f91574324153df18ce2538ed5db9d7..6d2def4716b952195738e6223fea44c026f88829 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -103,11 +103,11 @@ impl Presenter { view_id: *view_id, titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id), right_clicked_region_id: self .right_clicked_region .as_ref() - .map(MouseRegion::id), + .and_then(MouseRegion::id), refreshing: false, }) .unwrap(), @@ -125,11 +125,11 @@ impl Presenter { view_id: *view_id, titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), + clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id), right_clicked_region_id: self .right_clicked_region .as_ref() - .map(MouseRegion::id), + .and_then(MouseRegion::id), refreshing: true, }) .unwrap(); @@ -194,8 +194,8 @@ impl Presenter { view_stack: Vec::new(), refreshing, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id), - right_clicked_region_id: self.right_clicked_region.as_ref().map(MouseRegion::id), + clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id), + right_clicked_region_id: self.right_clicked_region.as_ref().and_then(MouseRegion::id), titlebar_height: self.titlebar_height, window_size, app: cx, @@ -293,21 +293,24 @@ impl Presenter { let mut hover_depth = None; for (region, depth) in self.mouse_regions.iter().rev() { - let region_id = region.id(); if region.bounds.contains_point(position) && hover_depth.map_or(true, |hover_depth| hover_depth == *depth) { hover_depth = Some(*depth); - if !self.hovered_region_ids.contains(®ion_id) { - invalidated_views.push(region.view_id); - hovered_regions.push(region.clone()); - self.hovered_region_ids.insert(region_id); + if let Some(region_id) = region.id() { + if !self.hovered_region_ids.contains(®ion_id) { + invalidated_views.push(region.view_id); + hovered_regions.push(region.clone()); + self.hovered_region_ids.insert(region_id); + } } } else { - if self.hovered_region_ids.contains(®ion_id) { - invalidated_views.push(region.view_id); - unhovered_regions.push(region.clone()); - self.hovered_region_ids.remove(®ion_id); + if let Some(region_id) = region.id() { + if self.hovered_region_ids.contains(®ion_id) { + invalidated_views.push(region.view_id); + unhovered_regions.push(region.clone()); + self.hovered_region_ids.remove(®ion_id); + } } } } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index c9891177476ee3e2f40d490f0a9263a6cef8954f..22762ea9dafc21589d1491ef7e3c4975c325104c 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -43,11 +43,10 @@ pub struct CursorRegion { pub style: CursorStyle, } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct MouseRegion { pub view_id: usize, - pub tag: TypeId, - pub region_id: usize, + pub discriminant: Option<(TypeId, usize)>, pub bounds: RectF, pub hover: Option>, pub mouse_down: Option>, @@ -60,8 +59,7 @@ pub struct MouseRegion { #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct MouseRegionId { pub view_id: usize, - pub tag: TypeId, - pub region_id: usize, + pub discriminant: (TypeId, usize), } #[derive(Default, Debug)] @@ -544,12 +542,11 @@ impl ToJson for Border { } impl MouseRegion { - pub fn id(&self) -> MouseRegionId { - MouseRegionId { + pub fn id(&self) -> Option { + self.discriminant.map(|discriminant| MouseRegionId { view_id: self.view_id, - tag: self.tag, - region_id: self.region_id, - } + discriminant, + }) } } From 9909fc529b9a84e2413d9bdd897b527b1c9aa3f4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 May 2022 12:00:11 -0600 Subject: [PATCH 35/54] Allow context menu to be cancelled after deploying it twice Previously, two right clicks would cause an issue with cancelling the context menu via escape. --- crates/context_menu/src/context_menu.rs | 4 +++- crates/gpui/src/app.rs | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 507a88c5f2df0404a72b2a4ac7ebe9badd532285..dc3eee2ed8ae8c0959896968870882859cbe2864 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -169,7 +169,9 @@ impl ContextMenu { self.items = items.collect(); self.position = position; self.visible = true; - self.previously_focused_view_id = cx.focused_view_id(cx.window_id()); + if !cx.is_self_focused() { + self.previously_focused_view_id = cx.focused_view_id(cx.window_id()); + } cx.focus_self(); } else { self.visible = false; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3575accbeb1b79e5dcbfb5f9aad2c976b597dfb7..788dadec49296ba4dce7fb59a78af24e6b3ddeaf 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3255,6 +3255,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.focus(self.window_id, Some(self.view_id)); } + pub fn is_self_focused(&self) -> bool { + self.app.focused_view_id(self.window_id) == Some(self.view_id) + } + pub fn blur(&mut self) { self.app.focus(self.window_id, None); } From 44c8ee5709db95d69d1a818d4b4a6eda459bb995 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 May 2022 12:56:44 -0600 Subject: [PATCH 36/54] Add mouse down out handlers These will fire whenever the left/right mouse button is pressed down outside a specific region. I'll use these to cancel the context menu in the next commit. --- .../gpui/src/elements/mouse_event_handler.rs | 46 +++++++++++-------- crates/gpui/src/presenter.rs | 33 +++++++++---- crates/gpui/src/scene.rs | 2 + 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index ee809746ae79adf586e0f51a61ddc516133d7bad..edeb5ccc692215fe6cc4bd9ec16b896b02f69866 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -18,11 +18,13 @@ pub struct MouseEventHandler { tag: TypeId, id: usize, cursor_style: Option, - mouse_down_handler: Option>, - click_handler: Option>, - right_mouse_down_handler: Option>, - right_click_handler: Option>, - drag_handler: Option>, + mouse_down: Option>, + click: Option>, + right_mouse_down: Option>, + right_click: Option>, + mouse_down_out: Option>, + right_mouse_down_out: Option>, + drag: Option>, padding: Padding, } @@ -38,11 +40,13 @@ impl MouseEventHandler { tag: TypeId::of::(), child: render_child(cx.mouse_state::(id), cx), cursor_style: None, - mouse_down_handler: None, - click_handler: None, - right_mouse_down_handler: None, - right_click_handler: None, - drag_handler: None, + mouse_down: None, + click: None, + right_mouse_down: None, + right_click: None, + mouse_down_out: None, + right_mouse_down_out: None, + drag: None, padding: Default::default(), } } @@ -56,7 +60,7 @@ impl MouseEventHandler { mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static, ) -> Self { - self.mouse_down_handler = Some(Rc::new(handler)); + self.mouse_down = Some(Rc::new(handler)); self } @@ -64,7 +68,7 @@ impl MouseEventHandler { mut self, handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static, ) -> Self { - self.click_handler = Some(Rc::new(handler)); + self.click = Some(Rc::new(handler)); self } @@ -72,7 +76,7 @@ impl MouseEventHandler { mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static, ) -> Self { - self.right_mouse_down_handler = Some(Rc::new(handler)); + self.right_mouse_down = Some(Rc::new(handler)); self } @@ -80,12 +84,12 @@ impl MouseEventHandler { mut self, handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static, ) -> Self { - self.right_click_handler = Some(Rc::new(handler)); + self.right_click = Some(Rc::new(handler)); self } pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self { - self.drag_handler = Some(Rc::new(handler)); + self.drag = Some(Rc::new(handler)); self } @@ -134,11 +138,13 @@ impl Element for MouseEventHandler { discriminant: Some((self.tag, self.id)), bounds: self.hit_bounds(bounds), hover: None, - click: self.click_handler.clone(), - mouse_down: self.mouse_down_handler.clone(), - right_click: self.right_click_handler.clone(), - right_mouse_down: self.right_mouse_down_handler.clone(), - drag: self.drag_handler.clone(), + click: self.click.clone(), + mouse_down: self.mouse_down.clone(), + right_click: self.right_click.clone(), + right_mouse_down: self.right_mouse_down.clone(), + mouse_down_out: self.mouse_down_out.clone(), + right_mouse_down_out: self.right_mouse_down_out.clone(), + drag: self.drag.clone(), }); self.child.paint(bounds.origin(), visible_bounds, cx); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 6d2def4716b952195738e6223fea44c026f88829..3ff4334f616a4f753589d9ed0c4fea4fa2cc0d39 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -222,6 +222,7 @@ impl Presenter { let mut invalidated_views = Vec::new(); let mut hovered_regions = Vec::new(); let mut unhovered_regions = Vec::new(); + let mut mouse_down_out_handlers = Vec::new(); let mut mouse_down_region = None; let mut clicked_region = None; let mut right_mouse_down_region = None; @@ -230,13 +231,18 @@ impl Presenter { match event { Event::LeftMouseDown { position, .. } => { + let mut hit = false; for (region, _) in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { - invalidated_views.push(region.view_id); - mouse_down_region = Some((region.clone(), position)); - self.clicked_region = Some(region.clone()); - self.prev_drag_position = Some(position); - break; + if !hit { + hit = true; + invalidated_views.push(region.view_id); + mouse_down_region = Some((region.clone(), position)); + self.clicked_region = Some(region.clone()); + self.prev_drag_position = Some(position); + } + } else if let Some(handler) = region.mouse_down_out.clone() { + mouse_down_out_handlers.push((handler, region.view_id, position)); } } } @@ -254,12 +260,17 @@ impl Presenter { } } Event::RightMouseDown { position, .. } => { + let mut hit = false; for (region, _) in self.mouse_regions.iter().rev() { if region.bounds.contains_point(position) { - invalidated_views.push(region.view_id); - right_mouse_down_region = Some((region.clone(), position)); - self.right_clicked_region = Some(region.clone()); - break; + if !hit { + hit = true; + invalidated_views.push(region.view_id); + right_mouse_down_region = Some((region.clone(), position)); + self.right_clicked_region = Some(region.clone()); + } + } else if let Some(handler) = region.right_mouse_down_out.clone() { + mouse_down_out_handlers.push((handler, region.view_id, position)); } } } @@ -355,6 +366,10 @@ impl Presenter { } } + for (handler, view_id, position) in mouse_down_out_handlers { + event_cx.with_current_view(view_id, |event_cx| handler(position, event_cx)) + } + if let Some((mouse_down_region, position)) = mouse_down_region { if let Some(mouse_down_callback) = mouse_down_region.mouse_down { handled = true; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 22762ea9dafc21589d1491ef7e3c4975c325104c..ee9bb8311ff576c1af42119af25e5dd2cfa35a44 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -54,6 +54,8 @@ pub struct MouseRegion { pub right_mouse_down: Option>, pub right_click: Option>, pub drag: Option>, + pub mouse_down_out: Option>, + pub right_mouse_down_out: Option>, } #[derive(Copy, Clone, Eq, PartialEq, Hash)] From fb26f8195b6605b6a2e81d2cb58e1d319d7dfc81 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 28 May 2022 08:45:10 +0200 Subject: [PATCH 37/54] Sort mouse regions by their stacking context's depth --- crates/gpui/src/scene.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index ee9bb8311ff576c1af42119af25e5dd2cfa35a44..1f503c8bf7750cc908a52ce04f926e55e130a3d6 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -20,6 +20,7 @@ pub struct Scene { struct StackingContext { layers: Vec, active_layer_stack: Vec, + depth: usize, } #[derive(Default)] @@ -187,7 +188,7 @@ pub struct Image { impl Scene { pub fn new(scale_factor: f32) -> Self { - let stacking_context = StackingContext::new(None); + let stacking_context = StackingContext::new(0, None); Scene { scale_factor, stacking_contexts: vec![stacking_context], @@ -212,21 +213,23 @@ impl Scene { pub fn mouse_regions(&self) -> Vec<(MouseRegion, usize)> { let mut regions = Vec::new(); - for (stacking_depth, stacking_context) in self.stacking_contexts.iter().enumerate() { + for stacking_context in self.stacking_contexts.iter() { for layer in &stacking_context.layers { for mouse_region in &layer.mouse_regions { - regions.push((mouse_region.clone(), stacking_depth)); + regions.push((mouse_region.clone(), stacking_context.depth)); } } } + regions.sort_by_key(|(_, depth)| *depth); regions } pub fn push_stacking_context(&mut self, clip_bounds: Option) { + let depth = self.active_stacking_context().depth + 1; self.active_stacking_context_stack .push(self.stacking_contexts.len()); self.stacking_contexts - .push(StackingContext::new(clip_bounds)) + .push(StackingContext::new(depth, clip_bounds)) } pub fn pop_stacking_context(&mut self) { @@ -293,10 +296,11 @@ impl Scene { } impl StackingContext { - fn new(clip_bounds: Option) -> Self { + fn new(depth: usize, clip_bounds: Option) -> Self { Self { layers: vec![Layer::new(clip_bounds)], active_layer_stack: vec![0], + depth, } } From e7ab61d12561af1f552a08d9d68551a8e0bf2766 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 28 May 2022 08:51:46 +0200 Subject: [PATCH 38/54] Dismiss context menu when (right-)mousing down outside of it --- crates/context_menu/src/context_menu.rs | 86 ++++++++++--------- .../gpui/src/elements/mouse_event_handler.rs | 16 ++++ 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index dc3eee2ed8ae8c0959896968870882859cbe2864..8ab5b3c4829f1e151ab7bfede4fe48e8240f2876 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -237,49 +237,55 @@ impl ContextMenu { } fn render_menu(&self, cx: &mut RenderContext) -> impl Element { - enum Tag {} + enum Menu {} + enum MenuItem {} let style = cx.global::().theme.context_menu.clone(); - Flex::column() - .with_children(self.items.iter().enumerate().map(|(ix, item)| { - match item { - ContextMenuItem::Item { label, action } => { - let action = action.boxed_clone(); - MouseEventHandler::new::(ix, cx, |state, _| { - let style = - style.item.style_for(state, Some(ix) == self.selected_index); - Flex::row() - .with_child( - Label::new(label.to_string(), style.label.clone()).boxed(), - ) - .with_child({ - KeystrokeLabel::new( - action.boxed_clone(), - style.keystroke.container, - style.keystroke.text.clone(), + MouseEventHandler::new::(0, cx, |_, cx| { + Flex::column() + .with_children(self.items.iter().enumerate().map(|(ix, item)| { + match item { + ContextMenuItem::Item { label, action } => { + let action = action.boxed_clone(); + MouseEventHandler::new::(ix, cx, |state, _| { + let style = + style.item.style_for(state, Some(ix) == self.selected_index); + Flex::row() + .with_child( + Label::new(label.to_string(), style.label.clone()).boxed(), ) - .flex_float() + .with_child({ + KeystrokeLabel::new( + action.boxed_clone(), + style.keystroke.container, + style.keystroke.text.clone(), + ) + .flex_float() + .boxed() + }) + .contained() + .with_style(style.container) .boxed() - }) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, _, cx| { - cx.dispatch_any_action(action.boxed_clone()); - cx.dispatch_action(Cancel); - }) - .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| { + cx.dispatch_any_action(action.boxed_clone()); + cx.dispatch_action(Cancel); + }) + .boxed() + } + ContextMenuItem::Separator => Empty::new() + .constrained() + .with_height(1.) + .contained() + .with_style(style.separator) + .boxed(), } - ContextMenuItem::Separator => Empty::new() - .constrained() - .with_height(1.) - .contained() - .with_style(style.separator) - .boxed(), - } - })) - .contained() - .with_style(style.container) + })) + .contained() + .with_style(style.container) + .boxed() + }) + .on_mouse_down_out(|_, cx| cx.dispatch_action(Cancel)) + .on_right_mouse_down_out(|_, cx| cx.dispatch_action(Cancel)) } } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index edeb5ccc692215fe6cc4bd9ec16b896b02f69866..2ad6eaf028fc9bb71426135a705b9cb4dbab69d0 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -88,6 +88,22 @@ impl MouseEventHandler { self } + pub fn on_mouse_down_out( + mut self, + handler: impl Fn(Vector2F, &mut EventContext) + 'static, + ) -> Self { + self.mouse_down_out = Some(Rc::new(handler)); + self + } + + pub fn on_right_mouse_down_out( + mut self, + handler: impl Fn(Vector2F, &mut EventContext) + 'static, + ) -> Self { + self.right_mouse_down_out = Some(Rc::new(handler)); + self + } + pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self { self.drag = Some(Rc::new(handler)); self From 2b9015c096022a2683477237d8044d8190e873b9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 10:01:23 +0200 Subject: [PATCH 39/54] Introduce `{MutableAppContext,ViewContext}::observe_actions` --- crates/gpui/src/app.rs | 80 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 788dadec49296ba4dce7fb59a78af24e6b3ddeaf..6b94ae40f6e325c98360fae00536724cde5d2e07 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -759,6 +759,7 @@ type ObservationCallback = Box bool>; type FocusObservationCallback = Box bool>; type GlobalObservationCallback = Box; type ReleaseObservationCallback = Box; +type ActionObservationCallback = Box; type DeserializeActionCallback = fn(json: &str) -> anyhow::Result>; pub struct MutableAppContext { @@ -784,6 +785,7 @@ pub struct MutableAppContext { global_observations: Arc>>>>, release_observations: Arc>>>, + action_dispatch_observations: Arc>>, presenters_and_platform_windows: HashMap>, Box)>, foreground: Rc, @@ -836,6 +838,7 @@ impl MutableAppContext { focus_observations: Default::default(), release_observations: Default::default(), global_observations: Default::default(), + action_dispatch_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), foreground, pending_effects: VecDeque::new(), @@ -1320,6 +1323,20 @@ impl MutableAppContext { } } + pub fn observe_actions(&mut self, callback: F) -> Subscription + where + F: 'static + FnMut(TypeId, &mut MutableAppContext), + { + let id = post_inc(&mut self.next_subscription_id); + self.action_dispatch_observations + .lock() + .insert(id, Box::new(callback)); + Subscription::ActionObservation { + id, + observations: Some(Arc::downgrade(&self.action_dispatch_observations)), + } + } + pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) { self.pending_effects.push_back(Effect::Deferred { callback: Box::new(callback), @@ -1513,6 +1530,11 @@ impl MutableAppContext { if !this.halt_action_dispatch { this.halt_action_dispatch = this.dispatch_global_action_any(action); } + + this.pending_effects + .push_back(Effect::ActionDispatchNotification { + action_id: action.id(), + }); this.halt_action_dispatch }) } @@ -1961,6 +1983,9 @@ impl MutableAppContext { Effect::RefreshWindows => { refreshing = true; } + Effect::ActionDispatchNotification { action_id } => { + self.handle_action_dispatch_notification_effect(action_id) + } } self.pending_notifications.clear(); self.remove_dropped_entities(); @@ -2402,6 +2427,14 @@ impl MutableAppContext { }) } + fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) { + let mut callbacks = mem::take(&mut *self.action_dispatch_observations.lock()); + for (_, callback) in &mut callbacks { + callback(action_id, self); + } + self.action_dispatch_observations.lock().extend(callbacks); + } + pub fn focus(&mut self, window_id: usize, view_id: Option) { if let Some(pending_focus_index) = self.pending_focus_index { self.pending_effects.remove(pending_focus_index); @@ -2776,6 +2809,9 @@ pub enum Effect { is_active: bool, }, RefreshWindows, + ActionDispatchNotification { + action_id: TypeId, + }, } impl Debug for Effect { @@ -2852,6 +2888,10 @@ impl Debug for Effect { .field("view_id", view_id) .field("subscription_id", subscription_id) .finish(), + Effect::ActionDispatchNotification { action_id, .. } => f + .debug_struct("Effect::ActionDispatchNotification") + .field("action_id", action_id) + .finish(), Effect::ResizeWindow { window_id } => f .debug_struct("Effect::RefreshWindow") .field("window_id", window_id) @@ -3376,6 +3416,20 @@ impl<'a, T: View> ViewContext<'a, T> { }) } + pub fn observe_actions(&mut self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut T, TypeId, &mut ViewContext), + { + let observer = self.weak_handle(); + self.app.observe_actions(move |action_id, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, action_id, cx); + }); + } + }) + } + pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.view_id, @@ -4682,6 +4736,10 @@ pub enum Subscription { observations: Option>>>>, }, + ActionObservation { + id: usize, + observations: Option>>>, + }, } impl Subscription { @@ -4705,6 +4763,9 @@ impl Subscription { Subscription::FocusObservation { observations, .. } => { observations.take(); } + Subscription::ActionObservation { observations, .. } => { + observations.take(); + } } } } @@ -4813,6 +4874,11 @@ impl Drop for Subscription { } } } + Subscription::ActionObservation { id, observations } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + observations.lock().remove(&id); + } + } } } } @@ -6246,7 +6312,7 @@ mod tests { } } - #[derive(Clone, Deserialize)] + #[derive(Clone, Default, Deserialize)] pub struct Action(pub String); impl_actions!(test, [Action]); @@ -6311,6 +6377,13 @@ mod tests { let view_3 = cx.add_view(window_id, |_| ViewA { id: 3 }); let view_4 = cx.add_view(window_id, |_| ViewB { id: 4 }); + let observed_actions = Rc::new(RefCell::new(Vec::new())); + cx.observe_actions({ + let observed_actions = observed_actions.clone(); + move |action_id, _| observed_actions.borrow_mut().push(action_id) + }) + .detach(); + cx.dispatch_action( window_id, vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], @@ -6331,6 +6404,7 @@ mod tests { "1 b" ] ); + assert_eq!(*observed_actions.borrow(), [Action::default().id()]); // Remove view_1, which doesn't propagate the action actions.borrow_mut().clear(); @@ -6353,6 +6427,10 @@ mod tests { "global" ] ); + assert_eq!( + *observed_actions.borrow(), + [Action::default().id(), Action::default().id()] + ); } #[crate::test(self)] From 63900612b0929de12845dbd6e055957dd1ee7ef3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 10:01:49 +0200 Subject: [PATCH 40/54] Dismiss context menu when one of its action is dispatched --- crates/context_menu/src/context_menu.rs | 53 ++++++++++++++++++++--- crates/project_panel/src/project_panel.rs | 2 +- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 8ab5b3c4829f1e151ab7bfede4fe48e8240f2876..33ccb35dbe97fc4d7b35fec2a575a0d82dff8335 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,6 +1,9 @@ +use std::{any::TypeId, time::Duration}; + use gpui::{ elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext, - Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, View, ViewContext, + Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, + ViewContext, }; use menu::*; use settings::Settings; @@ -37,15 +40,22 @@ impl ContextMenuItem { fn is_separator(&self) -> bool { matches!(self, Self::Separator) } + + fn action_id(&self) -> Option { + match self { + ContextMenuItem::Item { action, .. } => Some(action.id()), + ContextMenuItem::Separator => None, + } + } } -#[derive(Default)] pub struct ContextMenu { position: Vector2F, items: Vec, selected_index: Option, visible: bool, previously_focused_view_id: Option, + _actions_observation: Subscription, } impl Entity for ContextMenu { @@ -87,15 +97,36 @@ impl View for ContextMenu { } fn on_blur(&mut self, cx: &mut ViewContext) { - self.visible = false; - self.selected_index.take(); - cx.notify(); + self.reset(cx); } } impl ContextMenu { - pub fn new() -> Self { - Default::default() + pub fn new(cx: &mut ViewContext) -> Self { + Self { + position: Default::default(), + items: Default::default(), + selected_index: Default::default(), + visible: Default::default(), + previously_focused_view_id: Default::default(), + _actions_observation: cx.observe_actions(Self::action_dispatched), + } + } + + fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext) { + if let Some(ix) = self + .items + .iter() + .position(|item| item.action_id() == Some(action_id)) + { + self.selected_index = Some(ix); + cx.notify(); + cx.spawn(|this, mut cx| async move { + cx.background().timer(Duration::from_millis(100)).await; + this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx)); + }) + .detach(); + } } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { @@ -109,12 +140,20 @@ impl ContextMenu { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.reset(cx); if cx.handle().is_focused(cx) { let window_id = cx.window_id(); (**cx).focus(window_id, self.previously_focused_view_id.take()); } } + fn reset(&mut self, cx: &mut ViewContext) { + self.items.clear(); + self.visible = false; + self.selected_index.take(); + cx.notify(); + } + fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { self.selected_index = self.items.iter().position(|item| !item.is_separator()); cx.notify(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9046917e390d74d6973dcb7c3e8a136112d8ecd2..2eab508fe027e6f6641729bafcc85906ea2b037c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -172,7 +172,7 @@ impl ProjectPanel { selection: None, edit_state: None, filename_editor, - context_menu: cx.add_view(|_| ContextMenu::new()), + context_menu: cx.add_view(|cx| ContextMenu::new(cx)), }; this.update_visible_entries(None, cx); this From 6c145b2abcc84fbed70e505134e2b05c353aa698 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 12:23:03 +0200 Subject: [PATCH 41/54] Show keystrokes as uppercase --- assets/keymaps/default.json | 3 +++ crates/gpui/src/elements/keystroke_label.rs | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index f21138e541cf879511017ab5b1caa3217843d550..47b7cac7043b43f34a4316dfcd6f46ce5ec840b6 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -352,6 +352,9 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", + "cmd-x": "project_panel::Cut", + "cmd-c": "project_panel::Copy", + "cmd-v": "project_panel::Paste", "cmd-alt-c": "project_panel::CopyPath", "f2": "project_panel::Rename", "backspace": "project_panel::Delete" diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs index 0112b548463b450c0b49aa5e1ad4c93f633a5d10..2cd55e5ff0238a8fc801a801d9b710edaab33db4 100644 --- a/crates/gpui/src/elements/keystroke_label.rs +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -40,10 +40,13 @@ impl Element for KeystrokeLabel { let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) { Flex::row() .with_children(keystrokes.iter().map(|keystroke| { - Label::new(keystroke.to_string(), self.text_style.clone()) - .contained() - .with_style(self.container_style) - .boxed() + Label::new( + keystroke.to_string().to_uppercase(), + self.text_style.clone(), + ) + .contained() + .with_style(self.container_style) + .boxed() })) .boxed() } else { From 37a0c7f0467ef9a681773229680f35264bba5787 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 12:23:21 +0200 Subject: [PATCH 42/54] Implement cut/paste for `ProjectPanel` --- crates/project_panel/src/project_panel.rs | 124 ++++++++++++++++++++-- crates/theme/src/theme.rs | 1 + styles/src/styleTree/projectPanel.ts | 1 + 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2eab508fe027e6f6641729bafcc85906ea2b037c..bd678f37ac53b54c72fc69803cd8675e8d8d4231 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -22,7 +22,7 @@ use std::{ collections::{hash_map, HashMap}, ffi::OsStr, ops::Range, - path::PathBuf, + path::{Path, PathBuf}, }; use unicase::UniCase; use workspace::Workspace; @@ -37,6 +37,7 @@ pub struct ProjectPanel { selection: Option, edit_state: Option, filename_editor: ViewHandle, + clipboard_entry: Option, context_menu: ViewHandle, } @@ -55,6 +56,18 @@ struct EditState { processing_filename: Option, } +#[derive(Copy, Clone)] +pub enum ClipboardEntry { + Copied { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, + Cut { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, +} + #[derive(Debug, PartialEq, Eq)] struct EntryDetails { filename: String, @@ -65,6 +78,7 @@ struct EntryDetails { is_selected: bool, is_editing: bool, is_processing: bool, + is_cut: bool, } #[derive(Clone)] @@ -116,7 +130,11 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::copy); cx.add_action(ProjectPanel::copy_path); cx.add_action(ProjectPanel::cut); - cx.add_action(ProjectPanel::paste); + cx.add_action( + |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { + this.paste(action, cx); + }, + ); } pub enum Event { @@ -172,6 +190,7 @@ impl ProjectPanel { selection: None, edit_state: None, filename_editor, + clipboard_entry: None, context_menu: cx.add_view(|cx| ContextMenu::new(cx)), }; this.update_visible_entries(None, cx); @@ -239,6 +258,11 @@ impl ProjectPanel { menu_entries.push(ContextMenuItem::item("Copy", Copy)); menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath)); menu_entries.push(ContextMenuItem::item("Cut", Cut)); + if let Some(clipboard_entry) = self.clipboard_entry { + if clipboard_entry.worktree_id() == worktree.id() { + menu_entries.push(ContextMenuItem::item("Paste", Paste)); + } + } menu_entries.push(ContextMenuItem::Separator); menu_entries.push(ContextMenuItem::item("Rename", Rename)); if !is_root { @@ -608,15 +632,75 @@ impl ProjectPanel { } fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - todo!() + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Cut { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } } fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - todo!() + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Copied { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } } - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - todo!() + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { + if let Some((worktree, entry)) = self.selected_entry(cx) { + let clipboard_entry = self.clipboard_entry?; + if clipboard_entry.worktree_id() != worktree.id() { + return None; + } + + let clipboard_entry_file_name = self + .project + .read(cx) + .path_for_entry(clipboard_entry.entry_id(), cx)? + .path + .file_name()? + .to_os_string(); + + let mut new_path = entry.path.to_path_buf(); + if entry.is_file() { + new_path.pop(); + } + + new_path.push(&clipboard_entry_file_name); + let extension = new_path.extension().map(|e| e.to_os_string()); + let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?; + let mut ix = 0; + while worktree.entry_for_path(&new_path).is_some() { + new_path.pop(); + + let mut new_file_name = file_name_without_extension.to_os_string(); + new_file_name.push(" copy"); + if ix > 0 { + new_file_name.push(format!(" {}", ix)); + } + new_path.push(new_file_name); + if let Some(extension) = extension.as_ref() { + new_path.set_extension(&extension); + } + ix += 1; + } + + self.clipboard_entry.take(); + if clipboard_entry.is_cut() { + self.project + .update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .map(|task| task.detach_and_log_err(cx)); + } else { + } + } + None } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { @@ -834,6 +918,9 @@ impl ProjectPanel { }), is_editing: false, is_processing: false, + is_cut: self + .clipboard_entry + .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), }; if let Some(edit_state) = &self.edit_state { let is_edited_entry = if edit_state.is_new_entry { @@ -878,6 +965,10 @@ impl ProjectPanel { style.text.color.fade_out(theme.ignored_entry_fade); style.icon_color.fade_out(theme.ignored_entry_fade); } + if details.is_cut { + style.text.color.fade_out(theme.cut_entry_fade); + style.icon_color.fade_out(theme.cut_entry_fade); + } let row_container_style = if show_editor { theme.filename_editor.container } else { @@ -1018,6 +1109,27 @@ impl workspace::sidebar::SidebarItem for ProjectPanel { } } +impl ClipboardEntry { + fn is_cut(&self) -> bool { + matches!(self, Self::Cut { .. }) + } + + fn entry_id(&self) -> ProjectEntryId { + match self { + ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => { + *entry_id + } + } + } + + fn worktree_id(&self) -> WorktreeId { + match self { + ClipboardEntry::Copied { worktree_id, .. } + | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 2e38e501e26beefc6d4a0f5484f4e4e39c0616b0..a8c28f41f37f82f6aa63f13bc694ebe522e085e3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -224,6 +224,7 @@ pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, pub entry: Interactive, + pub cut_entry_fade: f32, pub ignored_entry_fade: f32, pub filename_editor: FieldEditor, pub indent_width: f32, diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 2f3e3eea72195e415a2a536bc959414f857b30b9..f68f69711cfabd6c35746e0136291b323aeac6c4 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -26,6 +26,7 @@ export default function projectPanel(theme: Theme) { text: text(theme, "mono", "active", { size: "sm" }), } }, + cutEntryFade: 0.4, ignoredEntryFade: 0.6, filenameEditor: { background: backgroundColor(theme, 500, "active"), From 3336bc6ab35d495e5df25df67bc6eda0e9cdd62a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 14:52:34 +0200 Subject: [PATCH 43/54] Implement copy paste for ProjectPanel --- crates/collab/src/rpc.rs | 1 + crates/project/src/fs.rs | 70 ++++++++++++ crates/project/src/project.rs | 72 ++++++++++++ crates/project/src/worktree.rs | 40 +++++++ crates/project_panel/src/project_panel.rs | 5 + crates/rpc/proto/zed.proto | 127 ++++++++++++---------- crates/rpc/src/proto.rs | 5 +- 7 files changed, 259 insertions(+), 61 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7ea925fce7d8dea824aeb24b6be936f0099655b0..a09a2b1f3380f86679810571529acfbe5b4433ff 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -172,6 +172,7 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 7da2a38a83bc2d5a15a1d369024314508ff29f2c..2eec02d66dcd053e2f3eeb8196cbc0f2563dc47b 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -15,6 +15,7 @@ use text::Rope; pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; @@ -44,6 +45,12 @@ pub struct CreateOptions { pub ignore_if_exists: bool, } +#[derive(Copy, Clone, Default)] +pub struct CopyOptions { + pub overwrite: bool, + pub ignore_if_exists: bool, +} + #[derive(Copy, Clone, Default)] pub struct RenameOptions { pub overwrite: bool, @@ -84,6 +91,35 @@ impl Fs for RealFs { Ok(()) } + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { + if !options.overwrite && smol::fs::metadata(target).await.is_ok() { + if options.ignore_if_exists { + return Ok(()); + } else { + return Err(anyhow!("{target:?} already exists")); + } + } + + let metadata = smol::fs::metadata(source).await?; + let _ = smol::fs::remove_dir_all(target).await; + if metadata.is_dir() { + self.create_dir(target).await?; + let mut children = smol::fs::read_dir(source).await?; + while let Some(child) = children.next().await { + if let Ok(child) = child { + let child_source_path = child.path(); + let child_target_path = target.join(child.file_name()); + self.copy(&child_source_path, &child_target_path, options) + .await?; + } + } + } else { + smol::fs::copy(source, target).await?; + } + + Ok(()) + } + async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { @@ -511,6 +547,40 @@ impl Fs for FakeFs { Ok(()) } + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { + let source = normalize_path(source); + let target = normalize_path(target); + + let mut state = self.state.lock().await; + state.validate_path(&source)?; + state.validate_path(&target)?; + + if !options.overwrite && state.entries.contains_key(&target) { + if options.ignore_if_exists { + return Ok(()); + } else { + return Err(anyhow!("{target:?} already exists")); + } + } + + let mut new_entries = Vec::new(); + for (path, entry) in &state.entries { + if let Ok(relative_path) = path.strip_prefix(&source) { + new_entries.push((relative_path.to_path_buf(), entry.clone())); + } + } + + let mut events = Vec::new(); + for (relative_path, entry) in new_entries { + let new_path = normalize_path(&target.join(relative_path)); + events.push(new_path.clone()); + state.entries.insert(new_path, entry); + } + + state.emit_event(&events).await; + Ok(()) + } + async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> { let dir_path = normalize_path(dir_path); let mut state = self.state.lock().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index abcd667293478ab1d13673d657e4e476852c5c38..923c188ffc603c06916db20a14fb7e6f36654edc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -281,6 +281,7 @@ impl Project { client.add_model_message_handler(Self::handle_update_worktree); client.add_model_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry); + client.add_model_request_handler(Self::handle_copy_project_entry); client.add_model_request_handler(Self::handle_delete_project_entry); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); @@ -778,6 +779,49 @@ impl Project { } } + pub fn copy_entry( + &mut self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let new_path = new_path.into(); + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .copy_entry(entry_id, new_path, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::CopyProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + new_path: new_path.as_os_str().as_bytes().to_vec(), + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + pub fn rename_entry( &mut self, entry_id: ProjectEntryId, @@ -4027,6 +4071,34 @@ impl Project { }) } + async fn handle_copy_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path)); + worktree + .as_local_mut() + .unwrap() + .copy_entry(entry_id, new_path, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + async fn handle_delete_project_entry( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 039ee0d838a630f17c639ea26b55a593a281c420..8eef61f2136411a84d7f540a9d4c6d3bf694de1f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -774,6 +774,46 @@ impl LocalWorktree { })) } + pub fn copy_entry( + &self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let old_path = self.entry_for_id(entry_id)?.path.clone(); + let new_path = new_path.into(); + let abs_old_path = self.absolutize(&old_path); + let abs_new_path = self.absolutize(&new_path); + let copy = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_new_path = abs_new_path.clone(); + async move { + fs.copy(&abs_old_path, &abs_new_path, Default::default()) + .await + } + }); + + Some(cx.spawn(|this, mut cx| async move { + copy.await?; + let entry = this + .update(&mut cx, |this, cx| { + this.as_local_mut().unwrap().refresh_entry( + new_path.clone(), + abs_new_path, + None, + cx, + ) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; + Ok(entry) + })) + } + fn write_entry_internal( &self, path: impl Into>, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bd678f37ac53b54c72fc69803cd8675e8d8d4231..6cc59728e642fdf7aad3fc60a241e4198dc073d3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -698,6 +698,11 @@ impl ProjectPanel { }) .map(|task| task.detach_and_log_err(cx)); } else { + self.project + .update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .map(|task| task.detach_and_log_err(cx)); } } None diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 0fee451c0d6a64df089592bc50a0ce9172e05ef7..2f6ed1f31808d2ab7ee5b6855155f5e0ebb6b162 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -41,66 +41,67 @@ message Envelope { CreateProjectEntry create_project_entry = 33; RenameProjectEntry rename_project_entry = 34; - DeleteProjectEntry delete_project_entry = 35; - ProjectEntryResponse project_entry_response = 36; - - UpdateDiagnosticSummary update_diagnostic_summary = 37; - StartLanguageServer start_language_server = 38; - UpdateLanguageServer update_language_server = 39; - - OpenBufferById open_buffer_by_id = 40; - OpenBufferByPath open_buffer_by_path = 41; - OpenBufferResponse open_buffer_response = 42; - UpdateBuffer update_buffer = 43; - UpdateBufferFile update_buffer_file = 44; - SaveBuffer save_buffer = 45; - BufferSaved buffer_saved = 46; - BufferReloaded buffer_reloaded = 47; - ReloadBuffers reload_buffers = 48; - ReloadBuffersResponse reload_buffers_response = 49; - FormatBuffers format_buffers = 50; - FormatBuffersResponse format_buffers_response = 51; - GetCompletions get_completions = 52; - GetCompletionsResponse get_completions_response = 53; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 54; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 55; - GetCodeActions get_code_actions = 56; - GetCodeActionsResponse get_code_actions_response = 57; - ApplyCodeAction apply_code_action = 58; - ApplyCodeActionResponse apply_code_action_response = 59; - PrepareRename prepare_rename = 60; - PrepareRenameResponse prepare_rename_response = 61; - PerformRename perform_rename = 62; - PerformRenameResponse perform_rename_response = 63; - SearchProject search_project = 64; - SearchProjectResponse search_project_response = 65; - - GetChannels get_channels = 66; - GetChannelsResponse get_channels_response = 67; - JoinChannel join_channel = 68; - JoinChannelResponse join_channel_response = 69; - LeaveChannel leave_channel = 70; - SendChannelMessage send_channel_message = 71; - SendChannelMessageResponse send_channel_message_response = 72; - ChannelMessageSent channel_message_sent = 73; - GetChannelMessages get_channel_messages = 74; - GetChannelMessagesResponse get_channel_messages_response = 75; - - UpdateContacts update_contacts = 76; - UpdateInviteInfo update_invite_info = 77; - ShowContacts show_contacts = 78; - - GetUsers get_users = 79; - FuzzySearchUsers fuzzy_search_users = 80; - UsersResponse users_response = 81; - RequestContact request_contact = 82; - RespondToContactRequest respond_to_contact_request = 83; - RemoveContact remove_contact = 84; - - Follow follow = 85; - FollowResponse follow_response = 86; - UpdateFollowers update_followers = 87; - Unfollow unfollow = 88; + CopyProjectEntry copy_project_entry = 35; + DeleteProjectEntry delete_project_entry = 36; + ProjectEntryResponse project_entry_response = 37; + + UpdateDiagnosticSummary update_diagnostic_summary = 38; + StartLanguageServer start_language_server = 39; + UpdateLanguageServer update_language_server = 40; + + OpenBufferById open_buffer_by_id = 41; + OpenBufferByPath open_buffer_by_path = 42; + OpenBufferResponse open_buffer_response = 43; + UpdateBuffer update_buffer = 44; + UpdateBufferFile update_buffer_file = 45; + SaveBuffer save_buffer = 46; + BufferSaved buffer_saved = 47; + BufferReloaded buffer_reloaded = 48; + ReloadBuffers reload_buffers = 49; + ReloadBuffersResponse reload_buffers_response = 50; + FormatBuffers format_buffers = 51; + FormatBuffersResponse format_buffers_response = 52; + GetCompletions get_completions = 53; + GetCompletionsResponse get_completions_response = 54; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 55; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 56; + GetCodeActions get_code_actions = 57; + GetCodeActionsResponse get_code_actions_response = 58; + ApplyCodeAction apply_code_action = 59; + ApplyCodeActionResponse apply_code_action_response = 60; + PrepareRename prepare_rename = 61; + PrepareRenameResponse prepare_rename_response = 62; + PerformRename perform_rename = 63; + PerformRenameResponse perform_rename_response = 64; + SearchProject search_project = 65; + SearchProjectResponse search_project_response = 66; + + GetChannels get_channels = 67; + GetChannelsResponse get_channels_response = 68; + JoinChannel join_channel = 69; + JoinChannelResponse join_channel_response = 70; + LeaveChannel leave_channel = 71; + SendChannelMessage send_channel_message = 72; + SendChannelMessageResponse send_channel_message_response = 73; + ChannelMessageSent channel_message_sent = 74; + GetChannelMessages get_channel_messages = 75; + GetChannelMessagesResponse get_channel_messages_response = 76; + + UpdateContacts update_contacts = 77; + UpdateInviteInfo update_invite_info = 78; + ShowContacts show_contacts = 79; + + GetUsers get_users = 80; + FuzzySearchUsers fuzzy_search_users = 81; + UsersResponse users_response = 82; + RequestContact request_contact = 83; + RespondToContactRequest respond_to_contact_request = 84; + RemoveContact remove_contact = 85; + + Follow follow = 86; + FollowResponse follow_response = 87; + UpdateFollowers update_followers = 88; + Unfollow unfollow = 89; } } @@ -210,6 +211,12 @@ message RenameProjectEntry { bytes new_path = 3; } +message CopyProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; + bytes new_path = 3; +} + message DeleteProjectEntry { uint64 project_id = 1; uint64 entry_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 67a12fcd87b3bed0fe70c1c4974b238ec9a111b0..7fe715064fa3b505403056d941d75f0cffcf1a30 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -84,6 +84,7 @@ messages!( (BufferSaved, Foreground), (RemoveContact, Foreground), (ChannelMessageSent, Foreground), + (CopyProjectEntry, Foreground), (CreateProjectEntry, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), @@ -167,6 +168,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (DeleteProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), @@ -211,8 +213,8 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + CopyProjectEntry, CreateProjectEntry, - RenameProjectEntry, DeleteProjectEntry, Follow, FormatBuffers, @@ -233,6 +235,7 @@ entity_messages!( ProjectUnshared, ReloadBuffers, RemoveProjectCollaborator, + RenameProjectEntry, RequestJoinProject, SaveBuffer, SearchProject, From 51adc6517e2d7cb15159b3dfb918ab6e73007cf4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 14:53:10 +0200 Subject: [PATCH 44/54] WIP: start on an integration test for `copy_entry` --- crates/collab/src/rpc.rs | 78 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index a09a2b1f3380f86679810571529acfbe5b4433ff..ed97001e98e9f255e40738ef2dd1e104cc0d6cfd 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2581,6 +2581,76 @@ mod tests { ); }); + project_b + .update(cx_b, |project, cx| { + project + .create_entry((worktree_id, "DIR/e.txt"), false, cx) + .unwrap() + }) + .await + .unwrap(); + project_b + .update(cx_b, |project, cx| { + project + .create_entry((worktree_id, "DIR/SUBDIR"), true, cx) + .unwrap() + }) + .await + .unwrap(); + project_b + .update(cx_b, |project, cx| { + project + .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) + .unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "a.txt", "b.txt", "d.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "a.txt", "b.txt", "d.txt"] + ); + }); + + project_b + .update(cx_b, |project, cx| { + project + .copy_entry(dir_entry.id, Path::new("DIR2"), cx) + .unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"] + ); + }); + project_b .update(cx_b, |project, cx| { project.delete_entry(dir_entry.id, cx).unwrap() @@ -2593,7 +2663,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - ["a.txt", "b.txt", "d.txt"] + ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2602,7 +2672,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - ["a.txt", "b.txt", "d.txt"] + ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"] ); }); @@ -2618,7 +2688,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - ["a.txt", "b.txt"] + ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2627,7 +2697,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - ["a.txt", "b.txt"] + ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt"] ); }); } From 88fdd8606a3765eace1d930cf3679e3e871138f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 18:01:46 +0200 Subject: [PATCH 45/54] Eagerly populate child entries when copying a directory via RPC --- crates/project/src/fs.rs | 42 +++++++++++++++++------- crates/project/src/project.rs | 29 +++++++++++----- crates/project/src/worktree.rs | 60 +++++++++++++++++++++++++--------- crates/rpc/proto/zed.proto | 3 +- 4 files changed, 98 insertions(+), 36 deletions(-) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 2eec02d66dcd053e2f3eeb8196cbc0f2563dc47b..a92516e3b9353f9f36caa9e402aafe17a5cecaca 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -15,7 +15,12 @@ use text::Rope; pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; - async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; + async fn copy( + &self, + source: &Path, + target: &Path, + options: CopyOptions, + ) -> Result>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; @@ -91,15 +96,21 @@ impl Fs for RealFs { Ok(()) } - async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { + async fn copy( + &self, + source: &Path, + target: &Path, + options: CopyOptions, + ) -> Result> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { - return Ok(()); + return Ok(Default::default()); } else { return Err(anyhow!("{target:?} already exists")); } } + let mut paths = vec![target.to_path_buf()]; let metadata = smol::fs::metadata(source).await?; let _ = smol::fs::remove_dir_all(target).await; if metadata.is_dir() { @@ -109,15 +120,17 @@ impl Fs for RealFs { if let Ok(child) = child { let child_source_path = child.path(); let child_target_path = target.join(child.file_name()); - self.copy(&child_source_path, &child_target_path, options) - .await?; + paths.extend( + self.copy(&child_source_path, &child_target_path, options) + .await?, + ); } } } else { smol::fs::copy(source, target).await?; } - Ok(()) + Ok(paths) } async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { @@ -547,7 +560,12 @@ impl Fs for FakeFs { Ok(()) } - async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { + async fn copy( + &self, + source: &Path, + target: &Path, + options: CopyOptions, + ) -> Result> { let source = normalize_path(source); let target = normalize_path(target); @@ -557,7 +575,7 @@ impl Fs for FakeFs { if !options.overwrite && state.entries.contains_key(&target) { if options.ignore_if_exists { - return Ok(()); + return Ok(Default::default()); } else { return Err(anyhow!("{target:?} already exists")); } @@ -570,15 +588,15 @@ impl Fs for FakeFs { } } - let mut events = Vec::new(); + let mut paths = Vec::new(); for (relative_path, entry) in new_entries { let new_path = normalize_path(&target.join(relative_path)); - events.push(new_path.clone()); + paths.push(new_path.clone()); state.entries.insert(new_path, entry); } - state.emit_event(&events).await; - Ok(()) + state.emit_event(&paths).await; + Ok(paths) } async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 923c188ffc603c06916db20a14fb7e6f36654edc..5346a869653e4fe5f5f9b884c798394b9d6d3afc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -784,7 +784,7 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { + ) -> Option)>>> { let worktree = self.worktree_for_entry(entry_id, cx)?; let new_path = new_path.into(); if self.is_local() { @@ -809,15 +809,24 @@ impl Project { let entry = response .entry .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + let (entry, child_entries) = worktree.update(&mut cx, |worktree, cx| { + let worktree = worktree.as_remote().unwrap(); + let root_entry = + worktree.insert_entry(entry, response.worktree_scan_id as usize, cx); + let mut child_entries = Vec::new(); + for entry in response.child_entries { + child_entries.push(worktree.insert_entry( entry, response.worktree_scan_id as usize, cx, - ) - }) - .await + )); + } + (root_entry, child_entries) + }); + Ok(( + entry.await?, + futures::future::try_join_all(child_entries).await?, + )) })) } } @@ -4039,6 +4048,7 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), + child_entries: Default::default(), worktree_scan_id: worktree_scan_id as u64, }) } @@ -4067,6 +4077,7 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), + child_entries: Default::default(), worktree_scan_id: worktree_scan_id as u64, }) } @@ -4083,7 +4094,7 @@ impl Project { .ok_or_else(|| anyhow!("worktree not found")) })?; let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); - let entry = worktree + let (entry, child_entries) = worktree .update(&mut cx, |worktree, cx| { let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path)); worktree @@ -4095,6 +4106,7 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), + child_entries: child_entries.iter().map(Into::into).collect(), worktree_scan_id: worktree_scan_id as u64, }) } @@ -4122,6 +4134,7 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: None, + child_entries: Default::default(), worktree_scan_id: worktree_scan_id as u64, }) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8eef61f2136411a84d7f540a9d4c6d3bf694de1f..a4e36da7497ac7fb587a07f08843ad0af9921d9f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -779,7 +779,7 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { + ) -> Option)>>> { let old_path = self.entry_for_id(entry_id)?.path.clone(); let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); @@ -794,23 +794,38 @@ impl LocalWorktree { }); Some(cx.spawn(|this, mut cx| async move { - copy.await?; - let entry = this - .update(&mut cx, |this, cx| { - this.as_local_mut().unwrap().refresh_entry( - new_path.clone(), - abs_new_path, - None, - cx, - ) - }) - .await?; + let copied_paths = copy.await?; + let (entry, child_entries) = this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + let root_entry = + this.refresh_entry(new_path.clone(), abs_new_path.clone(), None, cx); + + let mut child_entries = Vec::new(); + for copied_path in copied_paths { + if copied_path != abs_new_path { + let relative_copied_path = copied_path.strip_prefix(this.abs_path())?; + child_entries.push(this.refresh_entry( + relative_copied_path.into(), + copied_path, + None, + cx, + )); + } + } + + anyhow::Ok((root_entry, child_entries)) + })?; + let (entry, child_entries) = ( + entry.await?, + futures::future::try_join_all(child_entries).await?, + ); + this.update(&mut cx, |this, cx| { this.poll_snapshot(cx); this.as_local().unwrap().broadcast_snapshot() }) .await; - Ok(entry) + Ok((entry, child_entries)) })) } @@ -1202,8 +1217,23 @@ impl Snapshot { } fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool { - if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) { - self.entries_by_path.remove(&PathKey(entry.path), &()); + if let Some(removed_entry) = self.entries_by_id.remove(&entry_id, &()) { + self.entries_by_path = { + let mut cursor = self.entries_by_path.cursor(); + let mut new_entries_by_path = + cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &()); + while let Some(entry) = cursor.item() { + if entry.path.starts_with(&removed_entry.path) { + self.entries_by_id.remove(&entry.id, &()); + cursor.next(&()); + } else { + break; + } + } + new_entries_by_path.push_tree(cursor.suffix(&()), &()); + new_entries_by_path + }; + true } else { false diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 2f6ed1f31808d2ab7ee5b6855155f5e0ebb6b162..e72cce6b6d2da086e97a19517346df1660fb414d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -224,7 +224,8 @@ message DeleteProjectEntry { message ProjectEntryResponse { Entry entry = 1; - uint64 worktree_scan_id = 2; + repeated Entry child_entries = 2; + uint64 worktree_scan_id = 3; } message AddProjectCollaborator { From f832c0074fef58d6ebc0ca316369c9057de7289b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 18:29:51 +0200 Subject: [PATCH 46/54] Fix memory leak in `ListState` --- crates/gpui/src/elements/list.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 10f7ef7b79ef037248e77189ebcfe34aa53d4280..5a917f81c23530bac57f24eda8ea5cfca0a626fc 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -352,10 +352,11 @@ impl ListState { { let mut items = SumTree::new(); items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); - let handle = cx.handle(); + let handle = cx.weak_handle(); Self(Rc::new(RefCell::new(StateInner { last_layout_width: None, render_item: Box::new(move |ix, cx| { + let handle = handle.upgrade(cx)?; Some(cx.render(&handle, |view, cx| render_item(view, ix, cx))) }), rendered_range: 0..0, From 06ab2ace7271af2d751f01f984ef8567b184a7e5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 18:36:36 +0200 Subject: [PATCH 47/54] Don't steal focus from context menu when dispatching an action --- crates/context_menu/src/context_menu.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 33ccb35dbe97fc4d7b35fec2a575a0d82dff8335..273bb588afef076a2e1468a230c8444f5435a7cd 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -141,10 +141,12 @@ impl ContextMenu { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { self.reset(cx); - if cx.handle().is_focused(cx) { - let window_id = cx.window_id(); - (**cx).focus(window_id, self.previously_focused_view_id.take()); - } + cx.defer(|this, cx| { + if cx.handle().is_focused(cx) { + let window_id = cx.window_id(); + (**cx).focus(window_id, this.previously_focused_view_id.take()); + } + }); } fn reset(&mut self, cx: &mut ViewContext) { From 604b737d7c67958263ff32e5d13d8f1cb30d0415 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 May 2022 18:38:43 +0200 Subject: [PATCH 48/54] :lipstick: --- styles/src/styleTree/contextMenu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index e1677b066647c143e55a7876ea8610cf05ab08dd..4115c2937295436691e5ae38cee10a4f4a97dc73 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -33,4 +33,4 @@ export default function contextMenu(theme: Theme) { margin: { top: 2, bottom: 2 } }, } -} \ No newline at end of file +} From 354488ebdfae8b741de147db1e195ac025256a13 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 08:11:07 +0200 Subject: [PATCH 49/54] Don't eagerly populate copied subdirectory This can race anyway with snapshot updates, so we just eagerly refresh the root entry and wait for updates to come in to populate it. --- crates/collab/src/integration_tests.rs | 54 +++++--------------------- crates/project/src/fs.rs | 42 ++++++-------------- crates/project/src/project.rs | 29 ++++---------- crates/project/src/worktree.rs | 41 +++++++------------ crates/rpc/proto/zed.proto | 3 +- 5 files changed, 43 insertions(+), 126 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 00763023cebdf72d5992fba53ddfe91c56cfe54d..56822d21da2e9e13aaedc33585ea81a74ad2109e 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -824,7 +824,7 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { project - .copy_entry(dir_entry.id, Path::new("DIR2"), cx) + .copy_entry(entry.id, Path::new("f.txt"), cx) .unwrap() }) .await @@ -840,13 +840,10 @@ async fn test_fs_operations( "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", - "DIR2", - "DIR2/SUBDIR", - "DIR2/SUBDIR/f.txt", - "DIR2/e.txt", "a.txt", "b.txt", - "d.txt" + "d.txt", + "f.txt" ] ); }); @@ -861,13 +858,10 @@ async fn test_fs_operations( "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", - "DIR2", - "DIR2/SUBDIR", - "DIR2/SUBDIR/f.txt", - "DIR2/e.txt", "a.txt", "b.txt", - "d.txt" + "d.txt", + "f.txt" ] ); }); @@ -884,15 +878,7 @@ async fn test_fs_operations( .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [ - "DIR2", - "DIR2/SUBDIR", - "DIR2/SUBDIR/f.txt", - "DIR2/e.txt", - "a.txt", - "b.txt", - "d.txt" - ] + ["a.txt", "b.txt", "d.txt", "f.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -901,15 +887,7 @@ async fn test_fs_operations( .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [ - "DIR2", - "DIR2/SUBDIR", - "DIR2/SUBDIR/f.txt", - "DIR2/e.txt", - "a.txt", - "b.txt", - "d.txt" - ] + ["a.txt", "b.txt", "d.txt", "f.txt"] ); }); @@ -925,14 +903,7 @@ async fn test_fs_operations( .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [ - "DIR2", - "DIR2/SUBDIR", - "DIR2/SUBDIR/f.txt", - "DIR2/e.txt", - "a.txt", - "b.txt" - ] + ["a.txt", "b.txt", "f.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -941,14 +912,7 @@ async fn test_fs_operations( .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [ - "DIR2", - "DIR2/SUBDIR", - "DIR2/SUBDIR/f.txt", - "DIR2/e.txt", - "a.txt", - "b.txt" - ] + ["a.txt", "b.txt", "f.txt"] ); }); } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index a92516e3b9353f9f36caa9e402aafe17a5cecaca..2eec02d66dcd053e2f3eeb8196cbc0f2563dc47b 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -15,12 +15,7 @@ use text::Rope; pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; - async fn copy( - &self, - source: &Path, - target: &Path, - options: CopyOptions, - ) -> Result>; + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; @@ -96,21 +91,15 @@ impl Fs for RealFs { Ok(()) } - async fn copy( - &self, - source: &Path, - target: &Path, - options: CopyOptions, - ) -> Result> { + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { - return Ok(Default::default()); + return Ok(()); } else { return Err(anyhow!("{target:?} already exists")); } } - let mut paths = vec![target.to_path_buf()]; let metadata = smol::fs::metadata(source).await?; let _ = smol::fs::remove_dir_all(target).await; if metadata.is_dir() { @@ -120,17 +109,15 @@ impl Fs for RealFs { if let Ok(child) = child { let child_source_path = child.path(); let child_target_path = target.join(child.file_name()); - paths.extend( - self.copy(&child_source_path, &child_target_path, options) - .await?, - ); + self.copy(&child_source_path, &child_target_path, options) + .await?; } } } else { smol::fs::copy(source, target).await?; } - Ok(paths) + Ok(()) } async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { @@ -560,12 +547,7 @@ impl Fs for FakeFs { Ok(()) } - async fn copy( - &self, - source: &Path, - target: &Path, - options: CopyOptions, - ) -> Result> { + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { let source = normalize_path(source); let target = normalize_path(target); @@ -575,7 +557,7 @@ impl Fs for FakeFs { if !options.overwrite && state.entries.contains_key(&target) { if options.ignore_if_exists { - return Ok(Default::default()); + return Ok(()); } else { return Err(anyhow!("{target:?} already exists")); } @@ -588,15 +570,15 @@ impl Fs for FakeFs { } } - let mut paths = Vec::new(); + let mut events = Vec::new(); for (relative_path, entry) in new_entries { let new_path = normalize_path(&target.join(relative_path)); - paths.push(new_path.clone()); + events.push(new_path.clone()); state.entries.insert(new_path, entry); } - state.emit_event(&paths).await; - Ok(paths) + state.emit_event(&events).await; + Ok(()) } async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0924c4b866302d44cd50e5d846fea123d467cddd..2e549c5d3fd96a855ae66ae151754ab33cb1b6de 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -785,7 +785,7 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option)>>> { + ) -> Option>> { let worktree = self.worktree_for_entry(entry_id, cx)?; let new_path = new_path.into(); if self.is_local() { @@ -810,24 +810,15 @@ impl Project { let entry = response .entry .ok_or_else(|| anyhow!("missing entry in response"))?; - let (entry, child_entries) = worktree.update(&mut cx, |worktree, cx| { - let worktree = worktree.as_remote().unwrap(); - let root_entry = - worktree.insert_entry(entry, response.worktree_scan_id as usize, cx); - let mut child_entries = Vec::new(); - for entry in response.child_entries { - child_entries.push(worktree.insert_entry( + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, - )); - } - (root_entry, child_entries) - }); - Ok(( - entry.await?, - futures::future::try_join_all(child_entries).await?, - )) + ) + }) + .await })) } } @@ -4058,7 +4049,6 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), - child_entries: Default::default(), worktree_scan_id: worktree_scan_id as u64, }) } @@ -4087,7 +4077,6 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), - child_entries: Default::default(), worktree_scan_id: worktree_scan_id as u64, }) } @@ -4104,7 +4093,7 @@ impl Project { .ok_or_else(|| anyhow!("worktree not found")) })?; let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); - let (entry, child_entries) = worktree + let entry = worktree .update(&mut cx, |worktree, cx| { let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path)); worktree @@ -4116,7 +4105,6 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), - child_entries: child_entries.iter().map(Into::into).collect(), worktree_scan_id: worktree_scan_id as u64, }) } @@ -4144,7 +4132,6 @@ impl Project { .await?; Ok(proto::ProjectEntryResponse { entry: None, - child_entries: Default::default(), worktree_scan_id: worktree_scan_id as u64, }) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a4e36da7497ac7fb587a07f08843ad0af9921d9f..cadfaa520d878b21ca5af9558b6839db0d39ee8d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -779,7 +779,7 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option)>>> { + ) -> Option>> { let old_path = self.entry_for_id(entry_id)?.path.clone(); let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); @@ -794,38 +794,23 @@ impl LocalWorktree { }); Some(cx.spawn(|this, mut cx| async move { - let copied_paths = copy.await?; - let (entry, child_entries) = this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - let root_entry = - this.refresh_entry(new_path.clone(), abs_new_path.clone(), None, cx); - - let mut child_entries = Vec::new(); - for copied_path in copied_paths { - if copied_path != abs_new_path { - let relative_copied_path = copied_path.strip_prefix(this.abs_path())?; - child_entries.push(this.refresh_entry( - relative_copied_path.into(), - copied_path, - None, - cx, - )); - } - } - - anyhow::Ok((root_entry, child_entries)) - })?; - let (entry, child_entries) = ( - entry.await?, - futures::future::try_join_all(child_entries).await?, - ); - + copy.await?; + let entry = this + .update(&mut cx, |this, cx| { + this.as_local_mut().unwrap().refresh_entry( + new_path.clone(), + abs_new_path, + None, + cx, + ) + }) + .await?; this.update(&mut cx, |this, cx| { this.poll_snapshot(cx); this.as_local().unwrap().broadcast_snapshot() }) .await; - Ok((entry, child_entries)) + Ok(entry) })) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3a0243a08f918c6dd997e9551bb7412f1183d170..3b4d8cc4f9c548759c0d4a19b5a05d5adb918dac 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -224,8 +224,7 @@ message DeleteProjectEntry { message ProjectEntryResponse { Entry entry = 1; - repeated Entry child_entries = 2; - uint64 worktree_scan_id = 3; + uint64 worktree_scan_id = 2; } message AddProjectCollaborator { From 1eb03f2f4e56d1c7d80f0159b29e3a35f22f55fa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 08:13:05 +0200 Subject: [PATCH 50/54] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 27b666d6d08896fbd345a9b4b4cc26ad43ac222c..9512a430431c6c8b971a89920802eeb4526ffe30 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 20; +pub const PROTOCOL_VERSION: u32 = 21; From e4641da5981824a8b3e7ae60f339c5c1068e415d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 08:17:52 +0200 Subject: [PATCH 51/54] Don't show "add/remove folder to/from project" for remote projects --- crates/project_panel/src/project_panel.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6cc59728e642fdf7aad3fc60a241e4198dc073d3..cf8fd1d0c8708dae4103a8320926e40f9140a138 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -242,15 +242,17 @@ impl ProjectPanel { if let Some((worktree, entry)) = self.selected_entry(cx) { let is_root = Some(entry) == worktree.root_entry(); - menu_entries.push(ContextMenuItem::item( - "Add Folder to Project", - workspace::AddFolderToProject, - )); - if is_root { + if !self.project.read(cx).is_remote() { menu_entries.push(ContextMenuItem::item( - "Remove Folder from Project", - workspace::RemoveFolderFromProject(worktree_id), + "Add Folder to Project", + workspace::AddFolderToProject, )); + if is_root { + menu_entries.push(ContextMenuItem::item( + "Remove Folder from Project", + workspace::RemoveFolderFromProject(worktree_id), + )); + } } menu_entries.push(ContextMenuItem::item("New File", AddFile)); menu_entries.push(ContextMenuItem::item("New Folder", AddDirectory)); From 0fd47da8803ff684047a0eb06c3afcd005fba77b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 09:34:14 +0200 Subject: [PATCH 52/54] Prevent mouse down events from piercing through overlays --- crates/gpui/src/presenter.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 3ff4334f616a4f753589d9ed0c4fea4fa2cc0d39..87efeb2e5ffdbf598c75ce43c7342bf05c961eef 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -349,8 +349,8 @@ impl Presenter { let mut event_cx = self.build_event_context(cx); let mut handled = false; for unhovered_region in unhovered_regions { + handled = true; if let Some(hover_callback) = unhovered_region.hover { - handled = true; event_cx.with_current_view(unhovered_region.view_id, |event_cx| { hover_callback(false, event_cx); }) @@ -358,8 +358,8 @@ impl Presenter { } for hovered_region in hovered_regions { + handled = true; if let Some(hover_callback) = hovered_region.hover { - handled = true; event_cx.with_current_view(hovered_region.view_id, |event_cx| { hover_callback(true, event_cx); }) @@ -371,8 +371,8 @@ impl Presenter { } if let Some((mouse_down_region, position)) = mouse_down_region { + handled = true; if let Some(mouse_down_callback) = mouse_down_region.mouse_down { - handled = true; event_cx.with_current_view(mouse_down_region.view_id, |event_cx| { mouse_down_callback(position, event_cx); }) @@ -380,8 +380,8 @@ impl Presenter { } if let Some((clicked_region, position, click_count)) = clicked_region { + handled = true; if let Some(click_callback) = clicked_region.click { - handled = true; event_cx.with_current_view(clicked_region.view_id, |event_cx| { click_callback(position, click_count, event_cx); }) @@ -389,8 +389,8 @@ impl Presenter { } if let Some((right_mouse_down_region, position)) = right_mouse_down_region { + handled = true; if let Some(right_mouse_down_callback) = right_mouse_down_region.right_mouse_down { - handled = true; event_cx.with_current_view(right_mouse_down_region.view_id, |event_cx| { right_mouse_down_callback(position, event_cx); }) @@ -398,8 +398,8 @@ impl Presenter { } if let Some((right_clicked_region, position, click_count)) = right_clicked_region { + handled = true; if let Some(right_click_callback) = right_clicked_region.right_click { - handled = true; event_cx.with_current_view(right_clicked_region.view_id, |event_cx| { right_click_callback(position, click_count, event_cx); }) @@ -407,8 +407,8 @@ impl Presenter { } if let Some((dragged_region, delta)) = dragged_region { + handled = true; if let Some(drag_callback) = dragged_region.drag { - handled = true; event_cx.with_current_view(dragged_region.view_id, |event_cx| { drag_callback(delta, event_cx); }) From e067212ad4c08cef915e189ef763bc6a12d7f28d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 09:52:44 +0200 Subject: [PATCH 53/54] Always re-render visible elements in `List` --- crates/gpui/src/elements/list.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 5a917f81c23530bac57f24eda8ea5cfca0a626fc..6479f2ee283dac64c6b815f47b16131f05ec8796 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -131,13 +131,24 @@ impl Element for List { let mut cursor = old_items.cursor::(); cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); for (ix, item) in cursor.by_ref().enumerate() { - if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= size.y() + state.overdraw { break; } - if let Some(element) = - state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx) - { + // Force re-render if the item is visible, but attempt to re-use an existing one + // if we are inside the overdraw. + let existing_element = if visible_height >= size.y() { + Some(item) + } else { + None + }; + if let Some(element) = state.render_item( + scroll_top.item_ix + ix, + existing_element, + item_constraint, + cx, + ) { rendered_height += element.size().y(); rendered_items.push_back(ListItem::Rendered(element)); } @@ -151,9 +162,9 @@ impl Element for List { if rendered_height - scroll_top.offset_in_item < size.y() { while rendered_height < size.y() { cursor.prev(&()); - if let Some(item) = cursor.item() { + if cursor.item().is_some() { if let Some(element) = - state.render_item(cursor.start().0, item, item_constraint, cx) + state.render_item(cursor.start().0, None, item_constraint, cx) { rendered_height += element.size().y(); rendered_items.push_front(ListItem::Rendered(element)); @@ -189,7 +200,7 @@ impl Element for List { cursor.prev(&()); if let Some(item) = cursor.item() { if let Some(element) = - state.render_item(cursor.start().0, item, item_constraint, cx) + state.render_item(cursor.start().0, Some(item), item_constraint, cx) { leading_overdraw += element.size().y(); rendered_items.push_front(ListItem::Rendered(element)); @@ -426,11 +437,11 @@ impl StateInner { fn render_item( &mut self, ix: usize, - existing_item: &ListItem, + existing_element: Option<&ListItem>, constraint: SizeConstraint, cx: &mut LayoutContext, ) -> Option { - if let ListItem::Rendered(element) = existing_item { + if let Some(ListItem::Rendered(element)) = existing_element { Some(element.clone()) } else { let mut element = (self.render_item)(ix, cx)?; From 34bf2486144fb5756a02c66847856e544f2d6ffd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 10:36:10 +0200 Subject: [PATCH 54/54] Avoid notifying views that have been removed --- crates/gpui/src/app.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2e10606c09669518544ea1527601fe5d2fe78db0..19de98b3cef931b1a0cedd4abfd539aeb5f7281f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2269,21 +2269,22 @@ impl MutableAppContext { observed_window_id: usize, observed_view_id: usize, ) { - if let Some(window) = self.cx.windows.get_mut(&observed_window_id) { - window - .invalidation - .get_or_insert_with(Default::default) - .updated - .insert(observed_view_id); - } - let callbacks = self.observations.lock().remove(&observed_view_id); - if let Some(callbacks) = callbacks { - if self - .cx - .views - .contains_key(&(observed_window_id, observed_view_id)) - { + + if self + .cx + .views + .contains_key(&(observed_window_id, observed_view_id)) + { + if let Some(window) = self.cx.windows.get_mut(&observed_window_id) { + window + .invalidation + .get_or_insert_with(Default::default) + .updated + .insert(observed_view_id); + } + + if let Some(callbacks) = callbacks { for (id, callback) in callbacks { if let Some(mut callback) = callback { let alive = callback(self);