Context menu, Dragon Drop, for collab panel (#3441)

Conrad Irwin created

Release Notes:

- N/A

Change summary

crates/collab_ui2/src/collab_panel.rs           | 847 ++++++++++--------
crates/gpui2/src/elements/div.rs                |  15 
crates/gpui2/src/geometry.rs                    |   2 
crates/project_panel2/src/project_panel.rs      |   2 
crates/ui2/src/components/disclosure.rs         |  22 
crates/ui2/src/components/list.rs               |  10 
crates/ui2/src/components/list/list_header.rs   |  12 
crates/ui2/src/components/list/list_item.rs     |  12 
crates/ui2/src/components/stories/disclosure.rs |   6 
crates/ui2/src/toggleable.rs                    |  38 
crates/ui2/src/ui2.rs                           |   2 
11 files changed, 490 insertions(+), 478 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};
@@ -151,10 +152,10 @@ actions!(
 //     ]
 // );
 
-// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
-// struct ChannelMoveClipboard {
-//     channel_id: ChannelId,
-// }
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+struct ChannelMoveClipboard {
+    channel_id: ChannelId,
+}
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
@@ -168,17 +169,18 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div,
-    EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model,
-    ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView,
+    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, Stateful, Styled, Subscription, Task,
+    View, ViewContext, VisualContext, WeakView,
 };
 use project::Fs;
 use serde_derive::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use ui::{
-    h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List,
-    ListHeader, ListItem, Toggleable, Tooltip,
+    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
+    Label, List, ListHeader, ListItem, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -230,26 +232,6 @@ pub fn init(cx: &mut AppContext) {
     //         },
     //     );
 
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel,
-    //          action: &StartMoveChannelFor,
-    //          _: &mut ViewContext<CollabPanel>| {
-    //             panel.channel_clipboard = Some(ChannelMoveClipboard {
-    //                 channel_id: action.channel_id,
-    //             });
-    //         },
-    //     );
-
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
-    //             if let Some(channel) = panel.selected_channel() {
-    //                 panel.channel_clipboard = Some(ChannelMoveClipboard {
-    //                     channel_id: channel.id,
-    //                 })
-    //             }
-    //         },
-    //     );
-
     //     cx.add_action(
     //         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
     //             let Some(clipboard) = panel.channel_clipboard.take() else {
@@ -303,12 +285,12 @@ impl ChannelEditingState {
 }
 
 pub struct CollabPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
     fs: Arc<dyn Fs>,
     focus_handle: FocusHandle,
-    // channel_clipboard: Option<ChannelMoveClipboard>,
+    channel_clipboard: Option<ChannelMoveClipboard>,
     pending_serialization: Task<Option<()>>,
-    // context_menu: ViewHandle<ContextMenu>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
     filter_editor: View<Editor>,
     channel_name_editor: View<Editor>,
     channel_editing_state: Option<ChannelEditingState>,
@@ -337,7 +319,7 @@ enum ChannelDragTarget {
 
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
     collapsed_channels: Option<Vec<u64>>,
 }
 
@@ -589,10 +571,10 @@ impl CollabPanel {
             let mut this = Self {
                 width: None,
                 focus_handle: cx.focus_handle(),
-                //                 channel_clipboard: None,
+                channel_clipboard: None,
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
-                //                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+                context_menu: None,
                 channel_name_editor,
                 filter_editor,
                 entries: Vec::default(),
@@ -1685,156 +1667,123 @@ impl CollabPanel {
     //             .into_any()
     //     }
 
-    //     fn has_subchannels(&self, ix: usize) -> bool {
-    //         self.entries.get(ix).map_or(false, |entry| {
-    //             if let ListEntry::Channel { has_children, .. } = entry {
-    //                 *has_children
-    //             } else {
-    //                 false
-    //             }
-    //         })
-    //     }
-
-    //     fn deploy_channel_context_menu(
-    //         &mut self,
-    //         position: Option<Vector2F>,
-    //         channel: &Channel,
-    //         ix: usize,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         self.context_menu_on_selected = position.is_none();
+    fn has_subchannels(&self, ix: usize) -> bool {
+        self.entries.get(ix).map_or(false, |entry| {
+            if let ListEntry::Channel { has_children, .. } = entry {
+                *has_children
+            } else {
+                false
+            }
+        })
+    }
 
-    //         let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
-    //             self.channel_store
-    //                 .read(cx)
-    //                 .channel_for_id(clipboard.channel_id)
-    //                 .map(|channel| channel.name.clone())
-    //         });
+    fn deploy_channel_context_menu(
+        &mut self,
+        position: Point<Pixels>,
+        channel_id: ChannelId,
+        ix: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
+        // self.context_menu_on_selected = position.is_none();
 
-    //         self.context_menu.update(cx, |context_menu, cx| {
-    //             context_menu.set_position_mode(if self.context_menu_on_selected {
-    //                 OverlayPositionMode::Local
-    //             } else {
-    //                 OverlayPositionMode::Window
-    //             });
+        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
+            self.channel_store
+                .read(cx)
+                .channel_for_id(clipboard.channel_id)
+                .map(|channel| channel.name.clone())
+        });
+        let this = cx.view().clone();
 
-    //             let mut items = Vec::new();
+        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
+            if self.has_subchannels(ix) {
+                let expand_action_name = if self.is_channel_collapsed(channel_id) {
+                    "Expand Subchannels"
+                } else {
+                    "Collapse Subchannels"
+                };
+                context_menu = context_menu.entry(
+                    expand_action_name,
+                    cx.handler_for(&this, move |this, cx| {
+                        this.toggle_channel_collapsed(channel_id, cx)
+                    }),
+                );
+            }
 
-    //             let select_action_name = if self.selection == Some(ix) {
-    //                 "Unselect"
-    //             } else {
-    //                 "Select"
-    //             };
+            context_menu = context_menu
+                .entry(
+                    "Open Notes",
+                    cx.handler_for(&this, move |this, cx| {
+                        this.open_channel_notes(channel_id, cx)
+                    }),
+                )
+                .entry(
+                    "Open Chat",
+                    cx.handler_for(&this, move |this, cx| {
+                        this.join_channel_chat(channel_id, cx)
+                    }),
+                )
+                .entry(
+                    "Copy Channel Link",
+                    cx.handler_for(&this, move |this, cx| {
+                        this.copy_channel_link(channel_id, cx)
+                    }),
+                );
 
-    //             items.push(ContextMenuItem::action(
-    //                 select_action_name,
-    //                 ToggleSelectedIx { ix },
-    //             ));
+            if self.channel_store.read(cx).is_channel_admin(channel_id) {
+                context_menu = context_menu
+                    .separator()
+                    .entry(
+                        "New Subchannel",
+                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
+                    )
+                    .entry(
+                        "Rename",
+                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
+                    )
+                    .entry(
+                        "Move this channel",
+                        cx.handler_for(&this, move |this, cx| {
+                            this.start_move_channel(channel_id, cx)
+                        }),
+                    );
 
-    //             if self.has_subchannels(ix) {
-    //                 let expand_action_name = if self.is_channel_collapsed(channel.id) {
-    //                     "Expand Subchannels"
-    //                 } else {
-    //                     "Collapse Subchannels"
-    //                 };
-    //                 items.push(ContextMenuItem::action(
-    //                     expand_action_name,
-    //                     ToggleCollapse {
-    //                         location: channel.id,
-    //                     },
-    //                 ));
-    //             }
+                if let Some(channel_name) = clipboard_channel_name {
+                    context_menu = context_menu.separator().entry(
+                        format!("Move '#{}' here", channel_name),
+                        cx.handler_for(&this, move |this, cx| {
+                            this.move_channel_on_clipboard(channel_id, cx)
+                        }),
+                    );
+                }
 
-    //             items.push(ContextMenuItem::action(
-    //                 "Open Notes",
-    //                 OpenChannelNotes {
-    //                     channel_id: channel.id,
-    //                 },
-    //             ));
-
-    //             items.push(ContextMenuItem::action(
-    //                 "Open Chat",
-    //                 JoinChannelChat {
-    //                     channel_id: channel.id,
-    //                 },
-    //             ));
-
-    //             items.push(ContextMenuItem::action(
-    //                 "Copy Channel Link",
-    //                 CopyChannelLink {
-    //                     channel_id: channel.id,
-    //                 },
-    //             ));
-
-    //             if self.channel_store.read(cx).is_channel_admin(channel.id) {
-    //                 items.extend([
-    //                     ContextMenuItem::Separator,
-    //                     ContextMenuItem::action(
-    //                         "New Subchannel",
-    //                         NewChannel {
-    //                             location: channel.id,
-    //                         },
-    //                     ),
-    //                     ContextMenuItem::action(
-    //                         "Rename",
-    //                         RenameChannel {
-    //                             channel_id: channel.id,
-    //                         },
-    //                     ),
-    //                     ContextMenuItem::action(
-    //                         "Move this channel",
-    //                         StartMoveChannelFor {
-    //                             channel_id: channel.id,
-    //                         },
-    //                     ),
-    //                 ]);
-
-    //                 if let Some(channel_name) = clipboard_channel_name {
-    //                     items.push(ContextMenuItem::Separator);
-    //                     items.push(ContextMenuItem::action(
-    //                         format!("Move '#{}' here", channel_name),
-    //                         MoveChannel { to: channel.id },
-    //                     ));
-    //                 }
+                context_menu = context_menu
+                    .separator()
+                    .entry(
+                        "Invite Members",
+                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
+                    )
+                    .entry(
+                        "Manage Members",
+                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
+                    )
+                    .entry(
+                        "Delete",
+                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
+                    );
+            }
 
-    //                 items.extend([
-    //                     ContextMenuItem::Separator,
-    //                     ContextMenuItem::action(
-    //                         "Invite Members",
-    //                         InviteMembers {
-    //                             channel_id: channel.id,
-    //                         },
-    //                     ),
-    //                     ContextMenuItem::action(
-    //                         "Manage Members",
-    //                         ManageMembers {
-    //                             channel_id: channel.id,
-    //                         },
-    //                     ),
-    //                     ContextMenuItem::Separator,
-    //                     ContextMenuItem::action(
-    //                         "Delete",
-    //                         RemoveChannel {
-    //                             channel_id: channel.id,
-    //                         },
-    //                     ),
-    //                 ]);
-    //             }
+            context_menu
+        });
 
-    //             context_menu.show(
-    //                 position.unwrap_or_default(),
-    //                 if self.context_menu_on_selected {
-    //                     gpui::elements::AnchorCorner::TopRight
-    //                 } else {
-    //                     gpui::elements::AnchorCorner::BottomLeft
-    //                 },
-    //                 items,
-    //                 cx,
-    //             );
-    //         });
+        cx.focus_view(&context_menu);
+        let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
+            this.context_menu.take();
+            cx.notify();
+        });
+        self.context_menu = Some((context_menu, position, subscription));
 
