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 499b3ed99d..234319bdd6 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 460e01c527..29c64128d1 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 44aa0626c5..763772b89e 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 555d8962d3..c02fd73b8f 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 426f25629d..224e5e94a7 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 e5a80e44f4..556b7a9091 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 77d37bc3bf..969fe3cb84 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 1ee7c6cbb5..65cb6ed61d 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 0cac2ed863..3d90a7554c 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 b32ab952c7..61cfa99bfe 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 9d07177b16..4d3aa6cf9a 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 fbdd6963e3..2a2da34d63 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 d5d2105c3f..a58be77c03 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 67db36208b..8fd662978b 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 7056eb9ceb..f3506e2c94 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 5581cbd608..94b6261a0f 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 4549aa4f90..e3834f6f45 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 bc9e93025d..28e31ad3bb 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 f58e0b973e..ab1ae4931f 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 8b97ef1a80..ba2a21bce4 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 afdacc2a31..5aec332913 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 e9f0efa311..f4197e7296 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 2f3e3eea72..66aaa85952 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 eb4b9650a6..2d93e46c05 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 bd156ed661..dca752ed6f 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 355d1f4433..f71bc09d23 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 f3506e2c94..d3d4856f4d 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 28e31ad3bb..c69a8d3898 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 2be3830438..b0233d6827 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 0000000000..3392d68579 --- /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 0000000000..8d698ce9be --- /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 b0233d6827..e8b571add8 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 74401699ca..784987534d 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 3392d68579..c33b935c45 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 8d698ce9be..c3f00ac5b6 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 257bac21d9..7eb0282660 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 d3d4856f4d..85d1ed7407 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 c69a8d3898..941ae03d4d 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 66aaa85952..b2d8b9d4ac 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 c3f00ac5b6..0bcc97a25d 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 f12ed6900a..5ab01df1e1 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 2a2da34d63..053b69269c 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 85d1ed7407..4b060b4e70 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 941ae03d4d..4cbe60db3c 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 230bd3e57f..41266ff5f7 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 0000000000..5458ceda69 --- /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 b2d8b9d4ac..547414afc8 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 4b060b4e70..e310459cf7 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 e8b571add8..a103488028 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 c33b935c45..65f7f59a14 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 0bcc97a25d..4e5f15aae5 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 2d93e46c05..d11940b2c6 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 55c7bf22fe..231339d9e0 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 0000000000..0112b54846 --- /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 dca752ed6f..87b0287dc4 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 053b69269c..2c9c4719d6 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 e310459cf7..7d49a2d07d 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 4cbe60db3c..2edd3cef45 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 5458ceda69..e2b60f2685 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 4e5f15aae5..d12d8c0315 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 e2b60f2685..e0a78cafe6 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 d12d8c0315..5a740a91b9 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 65cb6ed61d..7e0281bffe 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 e0a78cafe6..e1677b0666 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 547414afc8..2f3e3eea72 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 5a740a91b9..ff1c3dda72 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 7d49a2d07d..d4c3410b1b 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 a103488028..b557431d80 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 ff1c3dda72..de4f05cade 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 d11940b2c6..6c66771082 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 525569b869..97a50e78d2 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 3e21e454f2..04a4f9aada 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 d4c3410b1b..13b27d62dd 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 f4197e7296..6fa4c9a6b2 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 b557431d80..f39f473ccc 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 95426517d7..e54245502f 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 29c64128d1..8bce551a8c 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 800bad497d..ab05a56ce7 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 763772b89e..4b965d3c1d 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 65f7f59a14..817893f43e 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 de4f05cade..7e42c2596e 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 ca3eb6b429..554cf433a2 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 f58c733cc7..84b6973533 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 76744274c7..93ae96f93e 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 bae5ffc46c..f2df235a7b 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 0000000000..cdcacd4416 --- /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 4528f00687..c74b6927ae 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 8fd662978b..19dc3054b7 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 7eb0282660..6d566699fa 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 13b27d62dd..5fda6f04fa 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 56c4fff651..3e80b5979e 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 e3834f6f45..9943ce5ded 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 6fa4c9a6b2..21361b8081 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 1049e216f3..f21138e541 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 5fda6f04fa..f1dac49f81 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 355d1f4433..319ed21252 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 62f19636b7..004052b9ba 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 1ee7c6cbb5..975a47a1ef 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 e58bbec1c6..085ec86791 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 fbdd6963e3..2bc241daee 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 7c358b85a0..0f10256cb0 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 085ec86791..c7d619fd7d 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 2bc241daee..cbd7b2cb82 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 0f10256cb0..003d9b066b 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 eb4b9650a6..ab4de50564 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 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, - pub titlebar_height: f32, - pub refreshing: bool, 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 cbd7b2cb82..899aa59309 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 | 129 ++++++++++++---------- 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, 193 insertions(+), 135 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e5a80e44f4..ae8335f242 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 - }; + 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(), - ); - } - }) + 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 319ed21252..68751b000b 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 ab4de50564..a0fca6aeb2 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 3f384b5ea5..c320f2662e 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 899aa59309..a5b874188a 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 d5d2105c3f..44576a0d95 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 67db36208b..0dfd7c0a49 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 7056eb9ceb..1be1f1e940 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 460e01c527..2240bbf9c6 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 44aa0626c5..888a0a28fb 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 77d37bc3bf..c6d3096a8b 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 1be1f1e940..e128efddbe 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 c320f2662e..de217a017c 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 a0fca6aeb2..e75461953f 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 3f42f98407..8d1fb37a08 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 975a47a1ef..7440be8c56 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 a5b874188a..bba95f33d4 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 bba95f33d4..349f8a1a6f 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 003d9b066b..6b9ef962cf 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 499b3ed99d..234319bdd6 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 2240bbf9c6..d3c5ef5592 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 9f0f396d85..0708826ea7 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 18e17a93d9..244cfcad4a 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 888a0a28fb..6d2885e867 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 555d8962d3..c02fd73b8f 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 426f25629d..224e5e94a7 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 ae8335f242..56e88add54 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 f58c733cc7..e19c1de3a8 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 e75461953f..63f7a67327 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 7440be8c56..ffe70caa12 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 349f8a1a6f..1c22ceb1b6 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 6b9ef962cf..caeae3c89f 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 44576a0d95..80c3ba2884 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 57c7441bfe..19b309116a 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 0dfd7c0a49..383d45ff3f 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 e128efddbe..17eca4d99f 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 20da49b1df..ea99767e0a 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 5581cbd608..94b6261a0f 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 4549aa4f90..e3834f6f45 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 bc9e93025d..d654363648 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 9f445c633a..106e6ad429 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 f58e0b973e..ab1ae4931f 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 8b97ef1a80..ba2a21bce4 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 afdacc2a31..5aec332913 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 e9f0efa311..f4197e7296 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 ffe70caa12..a9535667fd 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 63f7a67327..37bb86570b 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 1c22ceb1b6..42149feca8 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 37bb86570b..5c8f49ac18 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 a9535667fd..1ca4c13fc8 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 b32ab952c7..61cfa99bfe 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 9d07177b16..4d3aa6cf9a 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 42149feca8..9a8a77258e 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 caeae3c89f..843a3b62d6 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 9a8a77258e..58f6f127af 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 58f6f127af..68e624c0ce 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 5aec332913..9aaf2b832a 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 ba2a21bce4..db157ed850 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 87e40db1ea..54d5d90d49 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 843a3b62d6..c989117747 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 2ce233fa21..9046917e39 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 91c357ddee..3575accbeb 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 1ca4c13fc8..ee809746ae 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 3d90a7554c..d841bcbc04 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 54d5d90d49..6d2def4716 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 c989117747..22762ea9da 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 507a88c5f2..dc3eee2ed8 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 3575accbeb..788dadec49 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 ee809746ae..edeb5ccc69 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 6d2def4716..3ff4334f61 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 22762ea9da..ee9bb8311f 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 ee9bb8311f..1f503c8bf7 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 dc3eee2ed8..8ab5b3c482 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 edeb5ccc69..2ad6eaf028 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 788dadec49..6b94ae40f6 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 8ab5b3c482..33ccb35dbe 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 9046917e39..2eab508fe0 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 f21138e541..47b7cac704 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 0112b54846..2cd55e5ff0 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 2eab508fe0..bd678f37ac 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 2e38e501e2..a8c28f41f3 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 2f3e3eea72..f68f69711c 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 | 115 ++++++++++++---------- crates/rpc/src/proto.rs | 5 +- 7 files changed, 253 insertions(+), 55 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7ea925fce7..a09a2b1f33 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 7da2a38a83..2eec02d66d 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 abcd667293..923c188ffc 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 039ee0d838..8eef61f213 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 bd678f37ac..6cc59728e6 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 0fee451c0d..2f6ed1f318 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; + CopyProjectEntry copy_project_entry = 35; + DeleteProjectEntry delete_project_entry = 36; + ProjectEntryResponse project_entry_response = 37; - UpdateDiagnosticSummary update_diagnostic_summary = 37; - StartLanguageServer start_language_server = 38; - UpdateLanguageServer update_language_server = 39; + UpdateDiagnosticSummary update_diagnostic_summary = 38; + StartLanguageServer start_language_server = 39; + UpdateLanguageServer update_language_server = 40; - 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; + 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 = 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; + 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 = 76; - UpdateInviteInfo update_invite_info = 77; - ShowContacts show_contacts = 78; + UpdateContacts update_contacts = 77; + UpdateInviteInfo update_invite_info = 78; + ShowContacts show_contacts = 79; - 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; + 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 = 85; - FollowResponse follow_response = 86; - UpdateFollowers update_followers = 87; - Unfollow unfollow = 88; + 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 67a12fcd87..7fe715064f 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 a09a2b1f33..ed97001e98 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 2eec02d66d..a92516e3b9 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 923c188ffc..5346a86965 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 8eef61f213..a4e36da749 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 2f6ed1f318..e72cce6b6d 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 10f7ef7b79..5a917f81c2 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 33ccb35dbe..273bb588af 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 e1677b0666..4115c29372 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 00763023ce..56822d21da 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 a92516e3b9..2eec02d66d 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 0924c4b866..2e549c5d3f 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 a4e36da749..cadfaa520d 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 3a0243a08f..3b4d8cc4f9 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 27b666d6d0..9512a43043 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 6cc59728e6..cf8fd1d0c8 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 3ff4334f61..87efeb2e5f 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 5a917f81c2..6479f2ee28 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 2e10606c09..19de98b3ce 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);