diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 3f4a28b58b..dee285661a 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2557,7 +2557,7 @@ impl CollabPanel { let channel = channel.clone(); move |cx| { let channel = channel.clone(); - cx.build_view({ |cx| DraggedChannelView { channel, width } }) + cx.build_view(|cx| DraggedChannelView { channel, width }) } }) .drag_over::(|style| { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 25566e7483..7c365d8734 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -763,6 +763,11 @@ impl InteractiveBounds { pub fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { self.bounds.contains(point) && cx.was_top_layer(&point, &self.stacking_order) } + + pub fn drag_target_contains(&self, point: &Point, cx: &WindowContext) -> bool { + self.bounds.contains(point) + && cx.was_top_layer_under_active_drag(&point, &self.stacking_order) + } } impl Interactivity { @@ -888,30 +893,32 @@ impl Interactivity { if cx.active_drag.is_some() { let drop_listeners = mem::take(&mut self.drop_listeners); let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, &cx) - { - if let Some(drag_state_type) = - cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) + if !drop_listeners.is_empty() { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && interactive_bounds.drag_target_contains(&event.position, cx) { - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx - .active_drag - .take() - .expect("checked for type drag state type above"); + if let Some(drag_state_type) = + cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) + { + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); - listener(drag.view.clone(), cx); - cx.notify(); - cx.stop_propagation(); + listener(drag.view.clone(), cx); + cx.notify(); + cx.stop_propagation(); + } } + } else { + cx.active_drag = None; } - } else { - cx.active_drag = None; } - } - }); + }); + } } let click_listeners = mem::take(&mut self.click_listeners); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b84137fa96..16de7ef62a 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -38,6 +38,8 @@ use std::{ }; use util::ResultExt; +const ACTIVE_DRAG_Z_INDEX: u32 = 1; + /// A global stacking order, which is created by stacking successive z-index values. /// Each z-index will always be interpreted in the context of its parent z-index. #[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)] @@ -907,6 +909,23 @@ impl<'a> WindowContext<'a> { false } + pub fn was_top_layer_under_active_drag( + &self, + point: &Point, + level: &StackingOrder, + ) -> bool { + for (stack, bounds) in self.window.rendered_frame.depth_map.iter() { + if stack.starts_with(&[ACTIVE_DRAG_Z_INDEX]) { + continue; + } + if bounds.contains(point) { + return level.starts_with(stack) || stack.starts_with(level); + } + } + + false + } + /// Called during painting to get the current stacking order. pub fn stacking_order(&self) -> &StackingOrder { &self.window.next_frame.z_index_stack @@ -1238,7 +1257,7 @@ impl<'a> WindowContext<'a> { }); if let Some(active_drag) = self.app.active_drag.take() { - self.with_z_index(1, |cx| { + self.with_z_index(ACTIVE_DRAG_Z_INDEX, |cx| { let offset = cx.mouse_position() - active_drag.cursor_offset; let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); active_drag.view.draw(offset, available_space, cx); diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 75c7d6ed85..b8961810e7 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -29,6 +29,7 @@ use std::{ path::Path, sync::Arc, }; +use theme::ThemeSettings; use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; @@ -55,7 +56,7 @@ pub struct ProjectPanel { clipboard_entry: Option, _dragged_entry_destination: Option>, _workspace: WeakView, - width: Option, + width: Option, pending_serialization: Task>, } @@ -86,7 +87,7 @@ pub enum ClipboardEntry { }, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct EntryDetails { filename: String, icon: Option>, @@ -162,6 +163,12 @@ struct SerializedProjectPanel { width: Option, } +struct DraggedProjectEntryView { + entry_id: ProjectEntryId, + details: EntryDetails, + width: Pixels, +} + impl ProjectPanel { fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let project = workspace.project().clone(); @@ -236,7 +243,6 @@ impl ProjectPanel { context_menu: None, filename_editor, clipboard_entry: None, - // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), _dragged_entry_destination: None, _workspace: workspace.weak_handle(), width: None, @@ -331,7 +337,7 @@ impl ProjectPanel { let panel = ProjectPanel::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; + panel.width = serialized_panel.width.map(px); cx.notify(); }); } @@ -346,7 +352,9 @@ impl ProjectPanel { KEY_VALUE_STORE .write_kvp( PROJECT_PANEL_KEY.into(), - serde_json::to_string(&SerializedProjectPanel { width })?, + serde_json::to_string(&SerializedProjectPanel { + width: width.map(|p| p.0), + })?, ) .await?; anyhow::Ok(()) @@ -1003,37 +1011,36 @@ impl ProjectPanel { } } - // todo!() - // fn move_entry( - // &mut self, - // entry_to_move: ProjectEntryId, - // destination: ProjectEntryId, - // destination_is_file: bool, - // cx: &mut ViewContext, - // ) { - // let destination_worktree = self.project.update(cx, |project, cx| { - // let entry_path = project.path_for_entry(entry_to_move, cx)?; - // let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + fn move_entry( + &mut self, + entry_to_move: ProjectEntryId, + destination: ProjectEntryId, + destination_is_file: bool, + cx: &mut ViewContext, + ) { + let destination_worktree = self.project.update(cx, |project, cx| { + let entry_path = project.path_for_entry(entry_to_move, cx)?; + let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); - // let mut destination_path = destination_entry_path.as_ref(); - // if destination_is_file { - // destination_path = destination_path.parent()?; - // } + let mut destination_path = destination_entry_path.as_ref(); + if destination_is_file { + destination_path = destination_path.parent()?; + } - // let mut new_path = destination_path.to_path_buf(); - // new_path.push(entry_path.path.file_name()?); - // if new_path != entry_path.path.as_ref() { - // let task = project.rename_entry(entry_to_move, new_path, cx); - // cx.foreground_executor().spawn(task).detach_and_log_err(cx); - // } + let mut new_path = destination_path.to_path_buf(); + new_path.push(entry_path.path.file_name()?); + if new_path != entry_path.path.as_ref() { + let task = project.rename_entry(entry_to_move, new_path, cx); + cx.foreground_executor().spawn(task).detach_and_log_err(cx); + } - // Some(project.worktree_id_for_entry(destination, cx)?) - // }); + Some(project.worktree_id_for_entry(destination, cx)?) + }); - // if let Some(destination_worktree) = destination_worktree { - // self.expand_entry(destination_worktree, destination, cx); - // } - // } + if let Some(destination_worktree) = destination_worktree { + self.expand_entry(destination_worktree, destination, cx); + } + } fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { let mut entry_index = 0; @@ -1349,15 +1356,15 @@ impl ProjectPanel { &self, entry_id: ProjectEntryId, details: EntryDetails, - // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> ListItem { + ) -> Stateful
{ let kind = details.kind; let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; let is_selected = self .selection .map_or(false, |selection| selection.entry_id == entry_id); + let width = self.width.unwrap_or(px(0.)); let theme = cx.theme(); let filename_text_color = details @@ -1370,52 +1377,69 @@ impl ProjectPanel { }) .unwrap_or(theme.status().info); - ListItem::new(entry_id.to_proto() as usize) - .indent_level(details.depth) - .indent_step_size(px(settings.indent_size)) - .selected(is_selected) - .child(if let Some(icon) = &details.icon { - div().child(IconElement::from_path(icon.to_string())) - } else { - div() + div() + .id(entry_id.to_proto() as usize) + .on_drag({ + let details = details.clone(); + move |cx| { + let details = details.clone(); + cx.build_view(|_| DraggedProjectEntryView { + details, + width, + entry_id, + }) + } }) + .drag_over::(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .on_drop(cx.listener( + move |this, dragged_view: &View, cx| { + this.move_entry(dragged_view.read(cx).entry_id, entry_id, kind.is_file(), cx); + }, + )) .child( - if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { - div().h_full().w_full().child(editor.clone()) - } else { - div() - .text_color(filename_text_color) - .child(Label::new(details.filename.clone())) - } - .ml_1(), - ) - .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { - if event.down.button == MouseButton::Right { - return; - } - if !show_editor { - if kind.is_dir() { - this.toggle_expanded(entry_id, cx); + ListItem::new(entry_id.to_proto() as usize) + .indent_level(details.depth) + .indent_step_size(px(settings.indent_size)) + .selected(is_selected) + .child(if let Some(icon) = &details.icon { + div().child(IconElement::from_path(icon.to_string())) } else { - if event.down.modifiers.command { - this.split_entry(entry_id, cx); + div() + }) + .child( + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { + div().h_full().w_full().child(editor.clone()) } else { - this.open_entry(entry_id, event.up.click_count > 1, cx); + div() + .text_color(filename_text_color) + .child(Label::new(details.filename.clone())) } - } - } - })) - .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - })) - // .on_drop::(|this, event, cx| { - // this.move_entry( - // *dragged_entry, - // entry_id, - // matches!(details.kind, EntryKind::File(_)), - // cx, - // ); - // }) + .ml_1(), + ) + .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { + if event.down.button == MouseButton::Right { + return; + } + if !show_editor { + if kind.is_dir() { + this.toggle_expanded(entry_id, cx); + } else { + if event.down.modifiers.command { + this.split_entry(entry_id, cx); + } else { + this.open_entry(entry_id, event.up.click_count > 1, cx); + } + } + } + })) + .on_secondary_mouse_down(cx.listener( + move |this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + }, + )), + ) } fn dispatch_context(&self, cx: &ViewContext) -> KeyContext { @@ -1430,7 +1454,6 @@ impl ProjectPanel { }; dispatch_context.add(identifier); - dispatch_context } } @@ -1503,6 +1526,30 @@ impl Render for ProjectPanel { } } +impl Render for DraggedProjectEntryView { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let settings = ProjectPanelSettings::get_global(cx); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + h_stack() + .font(ui_font) + .bg(cx.theme().colors().background) + .w(self.width) + .child( + ListItem::new(self.entry_id.to_proto() as usize) + .indent_level(self.details.depth) + .indent_step_size(px(settings.indent_size)) + .child(if let Some(icon) = &self.details.icon { + div().child(IconElement::from_path(icon.to_string())) + } else { + div() + }) + .child(Label::new(self.details.filename.clone())), + ) + } +} + impl EventEmitter for ProjectPanel {} impl EventEmitter for ProjectPanel {} @@ -1534,12 +1581,14 @@ impl Panel for ProjectPanel { } fn size(&self, cx: &WindowContext) -> f32 { - self.width - .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) + self.width.map_or_else( + || ProjectPanelSettings::get_global(cx).default_width, + |width| width.0, + ) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; + self.width = size.map(px); self.serialize(cx); cx.notify(); } diff --git a/crates/ui2/src/components/disclosure.rs b/crates/ui2/src/components/disclosure.rs index 22cad27f58..7d9a69bb3a 100644 --- a/crates/ui2/src/components/disclosure.rs +++ b/crates/ui2/src/components/disclosure.rs @@ -1,14 +1,10 @@ -use std::rc::Rc; - +use crate::{prelude::*, Color, Icon, IconButton, IconSize}; use gpui::ClickEvent; -use crate::prelude::*; -use crate::{Color, Icon, IconButton, IconSize}; - #[derive(IntoElement)] pub struct Disclosure { is_open: bool, - on_toggle: Option>, + on_toggle: Option>, } impl Disclosure { @@ -21,7 +17,7 @@ impl Disclosure { pub fn on_toggle( mut self, - handler: impl Into>>, + handler: impl Into>>, ) -> Self { self.on_toggle = handler.into(); self diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 799b1c5dae..933a1a95d7 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -1,18 +1,14 @@ -use std::rc::Rc; - +use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label}; use gpui::{AnyElement, ClickEvent, Div}; use smallvec::SmallVec; -use crate::prelude::*; -use crate::{h_stack, Disclosure, Icon, IconElement, IconSize, Label}; - #[derive(IntoElement)] pub struct ListHeader { label: SharedString, left_icon: Option, meta: SmallVec<[AnyElement; 2]>, toggle: Option, - on_toggle: Option>, + on_toggle: Option>, inset: bool, selected: bool, } @@ -39,7 +35,7 @@ impl ListHeader { mut self, on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { - self.on_toggle = Some(Rc::new(on_toggle)); + self.on_toggle = Some(Box::new(on_toggle)); self } diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 529f2c2a58..f2cdd2055c 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,14 +1,10 @@ -use std::rc::Rc; - +use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize}; use gpui::{ px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful, }; use smallvec::SmallVec; -use crate::prelude::*; -use crate::{Avatar, Disclosure, Icon, IconElement, IconSize}; - #[derive(IntoElement)] pub struct ListItem { id: ElementId, @@ -20,10 +16,10 @@ pub struct ListItem { left_slot: Option, toggle: Option, inset: bool, - on_click: Option>, - on_toggle: Option>, + on_click: Option>, + on_toggle: Option>, tooltip: Option AnyView + 'static>>, - on_secondary_mouse_down: Option>, + on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -46,7 +42,7 @@ impl ListItem { } pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { - self.on_click = Some(Rc::new(handler)); + self.on_click = Some(Box::new(handler)); self } @@ -54,7 +50,15 @@ impl ListItem { mut self, handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { - self.on_secondary_mouse_down = Some(Rc::new(handler)); + self.on_secondary_mouse_down = Some(Box::new(handler)); + self + } + + pub fn on_drag( + mut self, + handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_secondary_mouse_down = Some(Box::new(handler)); self } @@ -87,7 +91,7 @@ impl ListItem { mut self, on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { - self.on_toggle = Some(Rc::new(on_toggle)); + self.on_toggle = Some(Box::new(on_toggle)); self }