-    //         cx.notify();
-    //     }
+        cx.notify();
+    }
 
     //     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
     //         if self.take_editing_state(cx) {
@@ -2116,65 +2065,88 @@ impl CollabPanel {
         });
     }
 
-    //     fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
-    //         self.collapsed_channels
-    //             .retain(|channel| *channel != action.location);
-    //         self.channel_editing_state = Some(ChannelEditingState::Create {
-    //             location: Some(action.location.to_owned()),
-    //             pending_name: None,
-    //         });
-    //         self.update_entries(false, cx);
-    //         self.select_channel_editor();
-    //         cx.focus(self.channel_name_editor.as_any());
-    //         cx.notify();
-    //     }
+    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        self.collapsed_channels
+            .retain(|channel| *channel != channel_id);
+        self.channel_editing_state = Some(ChannelEditingState::Create {
+            location: Some(channel_id),
+            pending_name: None,
+        });
+        self.update_entries(false, cx);
+        self.select_channel_editor();
+        cx.focus_view(&self.channel_name_editor);
+        cx.notify();
+    }
 
-    //     fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
-    //         self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
-    //     }
+    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        todo!();
+        // self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
+    }
 
-    //     fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
-    //         self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
-    //     }
+    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        todo!();
+        // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
+    }
 
-    //     fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
-    //         if let Some(channel) = self.selected_channel() {
-    //             self.remove_channel(channel.id, cx)
-    //         }
-    //     }
+    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.remove_channel(channel.id, cx)
+        }
+    }
 
