Rearrange collab titlebar items to avoid movement of the toggle contacts button

* Replace username in titelbar with a `...` user menu that shows
  the current user name and contains a sign-in/sign-out button.
* Move the '+' (toggle contacts) button back to the right side.
* Move the collaborators back to the right side.
* Move the share/unshare button to the left side, beside the project title
* Only show the share/unshare button when in a call.
This commit is contained in:
Max Brunsfeld 2023-02-22 16:58:49 -08:00
parent 24e0a027ee
commit bf5c3d963a
8 changed files with 183 additions and 156 deletions

1
Cargo.lock generated
View file

@ -1257,6 +1257,7 @@ dependencies = [
"client", "client",
"clock", "clock",
"collections", "collections",
"context_menu",
"editor", "editor",
"futures 0.3.25", "futures 0.3.25",
"fuzzy", "fuzzy",

View file

@ -0,0 +1,3 @@
<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View file

@ -66,7 +66,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [Authenticate]); actions!(client, [Authenticate, SignOut]);
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) { pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action({ cx.add_global_action({
@ -79,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
.detach(); .detach();
} }
}); });
cx.add_global_action({
let client = client.clone();
move |_: &SignOut, cx| {
let client = client.clone();
cx.spawn(|cx| async move {
client.set_status(Status::SignedOut, &cx);
})
.detach();
}
});
} }
pub struct Client { pub struct Client {

View file

@ -27,6 +27,7 @@ call = { path = "../call" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
editor = { path = "../editor" } editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }

View file

@ -4,9 +4,10 @@ use crate::{
ToggleScreenSharing, ToggleScreenSharing,
}; };
use call::{ActiveCall, ParticipantLocation, Room}; use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; use client::{proto::PeerId, Authenticate, ContactEventKind, SignOut, User, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use contacts_popover::ContactsPopover; use contacts_popover::ContactsPopover;
use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{ use gpui::{
actions, actions,
color::Color, color::Color,
@ -28,8 +29,9 @@ actions!(
[ [
ToggleCollaboratorList, ToggleCollaboratorList,
ToggleContactsMenu, ToggleContactsMenu,
ToggleUserMenu,
ShareProject, ShareProject,
UnshareProject UnshareProject,
] ]
); );
@ -38,25 +40,20 @@ impl_internal_actions!(collab, [LeaveCall]);
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
pub(crate) struct LeaveCall; pub(crate) struct LeaveCall;
#[derive(PartialEq, Eq)]
enum ContactsPopoverSide {
Left,
Right,
}
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover); cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::leave_call); cx.add_action(CollabTitlebarItem::leave_call);
cx.add_action(CollabTitlebarItem::toggle_user_menu);
} }
pub struct CollabTitlebarItem { pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>, contacts_popover: Option<ViewHandle<ContactsPopover>>,
contacts_popover_side: ContactsPopoverSide, user_menu: ViewHandle<ContextMenu>,
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>, collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -93,6 +90,7 @@ impl View for CollabTitlebarItem {
let user = workspace.read(cx).user_store().read(cx).current_user(); let user = workspace.read(cx).user_store().read(cx).current_user();
let mut left_container = Flex::row(); let mut left_container = Flex::row();
let mut right_container = Flex::row();
left_container.add_child( left_container.add_child(
Label::new(project_title, theme.workspace.titlebar.title.clone()) Label::new(project_title, theme.workspace.titlebar.title.clone())
@ -104,40 +102,25 @@ impl View for CollabTitlebarItem {
); );
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx)); left_container
left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
left_container.add_child(self.render_toggle_contacts_button(&theme, cx));
}
let mut right_container = Flex::row(); right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
right_container.add_child(self.render_current_user(&workspace, &theme, &user, cx));
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
right_container.add_child(self.render_leave_call_button(&theme, cx)); right_container.add_child(self.render_leave_call_button(&theme, cx));
right_container
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
} else {
right_container.add_child(self.render_outside_call_share_button(&theme, cx));
} }
right_container.add_children(self.render_connection_status(&workspace, cx)); let status = workspace.read(cx).client().status();
let status = &*status.borrow();
if let Some(user) = user { if matches!(status, client::Status::Connected { .. }) {
//TODO: Add style right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
right_container.add_child(
Label::new(
user.github_login.clone(),
theme.workspace.titlebar.title.clone(),
)
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed(),
);
} else { } else {
right_container.add_child(Self::render_authenticate(&theme, cx)); right_container.add_children(self.render_connection_status(status, cx));
} }
right_container.add_child(self.render_user_menu_button(&theme, cx));
Stack::new() Stack::new()
.with_child(left_container.boxed()) .with_child(left_container.boxed())
.with_child(right_container.aligned().right().boxed()) .with_child(right_container.aligned().right().boxed())
@ -186,7 +169,11 @@ impl CollabTitlebarItem {
workspace: workspace.downgrade(), workspace: workspace.downgrade(),
user_store: user_store.clone(), user_store: user_store.clone(),
contacts_popover: None, contacts_popover: None,
contacts_popover_side: ContactsPopoverSide::Right, user_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
collaborator_list_popover: None, collaborator_list_popover: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
@ -278,12 +265,6 @@ impl CollabTitlebarItem {
cx.notify(); cx.notify();
}) })
.detach(); .detach();
self.contacts_popover_side = match ActiveCall::global(cx).read(cx).room() {
Some(_) => ContactsPopoverSide::Left,
None => ContactsPopoverSide::Right,
};
self.contacts_popover = Some(view); self.contacts_popover = Some(view);
} }
} }
@ -291,6 +272,44 @@ impl CollabTitlebarItem {
cx.notify(); cx.notify();
} }
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = cx.global::<Settings>().theme.clone();
let label_style = theme.context_menu.item.disabled_style().label.clone();
self.user_menu.update(cx, |user_menu, cx| {
let items = if let Some(user) = self.user_store.read(cx).current_user() {
vec![
ContextMenuItem::Static(Box::new(move |_| {
Label::new(user.github_login.clone(), label_style.clone()).boxed()
})),
ContextMenuItem::Item {
label: "Sign out".into(),
action: Box::new(SignOut),
},
]
} else {
vec![ContextMenuItem::Item {
label: "Sign in".into(),
action: Box::new(Authenticate),
}]
};
user_menu.show(
vec2f(
theme
.workspace
.titlebar
.user_menu_button
.default
.button_width,
theme.workspace.titlebar.height,
),
AnchorCorner::TopRight,
items,
cx,
);
});
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) { fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx) ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx)) .update(cx, |call, cx| call.hang_up(cx))
@ -328,11 +347,9 @@ impl CollabTitlebarItem {
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| { MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
let style = titlebar.toggle_contacts_button.style_for( let style = titlebar
state, .toggle_contacts_button
self.contacts_popover.is_some() .style_for(state, self.contacts_popover.is_some());
&& self.contacts_popover_side == ContactsPopoverSide::Left,
);
Svg::new("icons/plus_8.svg") Svg::new("icons/plus_8.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -360,11 +377,7 @@ impl CollabTitlebarItem {
.boxed(), .boxed(),
) )
.with_children(badge) .with_children(badge)
.with_children(self.render_contacts_popover_host( .with_children(self.render_contacts_popover_host(titlebar, cx))
ContactsPopoverSide::Left,
titlebar,
cx,
))
.boxed() .boxed()
} }
@ -475,11 +488,9 @@ impl CollabTitlebarItem {
.with_child( .with_child(
MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| { MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations //TODO: Ensure this button has consistant width for both text variations
let style = titlebar.share_button.style_for( let style = titlebar
state, .share_button
self.contacts_popover.is_some() .style_for(state, self.contacts_popover.is_some());
&& self.contacts_popover_side == ContactsPopoverSide::Right,
);
Label::new(label, style.text.clone()) Label::new(label, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -502,11 +513,6 @@ impl CollabTitlebarItem {
) )
.boxed(), .boxed(),
) )
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Right,
titlebar,
cx,
))
.aligned() .aligned()
.contained() .contained()
.with_margin_left(theme.workspace.titlebar.item_spacing) .with_margin_left(theme.workspace.titlebar.item_spacing)
@ -514,83 +520,71 @@ impl CollabTitlebarItem {
) )
} }
fn render_outside_call_share_button( fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let tooltip = "Share project with new call";
let titlebar = &theme.workspace.titlebar; let titlebar = &theme.workspace.titlebar;
enum OutsideCallShare {}
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<OutsideCallShare>::new(0, cx, |state, _| { MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations let style = titlebar.call_control.style_for(state, false);
let style = titlebar.share_button.style_for( Svg::new("icons/ellipsis_14.svg")
state, .with_color(style.color)
self.contacts_popover.is_some() .constrained()
&& self.contacts_popover_side == ContactsPopoverSide::Right, .with_width(style.icon_width)
); .aligned()
Label::new("Share".to_owned(), style.text.clone()) .constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.boxed() .boxed()
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| { .on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleContactsMenu); cx.dispatch_action(ToggleUserMenu);
}) })
.with_tooltip::<OutsideCallShare, _>( .with_tooltip::<ToggleUserMenu, _>(
0, 0,
tooltip.to_owned(), "Toggle user menu".to_owned(),
None, Some(Box::new(ToggleUserMenu)),
theme.tooltip.clone(), theme.tooltip.clone(),
cx, cx,
) )
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.aligned()
.boxed(), .boxed(),
) )
.with_children(self.render_contacts_popover_host( .with_child(ChildView::new(&self.user_menu, cx).boxed())
ContactsPopoverSide::Right,
titlebar,
cx,
))
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed() .boxed()
} }
fn render_contacts_popover_host<'a>( fn render_contacts_popover_host<'a>(
&'a self, &'a self,
side: ContactsPopoverSide,
theme: &'a theme::Titlebar, theme: &'a theme::Titlebar,
cx: &'a RenderContext<Self>, cx: &'a RenderContext<Self>,
) -> impl Iterator<Item = ElementBox> + 'a { ) -> Option<ElementBox> {
self.contacts_popover self.contacts_popover.as_ref().map(|popover| {
.iter() Overlay::new(
.filter(move |_| self.contacts_popover_side == side) ChildView::new(popover, cx)
.map(|popover| { .contained()
Overlay::new( .with_margin_top(theme.height)
ChildView::new(popover, cx) .with_margin_left(theme.toggle_contacts_button.default.button_width)
.contained() .with_margin_right(-theme.toggle_contacts_button.default.button_width)
.with_margin_top(theme.height) .boxed(),
.with_margin_left(theme.toggle_contacts_button.default.button_width) )
.with_margin_right(-theme.toggle_contacts_button.default.button_width) .with_fit_mode(OverlayFitMode::SwitchAnchor)
.boxed(), .with_anchor_corner(AnchorCorner::BottomLeft)
) .with_z_index(999)
.with_fit_mode(OverlayFitMode::SwitchAnchor) .boxed()
.with_anchor_corner(AnchorCorner::BottomLeft) })
.with_z_index(999)
.boxed()
})
} }
fn render_collaborators( fn render_collaborators(
&self, &self,
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,
theme: &Theme, theme: &Theme,
room: ModelHandle<Room>, room: &ModelHandle<Room>,
cx: &mut RenderContext<Self>, cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> { ) -> Vec<ElementBox> {
let project = workspace.read(cx).project().read(cx); let project = workspace.read(cx).project().read(cx);
@ -622,7 +616,7 @@ impl CollabTitlebarItem {
theme, theme,
cx, cx,
)) ))
.with_margin_left(theme.workspace.titlebar.face_pile_spacing) .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
.boxed(), .boxed(),
) )
}) })
@ -643,25 +637,16 @@ impl CollabTitlebarItem {
.client() .client()
.peer_id() .peer_id()
.expect("Active call without peer id"); .expect("Active call without peer id");
self.render_face_pile(user, Some(replica_id), peer_id, None, workspace, theme, cx) Container::new(self.render_face_pile(
} user,
Some(replica_id),
fn render_authenticate(theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox { peer_id,
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| { None,
let style = theme workspace,
.workspace theme,
.titlebar cx,
.sign_in_prompt ))
.style_for(state, false); .with_margin_right(theme.workspace.titlebar.item_spacing)
Label::new("Sign in", style.text.clone())
.contained()
.with_style(style.container)
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed() .boxed()
} }
@ -717,7 +702,7 @@ impl CollabTitlebarItem {
} }
} }
let content = Stack::new() let mut content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| { .with_children(user.avatar.as_ref().map(|avatar| {
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap) let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
.with_child(Self::render_face( .with_child(Self::render_face(
@ -789,7 +774,10 @@ impl CollabTitlebarItem {
if let Some(location) = location { if let Some(location) = location {
if let Some(replica_id) = replica_id { if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content) content =
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
content
})
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| { .on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id)) cx.dispatch_action(ToggleFollow(peer_id))
@ -805,12 +793,14 @@ impl CollabTitlebarItem {
theme.tooltip.clone(), theme.tooltip.clone(),
cx, cx,
) )
.boxed() .boxed();
} else if let ParticipantLocation::SharedProject { project_id } = location { } else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id; let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| { content = MouseEventHandler::<JoinProject>::new(
content peer_id.as_u64() as usize,
}) cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| { .on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject { cx.dispatch_action(JoinProject {
@ -825,13 +815,10 @@ impl CollabTitlebarItem {
theme.tooltip.clone(), theme.tooltip.clone(),
cx, cx,
) )
.boxed() .boxed();
} else {
content
} }
} else {
content
} }
content
} }
fn render_face( fn render_face(
@ -854,13 +841,13 @@ impl CollabTitlebarItem {
fn render_connection_status( fn render_connection_status(
&self, &self,
workspace: &ViewHandle<Workspace>, status: &client::Status,
cx: &mut RenderContext<Self>, cx: &mut RenderContext<Self>,
) -> Option<ElementBox> { ) -> Option<ElementBox> {
enum ConnectionStatusButton {} enum ConnectionStatusButton {}
let theme = &cx.global::<Settings>().theme.clone(); let theme = &cx.global::<Settings>().theme.clone();
match &*workspace.read(cx).client().status().borrow() { match status {
client::Status::ConnectionError client::Status::ConnectionError
| client::Status::ConnectionLost | client::Status::ConnectionLost
| client::Status::Reauthenticating { .. } | client::Status::Reauthenticating { .. }

View file

@ -5,7 +5,9 @@ use gpui::{
}; };
use menu::*; use menu::*;
use settings::Settings; use settings::Settings;
use std::{any::TypeId, time::Duration}; use std::{any::TypeId, borrow::Cow, time::Duration};
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
struct Clicked; struct Clicked;
@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
pub enum ContextMenuItem { pub enum ContextMenuItem {
Item { Item {
label: String, label: Cow<'static, str>,
action: Box<dyn Action>, action: Box<dyn Action>,
}, },
Static(StaticItem),
Separator, Separator,
} }
impl ContextMenuItem { impl ContextMenuItem {
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self { pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item { Self::Item {
label: label.to_string(), label: label.into(),
action: Box::new(action), action: Box::new(action),
} }
} }
@ -42,14 +45,14 @@ impl ContextMenuItem {
Self::Separator Self::Separator
} }
fn is_separator(&self) -> bool { fn is_action(&self) -> bool {
matches!(self, Self::Separator) matches!(self, Self::Item { .. })
} }
fn action_id(&self) -> Option<TypeId> { fn action_id(&self) -> Option<TypeId> {
match self { match self {
ContextMenuItem::Item { action, .. } => Some(action.id()), ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Separator => None, ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
} }
} }
} }
@ -58,6 +61,7 @@ pub struct ContextMenu {
show_count: usize, show_count: usize,
anchor_position: Vector2F, anchor_position: Vector2F,
anchor_corner: AnchorCorner, anchor_corner: AnchorCorner,
position_mode: OverlayPositionMode,
items: Vec<ContextMenuItem>, items: Vec<ContextMenuItem>,
selected_index: Option<usize>, selected_index: Option<usize>,
visible: bool, visible: bool,
@ -105,6 +109,7 @@ impl View for ContextMenu {
.with_fit_mode(OverlayFitMode::SnapToWindow) .with_fit_mode(OverlayFitMode::SnapToWindow)
.with_anchor_position(self.anchor_position) .with_anchor_position(self.anchor_position)
.with_anchor_corner(self.anchor_corner) .with_anchor_corner(self.anchor_corner)
.with_position_mode(self.position_mode)
.boxed() .boxed()
} }
@ -121,6 +126,7 @@ impl ContextMenu {
show_count: 0, show_count: 0,
anchor_position: Default::default(), anchor_position: Default::default(),
anchor_corner: AnchorCorner::TopLeft, anchor_corner: AnchorCorner::TopLeft,
position_mode: OverlayPositionMode::Window,
items: Default::default(), items: Default::default(),
selected_index: Default::default(), selected_index: Default::default(),
visible: Default::default(), visible: Default::default(),
@ -188,13 +194,13 @@ impl ContextMenu {
} }
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) { fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = self.items.iter().position(|item| !item.is_separator()); self.selected_index = self.items.iter().position(|item| item.is_action());
cx.notify(); cx.notify();
} }
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) { fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
for (ix, item) in self.items.iter().enumerate().rev() { for (ix, item) in self.items.iter().enumerate().rev() {
if !item.is_separator() { if item.is_action() {
self.selected_index = Some(ix); self.selected_index = Some(ix);
cx.notify(); cx.notify();
break; break;
@ -205,7 +211,7 @@ impl ContextMenu {
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index { if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) { for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
if !item.is_separator() { if item.is_action() {
self.selected_index = Some(ix); self.selected_index = Some(ix);
cx.notify(); cx.notify();
break; break;
@ -219,7 +225,7 @@ impl ContextMenu {
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index { if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() { for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if !item.is_separator() { if item.is_action() {
self.selected_index = Some(ix); self.selected_index = Some(ix);
cx.notify(); cx.notify();
break; break;
@ -234,7 +240,7 @@ impl ContextMenu {
&mut self, &mut self,
anchor_position: Vector2F, anchor_position: Vector2F,
anchor_corner: AnchorCorner, anchor_corner: AnchorCorner,
items: impl IntoIterator<Item = ContextMenuItem>, items: Vec<ContextMenuItem>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let mut items = items.into_iter().peekable(); let mut items = items.into_iter().peekable();
@ -254,6 +260,10 @@ impl ContextMenu {
cx.notify(); cx.notify();
} }
pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
self.position_mode = mode;
}
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element { fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id(); let window_id = cx.window_id();
let style = cx.global::<Settings>().theme.context_menu.clone(); let style = cx.global::<Settings>().theme.context_menu.clone();
@ -273,6 +283,9 @@ impl ContextMenu {
.with_style(style.container) .with_style(style.container)
.boxed() .boxed()
} }
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new() ContextMenuItem::Separator => Empty::new()
.collapsed() .collapsed()
.contained() .contained()
@ -302,6 +315,9 @@ impl ContextMenu {
) )
.boxed() .boxed()
} }
ContextMenuItem::Static(_) => Empty::new().boxed(),
ContextMenuItem::Separator => Empty::new() ContextMenuItem::Separator => Empty::new()
.collapsed() .collapsed()
.constrained() .constrained()
@ -339,7 +355,7 @@ impl ContextMenu {
Flex::row() Flex::row()
.with_child( .with_child(
Label::new(label.to_string(), style.label.clone()) Label::new(label.clone(), style.label.clone())
.contained() .contained()
.boxed(), .boxed(),
) )
@ -366,6 +382,9 @@ impl ContextMenu {
.on_drag(MouseButton::Left, |_, _| {}) .on_drag(MouseButton::Left, |_, _| {})
.boxed() .boxed()
} }
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new() ContextMenuItem::Separator => Empty::new()
.constrained() .constrained()
.with_height(1.) .with_height(1.)

View file

@ -88,6 +88,7 @@ pub struct Titlebar {
pub share_button: Interactive<ContainedText>, pub share_button: Interactive<ContainedText>,
pub call_control: Interactive<IconButton>, pub call_control: Interactive<IconButton>,
pub toggle_contacts_button: Interactive<IconButton>, pub toggle_contacts_button: Interactive<IconButton>,
pub user_menu_button: Interactive<IconButton>,
pub toggle_contacts_badge: ContainerStyle, pub toggle_contacts_badge: ContainerStyle,
} }

View file

@ -197,6 +197,11 @@ export default function workspace(colorScheme: ColorScheme) {
color: foreground(layer, "variant", "hovered"), color: foreground(layer, "variant", "hovered"),
}, },
}, },
userMenuButton: {
buttonWidth: 20,
iconWidth: 12,
...titlebarButton,
},
toggleContactsBadge: { toggleContactsBadge: {
cornerRadius: 3, cornerRadius: 3,
padding: 2, padding: 2,