diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 32e73f5ee1..62a4dffbfd 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}; @@ -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| { - // panel.channel_clipboard = Some(ChannelMoveClipboard { - // channel_id: action.channel_id, - // }); - // }, - // ); - - // cx.add_action( - // |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { - // 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| { // let Some(clipboard) = panel.channel_clipboard.take() else { @@ -303,12 +285,12 @@ impl ChannelEditingState { } pub struct CollabPanel { - width: Option, + width: Option, fs: Arc, focus_handle: FocusHandle, - // channel_clipboard: Option, + channel_clipboard: Option, pending_serialization: Task>, - // context_menu: ViewHandle, + context_menu: Option<(View, Point, Subscription)>, filter_editor: View, channel_name_editor: View, channel_editing_state: Option, @@ -337,7 +319,7 @@ enum ChannelDragTarget { #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { - width: Option, + width: Option, collapsed_channels: Option>, } @@ -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 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, - // channel: &Channel, - // ix: usize, - // cx: &mut ViewContext, - // ) { - // self.context_menu_on_selected = position.is_none(); + fn deploy_channel_context_menu( + &mut self, + position: Point, + channel_id: ChannelId, + ix: usize, + cx: &mut ViewContext, + ) { + // self.context_menu_on_selected = position.is_none(); - // 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 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(); - // 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 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 mut items = Vec::new(); + 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) + }), + ); - // let select_action_name = if self.selection == Some(ix) { - // "Unselect" - // } else { - // "Select" - // }; + 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) + }), + ); - // items.push(ContextMenuItem::action( - // select_action_name, - // ToggleSelectedIx { ix }, - // )); + 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) + }), + ); + } - // 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, - // }, - // )); - // } + 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.push(ContextMenuItem::action( - // "Open Notes", - // OpenChannelNotes { - // channel_id: channel.id, - // }, - // )); + context_menu + }); - // items.push(ContextMenuItem::action( - // "Open Chat", - // JoinChannelChat { - // channel_id: channel.id, - // }, - // )); + 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)); - // 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 }, - // )); - // } - - // 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.show( - // position.unwrap_or_default(), - // if self.context_menu_on_selected { - // gpui::elements::AnchorCorner::TopRight - // } else { - // gpui::elements::AnchorCorner::BottomLeft - // }, - // items, - // cx, - // ); - // }); - - // cx.notify(); - // } + cx.notify(); + } // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { // if self.take_editing_state(cx) { @@ -2116,65 +2065,88 @@ impl CollabPanel { }); } - // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { - // 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.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.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); - // } + fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + todo!(); + // self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx); + } - // fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { - // self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); - // } + fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + todo!(); + // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); + } - // fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - // if let Some(channel) = self.selected_channel() { - // self.remove_channel(channel.id, cx) - // } - // } + fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.remove_channel(channel.id, cx) + } + } - // fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - // 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) { + if let Some(channel) = self.selected_channel() { + self.rename_channel(channel.id, cx); + } + } - // fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { - // 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) { + 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 open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { + fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + self.channel_clipboard = Some(ChannelMoveClipboard { channel_id }); + } + + fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + 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, + ) { + 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) { 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) { - // 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) { + 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) { - // 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::(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) { + 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::(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) { - // 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) { + 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) -> 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) -> 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(§ion); - 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(§ion); + 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::(|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) + }, + )) }) } @@ -2748,6 +2736,7 @@ impl CollabPanel { depth: usize, has_children: bool, is_selected: bool, + ix: usize, cx: &mut ViewContext, ) -> 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::(|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, + 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, cx: &mut ViewContext) { - 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::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())) + } +} diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 635e8b634f..3922c0c4e4 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/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); } 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/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index e4846af76c..dc584d52ff 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/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 { diff --git a/crates/ui2/src/components/disclosure.rs b/crates/ui2/src/components/disclosure.rs index eafa11c6ea..b3736f36a0 100644 --- a/crates/ui2/src/components/disclosure.rs +++ b/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>, } 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 { - match toggleable { - Toggleable::Toggleable(state) => Some(Self::new(state)), - Toggleable::NotToggleable => None, - } - } - pub fn on_toggle( mut self, handler: impl Into>>, @@ -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) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 51d3ad8ecb..f5eb2eb44b 100644 --- a/crates/ui2/src/components/list.rs +++ b/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, - toggle: Toggleable, + toggle: Option, 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) -> 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)), }) } diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 70c746d077..a205de6220 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/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), @@ -17,7 +17,7 @@ pub struct ListHeader { label: SharedString, left_icon: Option, meta: Option, - toggle: Toggleable, + toggle: Option, on_toggle: Option>, 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) -> 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), diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index feb123c8cc..d8630289a7 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/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, - toggle: Toggleable, + toggle: Option, inset: bool, on_click: Option>, on_toggle: Option>, @@ -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) -> 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( diff --git a/crates/ui2/src/components/stories/disclosure.rs b/crates/ui2/src/components/stories/disclosure.rs index f08bd43f4e..7a2c419456 100644 --- a/crates/ui2/src/components/stories/disclosure.rs +++ b/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::()) .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)) } } diff --git a/crates/ui2/src/toggleable.rs b/crates/ui2/src/toggleable.rs deleted file mode 100644 index 99c2df0b0d..0000000000 --- a/crates/ui2/src/toggleable.rs +++ /dev/null @@ -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 for ToggleState { - fn from(toggled: bool) -> Self { - match toggled { - true => Self::Toggled, - false => Self::NotToggled, - } - } -} - -impl From for bool { - fn from(value: ToggleState) -> Self { - value.is_toggled() - } -} diff --git a/crates/ui2/src/ui2.rs b/crates/ui2/src/ui2.rs index d1da22149c..7b054890a6 100644 --- a/crates/ui2/src/ui2.rs +++ b/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::*;