-    //     fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-    //         if let Some(channel) = self.selected_channel() {
-    //             self.rename_channel(
-    //                 &RenameChannel {
-    //                     channel_id: channel.id,
-    //                 },
-    //                 cx,
-    //             );
-    //         }
-    //     }
+    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.rename_channel(channel.id, cx);
+        }
+    }
 
-    //     fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
-    //         let channel_store = self.channel_store.read(cx);
-    //         if !channel_store.is_channel_admin(action.channel_id) {
-    //             return;
-    //         }
-    //         if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
-    //             self.channel_editing_state = Some(ChannelEditingState::Rename {
-    //                 location: action.channel_id.to_owned(),
-    //                 pending_name: None,
-    //             });
-    //             self.channel_name_editor.update(cx, |editor, cx| {
-    //                 editor.set_text(channel.name.clone(), cx);
-    //                 editor.select_all(&Default::default(), cx);
-    //             });
-    //             cx.focus(self.channel_name_editor.as_any());
-    //             self.update_entries(false, cx);
-    //             self.select_channel_editor();
-    //         }
-    //     }
+    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        if !channel_store.is_channel_admin(channel_id) {
+            return;
+        }
+        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
+            self.channel_editing_state = Some(ChannelEditingState::Rename {
+                location: channel_id,
+                pending_name: None,
+            });
+            self.channel_name_editor.update(cx, |editor, cx| {
+                editor.set_text(channel.name.clone(), cx);
+                editor.select_all(&Default::default(), cx);
+            });
+            cx.focus_view(&self.channel_name_editor);
+            self.update_entries(false, cx);
+            self.select_channel_editor();
+        }
+    }
+
+    fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
+    }
 
