Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use std::{
use super::ImageCacheProvider;

const DRAG_THRESHOLD: f64 = 2.;
const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500);
const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500);

/// The styling information for a given group.
Expand Down Expand Up @@ -644,6 +644,12 @@ impl Interactivity {
});
}

/// Set the delay before this element's tooltip is shown.
/// The imperative API equivalent to [`StatefulInteractiveElement::tooltip_show_delay`].
pub fn tooltip_show_delay(&mut self, delay: Duration) {
self.tooltip_show_delay = Some(delay);
}

/// Block the mouse from all interactions with elements behind this element's hitbox. Typically
/// `block_mouse_except_scroll` should be preferred.
///
Expand Down Expand Up @@ -1439,6 +1445,16 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self.interactivity().hoverable_tooltip(build_tooltip);
self
}

/// Set the delay before this element's tooltip is shown.
/// The fluent API equivalent to [`Interactivity::tooltip_show_delay`].
fn tooltip_show_delay(mut self, delay: Duration) -> Self
where
Self: Sized,
{
self.interactivity().tooltip_show_delay(delay);
self
}
}

pub(crate) type MouseDownListener =
Expand Down Expand Up @@ -1834,6 +1850,7 @@ pub struct Interactivity {
pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>,
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>,
pub(crate) tooltip_builder: Option<TooltipBuilder>,
pub(crate) tooltip_show_delay: Option<Duration>,
pub(crate) window_control: Option<WindowControlArea>,
pub(crate) hitbox_behavior: HitboxBehavior,
pub(crate) tab_index: Option<isize>,
Expand Down Expand Up @@ -2746,6 +2763,7 @@ impl Interactivity {
build_tooltip,
check_is_hovered,
check_is_hovered_during_prepaint,
self.tooltip_show_delay,
window,
);
}
Expand Down Expand Up @@ -3193,9 +3211,11 @@ 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>,
show_delay: Option<Duration>,
window: &mut Window,
) {
let current_view = window.current_view();
let show_delay = show_delay.unwrap_or(DEFAULT_TOOLTIP_SHOW_DELAY);

window.on_mouse_event({
let active_tooltip = active_tooltip.clone();
Expand All @@ -3210,6 +3230,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
tooltip_id,
current_view,
phase,
show_delay,
window,
cx,
)
Expand Down Expand Up @@ -3254,6 +3275,7 @@ fn handle_tooltip_mouse_move(
tooltip_id: Option<TooltipId>,
current_view: EntityId,
phase: DispatchPhase,
show_delay: Duration,
window: &mut Window,
cx: &mut App,
) {
Expand Down Expand Up @@ -3318,7 +3340,7 @@ fn handle_tooltip_mouse_move(
let build_tooltip = build_tooltip.clone();
let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone();
async move |cx| {
cx.background_executor().timer(TOOLTIP_SHOW_DELAY).await;
cx.background_executor().timer(show_delay).await;
let Some(active_tooltip) = weak_active_tooltip.upgrade() else {
return;
};
Expand Down Expand Up @@ -3844,7 +3866,10 @@ impl ScrollHandle {
#[cfg(test)]
mod tests {
use super::*;
use crate::{AppContext as _, Context, InputEvent, MouseMoveEvent, TestAppContext};
use crate::{
AppContext as _, Context, InputEvent, MouseMoveEvent, TestAppContext,
util::FluentBuilder as _,
};
use std::rc::Weak;

struct TestTooltipView;
Expand Down Expand Up @@ -3931,6 +3956,7 @@ mod tests {

struct TooltipOwner {
captured_active_tooltip: CapturedActiveTooltip,
show_delay_override: Option<Duration>,
}

impl Render for TooltipOwner {
Expand All @@ -3943,7 +3969,10 @@ mod tests {
.id("target")
.w(px(50.))
.h(px(50.))
.tooltip(|_, cx| cx.new(|_| TestTooltipView).into()),
.tooltip(|_, cx| cx.new(|_| TestTooltipView).into())
.when_some(self.show_delay_override, |this, delay| {
this.tooltip_show_delay(delay)
}),
)
.into_any_element(),
captured_active_tooltip: self.captured_active_tooltip.clone(),
Expand Down Expand Up @@ -3989,7 +4018,9 @@ mod tests {
assert_eq!(handle.offset().y, px(-25.));
}

fn setup_tooltip_owner_test() -> (
fn setup_tooltip_owner_test(
show_delay_override: Option<Duration>,
) -> (
TestAppContext,
crate::AnyWindowHandle,
CapturedActiveTooltip,
Expand All @@ -4000,6 +4031,7 @@ mod tests {
let captured_active_tooltip = captured_active_tooltip.clone();
move |_, _| TooltipOwner {
captured_active_tooltip,
show_delay_override,
}
});
let any_window = window.into();
Expand Down Expand Up @@ -4035,7 +4067,7 @@ mod tests {

#[test]
fn tooltip_waiting_for_show_is_released_when_its_owner_disappears() {
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test();
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None);

let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap();
let active_tooltip = weak_active_tooltip.upgrade().unwrap();
Expand All @@ -4055,14 +4087,45 @@ mod tests {
assert!(weak_active_tooltip.upgrade().is_none());
}

#[test]
fn tooltip_respects_custom_show_delay() {
let extra_delay = Duration::from_secs(1);
let show_delay_override = DEFAULT_TOOLTIP_SHOW_DELAY + extra_delay;
let (mut test_app, _any_window, captured_active_tooltip) =
setup_tooltip_owner_test(Some(show_delay_override));

let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap();
let active_tooltip = weak_active_tooltip.upgrade().unwrap();

test_app
.dispatcher
.advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY);
test_app.run_until_parked();

assert!(matches!(
active_tooltip.borrow().as_ref(),
Some(ActiveTooltip::WaitingForShow { .. })
));

test_app.dispatcher.advance_clock(extra_delay);
test_app.run_until_parked();

assert!(matches!(
active_tooltip.borrow().as_ref(),
Some(ActiveTooltip::Visible { .. })
));
}

#[test]
fn tooltip_is_released_when_its_owner_disappears() {
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test();
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None);

let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap();
let active_tooltip = weak_active_tooltip.upgrade().unwrap();

test_app.dispatcher.advance_clock(TOOLTIP_SHOW_DELAY);
test_app
.dispatcher
.advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY);
test_app.run_until_parked();

assert!(matches!(
Expand All @@ -4083,12 +4146,14 @@ mod tests {

#[test]
fn tooltip_hides_after_mouse_leaves_origin() {
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test();
let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None);

let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap();
let active_tooltip = weak_active_tooltip.upgrade().unwrap();

test_app.dispatcher.advance_clock(TOOLTIP_SHOW_DELAY);
test_app
.dispatcher
.advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY);
test_app.run_until_parked();

assert!(matches!(
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/elements/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,7 @@ impl Element for InteractiveText {
build_tooltip,
check_is_hovered,
check_is_hovered_during_prepaint,
None,
window,
);
}
Expand Down
Loading