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

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2023-11-29 13:40:46 -07:00 committed by GitHub
commit fb377aed73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 490 additions and 478 deletions

View file

@ -19,6 +19,7 @@ mod contact_finder;
use contact_finder::ContactFinder; use contact_finder::ContactFinder;
use menu::Confirm; use menu::Confirm;
use rpc::proto; use rpc::proto;
use theme::{ActiveTheme, ThemeSettings};
// use context_menu::{ContextMenu, ContextMenuItem}; // use context_menu::{ContextMenu, ContextMenuItem};
// use db::kvp::KEY_VALUE_STORE; // use db::kvp::KEY_VALUE_STORE;
// use drag_and_drop::{DragAndDrop, Draggable}; // use drag_and_drop::{DragAndDrop, Draggable};
@ -151,10 +152,10 @@ actions!(
// ] // ]
// ); // );
// #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
// struct ChannelMoveClipboard { struct ChannelMoveClipboard {
// channel_id: ChannelId, channel_id: ChannelId,
// } }
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
@ -168,17 +169,18 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div, actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable,
ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Pixels,
ViewContext, VisualContext, WeakView, Point, PromptLevel, Render, RenderOnce, SharedString, Stateful, Styled, Subscription, Task,
View, ViewContext, VisualContext, WeakView,
}; };
use project::Fs; use project::Fs;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use ui::{ use ui::{
h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List, h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
ListHeader, ListItem, Toggleable, Tooltip, Label, List, ListHeader, ListItem, Tooltip,
}; };
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ 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( // cx.add_action(
// |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| { // |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
// let Some(clipboard) = panel.channel_clipboard.take() else { // let Some(clipboard) = panel.channel_clipboard.take() else {
@ -303,12 +285,12 @@ impl ChannelEditingState {
} }
pub struct CollabPanel { pub struct CollabPanel {
width: Option<f32>, width: Option<Pixels>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
// channel_clipboard: Option<ChannelMoveClipboard>, channel_clipboard: Option<ChannelMoveClipboard>,
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
// context_menu: ViewHandle<ContextMenu>, context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
filter_editor: View<Editor>, filter_editor: View<Editor>,
channel_name_editor: View<Editor>, channel_name_editor: View<Editor>,
channel_editing_state: Option<ChannelEditingState>, channel_editing_state: Option<ChannelEditingState>,
@ -337,7 +319,7 @@ enum ChannelDragTarget {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct SerializedCollabPanel { struct SerializedCollabPanel {
width: Option<f32>, width: Option<Pixels>,
collapsed_channels: Option<Vec<u64>>, collapsed_channels: Option<Vec<u64>>,
} }
@ -589,10 +571,10 @@ impl CollabPanel {
let mut this = Self { let mut this = Self {
width: None, width: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
// channel_clipboard: None, channel_clipboard: None,
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), context_menu: None,
channel_name_editor, channel_name_editor,
filter_editor, filter_editor,
entries: Vec::default(), entries: Vec::default(),
@ -1685,156 +1667,123 @@ impl CollabPanel {
// .into_any() // .into_any()
// } // }
// fn has_subchannels(&self, ix: usize) -> bool { fn has_subchannels(&self, ix: usize) -> bool {
// self.entries.get(ix).map_or(false, |entry| { self.entries.get(ix).map_or(false, |entry| {
// if let ListEntry::Channel { has_children, .. } = entry { if let ListEntry::Channel { has_children, .. } = entry {
// *has_children *has_children
// } else { } else {
// false false
// } }
// }) })
// } }
// fn deploy_channel_context_menu( fn deploy_channel_context_menu(
// &mut self, &mut self,
// position: Option<Vector2F>, position: Point<Pixels>,
// channel: &Channel, channel_id: ChannelId,
// ix: usize, ix: usize,
// cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
// ) { ) {
// self.context_menu_on_selected = position.is_none(); // self.context_menu_on_selected = position.is_none();
// let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
// self.channel_store self.channel_store
// .read(cx) .read(cx)
// .channel_for_id(clipboard.channel_id) .channel_for_id(clipboard.channel_id)
// .map(|channel| channel.name.clone()) .map(|channel| channel.name.clone())
// }); });
let this = cx.view().clone();
// self.context_menu.update(cx, |context_menu, cx| { let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
// context_menu.set_position_mode(if self.context_menu_on_selected { if self.has_subchannels(ix) {
// OverlayPositionMode::Local let expand_action_name = if self.is_channel_collapsed(channel_id) {
// } else { "Expand Subchannels"
// OverlayPositionMode::Window } 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) { if self.channel_store.read(cx).is_channel_admin(channel_id) {
// "Unselect" context_menu = context_menu
// } else { .separator()
// "Select" .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( if let Some(channel_name) = clipboard_channel_name {
// select_action_name, context_menu = context_menu.separator().entry(
// ToggleSelectedIx { ix }, 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) { context_menu = context_menu
// let expand_action_name = if self.is_channel_collapsed(channel.id) { .separator()
// "Expand Subchannels" .entry(
// } else { "Invite Members",
// "Collapse Subchannels" cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
// }; )
// items.push(ContextMenuItem::action( .entry(
// expand_action_name, "Manage Members",
// ToggleCollapse { cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
// location: channel.id, )
// }, .entry(
// )); "Delete",
// } cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
);
}
// items.push(ContextMenuItem::action( context_menu
// "Open Notes", });
// OpenChannelNotes {
// channel_id: channel.id,
// },
// ));
// items.push(ContextMenuItem::action( cx.focus_view(&context_menu);
// "Open Chat", let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
// JoinChannelChat { this.context_menu.take();
// channel_id: channel.id, cx.notify();
// }, });
// )); self.context_menu = Some((context_menu, position, subscription));
// items.push(ContextMenuItem::action( cx.notify();
// "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();
// }
// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// if self.take_editing_state(cx) { // if self.take_editing_state(cx) {
@ -2116,65 +2065,88 @@ impl CollabPanel {
}); });
} }
// fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) { fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// self.collapsed_channels self.collapsed_channels
// .retain(|channel| *channel != action.location); .retain(|channel| *channel != channel_id);
// self.channel_editing_state = Some(ChannelEditingState::Create { self.channel_editing_state = Some(ChannelEditingState::Create {
// location: Some(action.location.to_owned()), location: Some(channel_id),
// pending_name: None, pending_name: None,
// }); });
// self.update_entries(false, cx); self.update_entries(false, cx);
// self.select_channel_editor(); self.select_channel_editor();
// cx.focus(self.channel_name_editor.as_any()); cx.focus_view(&self.channel_name_editor);
// cx.notify(); cx.notify();
// } }
// fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) { fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); todo!();
// } // self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
}
// fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) { fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); todo!();
// } // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
}
// fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) { fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
// if let Some(channel) = self.selected_channel() { if let Some(channel) = self.selected_channel() {
// self.remove_channel(channel.id, cx) self.remove_channel(channel.id, cx)
// } }
// } }
// fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) { fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
// if let Some(channel) = self.selected_channel() { if let Some(channel) = self.selected_channel() {
// self.rename_channel( self.rename_channel(channel.id, cx);
// &RenameChannel { }
// channel_id: channel.id, }
// },
// cx,
// );
// }
// }
// fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) { fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// let channel_store = self.channel_store.read(cx); let channel_store = self.channel_store.read(cx);
// if !channel_store.is_channel_admin(action.channel_id) { if !channel_store.is_channel_admin(channel_id) {
// return; return;
// } }
// if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
// self.channel_editing_state = Some(ChannelEditingState::Rename { self.channel_editing_state = Some(ChannelEditingState::Rename {
// location: action.channel_id.to_owned(), location: channel_id,
// pending_name: None, pending_name: None,
// }); });
// self.channel_name_editor.update(cx, |editor, cx| { self.channel_name_editor.update(cx, |editor, cx| {
// editor.set_text(channel.name.clone(), cx); editor.set_text(channel.name.clone(), cx);
// editor.select_all(&Default::default(), cx); editor.select_all(&Default::default(), cx);
// }); });
// cx.focus(self.channel_name_editor.as_any()); cx.focus_view(&self.channel_name_editor);
// self.update_entries(false, cx); self.update_entries(false, cx);
// self.select_channel_editor(); self.select_channel_editor();
// } }
// } }
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) { fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
}
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() { if let Some(workspace) = self.workspace.upgrade() {
todo!(); todo!();
// ChannelView::open(action.channel_id, workspace, cx).detach(); // ChannelView::open(action.channel_id, workspace, cx).detach();
@ -2235,35 +2207,29 @@ impl CollabPanel {
// self.remove_channel(action.channel_id, cx) // self.remove_channel(action.channel_id, cx)
// } // }
// fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) { fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// let channel_store = self.channel_store.clone(); let channel_store = self.channel_store.clone();
// if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
// let prompt_message = format!( let prompt_message = format!(
// "Are you sure you want to remove the channel \"{}\"?", "Are you sure you want to remove the channel \"{}\"?",
// channel.name channel.name
// ); );
// let mut answer = let mut answer =
// cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
// let window = cx.window(); let window = cx.window();
// cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
// if answer.next().await == Some(0) { if answer.await? == 0 {
// if let Err(e) = channel_store channel_store
// .update(&mut cx, |channels, _| channels.remove_channel(channel_id)) .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
// .await .await
// { .notify_async_err(&mut cx);
// window.prompt( this.update(&mut cx, |_, cx| cx.focus_self()).ok();
// PromptLevel::Info, }
// &format!("Failed to remove channel: {}", e), anyhow::Ok(())
// &["Ok"], })
// &mut cx, .detach();
// ); }
// } }
// this.update(&mut cx, |_, cx| cx.focus_self()).ok();
// }
// })
// .detach();
// }
// }
// // Should move to the filter editor if clicking on it // // Should move to the filter editor if clicking on it
// // Should move selection to the channel editor if activating it // // Should move selection to the channel editor if activating it
@ -2344,11 +2310,13 @@ impl CollabPanel {
.detach() .detach()
} }
// fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) { fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// let channel_id = action.channel_id; let Some(workspace) = self.workspace.upgrade() else {
// if let Some(workspace) = self.workspace.upgrade(cx) { return;
// cx.app_context().defer(move |cx| { };
// workspace.update(cx, |workspace, cx| { cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
todo!();
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) { // if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// panel.update(cx, |panel, cx| { // panel.update(cx, |panel, cx| {
// panel // panel
@ -2356,19 +2324,18 @@ impl CollabPanel {
// .detach_and_log_err(cx); // .detach_and_log_err(cx);
// }); // });
// } // }
// }); });
// }); });
// } }
// }
// fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) { fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// let channel_store = self.channel_store.read(cx); let channel_store = self.channel_store.read(cx);
// let Some(channel) = channel_store.channel_for_id(action.channel_id) else { let Some(channel) = channel_store.channel_for_id(channel_id) else {
// return; return;
// }; };
// let item = ClipboardItem::new(channel.link()); let item = ClipboardItem::new(channel.link());
// cx.write_to_clipboard(item) cx.write_to_clipboard(item)
// } }
fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div { fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener( v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener(
@ -2388,8 +2355,12 @@ impl CollabPanel {
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List { fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List {
let is_selected = false; // todo!() this.selection == Some(ix); let is_selected = false; // todo!() this.selection == Some(ix);
List::new().children(self.entries.clone().into_iter().map(|entry| { List::new().children(
match entry { self.entries
.clone()
.into_iter()
.enumerate()
.map(|(ix, entry)| match entry {
ListEntry::Header(section) => { ListEntry::Header(section) => {
let is_collapsed = self.collapsed_sections.contains(&section); let is_collapsed = self.collapsed_sections.contains(&section);
self.render_header(section, is_selected, is_collapsed, cx) self.render_header(section, is_selected, is_collapsed, cx)
@ -2412,13 +2383,13 @@ impl CollabPanel {
depth, depth,
has_children, has_children,
} => self } => self
.render_channel(&*channel, depth, has_children, is_selected, cx) .render_channel(&*channel, depth, has_children, is_selected, ix, cx)
.into_any_element(), .into_any_element(),
ListEntry::ChannelEditor { depth } => { ListEntry::ChannelEditor { depth } => {
self.render_channel_editor(depth, cx).into_any_element() self.render_channel_editor(depth, cx).into_any_element()
} }
} }),
})) )
} }
fn render_header( fn render_header(
@ -2530,14 +2501,31 @@ impl CollabPanel {
| Section::Offline => true, | Section::Offline => true,
}; };
ListHeader::new(text) let header = ListHeader::new(text)
.when_some(button, |el, button| el.right_button(button)) .when_some(button, |el, button| el.right_button(button))
.selected(is_selected) .selected(is_selected)
.when(can_collapse, |el| { .when(can_collapse, |el| {
el.toggle(Toggleable::Toggleable(is_collapsed.into())) el.toggle(Some(is_collapsed)).on_toggle(
.on_toggle(
cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)), 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, depth: usize,
has_children: bool, has_children: bool,
is_selected: bool, is_selected: bool,
ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let channel_id = channel.id; let channel_id = channel.id;
@ -2768,9 +2757,8 @@ impl CollabPanel {
.map(|channel| channel.visibility) .map(|channel| channel.visibility)
== Some(proto::ChannelVisibility::Public); == Some(proto::ChannelVisibility::Public);
let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
let disclosed = has_children let disclosed =
.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()) has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
.unwrap_or(false);
let has_messages_notification = channel.unseen_message_id.is_some(); let has_messages_notification = channel.unseen_message_id.is_some();
let has_notes_notification = channel.unseen_note_version.is_some(); let has_notes_notification = channel.unseen_note_version.is_some();
@ -2801,9 +2789,38 @@ impl CollabPanel {
None None
}; };
div().group("").child( 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) ListItem::new(channel_id as usize)
.indent_level(depth) .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 }) .left_icon(if is_public { Icon::Public } else { Icon::Hash })
.selected(is_selected || is_active) .selected(is_selected || is_active)
.child( .child(
@ -2825,7 +2842,10 @@ impl CollabPanel {
.when(!has_messages_notification, |el| el.invisible()) .when(!has_messages_notification, |el| el.invisible())
.group_hover("", |style| style.visible()) .group_hover("", |style| style.visible())
.child( .child(
IconButton::new("channel_chat", Icon::MessageBubbles) IconButton::new(
"channel_chat",
Icon::MessageBubbles,
)
.color(if has_messages_notification { .color(if has_messages_notification {
Color::Default Color::Default
} else { } else {
@ -2853,26 +2873,26 @@ impl CollabPanel {
), ),
), ),
) )
.toggle(if has_children { .toggle(disclosed)
Toggleable::Toggleable(disclosed.into())
} else {
Toggleable::NotToggleable
})
.on_toggle( .on_toggle(
cx.listener(move |this, _, cx| this.toggle_channel_collapsed(channel_id, cx)), cx.listener(move |this, _, cx| {
this.toggle_channel_collapsed(channel_id, cx)
}),
) )
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
if this.drag_target_channel == ChannelDragTarget::None { if this.drag_target_channel == ChannelDragTarget::None {
if is_active { if is_active {
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) this.open_channel_notes(channel_id, cx)
} else { } else {
this.join_channel(channel_id, cx) this.join_channel(channel_id, cx)
} }
} }
})) }))
.on_secondary_mouse_down(cx.listener(|this, _, cx| { .on_secondary_mouse_down(cx.listener(
todo!() // open context menu move |this, event: &MouseDownEvent, cx| {
})), this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
},
)),
) )
// let channel_id = channel.id; // let channel_id = channel.id;
@ -3101,7 +3121,8 @@ impl CollabPanel {
// ) // )
// }) // })
// .on_click(MouseButton::Left, move |_, this, cx| { // .on_click(MouseButton::Left, move |_, this, cx| {
// if this.drag_target_channel == ChannelDragTarget::None { // if this.
// drag_target_channel == ChannelDragTarget::None {
// if is_active { // if is_active {
// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
// } else { // } else {
@ -3270,6 +3291,12 @@ impl Render for CollabPanel {
el.child(self.render_signed_in(cx)) 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 { fn size(&self, cx: &gpui::WindowContext) -> f32 {
self.width self.width.map_or_else(
.unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width) || CollaborationPanelSettings::get_global(cx).default_width,
|width| width.0,
)
} }
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) { fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
self.width = size; self.width = size.map(|s| px(s));
// todo!() self.serialize(cx);
// self.serialize(cx);
cx.notify(); cx.notify();
} }
@ -3541,3 +3569,34 @@ impl FocusableView for CollabPanel {
// .contained() // .contained()
// .with_style(style.container) // .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()))
}
}

View file

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

View file

@ -740,7 +740,7 @@ impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
Deserialize, Deserialize,
)] )]
#[repr(transparent)] #[repr(transparent)]
pub struct Pixels(pub(crate) f32); pub struct Pixels(pub f32);
impl std::ops::Div for Pixels { impl std::ops::Div for Pixels {
type Output = f32; type Output = f32;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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