-    fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+    fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.channel_clipboard = Some(ChannelMoveClipboard {
+                channel_id: channel.id,
+            })
+        }
+    }
+
+    fn move_channel_on_clipboard(
+        &mut self,
+        to_channel_id: ChannelId,
+        cx: &mut ViewContext<CollabPanel>,
+    ) {
+        if let Some(clipboard) = self.channel_clipboard.take() {
+            self.channel_store.update(cx, |channel_store, cx| {
+                channel_store
+                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
+                    .detach_and_log_err(cx)
+            })
+        }
+    }
+
+    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade() {
             todo!();
             // ChannelView::open(action.channel_id, workspace, cx).detach();
@@ -2235,35 +2207,29 @@ impl CollabPanel {
     //         self.remove_channel(action.channel_id, cx)
     //     }
 
-    //     fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-    //         let channel_store = self.channel_store.clone();
-    //         if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
-    //             let prompt_message = format!(
-    //                 "Are you sure you want to remove the channel \"{}\"?",
-    //                 channel.name
-    //             );
-    //             let mut answer =
-    //                 cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-    //             let window = cx.window();
-    //             cx.spawn(|this, mut cx| async move {
-    //                 if answer.next().await == Some(0) {
-    //                     if let Err(e) = channel_store
-    //                         .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
-    //                         .await
-    //                     {
-    //                         window.prompt(
-    //                             PromptLevel::Info,
-    //                             &format!("Failed to remove channel: {}", e),
-    //                             &["Ok"],
-    //                             &mut cx,
-    //                         );
-    //                     }
-    //                     this.update(&mut cx, |_, cx| cx.focus_self()).ok();
-    //                 }
-    //             })
-    //             .detach();
-    //         }
-    //     }
+    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.clone();
+        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
+            let prompt_message = format!(
+                "Are you sure you want to remove the channel \"{}\"?",
+                channel.name
+            );
+            let mut answer =
+                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+            let window = cx.window();
+            cx.spawn(|this, mut cx| async move {
+                if answer.await? == 0 {
+                    channel_store
+                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
+                        .await
+                        .notify_async_err(&mut cx);
+                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
+                }
+                anyhow::Ok(())
+            })
+            .detach();
+        }
+    }
 
     //     // Should move to the filter editor if clicking on it
     //     // Should move selection to the channel editor if activating it
