gpui: Long-press to activate tooltips (#63948)
Tooltips on web need long-press, rather than hover --- Release Notes: - N/A or Added/Fixed/Improved ...
This commit is contained in:
1 parent
85ad201397
commit
9eb022bd23
2 files changed
+235
-64
No files matched your search
+232
-64
@@ -20,11 +20,11 @@ use crate::{
|
||||
Display, Element, ElementId, Entity, EntityId, ExternalDragPayload, ExternalDragPayloadSource,
|
||||
FileDropEvent, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, HitboxId,
|
||||
InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, KeyboardButton,
|
||||
KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
|
||||
OngoingScroll, Overflow, ParentElement, PinchEvent, Pixels, Point, Render, ScrollWheelEvent,
|
||||
SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, Visibility, Window,
|
||||
WindowControlArea, point, px, size,
|
||||
KeyboardClickEvent, LayoutId, LongPressEvent, ModifiersChangedEvent, MouseButton,
|
||||
MouseClickEvent, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent,
|
||||
MouseUpEvent, OngoingScroll, Overflow, ParentElement, PinchEvent, Pixels, Point, Render,
|
||||
ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId,
|
||||
TouchPhase, Visibility, Window, WindowControlArea, point, px, size,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use gpui_util::ResultExt;
|
||||
@@ -32,7 +32,7 @@ use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Ordering,
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
@@ -699,7 +699,7 @@ impl Interactivity {
|
||||
self.hover_listener_mode = mode;
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// Constructs a tooltip when the element is hovered or long-pressed.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`].
|
||||
pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static)
|
||||
where
|
||||
@@ -715,7 +715,7 @@ impl Interactivity {
|
||||
});
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// Constructs a tooltip when the element is hovered or long-pressed.
|
||||
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
||||
/// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`].
|
||||
pub fn hoverable_tooltip(
|
||||
@@ -734,7 +734,9 @@ impl Interactivity {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the delay before this element's tooltip is shown.
|
||||
/// Sets the delay before this element's tooltip is shown on hover.
|
||||
///
|
||||
/// Touch long presses show the tooltip immediately once the gesture is recognized.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::tooltip_show_delay`].
|
||||
pub fn tooltip_show_delay(&mut self, delay: Duration) {
|
||||
self.tooltip_show_delay = Some(delay);
|
||||
@@ -1670,7 +1672,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// Constructs a tooltip when the element is hovered or long-pressed.
|
||||
/// The fluent API equivalent to [`Interactivity::tooltip`].
|
||||
fn tooltip(mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self
|
||||
where
|
||||
@@ -1680,7 +1682,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// Constructs a tooltip when the element is hovered or long-pressed.
|
||||
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
||||
/// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`].
|
||||
fn hoverable_tooltip(
|
||||
@@ -1694,7 +1696,9 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the delay before this element's tooltip is shown.
|
||||
/// Sets the delay before this element's tooltip is shown on hover.
|
||||
///
|
||||
/// Touch long presses show the tooltip immediately once the gesture is recognized.
|
||||
/// The fluent API equivalent to [`Interactivity::tooltip_show_delay`].
|
||||
fn tooltip_show_delay(mut self, delay: Duration) -> Self
|
||||
where
|
||||
@@ -2364,6 +2368,11 @@ impl Interactivity {
|
||||
self.tooltip_id = set_tooltip_on_window(active_tooltip, window);
|
||||
} else {
|
||||
// If there is no longer a tooltip builder, remove the active tooltip.
|
||||
if let Some(long_press_tooltip_active) =
|
||||
element_state.long_press_tooltip_active.as_ref()
|
||||
{
|
||||
long_press_tooltip_active.set(false);
|
||||
}
|
||||
element_state.active_tooltip.take();
|
||||
}
|
||||
}
|
||||
@@ -3180,6 +3189,10 @@ impl Interactivity {
|
||||
.pending_mouse_down
|
||||
.get_or_insert_with(Default::default)
|
||||
.clone();
|
||||
let long_press_tooltip_active = element_state
|
||||
.long_press_tooltip_active
|
||||
.get_or_insert_with(Default::default)
|
||||
.clone();
|
||||
|
||||
let tooltip_is_hoverable = tooltip_builder.hoverable;
|
||||
let build_tooltip = Rc::new(move |window: &mut Window, cx: &mut App| {
|
||||
@@ -3207,6 +3220,7 @@ impl Interactivity {
|
||||
build_tooltip,
|
||||
check_is_hovered,
|
||||
check_is_hovered_during_prepaint,
|
||||
long_press_tooltip_active,
|
||||
self.tooltip_show_delay,
|
||||
window,
|
||||
);
|
||||
@@ -3605,6 +3619,7 @@ pub struct InteractiveElementState {
|
||||
pub(crate) scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
||||
ongoing_scroll: Option<Rc<RefCell<OngoingScroll>>>,
|
||||
pub(crate) active_tooltip: Option<Rc<RefCell<Option<ActiveTooltip>>>>,
|
||||
long_press_tooltip_active: Option<Rc<Cell<bool>>>,
|
||||
}
|
||||
|
||||
/// Whether or not the element or a group that contains it is clicked by the mouse.
|
||||
@@ -3696,6 +3711,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
|
||||
build_tooltip: Rc<dyn Fn(&mut Window, &mut App) -> Option<(AnyView, bool)>>,
|
||||
check_is_hovered: Rc<dyn Fn(&Window) -> bool>,
|
||||
check_is_hovered_during_prepaint: Rc<dyn Fn(&Window) -> bool>,
|
||||
long_press_tooltip_active: Rc<Cell<bool>>,
|
||||
show_delay: Option<Duration>,
|
||||
window: &mut Window,
|
||||
) {
|
||||
@@ -3706,6 +3722,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let build_tooltip = build_tooltip.clone();
|
||||
let check_is_hovered = check_is_hovered.clone();
|
||||
let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone();
|
||||
move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
handle_tooltip_mouse_move(
|
||||
&active_tooltip,
|
||||
@@ -3731,6 +3748,44 @@ pub(crate) fn register_tooltip_mouse_handlers(
|
||||
}
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let build_tooltip = build_tooltip.clone();
|
||||
let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone();
|
||||
move |event: &LongPressEvent, phase, window, cx| {
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
|
||||
match event.phase {
|
||||
TouchPhase::Started if !window.default_prevented() && check_is_hovered(window) => {
|
||||
if show_tooltip(
|
||||
&active_tooltip,
|
||||
&build_tooltip,
|
||||
&check_is_hovered_during_prepaint,
|
||||
Some(long_press_tooltip_active.clone()),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
long_press_tooltip_active.set(true);
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
TouchPhase::Ended | TouchPhase::Cancelled
|
||||
if long_press_tooltip_active.replace(false) =>
|
||||
{
|
||||
clear_active_tooltip(&active_tooltip, window);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
TouchPhase::Started
|
||||
| TouchPhase::Moved
|
||||
| TouchPhase::Ended
|
||||
| TouchPhase::Cancelled => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| {
|
||||
@@ -3830,36 +3885,14 @@ fn handle_tooltip_mouse_move(
|
||||
return;
|
||||
};
|
||||
cx.update(|window, cx| {
|
||||
let new_tooltip =
|
||||
build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| {
|
||||
let weak_active_tooltip = Rc::downgrade(&active_tooltip);
|
||||
ActiveTooltip::Visible {
|
||||
tooltip: AnyTooltip {
|
||||
view,
|
||||
mouse_position: window.mouse_position(),
|
||||
check_visible_and_update: Rc::new(
|
||||
move |tooltip_bounds, window, cx| {
|
||||
let Some(active_tooltip) =
|
||||
weak_active_tooltip.upgrade()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
handle_tooltip_check_visible_and_update(
|
||||
&active_tooltip,
|
||||
tooltip_is_hoverable,
|
||||
&check_is_hovered_during_prepaint,
|
||||
tooltip_bounds,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
},
|
||||
is_hoverable: tooltip_is_hoverable,
|
||||
}
|
||||
});
|
||||
*active_tooltip.borrow_mut() = new_tooltip;
|
||||
window.refresh();
|
||||
show_tooltip(
|
||||
&active_tooltip,
|
||||
&build_tooltip,
|
||||
&check_is_hovered_during_prepaint,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -3874,6 +3907,50 @@ fn handle_tooltip_mouse_move(
|
||||
}
|
||||
}
|
||||
|
||||
fn show_tooltip(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
build_tooltip: &Rc<dyn Fn(&mut Window, &mut App) -> Option<(AnyView, bool)>>,
|
||||
check_is_hovered_during_prepaint: &Rc<dyn Fn(&Window) -> bool>,
|
||||
long_press_tooltip_active: Option<Rc<Cell<bool>>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> bool {
|
||||
let new_tooltip = build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| {
|
||||
let weak_active_tooltip = Rc::downgrade(active_tooltip);
|
||||
let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone();
|
||||
ActiveTooltip::Visible {
|
||||
tooltip: AnyTooltip {
|
||||
view,
|
||||
mouse_position: window.mouse_position(),
|
||||
check_visible_and_update: Rc::new(move |tooltip_bounds, window, cx| {
|
||||
if long_press_tooltip_active
|
||||
.as_ref()
|
||||
.is_some_and(|active| active.get())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let Some(active_tooltip) = weak_active_tooltip.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
handle_tooltip_check_visible_and_update(
|
||||
&active_tooltip,
|
||||
tooltip_is_hoverable,
|
||||
&check_is_hovered_during_prepaint,
|
||||
tooltip_bounds,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
},
|
||||
is_hoverable: tooltip_is_hoverable,
|
||||
}
|
||||
});
|
||||
let did_show = new_tooltip.is_some();
|
||||
*active_tooltip.borrow_mut() = new_tooltip;
|
||||
window.refresh();
|
||||
did_show
|
||||
}
|
||||
|
||||
/// Returns a callback which will be called by window prepaint to update tooltip visibility. The
|
||||
/// purpose of doing this logic here instead of the mouse move handler is that the mouse move
|
||||
/// handler won't get called when the element is not painted (e.g. via use of `visible_on_hover`).
|
||||
@@ -4361,8 +4438,8 @@ impl ScrollHandle {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
AnyWindowHandle, AppContext as _, Context, InputEvent, Keystroke, MouseMoveEvent,
|
||||
TestAppContext, canvas, util::FluentBuilder as _,
|
||||
AnyWindowHandle, AppContext as _, Context, GestureTuning, InputEvent, Keystroke,
|
||||
MouseMoveEvent, TestAppContext, TouchEvent, TouchId, canvas, util::FluentBuilder as _,
|
||||
};
|
||||
use std::{cell::Cell, rc::Weak};
|
||||
|
||||
@@ -4837,12 +4914,123 @@ mod tests {
|
||||
assert_eq!(handle.offset().y, px(-25.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_press_shows_tooltip_until_touch_ends() {
|
||||
let (mut test_app, any_window, captured_active_tooltip) = create_tooltip_owner_test(None);
|
||||
let active_tooltip = captured_active_tooltip
|
||||
.borrow()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.upgrade()
|
||||
.unwrap();
|
||||
let touch_id = TouchId(1);
|
||||
let touch_position = point(px(49.), px(10.));
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
TouchEvent {
|
||||
id: touch_id,
|
||||
phase: TouchPhase::Started,
|
||||
position: touch_position,
|
||||
predicted_position: None,
|
||||
force: None,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
test_app
|
||||
.dispatcher
|
||||
.advance_clock(GestureTuning::default().long_press_duration + Duration::from_millis(1));
|
||||
test_app.run_until_parked();
|
||||
|
||||
assert!(matches!(
|
||||
active_tooltip.borrow().as_ref(),
|
||||
Some(ActiveTooltip::Visible { .. })
|
||||
));
|
||||
|
||||
let moved_position = point(px(55.), px(10.));
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
TouchEvent {
|
||||
id: touch_id,
|
||||
phase: TouchPhase::Moved,
|
||||
position: moved_position,
|
||||
predicted_position: None,
|
||||
force: None,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
active_tooltip.borrow().as_ref(),
|
||||
Some(ActiveTooltip::Visible { .. })
|
||||
));
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
TouchEvent {
|
||||
id: touch_id,
|
||||
phase: TouchPhase::Ended,
|
||||
position: moved_position,
|
||||
predicted_position: None,
|
||||
force: None,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(active_tooltip.borrow().is_none());
|
||||
}
|
||||
|
||||
fn setup_tooltip_owner_test(
|
||||
show_delay_override: Option<Duration>,
|
||||
) -> (
|
||||
TestAppContext,
|
||||
crate::AnyWindowHandle,
|
||||
CapturedActiveTooltip,
|
||||
) {
|
||||
let (mut test_app, any_window, captured_active_tooltip) =
|
||||
create_tooltip_owner_test(show_delay_override);
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
MouseMoveEvent {
|
||||
position: point(px(10.), px(10.)),
|
||||
modifiers: Default::default(),
|
||||
pressed_button: None,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
(test_app, any_window, captured_active_tooltip)
|
||||
}
|
||||
|
||||
fn create_tooltip_owner_test(
|
||||
show_delay_override: Option<Duration>,
|
||||
) -> (
|
||||
TestAppContext,
|
||||
crate::AnyWindowHandle,
|
||||
CapturedActiveTooltip,
|
||||
) {
|
||||
let mut test_app = TestAppContext::single();
|
||||
let captured_active_tooltip: CapturedActiveTooltip = Rc::new(RefCell::new(None));
|
||||
@@ -4855,26 +5043,6 @@ mod tests {
|
||||
});
|
||||
let any_window = window.into();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.dispatch_event(
|
||||
MouseMoveEvent {
|
||||
position: point(px(10.), px(10.)),
|
||||
modifiers: Default::default(),
|
||||
pressed_button: None,
|
||||
}
|
||||
.to_platform_input(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
test_app
|
||||
.update_window(any_window, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
|
||||
@@ -1000,6 +1000,7 @@ pub struct InteractiveTextState {
|
||||
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||
hovered_index: Rc<Cell<Option<usize>>>,
|
||||
active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
long_press_tooltip_active: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
|
||||
@@ -1107,6 +1108,7 @@ impl Element for InteractiveText {
|
||||
set_tooltip_on_window(&interactive_state.active_tooltip, window);
|
||||
} else {
|
||||
// If there is no longer a tooltip builder, remove the active tooltip.
|
||||
interactive_state.long_press_tooltip_active.set(false);
|
||||
interactive_state.active_tooltip.take();
|
||||
}
|
||||
}
|
||||
@@ -1255,6 +1257,7 @@ impl Element for InteractiveText {
|
||||
build_tooltip,
|
||||
check_is_hovered,
|
||||
check_is_hovered_during_prepaint,
|
||||
interactive_state.long_press_tooltip_active.clone(),
|
||||
None,
|
||||
window,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user