Detailed changes
@@ -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.));
+ }
}
+ 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);
@@ -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 =