@@ -2344,31 +2310,32 @@ impl CollabPanel {
         .detach()
     }
 
-    //     fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
-    //         let channel_id = action.channel_id;
-    //         if let Some(workspace) = self.workspace.upgrade(cx) {
-    //             cx.app_context().defer(move |cx| {
-    //                 workspace.update(cx, |workspace, cx| {
-    //                     if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-    //                         panel.update(cx, |panel, cx| {
-    //                             panel
-    //                                 .select_channel(channel_id, None, cx)
-    //                                 .detach_and_log_err(cx);
-    //                         });
-    //                     }
-    //                 });
-    //             });
-    //         }
-    //     }
+    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
+        cx.window_context().defer(move |cx| {
+            workspace.update(cx, |workspace, cx| {
+                todo!();
+                // if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+                //     panel.update(cx, |panel, cx| {
+                //         panel
+                //             .select_channel(channel_id, None, cx)
+                //             .detach_and_log_err(cx);
+                //     });
+                // }
+            });
+        });
+    }
 
-    //     fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
-    //         let channel_store = self.channel_store.read(cx);
-    //         let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
-    //             return;
-    //         };
-    //         let item = ClipboardItem::new(channel.link());
-    //         cx.write_to_clipboard(item)
-    //     }
+    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        let Some(channel) = channel_store.channel_for_id(channel_id) else {
+            return;
+        };
+        let item = ClipboardItem::new(channel.link());
+        cx.write_to_clipboard(item)
+    }
 
     fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
         v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener(
@@ -2388,37 +2355,41 @@ impl CollabPanel {
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List {
         let is_selected = false; // todo!() this.selection == Some(ix);
 
-        List::new().children(self.entries.clone().into_iter().map(|entry| {
-            match entry {
-                ListEntry::Header(section) => {
-                    let is_collapsed = self.collapsed_sections.contains(&section);
-                    self.render_header(section, is_selected, is_collapsed, cx)
-                        .into_any_element()
-                }
-                ListEntry::Contact { contact, calling } => self
-                    .render_contact(&*contact, calling, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::ContactPlaceholder => self
-                    .render_contact_placeholder(is_selected, cx)
-                    .into_any_element(),
-                ListEntry::IncomingRequest(user) => self
-                    .render_contact_request(user, true, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::OutgoingRequest(user) => self
-                    .render_contact_request(user, false, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::Channel {
-                    channel,
-                    depth,
-                    has_children,
-                } => self
-                    .render_channel(&*channel, depth, has_children, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::ChannelEditor { depth } => {
-                    self.render_channel_editor(depth, cx).into_any_element()
-                }
-            }
-        }))
+        List::new().children(
+            self.entries
+                .clone()
+                .into_iter()
+                .enumerate()
+                .map(|(ix, entry)| match entry {
+                    ListEntry::Header(section) => {
+                        let is_collapsed = self.collapsed_sections.contains(&section);
+                        self.render_header(section, is_selected, is_collapsed, cx)
+                            .into_any_element()
+                    }
+                    ListEntry::Contact { contact, calling } => self
+                        .render_contact(&*contact, calling, is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::ContactPlaceholder => self
+                        .render_contact_placeholder(is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::IncomingRequest(user) => self
+                        .render_contact_request(user, true, is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::OutgoingRequest(user) => self
+                        .render_contact_request(user, false, is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::Channel {
+                        channel,
+                        depth,
+                        has_children,
+                    } => self
+                        .render_channel(&*channel, depth, has_children, is_selected, ix, cx)
+                        .into_any_element(),
+                    ListEntry::ChannelEditor { depth } => {
+                        self.render_channel_editor(depth, cx).into_any_element()
+                    }
+                }),
+        )
     }
 
     fn render_header(
@@ -2530,14 +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(Toggleable::Toggleable(is_collapsed.into()))
-                    .on_toggle(
-                        cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)),
-                    )
+                el.toggle(Some(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)
+                    },
+                ))
             })
     }
 
@@ -2748,6 +2736,7 @@ impl CollabPanel {
         depth: usize,
         has_children: bool,
         is_selected: bool,
+        ix: usize,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
         let channel_id = channel.id;
@@ -2768,9 +2757,8 @@ impl CollabPanel {
             .map(|channel| channel.visibility)
             == Some(proto::ChannelVisibility::Public);
         let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
-        let disclosed = has_children
-            .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok())
-            .unwrap_or(false);
+        let disclosed =
+            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
 
         let has_messages_notification = channel.unseen_message_id.is_some();
         let has_notes_notification = channel.unseen_note_version.is_some();
@@ -2801,79 +2789,111 @@ impl CollabPanel {
             None
         };
 
-        div().group("").child(
-            ListItem::new(channel_id as usize)
-                .indent_level(depth)
-                .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 } })
+                }
+            })
+            .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,
+                                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 {
-                    Toggleable::Toggleable(disclosed.into())
-                } else {
-                    Toggleable::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(&OpenChannelNotes { 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(disclosed)
+                    .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(|this, _, cx| {
-                    todo!() // open context menu
-                })),
-        )
+                    }))
+                    .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;
@@ -3101,7 +3121,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 {
@@ -3270,6 +3291,12 @@ impl Render for CollabPanel {
                     el.child(self.render_signed_in(cx))
                 }
             })
