Add channel drag'n'drop

Conrad Irwin and Max created

Co-Authored-By: Max <max@zed.dev>

Change summary

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(-)

Detailed changes

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<f32>,
+    width: Option<Pixels>,
     fs: Arc<dyn Fs>,
     focus_handle: FocusHandle,
     channel_clipboard: Option<ChannelMoveClipboard>,
@@ -318,7 +319,7 @@ enum ChannelDragTarget {
 
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
     collapsed_channels: Option<Vec<u64>>,
 }
 
@@ -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::<DraggedChannelView>(|style| {
+                    style.bg(cx.theme().colors().ghost_element_hover)
+                })
+                .on_drop(cx.listener(
+                    move |this, view: &View<DraggedChannelView>, 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<DraggedChannelView>, 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<f32>, cx: &mut ViewContext<Self>) {
-        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>) -> 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()))
+    }
+}

crates/gpui2/src/geometry.rs 🔗

@@ -740,7 +740,7 @@ impl<T> Copy for Corners<T> 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;

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),
+        )
     }
 }