From f2a48c6b027d08f316356d25e3c48db3cb7b1e15 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Jun 2022 13:50:04 +0200 Subject: [PATCH 1/4] Add tooltips for sidebar buttons --- crates/workspace/src/sidebar.rs | 55 +++++++++++++++++++-------------- crates/zed/src/zed.rs | 14 +++++++-- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 7eec00fc4c01ae097990be24ea2eea95df3eb406..0b8affa9cafde4f9da17a7f238381a940438eb31 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -68,6 +68,7 @@ pub enum Side { struct Item { icon_path: &'static str, + tooltip: String, view: Rc, _subscriptions: [Subscription; 2], } @@ -104,6 +105,7 @@ impl Sidebar { pub fn add_item( &mut self, icon_path: &'static str, + tooltip: String, view: ViewHandle, cx: &mut ViewContext, ) { @@ -123,6 +125,7 @@ impl Sidebar { ]; self.items.push(Item { icon_path, + tooltip, view: Rc::new(view), _subscriptions: subscriptions, }); @@ -239,12 +242,9 @@ impl View for SidebarButtons { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx - .global::() - .theme - .workspace - .status_bar - .sidebar_buttons; + let theme = &cx.global::().theme; + let tooltip_style = theme.tooltip.clone(); + let theme = &theme.workspace.status_bar.sidebar_buttons; let sidebar = self.sidebar.read(cx); let item_style = theme.item; let badge_style = theme.badge; @@ -257,15 +257,19 @@ impl View for SidebarButtons { let items = sidebar .items .iter() - .map(|item| (item.icon_path, item.view.clone())) + .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone())) .collect::>(); Flex::row() - .with_children( - items - .into_iter() - .enumerate() - .map(|(ix, (icon_path, item_view))| { - MouseEventHandler::new::(ix, cx, move |state, cx| { + .with_children(items.into_iter().enumerate().map( + |(ix, (icon_path, tooltip, item_view))| { + let action = ToggleSidebarItem { + side, + item_index: ix, + }; + MouseEventHandler::new::(ix, cx, { + let action = action.clone(); + let tooltip_style = tooltip_style.clone(); + move |state, cx| { let is_active = Some(ix) == active_ix; let style = item_style.style_for(state, is_active); Stack::new() @@ -291,18 +295,21 @@ impl View for SidebarButtons { .with_height(style.icon_size) .contained() .with_style(style.container) + .with_tooltip( + ix, + tooltip, + Some(Box::new(action.clone())), + tooltip_style.clone(), + cx, + ) .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleSidebarItem { - side, - item_index: ix, - }) - }) - .boxed() - }), - ) + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| cx.dispatch_action(action.clone())) + .boxed() + }, + )) .contained() .with_style(group_style) .boxed() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 296ff4cd8e9f09c0bbee0c23797b9f2bee645ddb..a8beab211703c23e37b4512a9dc86f29580cd48f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -191,10 +191,20 @@ pub fn initialize_workspace( }); workspace.left_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item("icons/folder-tree-solid-14.svg", project_panel.into(), cx) + sidebar.add_item( + "icons/folder-tree-solid-14.svg", + "Project Panel".to_string(), + project_panel.into(), + cx, + ) }); workspace.right_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item("icons/contacts-solid-14.svg", contact_panel.into(), cx) + sidebar.add_item( + "icons/contacts-solid-14.svg", + "Contacts Panel".to_string(), + contact_panel.into(), + cx, + ) }); let diagnostic_summary = From 8a146e49ca11991bc8de05a882a5a30d5a4b1466 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Jun 2022 16:44:08 +0200 Subject: [PATCH 2/4] Use a different fit mode for tooltips --- crates/context_menu/src/context_menu.rs | 2 +- crates/gpui/src/elements/overlay.rs | 50 +++++++++++++++++-------- crates/gpui/src/elements/tooltip.rs | 4 +- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 6915bd775335b4fd933e5bf61be9a10c6d1e6e4f..2123aadf4c34e97a8f0348ead2e8f140c4ec7dc0 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -94,7 +94,7 @@ impl View for ContextMenu { Overlay::new(expanded_menu) .hoverable(true) - .move_to_fit(true) + .fit_mode(OverlayFitMode::SnapToWindow) .with_abs_position(self.position) .boxed() } diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 1843b65c10a10dcf7818c113e3ac2bc2f6aeebdf..4ed0ca7d228db1524d5519336e7da579e8100d25 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -1,25 +1,31 @@ -use serde_json::json; - use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, PaintContext, SizeConstraint, }; +use serde_json::json; pub struct Overlay { child: ElementBox, abs_position: Option, - move_to_fit: bool, + fit_mode: OverlayFitMode, hoverable: bool, } +#[derive(Copy, Clone)] +pub enum OverlayFitMode { + SnapToWindow, + FlipAlignment, + None, +} + impl Overlay { pub fn new(child: ElementBox) -> Self { Self { child, abs_position: None, - move_to_fit: false, + fit_mode: OverlayFitMode::None, hoverable: false, } } @@ -29,8 +35,8 @@ impl Overlay { self } - pub fn move_to_fit(mut self, align_to_fit: bool) -> Self { - self.move_to_fit = align_to_fit; + pub fn fit_mode(mut self, fit_mode: OverlayFitMode) -> Self { + self.fit_mode = fit_mode; self } @@ -76,18 +82,32 @@ impl Element for Overlay { }); } - if self.move_to_fit { - // Snap the right edge of the overlay to the right edge of the window if - // its horizontal bounds overflow. - if bounds.lower_right().x() > cx.window_size.x() { - bounds.set_origin_x((cx.window_size.x() - bounds.width()).max(0.)); + match self.fit_mode { + OverlayFitMode::SnapToWindow => { + // Snap the right edge of the overlay to the right edge of the window if + // its horizontal bounds overflow. + if bounds.lower_right().x() > cx.window_size.x() { + bounds.set_origin_x((cx.window_size.x() - bounds.width()).max(0.)); + } + + // Snap the bottom edge of the overlay to the bottom edge of the window if + // its vertical bounds overflow. + if bounds.lower_right().y() > cx.window_size.y() { + bounds.set_origin_y((cx.window_size.y() - bounds.height()).max(0.)); + } } + OverlayFitMode::FlipAlignment => { + // Right-align overlay if its horizontal bounds overflow. + if bounds.lower_right().x() > cx.window_size.x() { + bounds.set_origin_x(bounds.origin_x() - bounds.width()); + } - // Snap the bottom edge of the overlay to the bottom edge of the window if - // its vertical bounds overflow. - if bounds.lower_right().y() > cx.window_size.y() { - bounds.set_origin_y((cx.window_size.y() - bounds.height()).max(0.)); + // Bottom-align overlay if its vertical bounds overflow. + if bounds.lower_right().y() > cx.window_size.y() { + bounds.set_origin_y(bounds.origin_y() - bounds.height()); + } } + OverlayFitMode::None => {} } self.child.paint(bounds.origin(), bounds, cx); diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 252d81e46d4f653f3a25780c104cb79cfe074cc9..a7800a28f62335caaa826fc91d7d9d24407b7bcf 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -1,6 +1,6 @@ use super::{ ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay, - ParentElement, Text, + OverlayFitMode, ParentElement, Text, }; use crate::{ fonts::TextStyle, @@ -79,7 +79,7 @@ impl Tooltip { }) .boxed(), ) - .move_to_fit(true) + .fit_mode(OverlayFitMode::FlipAlignment) .with_abs_position(state.position.get()) .boxed(), ) From 516bd13474ab2a6868191690d78940dc0a58745b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Jun 2022 17:01:48 +0200 Subject: [PATCH 3/4] Add tooltip to follow collaborators --- crates/contacts_panel/src/contacts_panel.rs | 6 +- crates/editor/src/element.rs | 2 +- crates/gpui/src/elements.rs | 4 +- crates/gpui/src/elements/tooltip.rs | 54 ++++++++------- crates/workspace/src/sidebar.rs | 73 ++++++++++----------- crates/workspace/src/workspace.rs | 19 ++++-- 6 files changed, 86 insertions(+), 72 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index a21a9d98ab64c1e5f7b74e6716ed5e2808caaddb..9bf9d0fa0b73448f9e17e94aefc21f930b0bd947 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -350,6 +350,8 @@ impl ContactsPanel { is_selected: bool, cx: &mut RenderContext, ) -> ElementBox { + enum ToggleOnline {} + let project = &contact.projects[project_index]; let project_id = project.id; let is_host = Some(contact.user.id) == current_user_id; @@ -445,7 +447,7 @@ impl ContactsPanel { project: Some(open_project.clone()), }) }) - .with_tooltip( + .with_tooltip::( project_id as usize, "Take project offline".to_string(), None, @@ -565,7 +567,7 @@ impl ContactsPanel { project: Some(project.clone()), }) }) - .with_tooltip( + .with_tooltip::( project_id, "Take project online".to_string(), None, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7311141074a23b52ac1da0e0c8938077e3dd2250..c3d6614f23fabd88f1c3f23e814a43eccc1b3b3e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -883,7 +883,7 @@ impl EditorElement { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| cx.dispatch_action(jump_action.clone())) - .with_tooltip( + .with_tooltip::( *key, "Jump to Buffer".to_string(), Some(Box::new(crate::OpenExcerpts)), diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index d2d254d93eb1d7da3949d60bbfdc011b7fa65293..35703b8a1fb945deabef035f8f3cc2b196dd61e8 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -157,7 +157,7 @@ pub trait Element { FlexItem::new(self.boxed()).float() } - fn with_tooltip( + fn with_tooltip( self, id: usize, text: String, @@ -168,7 +168,7 @@ pub trait Element { where Self: 'static + Sized, { - Tooltip::new(id, text, action, style, self.boxed(), cx) + Tooltip::new::(id, text, action, style, self.boxed(), cx) } } diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index a7800a28f62335caaa826fc91d7d9d24407b7bcf..9a65b2661d71c216a50432605138336a35bb67db 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -49,7 +49,7 @@ pub struct KeystrokeStyle { } impl Tooltip { - pub fn new( + pub fn new( id: usize, text: String, action: Option>, @@ -57,7 +57,10 @@ impl Tooltip { child: ElementBox, cx: &mut RenderContext, ) -> Self { - let state_handle = cx.element_state::>(id); + struct ElementState(Tag); + struct MouseEventHandlerState(Tag); + + let state_handle = cx.element_state::, Rc>(id); let state = state_handle.read(cx).clone(); let tooltip = if state.visible.get() { let mut collapsed_tooltip = Self::render_tooltip( @@ -86,33 +89,34 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::new::(id, cx, |_, _| child) - .on_hover(move |position, hover, cx| { - let window_id = cx.window_id(); - if let Some(view_id) = cx.view_id() { - if hover { - if !state.visible.get() { - state.position.set(position); + let child = + MouseEventHandler::new::, _, _>(id, cx, |_, _| child) + .on_hover(move |position, hover, cx| { + let window_id = cx.window_id(); + if let Some(view_id) = cx.view_id() { + if hover { + if !state.visible.get() { + state.position.set(position); - let mut debounce = state.debounce.borrow_mut(); - if debounce.is_none() { - *debounce = Some(cx.spawn({ - let state = state.clone(); - |mut cx| async move { - cx.background().timer(DEBOUNCE_TIMEOUT).await; - state.visible.set(true); - cx.update(|cx| cx.notify_view(window_id, view_id)); - } - })); + let mut debounce = state.debounce.borrow_mut(); + if debounce.is_none() { + *debounce = Some(cx.spawn({ + let state = state.clone(); + |mut cx| async move { + cx.background().timer(DEBOUNCE_TIMEOUT).await; + state.visible.set(true); + cx.update(|cx| cx.notify_view(window_id, view_id)); + } + })); + } } + } else { + state.visible.set(false); + state.debounce.take(); } - } else { - state.visible.set(false); - state.debounce.take(); } - } - }) - .boxed(); + }) + .boxed(); Self { child, tooltip, diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 0b8affa9cafde4f9da17a7f238381a940438eb31..0cfb7f7865232679873a7c924264465f903d8929 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -266,47 +266,44 @@ impl View for SidebarButtons { side, item_index: ix, }; - MouseEventHandler::new::(ix, cx, { - let action = action.clone(); - let tooltip_style = tooltip_style.clone(); - move |state, cx| { - let is_active = Some(ix) == active_ix; - let style = item_style.style_for(state, is_active); - Stack::new() - .with_child( - Svg::new(icon_path).with_color(style.icon_color).boxed(), - ) - .with_children(if !is_active && item_view.should_show_badge(cx) { - Some( - Empty::new() - .collapsed() - .contained() - .with_style(badge_style) - .aligned() - .bottom() - .right() - .boxed(), - ) - } else { - None - }) - .constrained() - .with_width(style.icon_size) - .with_height(style.icon_size) - .contained() - .with_style(style.container) - .with_tooltip( - ix, - tooltip, - Some(Box::new(action.clone())), - tooltip_style.clone(), - cx, + MouseEventHandler::new::(ix, cx, move |state, cx| { + let is_active = Some(ix) == active_ix; + let style = item_style.style_for(state, is_active); + Stack::new() + .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed()) + .with_children(if !is_active && item_view.should_show_badge(cx) { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(badge_style) + .aligned() + .bottom() + .right() + .boxed(), ) - .boxed() - } + } else { + None + }) + .constrained() + .with_width(style.icon_size) + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, _, cx| cx.dispatch_action(action.clone())) + .on_click({ + let action = action.clone(); + move |_, _, cx| cx.dispatch_action(action.clone()) + }) + .with_tooltip::( + ix, + tooltip, + Some(Box::new(action)), + tooltip_style.clone(), + cx, + ) .boxed() }, )) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index aed1bf5b46aecdb4d619744b37b2d973d60be03a..63e1d60df6cf48862a59f026d0640cd7083fc9b8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1788,7 +1788,7 @@ impl Workspace { Some(self.render_avatar( collaborator.user.avatar.clone()?, collaborator.replica_id, - Some(collaborator.peer_id), + Some((collaborator.peer_id, &collaborator.user.github_login)), theme, cx, )) @@ -1833,12 +1833,12 @@ impl Workspace { &self, avatar: Arc, replica_id: ReplicaId, - peer_id: Option, + peer: Option<(PeerId, &str)>, theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { let replica_color = theme.editor.replica_selection_style(replica_id).cursor; - let is_followed = peer_id.map_or(false, |peer_id| { + let is_followed = peer.map_or(false, |(peer_id, _)| { self.follower_states_by_leader.contains_key(&peer_id) }); let mut avatar_style = theme.workspace.titlebar.avatar; @@ -1869,10 +1869,21 @@ impl Workspace { .with_margin_left(theme.workspace.titlebar.avatar_margin) .boxed(); - if let Some(peer_id) = peer_id { + if let Some((peer_id, peer_github_login)) = peer { MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id))) + .with_tooltip::( + peer_id.0 as usize, + if is_followed { + format!("Unfollow {}", peer_github_login) + } else { + format!("Follow {}", peer_github_login) + }, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) .boxed() } else { content From d02bc2f9f8e0572ac74b3a8e7c64d8138f7c662e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Jun 2022 17:12:47 +0200 Subject: [PATCH 4/4] Add tooltip for project diagnostics --- crates/diagnostics/src/items.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 4a402522bdd5a055ebb6ab68aadeeab990cf30f9..e2961ff3bd8fbd83af363dfd2627867c5fdf3cc7 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -86,10 +86,11 @@ impl View for DiagnosticIndicator { enum Summary {} enum Message {} + let tooltip_style = cx.global::().theme.tooltip.clone(); let in_progress = !self.in_progress_checks.is_empty(); let mut element = Flex::row().with_child( MouseEventHandler::new::(0, cx, |state, cx| { - let style = &cx + let style = cx .global::() .theme .workspace @@ -161,6 +162,13 @@ impl View for DiagnosticIndicator { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(|_, _, cx| cx.dispatch_action(crate::Deploy)) + .with_tooltip::( + 0, + "Project Diagnostics".to_string(), + Some(Box::new(crate::Deploy)), + tooltip_style, + cx, + ) .aligned() .boxed(), );