+            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
+                overlay()
+                    .position(*position)
+                    .anchor(gpui::AnchorCorner::TopLeft)
+                    .child(menu.clone())
+            }))
     }
 }
 
@@ -3392,14 +3419,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();
     }
 
@@ -3541,3 +3569,34 @@ 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
+                    },
+                )
+                .size(IconSize::Small)
+                .color(Color::Muted),
+            )
+            .child(Label::new(self.channel.name.clone()))
+    }
+}

crates/gpui2/src/elements/div.rs 🔗

@@ -824,7 +824,6 @@ impl Interactivity {
             .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
 
         if let Some(group_bounds) = hover_group_bounds {
-            // todo!() needs cx.was_top_layer
             let hovered = group_bounds.contains_point(&cx.mouse_position());
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
@@ -836,13 +835,13 @@ impl Interactivity {
         }
 
         if self.hover_style.is_some()
-            || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
+            || cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
         {
-            let interactive_bounds = interactive_bounds.clone();
-            let hovered = interactive_bounds.visibly_contains(&cx.mouse_position(), cx);
+            let bounds = bounds.intersect(&cx.content_mask().bounds);
+            let hovered = bounds.contains_point(&cx.mouse_position());
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
-                    if interactive_bounds.visibly_contains(&event.position, cx) != hovered {
+                    if bounds.contains_point(&event.position) != hovered {
                         cx.notify();
                     }
                 }
@@ -1143,7 +1142,9 @@ impl Interactivity {
             let mouse_position = cx.mouse_position();
             if let Some(group_hover) = self.group_hover_style.as_ref() {
                 if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
-                    if group_bounds.contains_point(&mouse_position) {
+                    if group_bounds.contains_point(&mouse_position)
+                        && cx.was_top_layer(&mouse_position, cx.stacking_order())
+                    {
                         style.refine(&group_hover.style);
                     }
                 }
@@ -1162,7 +1163,6 @@ impl Interactivity {
                 for (state_type, group_drag_style) in &self.group_drag_over_styles {
                     if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
                         if *state_type == drag.view.entity_type()
-                        // todo!() needs to handle cx.content_mask() and cx.is_top()
                             && group_bounds.contains_point(&mouse_position)
                         {
                             style.refine(&group_drag_style.style);
@@ -1175,7 +1175,6 @@ impl Interactivity {
                         && bounds
                             .intersect(&cx.content_mask().bounds)
                             .contains_point(&mouse_position)
-                        && cx.was_top_layer(&mouse_position, cx.stacking_order())
                     {
                         style.refine(drag_over_style);
                     }

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/project_panel2/src/project_panel.rs 🔗

@@ -1480,7 +1480,7 @@ impl Render for ProjectPanel {
                 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
                     overlay()
                         .position(*position)
-                        .anchor(gpui::AnchorCorner::BottomLeft)
+                        .anchor(gpui::AnchorCorner::TopLeft)
                         .child(menu.clone())
                 }))
         } else {

crates/ui2/src/components/disclosure.rs 🔗

@@ -3,29 +3,22 @@ use std::rc::Rc;
 use gpui::ClickEvent;
 
 use crate::prelude::*;
-use crate::{Color, Icon, IconButton, IconSize, ToggleState, Toggleable};
+use crate::{Color, Icon, IconButton, IconSize};
 
 #[derive(IntoElement)]
 pub struct Disclosure {
-    state: ToggleState,
+    is_open: bool,
     on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
 }
 
 impl Disclosure {
-    pub fn new(state: ToggleState) -> Self {
+    pub fn new(is_open: bool) -> Self {
         Self {
-            state,
+            is_open,
             on_toggle: None,
         }
     }
 
-    pub fn from_toggleable(toggleable: Toggleable) -> Option<Self> {
-        match toggleable {
-            Toggleable::Toggleable(state) => Some(Self::new(state)),
-            Toggleable::NotToggleable => None,
-        }
-    }
-
     pub fn on_toggle(
         mut self,
         handler: impl Into<Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>>,
@@ -41,9 +34,10 @@ impl RenderOnce for Disclosure {
     fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
         IconButton::new(
             "toggle",
-            match self.state {
-                ToggleState::Toggled => Icon::ChevronDown,
-                ToggleState::NotToggled => Icon::ChevronRight,
+            if self.is_open {
+                Icon::ChevronDown
+            } else {
+                Icon::ChevronRight
             },
         )
         .color(Color::Muted)

crates/ui2/src/components/list.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{AnyElement, Div};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
-use crate::{v_stack, Label, ToggleState, Toggleable};
+use crate::{v_stack, Label};
 
 pub use list_header::*;
 pub use list_item::*;
@@ -20,7 +20,7 @@ pub struct List {
     /// Defaults to "No items"
     empty_message: SharedString,
     header: Option<ListHeader>,
-    toggle: Toggleable,
+    toggle: Option<bool>,
     children: SmallVec<[AnyElement; 2]>,
 }
 
@@ -29,7 +29,7 @@ impl List {
         Self {
             empty_message: "No items".into(),
             header: None,
-            toggle: Toggleable::NotToggleable,
+            toggle: None,
             children: SmallVec::new(),
         }
     }
@@ -44,7 +44,7 @@ impl List {
         self
     }
 
-    pub fn toggle(mut self, toggle: Toggleable) -> Self {
+    pub fn toggle(mut self, toggle: Option<bool>) -> Self {
         self.toggle = toggle;
         self
     }
@@ -66,7 +66,7 @@ impl RenderOnce for List {
             .children(self.header.map(|header| header))
             .map(|this| match (self.children.is_empty(), self.toggle) {
                 (false, _) => this.children(self.children),
-                (true, Toggleable::Toggleable(ToggleState::NotToggled)) => this,
+                (true, Some(false)) => this,
                 (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
             })
     }

crates/ui2/src/components/list/list_header.rs 🔗

@@ -3,7 +3,7 @@ use std::rc::Rc;
 use gpui::{ClickEvent, Div};
 
 use crate::prelude::*;
-use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label, Toggleable};
+use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
 
 pub enum ListHeaderMeta {
     Tools(Vec<IconButton>),
@@ -17,7 +17,7 @@ pub struct ListHeader {
     label: SharedString,
     left_icon: Option<Icon>,
     meta: Option<ListHeaderMeta>,
-    toggle: Toggleable,
+    toggle: Option<bool>,
     on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     inset: bool,
     selected: bool,
@@ -30,13 +30,13 @@ impl ListHeader {
             left_icon: None,
             meta: None,
             inset: false,
-            toggle: Toggleable::NotToggleable,
+            toggle: None,
             on_toggle: None,
             selected: false,
         }
     }
 
-    pub fn toggle(mut self, toggle: Toggleable) -> Self {
+    pub fn toggle(mut self, toggle: Option<bool>) -> Self {
         self.toggle = toggle;
         self
     }
@@ -114,8 +114,8 @@ impl RenderOnce for ListHeader {
                                 .child(Label::new(self.label.clone()).color(Color::Muted)),
                         )
                         .children(
-                            Disclosure::from_toggleable(self.toggle)
-                                .map(|disclosure| disclosure.on_toggle(self.on_toggle)),
+                            self.toggle
+                                .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
                         ),
                 )
                 .child(meta),

crates/ui2/src/components/list/list_item.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
 use smallvec::SmallVec;
 
 use crate::prelude::*;
-use crate::{Avatar, Disclosure, GraphicSlot, Icon, IconElement, IconSize, Toggleable};
+use crate::{Avatar, Disclosure, GraphicSlot, Icon, IconElement, IconSize};
 
 #[derive(IntoElement)]
 pub struct ListItem {
@@ -17,7 +17,7 @@ pub struct ListItem {
     indent_level: usize,
     indent_step_size: Pixels,
     left_slot: Option<GraphicSlot>,
-    toggle: Toggleable,
+    toggle: Option<bool>,
     inset: bool,
     on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@@ -33,7 +33,7 @@ impl ListItem {
             indent_level: 0,
             indent_step_size: px(12.),
             left_slot: None,
-            toggle: Toggleable::NotToggleable,
+            toggle: None,
             inset: false,
             on_click: None,
             on_secondary_mouse_down: None,
@@ -70,7 +70,7 @@ impl ListItem {
         self
     }
 
-    pub fn toggle(mut self, toggle: Toggleable) -> Self {
+    pub fn toggle(mut self, toggle: Option<bool>) -> Self {
         self.toggle = toggle;
         self
     }
@@ -151,8 +151,8 @@ impl RenderOnce for ListItem {
                     .items_center()
                     .relative()
                     .children(
-                        Disclosure::from_toggleable(self.toggle)
-                            .map(|disclosure| disclosure.on_toggle(self.on_toggle)),
+                        self.toggle
+                            .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
                     )
                     .map(|this| match self.left_slot {
                         Some(GraphicSlot::Icon(i)) => this.child(

crates/ui2/src/components/stories/disclosure.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{Div, Render};
 use story::Story;
 
 use crate::prelude::*;
-use crate::{Disclosure, ToggleState};
+use crate::Disclosure;
 
 pub struct DisclosureStory;
 
@@ -13,8 +13,8 @@ impl Render for DisclosureStory {
         Story::container()
             .child(Story::title_for::<Disclosure>())
             .child(Story::label("Toggled"))
-            .child(Disclosure::new(ToggleState::Toggled))
+            .child(Disclosure::new(true))
             .child(Story::label("Not Toggled"))
-            .child(Disclosure::new(ToggleState::NotToggled))
+            .child(Disclosure::new(false))
     }
 }

crates/ui2/src/toggleable.rs 🔗

@@ -1,38 +0,0 @@
-/// Whether an element is able to be toggled.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum Toggleable {
-    Toggleable(ToggleState),
-    NotToggleable,
-}
-
-/// The current state of a [`Toggleable`] element.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum ToggleState {
-    Toggled,
-    NotToggled,
-}
-
-impl ToggleState {
-    /// Returns whether an entry is toggled.
-    pub fn is_toggled(&self) -> bool {
-        match self {
-            ToggleState::Toggled => true,
-            ToggleState::NotToggled => false,
-        }
-    }
-}
-
-impl From<bool> for ToggleState {
-    fn from(toggled: bool) -> Self {
-        match toggled {
-            true => Self::Toggled,
-            false => Self::NotToggled,
-        }
-    }
-}
-
-impl From<ToggleState> for bool {
-    fn from(value: ToggleState) -> Self {
-        value.is_toggled()
-    }
-}

crates/ui2/src/ui2.rs 🔗

@@ -21,7 +21,6 @@ mod selectable;
 mod slot;
 mod styled_ext;
 mod styles;
-mod toggleable;
 pub mod utils;
 
 pub use clickable::*;
@@ -33,4 +32,3 @@ pub use selectable::*;
 pub use slot::*;
 pub use styled_ext::*;
 pub use styles::*;
-pub use toggleable::*;