From e377bd805b955b3b289f1b11b604ae28d5304a75 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 29 Nov 2023 12:24:04 -0700 Subject: [PATCH] Add channel drag'n'drop Co-Authored-By: Max --- crates/collab_ui2/src/collab_panel.rs | 238 +++++++++++++++++--------- crates/gpui2/src/geometry.rs | 2 +- crates/ui2/src/components/list.rs | 68 ++++---- 3 files changed, 192 insertions(+), 116 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 636e519827..a9aa2e7a8e 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -19,6 +19,7 @@ mod contact_finder; use contact_finder::ContactFinder; use menu::Confirm; use rpc::proto; +use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; // use drag_and_drop::{DragAndDrop, Draggable}; @@ -171,8 +172,8 @@ use gpui::{ actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Pixels, - Point, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + Point, PromptLevel, Render, RenderOnce, SharedString, Stateful, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; @@ -284,7 +285,7 @@ impl ChannelEditingState { } pub struct CollabPanel { - width: Option, + width: Option, fs: Arc, focus_handle: FocusHandle, channel_clipboard: Option, @@ -318,7 +319,7 @@ enum ChannelDragTarget { #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { - width: Option, + width: Option, collapsed_channels: Option>, } @@ -2500,13 +2501,31 @@ impl CollabPanel { | Section::Offline => true, }; - ListHeader::new(text) + let header = ListHeader::new(text) .when_some(button, |el, button| el.right_button(button)) .selected(is_selected) .when(can_collapse, |el| { el.toggle(ui::Toggle::Toggled(is_collapsed)).on_toggle( cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)), ) + }); + + h_stack() + .w_full() + .child(header) + .when(section == Section::Channels, |el| { + el.drag_over::(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .on_drop(cx.listener( + move |this, view: &View, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(view.read(cx).channel.id, None, cx) + }) + .detach_and_log_err(cx) + }, + )) }) } @@ -2771,80 +2790,112 @@ impl CollabPanel { None }; - div().group("").child( - ListItem::new(channel_id as usize) - .indent_level(depth) - .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle - .left_icon(if is_public { Icon::Public } else { Icon::Hash }) - .selected(is_selected || is_active) - .child( - h_stack() - .w_full() - .justify_between() - .child( - h_stack() - .id(channel_id as usize) - .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.render(cx))) - .tooltip(|cx| Tooltip::text("Join channel", cx)), - ) - .child( - h_stack() - .child( - div() - .id("channel_chat") - .when(!has_messages_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new("channel_chat", Icon::MessageBubbles) + let width = self.width.unwrap_or(px(240.)); + + div() + .id(channel_id as usize) + .group("") + .on_drag({ + let channel = channel.clone(); + move |cx| { + let channel = channel.clone(); + cx.build_view({ |cx| DraggedChannelView { channel, width } }) + } + }) + .on_drop( + cx.listener(move |this, view: &View, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel( + view.read(cx).channel.id, + Some(channel_id), + cx, + ) + }) + .detach_and_log_err(cx) + }), + ) + .child( + ListItem::new(channel_id as usize) + .indent_level(depth) + .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle + .left_icon(if is_public { Icon::Public } else { Icon::Hash }) + .selected(is_selected || is_active) + .child( + h_stack() + .w_full() + .justify_between() + .child( + h_stack() + .id(channel_id as usize) + .child(Label::new(channel.name.clone())) + .children(face_pile.map(|face_pile| face_pile.render(cx))) + .tooltip(|cx| Tooltip::text("Join channel", cx)), + ) + .child( + h_stack() + .child( + div() + .id("channel_chat") + .when(!has_messages_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new( + "channel_chat", + Icon::MessageBubbles, + ) .color(if has_messages_notification { Color::Default } else { Color::Muted }), - ) - .tooltip(|cx| Tooltip::text("Open channel chat", cx)), - ) - .child( - div() - .id("channel_notes") - .when(!has_notes_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new("channel_notes", Icon::File) - .color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .tooltip(|cx| { - Tooltip::text("Open channel notes", cx) - }), - ), - ), - ), - ) - .toggle(if has_children { - Toggle::Toggled(disclosed) - } else { - Toggle::NotToggleable - }) - .on_toggle( - cx.listener(move |this, _, cx| this.toggle_channel_collapsed(channel_id, cx)), - ) - .on_click(cx.listener(move |this, _, cx| { - if this.drag_target_channel == ChannelDragTarget::None { - if is_active { - this.open_channel_notes(channel_id, cx) - } else { - this.join_channel(channel_id, cx) + ) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)), + ) + .child( + div() + .id("channel_notes") + .when(!has_notes_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_notes", Icon::File) + .color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .tooltip(|cx| { + Tooltip::text("Open channel notes", cx) + }), + ), + ), + ), + ) + .toggle(if has_children { + Toggle::Toggled(disclosed) + } else { + Toggle::NotToggleable + }) + .on_toggle( + cx.listener(move |this, _, cx| { + this.toggle_channel_collapsed(channel_id, cx) + }), + ) + .on_click(cx.listener(move |this, _, cx| { + if this.drag_target_channel == ChannelDragTarget::None { + if is_active { + this.open_channel_notes(channel_id, cx) + } else { + this.join_channel(channel_id, cx) + } } - } - })) - .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_channel_context_menu(event.position, channel_id, ix, cx) - })), - ) + })) + .on_secondary_mouse_down(cx.listener( + move |this, event: &MouseDownEvent, cx| { + this.deploy_channel_context_menu(event.position, channel_id, ix, cx) + }, + )), + ) // let channel_id = channel.id; // let collab_theme = &theme.collab_panel; @@ -3072,7 +3123,8 @@ impl CollabPanel { // ) // }) // .on_click(MouseButton::Left, move |_, this, cx| { - // if this.drag_target_channel == ChannelDragTarget::None { + // if this. + // drag_target_channel == ChannelDragTarget::None { // if is_active { // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) // } else { @@ -3369,14 +3421,15 @@ impl Panel for CollabPanel { } fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width) + self.width.map_or_else( + || CollaborationPanelSettings::get_global(cx).default_width, + |width| width.0, + ) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - // todo!() - // self.serialize(cx); + self.width = size.map(|s| px(s)); + self.serialize(cx); cx.notify(); } @@ -3518,3 +3571,30 @@ impl FocusableView for CollabPanel { // .contained() // .with_style(style.container) // } + +struct DraggedChannelView { + channel: Channel, + width: Pixels, +} + +impl Render for DraggedChannelView { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + h_stack() + .font(ui_font) + .bg(cx.theme().colors().background) + .w(self.width) + .p_1() + .gap_1() + .child(IconElement::new( + if self.channel.visibility == proto::ChannelVisibility::Public { + Icon::Public + } else { + Icon::Hash + }, + )) + .child(Label::new(self.channel.name.clone())) + } +} diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index e1f039e309..7f9d07e20a 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -740,7 +740,7 @@ impl Copy for Corners where T: Copy + Clone + Default + Debug {} Deserialize, )] #[repr(transparent)] -pub struct Pixels(pub(crate) f32); +pub struct Pixels(pub f32); impl std::ops::Div for Pixels { type Output = f32; diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 5cd9c7f709..31290a26f5 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -94,42 +94,38 @@ impl RenderOnce for ListHeader { None => div(), }; - h_stack() - .w_full() - .bg(cx.theme().colors().surface_background) - .relative() - .child( - div() - .h_5() - .when(self.inset, |this| this.px_2()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - .flex() - .flex_1() - .items_center() - .justify_between() - .w_full() - .gap_1() - .child( - h_stack() - .gap_1() - .child( - div() - .flex() - .gap_1() - .items_center() - .children(self.left_icon.map(|i| { - IconElement::new(i) - .color(Color::Muted) - .size(IconSize::Small) - })) - .child(Label::new(self.label.clone()).color(Color::Muted)), - ) - .child(disclosure_control), - ) - .child(meta), - ) + h_stack().w_full().relative().child( + div() + .h_5() + .when(self.inset, |this| this.px_2()) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) + .flex() + .flex_1() + .items_center() + .justify_between() + .w_full() + .gap_1() + .child( + h_stack() + .gap_1() + .child( + div() + .flex() + .gap_1() + .items_center() + .children(self.left_icon.map(|i| { + IconElement::new(i) + .color(Color::Muted) + .size(IconSize::Small) + })) + .child(Label::new(self.label.clone()).color(Color::Muted)), + ) + .child(disclosure_control), + ) + .child(meta), + ) } }