mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 11:29:25 +00:00
Merge pull request #1193 from zed-industries/tooltips
Add some tooltips to aid discoverability
This commit is contained in:
commit
7239aac532
10 changed files with 163 additions and 104 deletions
|
@ -350,6 +350,8 @@ impl ContactsPanel {
|
|||
is_selected: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> 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::<ToggleOnline, _>(
|
||||
project_id as usize,
|
||||
"Take project offline".to_string(),
|
||||
None,
|
||||
|
@ -565,7 +567,7 @@ impl ContactsPanel {
|
|||
project: Some(project.clone()),
|
||||
})
|
||||
})
|
||||
.with_tooltip(
|
||||
.with_tooltip::<ToggleOnline, _>(
|
||||
project_id,
|
||||
"Take project online".to_string(),
|
||||
None,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -86,10 +86,11 @@ impl View for DiagnosticIndicator {
|
|||
enum Summary {}
|
||||
enum Message {}
|
||||
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let in_progress = !self.in_progress_checks.is_empty();
|
||||
let mut element = Flex::row().with_child(
|
||||
MouseEventHandler::new::<Summary, _, _>(0, cx, |state, cx| {
|
||||
let style = &cx
|
||||
let style = cx
|
||||
.global::<Settings>()
|
||||
.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::<Summary, _>(
|
||||
0,
|
||||
"Project Diagnostics".to_string(),
|
||||
Some(Box::new(crate::Deploy)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
);
|
||||
|
|
|
@ -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::<JumpIcon, _>(
|
||||
*key,
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
|
|
|
@ -157,7 +157,7 @@ pub trait Element {
|
|||
FlexItem::new(self.boxed()).float()
|
||||
}
|
||||
|
||||
fn with_tooltip<T: View>(
|
||||
fn with_tooltip<Tag: 'static, T: View>(
|
||||
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::<Tag, T>(id, text, action, style, self.boxed(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Vector2F>,
|
||||
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.));
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::{
|
||||
ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
|
||||
ParentElement, Text,
|
||||
OverlayFitMode, ParentElement, Text,
|
||||
};
|
||||
use crate::{
|
||||
fonts::TextStyle,
|
||||
|
@ -49,7 +49,7 @@ pub struct KeystrokeStyle {
|
|||
}
|
||||
|
||||
impl Tooltip {
|
||||
pub fn new<T: View>(
|
||||
pub fn new<Tag: 'static, T: View>(
|
||||
id: usize,
|
||||
text: String,
|
||||
action: Option<Box<dyn Action>>,
|
||||
|
@ -57,7 +57,10 @@ impl Tooltip {
|
|||
child: ElementBox,
|
||||
cx: &mut RenderContext<T>,
|
||||
) -> Self {
|
||||
let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
|
||||
struct ElementState<Tag>(Tag);
|
||||
struct MouseEventHandlerState<Tag>(Tag);
|
||||
|
||||
let state_handle = cx.element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
|
||||
let state = state_handle.read(cx).clone();
|
||||
let tooltip = if state.visible.get() {
|
||||
let mut collapsed_tooltip = Self::render_tooltip(
|
||||
|
@ -79,40 +82,41 @@ impl Tooltip {
|
|||
})
|
||||
.boxed(),
|
||||
)
|
||||
.move_to_fit(true)
|
||||
.fit_mode(OverlayFitMode::FlipAlignment)
|
||||
.with_abs_position(state.position.get())
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let child = MouseEventHandler::new::<Self, _, _>(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::<MouseEventHandlerState<Tag>, _, _>(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,
|
||||
|
|
|
@ -68,6 +68,7 @@ pub enum Side {
|
|||
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
view: Rc<dyn SidebarItemHandle>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
@ -104,6 +105,7 @@ impl Sidebar {
|
|||
pub fn add_item<T: SidebarItem>(
|
||||
&mut self,
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
view: ViewHandle<T>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
|
@ -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<Self>) -> ElementBox {
|
||||
let theme = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons;
|
||||
let theme = &cx.global::<Settings>().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,52 +257,56 @@ 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::<Vec<_>>();
|
||||
Flex::row()
|
||||
.with_children(
|
||||
items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (icon_path, item_view))| {
|
||||
MouseEventHandler::new::<Self, _, _>(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(items.into_iter().enumerate().map(
|
||||
|(ix, (icon_path, tooltip, item_view))| {
|
||||
let action = ToggleSidebarItem {
|
||||
side,
|
||||
item_index: ix,
|
||||
};
|
||||
MouseEventHandler::new::<Self, _, _>(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(),
|
||||
)
|
||||
.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)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.dispatch_action(ToggleSidebarItem {
|
||||
side,
|
||||
item_index: ix,
|
||||
} else {
|
||||
None
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
}),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click({
|
||||
let action = action.clone();
|
||||
move |_, _, cx| cx.dispatch_action(action.clone())
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
ix,
|
||||
tooltip,
|
||||
Some(Box::new(action)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(group_style)
|
||||
.boxed()
|
||||
|
|
|
@ -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<ImageData>,
|
||||
replica_id: ReplicaId,
|
||||
peer_id: Option<PeerId>,
|
||||
peer: Option<(PeerId, &str)>,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> 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::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
|
||||
.with_tooltip::<ToggleFollow, _>(
|
||||
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
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in a new issue