Delay hiding git blame tooltip (#22644)
It's easy to overshoot the bottom of the tooltip when cursoring to a button, such as opening the commit from a blame tooltip. Before this change the tooltip would immediately disappear, and now it sticks around for a bit. Also: * Shares the implementation with `elements/text.rs`. This will particularly be handy when it makes use of hoverable tooltips. * Improves the fix to #21657. - Now the element will no longer think it has an active tooltip that it registers with the window. - It will instead display the next available tooltip, whereas I believe before the next available tooltip would be suppressed. * Fixes bug where `cx.refresh()` wasn't called when text tooltip is hidden due to a mouse down event. * Ports over fix in https://github.com/zed-industries/zed/pull/14832 to `elements/text.rs` Release Notes: - The tooltip for inline git blame now waits a bit before disappearing when the mouse leaves it.
This commit is contained in:
@@ -1635,11 +1635,10 @@ pub struct AnyTooltip {
|
||||
/// The absolute position of the mouse when the tooltip was deployed.
|
||||
pub mouse_position: Point<Pixels>,
|
||||
|
||||
/// Whether the tooltitp can be hovered or not.
|
||||
pub hoverable: bool,
|
||||
|
||||
/// Bounds of the element that triggered the tooltip appearance.
|
||||
pub origin_bounds: Bounds<Pixels>,
|
||||
/// Given the bounds of the tooltip, checks whether the tooltip should still be visible and
|
||||
/// updates its state accordingly. This is needed atop the hovered element's mouse move handler
|
||||
/// to handle the case where the element is not painted (e.g. via use of `visible_on_hover`).
|
||||
pub check_visible_and_update: Rc<dyn Fn(Bounds<Pixels>, &mut WindowContext) -> bool>,
|
||||
}
|
||||
|
||||
/// A keystroke event, and potentially the associated action
|
||||
|
||||
+287
-96
@@ -42,7 +42,8 @@ use taffy::style::Overflow;
|
||||
use util::ResultExt;
|
||||
|
||||
const DRAG_THRESHOLD: f64 = 2.;
|
||||
pub(crate) const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
|
||||
const 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.
|
||||
pub struct GroupStyle {
|
||||
@@ -1425,17 +1426,17 @@ impl Interactivity {
|
||||
element_state.map(|element_state| element_state.unwrap_or_default());
|
||||
let style = self.compute_style_internal(None, element_state.as_mut(), cx);
|
||||
|
||||
if let Some(element_state) = element_state.as_ref() {
|
||||
if let Some(element_state) = element_state.as_mut() {
|
||||
if let Some(clicked_state) = element_state.clicked_state.as_ref() {
|
||||
let clicked_state = clicked_state.borrow();
|
||||
self.active = Some(clicked_state.element);
|
||||
}
|
||||
|
||||
if let Some(active_tooltip) = element_state.active_tooltip.as_ref() {
|
||||
if let Some(active_tooltip) = active_tooltip.borrow().as_ref() {
|
||||
if let Some(tooltip) = active_tooltip.tooltip.clone() {
|
||||
self.tooltip_id = Some(cx.set_tooltip(tooltip));
|
||||
}
|
||||
if self.tooltip_builder.is_some() {
|
||||
self.tooltip_id = set_tooltip_on_window(active_tooltip, cx);
|
||||
} else {
|
||||
// If there is no longer a tooltip builder, remove the active tooltip.
|
||||
element_state.active_tooltip.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1935,13 +1936,7 @@ impl Interactivity {
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure to remove active tooltip if tooltip builder is none
|
||||
if self.tooltip_builder.is_none() {
|
||||
element_state.active_tooltip.take();
|
||||
}
|
||||
|
||||
if let Some(tooltip_builder) = self.tooltip_builder.take() {
|
||||
let tooltip_is_hoverable = tooltip_builder.hoverable;
|
||||
let active_tooltip = element_state
|
||||
.active_tooltip
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -1951,85 +1946,24 @@ impl Interactivity {
|
||||
.get_or_insert_with(Default::default)
|
||||
.clone();
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
let source_bounds = hitbox.bounds;
|
||||
let tooltip_id = self.tooltip_id;
|
||||
move |_: &MouseMoveEvent, phase, cx| {
|
||||
let is_hovered =
|
||||
pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx);
|
||||
let tooltip_is_hovered =
|
||||
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
|
||||
if !is_hovered && (!tooltip_is_hoverable || !tooltip_is_hovered) {
|
||||
if active_tooltip.borrow_mut().take().is_some() {
|
||||
cx.refresh();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
if active_tooltip.borrow().is_none() {
|
||||
let task = cx.spawn({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let build_tooltip = tooltip_builder.build.clone();
|
||||
move |mut cx| async move {
|
||||
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
||||
cx.update(|cx| {
|
||||
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||
tooltip: Some(AnyTooltip {
|
||||
view: build_tooltip(cx),
|
||||
mouse_position: cx.mouse_position(),
|
||||
hoverable: tooltip_is_hoverable,
|
||||
origin_bounds: source_bounds,
|
||||
}),
|
||||
_task: None,
|
||||
});
|
||||
cx.refresh();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||
tooltip: None,
|
||||
_task: Some(task),
|
||||
});
|
||||
}
|
||||
}
|
||||
let tooltip_is_hoverable = tooltip_builder.hoverable;
|
||||
let build_tooltip = Rc::new(move |cx: &mut WindowContext| {
|
||||
Some(((tooltip_builder.build)(cx), tooltip_is_hoverable))
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let tooltip_id = self.tooltip_id;
|
||||
move |_: &MouseDownEvent, _, cx| {
|
||||
let tooltip_is_hovered =
|
||||
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
|
||||
|
||||
if (!tooltip_is_hoverable || !tooltip_is_hovered)
|
||||
&& active_tooltip.borrow_mut().take().is_some()
|
||||
{
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
// Use bounds instead of testing hitbox since check_is_hovered is also called
|
||||
// during prepaint.
|
||||
let source_bounds = hitbox.bounds;
|
||||
let check_is_hovered = Rc::new(move |cx: &WindowContext| {
|
||||
pending_mouse_down.borrow().is_none()
|
||||
&& source_bounds.contains(&cx.mouse_position())
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let tooltip_id = self.tooltip_id;
|
||||
move |_: &ScrollWheelEvent, _, cx| {
|
||||
let tooltip_is_hovered =
|
||||
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
|
||||
if (!tooltip_is_hoverable || !tooltip_is_hovered)
|
||||
&& active_tooltip.borrow_mut().take().is_some()
|
||||
{
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
})
|
||||
register_tooltip_mouse_handlers(
|
||||
&active_tooltip,
|
||||
self.tooltip_id,
|
||||
build_tooltip,
|
||||
check_is_hovered,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let active_state = element_state
|
||||
@@ -2284,12 +2218,6 @@ pub struct InteractiveElementState {
|
||||
pub(crate) active_tooltip: Option<Rc<RefCell<Option<ActiveTooltip>>>>,
|
||||
}
|
||||
|
||||
/// The current active tooltip
|
||||
pub struct ActiveTooltip {
|
||||
pub(crate) tooltip: Option<AnyTooltip>,
|
||||
pub(crate) _task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
/// Whether or not the element or a group that contains it is clicked by the mouse.
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ElementClickedState {
|
||||
@@ -2306,6 +2234,269 @@ impl ElementClickedState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ActiveTooltip {
|
||||
/// Currently delaying before showing the tooltip.
|
||||
WaitingForShow { _task: Task<()> },
|
||||
/// Tooltip is visible, element was hovered or for hoverable tooltips, the tooltip was hovered.
|
||||
Visible {
|
||||
tooltip: AnyTooltip,
|
||||
is_hoverable: bool,
|
||||
},
|
||||
/// Tooltip is visible and hoverable, but the mouse is no longer hovering. Currently delaying
|
||||
/// before hiding it.
|
||||
WaitingForHide {
|
||||
tooltip: AnyTooltip,
|
||||
_task: Task<()>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn clear_active_tooltip(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
match active_tooltip.borrow_mut().take() {
|
||||
None => {}
|
||||
Some(ActiveTooltip::WaitingForShow { .. }) => {}
|
||||
Some(ActiveTooltip::Visible { .. }) => cx.refresh(),
|
||||
Some(ActiveTooltip::WaitingForHide { .. }) => cx.refresh(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_active_tooltip_if_not_hoverable(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let should_clear = match active_tooltip.borrow().as_ref() {
|
||||
None => false,
|
||||
Some(ActiveTooltip::WaitingForShow { .. }) => false,
|
||||
Some(ActiveTooltip::Visible { is_hoverable, .. }) => !is_hoverable,
|
||||
Some(ActiveTooltip::WaitingForHide { .. }) => false,
|
||||
};
|
||||
if should_clear {
|
||||
active_tooltip.borrow_mut().take();
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_tooltip_on_window(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<TooltipId> {
|
||||
let tooltip = match active_tooltip.borrow().as_ref() {
|
||||
None => return None,
|
||||
Some(ActiveTooltip::WaitingForShow { .. }) => return None,
|
||||
Some(ActiveTooltip::Visible { tooltip, .. }) => tooltip.clone(),
|
||||
Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => tooltip.clone(),
|
||||
};
|
||||
Some(cx.set_tooltip(tooltip))
|
||||
}
|
||||
|
||||
pub(crate) fn register_tooltip_mouse_handlers(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
tooltip_id: Option<TooltipId>,
|
||||
build_tooltip: Rc<dyn Fn(&mut WindowContext) -> Option<(AnyView, bool)>>,
|
||||
check_is_hovered: Rc<dyn Fn(&WindowContext) -> bool>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let build_tooltip = build_tooltip.clone();
|
||||
let check_is_hovered = check_is_hovered.clone();
|
||||
move |_: &MouseMoveEvent, phase, cx| {
|
||||
handle_tooltip_mouse_move(
|
||||
&active_tooltip,
|
||||
&build_tooltip,
|
||||
&check_is_hovered,
|
||||
phase,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
move |_: &MouseDownEvent, _, cx| {
|
||||
if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)) {
|
||||
clear_active_tooltip_if_not_hoverable(&active_tooltip, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
move |_: &ScrollWheelEvent, _, cx| {
|
||||
if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)) {
|
||||
clear_active_tooltip_if_not_hoverable(&active_tooltip, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_tooltip_mouse_move(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
build_tooltip: &Rc<dyn Fn(&mut WindowContext) -> Option<(AnyView, bool)>>,
|
||||
check_is_hovered: &Rc<dyn Fn(&WindowContext) -> bool>,
|
||||
phase: DispatchPhase,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
// Separates logic for what mutation should occur from applying it, to avoid overlapping
|
||||
// RefCell borrows.
|
||||
enum Action {
|
||||
None,
|
||||
CancelShow,
|
||||
ScheduleShow,
|
||||
}
|
||||
|
||||
let action = match active_tooltip.borrow().as_ref() {
|
||||
None => {
|
||||
let is_hovered = check_is_hovered(cx);
|
||||
if is_hovered && phase.bubble() {
|
||||
Action::ScheduleShow
|
||||
} else {
|
||||
Action::None
|
||||
}
|
||||
}
|
||||
Some(ActiveTooltip::WaitingForShow { .. }) => {
|
||||
let is_hovered = check_is_hovered(cx);
|
||||
if is_hovered {
|
||||
Action::None
|
||||
} else {
|
||||
Action::CancelShow
|
||||
}
|
||||
}
|
||||
// These are handled in check_visible_and_update.
|
||||
Some(ActiveTooltip::Visible { .. }) | Some(ActiveTooltip::WaitingForHide { .. }) => {
|
||||
Action::None
|
||||
}
|
||||
};
|
||||
|
||||
match action {
|
||||
Action::None => {}
|
||||
Action::CancelShow => {
|
||||
// Cancel waiting to show tooltip when it is no longer hovered.
|
||||
active_tooltip.borrow_mut().take();
|
||||
}
|
||||
Action::ScheduleShow => {
|
||||
let delayed_show_task = cx.spawn({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let build_tooltip = build_tooltip.clone();
|
||||
let check_is_hovered = check_is_hovered.clone();
|
||||
move |mut cx| async move {
|
||||
cx.background_executor().timer(TOOLTIP_SHOW_DELAY).await;
|
||||
cx.update(|cx| {
|
||||
let new_tooltip = build_tooltip(cx).map(|(view, tooltip_is_hoverable)| {
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
ActiveTooltip::Visible {
|
||||
tooltip: AnyTooltip {
|
||||
view,
|
||||
mouse_position: cx.mouse_position(),
|
||||
check_visible_and_update: Rc::new(move |tooltip_bounds, cx| {
|
||||
handle_tooltip_check_visible_and_update(
|
||||
&active_tooltip,
|
||||
tooltip_is_hoverable,
|
||||
&check_is_hovered,
|
||||
tooltip_bounds,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
},
|
||||
is_hoverable: tooltip_is_hoverable,
|
||||
}
|
||||
});
|
||||
*active_tooltip.borrow_mut() = new_tooltip;
|
||||
cx.refresh();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
active_tooltip
|
||||
.borrow_mut()
|
||||
.replace(ActiveTooltip::WaitingForShow {
|
||||
_task: delayed_show_task,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
fn handle_tooltip_check_visible_and_update(
|
||||
active_tooltip: &Rc<RefCell<Option<ActiveTooltip>>>,
|
||||
tooltip_is_hoverable: bool,
|
||||
check_is_hovered: &Rc<dyn Fn(&WindowContext) -> bool>,
|
||||
tooltip_bounds: Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) -> bool {
|
||||
// Separates logic for what mutation should occur from applying it, to avoid overlapping RefCell
|
||||
// borrows.
|
||||
enum Action {
|
||||
None,
|
||||
Hide,
|
||||
ScheduleHide(AnyTooltip),
|
||||
CancelHide(AnyTooltip),
|
||||
}
|
||||
|
||||
let is_hovered = check_is_hovered(cx)
|
||||
|| (tooltip_is_hoverable && tooltip_bounds.contains(&cx.mouse_position()));
|
||||
let action = match active_tooltip.borrow().as_ref() {
|
||||
Some(ActiveTooltip::Visible { tooltip, .. }) => {
|
||||
if is_hovered {
|
||||
Action::None
|
||||
} else {
|
||||
if tooltip_is_hoverable {
|
||||
Action::ScheduleHide(tooltip.clone())
|
||||
} else {
|
||||
Action::Hide
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => {
|
||||
if is_hovered {
|
||||
Action::CancelHide(tooltip.clone())
|
||||
} else {
|
||||
Action::None
|
||||
}
|
||||
}
|
||||
None | Some(ActiveTooltip::WaitingForShow { .. }) => Action::None,
|
||||
};
|
||||
|
||||
match action {
|
||||
Action::None => {}
|
||||
Action::Hide => {
|
||||
clear_active_tooltip(&active_tooltip, cx);
|
||||
}
|
||||
Action::ScheduleHide(tooltip) => {
|
||||
let delayed_hide_task = cx.spawn({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
move |mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(HOVERABLE_TOOLTIP_HIDE_DELAY)
|
||||
.await;
|
||||
if active_tooltip.borrow_mut().take().is_some() {
|
||||
cx.update(|cx| cx.refresh()).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
active_tooltip
|
||||
.borrow_mut()
|
||||
.replace(ActiveTooltip::WaitingForHide {
|
||||
tooltip,
|
||||
_task: delayed_hide_task,
|
||||
});
|
||||
}
|
||||
Action::CancelHide(tooltip) => {
|
||||
// Cancel waiting to hide tooltip when it becomes hovered.
|
||||
active_tooltip.borrow_mut().replace(ActiveTooltip::Visible {
|
||||
tooltip,
|
||||
is_hoverable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
active_tooltip.borrow().is_some()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GroupHitboxes(HashMap<SharedString, SmallVec<[HitboxId; 1]>>);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext,
|
||||
WrappedLine, WrappedLineLayout, TOOLTIP_DELAY,
|
||||
register_tooltip_mouse_handlers, set_tooltip_on_window, ActiveTooltip, AnyView, Bounds,
|
||||
DispatchPhase, Element, ElementId, GlobalElementId, HighlightStyle, Hitbox, IntoElement,
|
||||
LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size,
|
||||
TextRun, TextStyle, TooltipId, Truncate, WhiteSpace, WindowContext, WrappedLine,
|
||||
WrappedLineLayout,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
@@ -505,6 +506,7 @@ pub struct InteractiveText {
|
||||
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext)>>,
|
||||
hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext)>>,
|
||||
tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext) -> Option<AnyView>>>,
|
||||
tooltip_id: Option<TooltipId>,
|
||||
clickable_ranges: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
@@ -531,6 +533,7 @@ impl InteractiveText {
|
||||
click_listener: None,
|
||||
hover_listener: None,
|
||||
tooltip_builder: None,
|
||||
tooltip_id: None,
|
||||
clickable_ranges: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -600,15 +603,16 @@ impl Element for InteractiveText {
|
||||
cx.with_optional_element_state::<InteractiveTextState, _>(
|
||||
global_id,
|
||||
|interactive_state, cx| {
|
||||
let interactive_state = interactive_state
|
||||
let mut interactive_state = interactive_state
|
||||
.map(|interactive_state| interactive_state.unwrap_or_default());
|
||||
|
||||
if let Some(interactive_state) = interactive_state.as_ref() {
|
||||
if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
|
||||
{
|
||||
if let Some(tooltip) = active_tooltip.tooltip.clone() {
|
||||
cx.set_tooltip(tooltip);
|
||||
}
|
||||
if let Some(interactive_state) = interactive_state.as_mut() {
|
||||
if self.tooltip_builder.is_some() {
|
||||
self.tooltip_id =
|
||||
set_tooltip_on_window(&interactive_state.active_tooltip, cx);
|
||||
} else {
|
||||
// If there is no longer a tooltip builder, remove the active tooltip.
|
||||
interactive_state.active_tooltip.take();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,64 +708,37 @@ impl Element for InteractiveText {
|
||||
});
|
||||
|
||||
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
|
||||
let hitbox = hitbox.clone();
|
||||
let source_bounds = hitbox.bounds;
|
||||
let active_tooltip = interactive_state.active_tooltip.clone();
|
||||
let pending_mouse_down = interactive_state.mouse_down_index.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
|
||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||
let position = text_layout.index_for_position(event.position).ok();
|
||||
let is_hovered = position.is_some()
|
||||
&& hitbox.is_hovered(cx)
|
||||
&& pending_mouse_down.get().is_none();
|
||||
if !is_hovered {
|
||||
active_tooltip.take();
|
||||
return;
|
||||
}
|
||||
let position = position.unwrap();
|
||||
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
if active_tooltip.borrow().is_none() {
|
||||
let task = cx.spawn({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let tooltip_builder = tooltip_builder.clone();
|
||||
|
||||
move |mut cx| async move {
|
||||
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
||||
cx.update(|cx| {
|
||||
let new_tooltip =
|
||||
tooltip_builder(position, cx).map(|tooltip| {
|
||||
ActiveTooltip {
|
||||
tooltip: Some(AnyTooltip {
|
||||
view: tooltip,
|
||||
mouse_position: cx.mouse_position(),
|
||||
hoverable: true,
|
||||
origin_bounds: source_bounds,
|
||||
}),
|
||||
_task: None,
|
||||
}
|
||||
});
|
||||
*active_tooltip.borrow_mut() = new_tooltip;
|
||||
cx.refresh();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
*active_tooltip.borrow_mut() = Some(ActiveTooltip {
|
||||
tooltip: None,
|
||||
_task: Some(task),
|
||||
});
|
||||
let build_tooltip = Rc::new({
|
||||
let tooltip_is_hoverable = false;
|
||||
let text_layout = text_layout.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
text_layout
|
||||
.index_for_position(cx.mouse_position())
|
||||
.ok()
|
||||
.and_then(|position| tooltip_builder(position, cx))
|
||||
.map(|view| (view, tooltip_is_hoverable))
|
||||
}
|
||||
});
|
||||
|
||||
let active_tooltip = interactive_state.active_tooltip.clone();
|
||||
cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
|
||||
active_tooltip.take();
|
||||
// Use bounds instead of testing hitbox since check_is_hovered is also
|
||||
// called during prepaint.
|
||||
let source_bounds = hitbox.bounds;
|
||||
let check_is_hovered = Rc::new({
|
||||
let text_layout = text_layout.clone();
|
||||
move |cx: &WindowContext| {
|
||||
text_layout.index_for_position(cx.mouse_position()).is_ok()
|
||||
&& source_bounds.contains(&cx.mouse_position())
|
||||
&& pending_mouse_down.get().is_none()
|
||||
}
|
||||
});
|
||||
register_tooltip_mouse_handlers(
|
||||
&active_tooltip,
|
||||
self.tooltip_id,
|
||||
build_tooltip,
|
||||
check_is_hovered,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
self.text.paint(None, bounds, &mut (), &mut (), cx);
|
||||
|
||||
+59
-50
@@ -1550,62 +1550,71 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
fn prepaint_tooltip(&mut self) -> Option<AnyElement> {
|
||||
let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?;
|
||||
let tooltip_request = tooltip_request.unwrap();
|
||||
let mut element = tooltip_request.tooltip.view.clone().into_any();
|
||||
let mouse_position = tooltip_request.tooltip.mouse_position;
|
||||
let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self);
|
||||
// Use indexing instead of iteration to avoid borrowing self for the duration of the loop.
|
||||
for tooltip_request_index in (0..self.window.next_frame.tooltip_requests.len()).rev() {
|
||||
let Some(Some(tooltip_request)) = self
|
||||
.window
|
||||
.next_frame
|
||||
.tooltip_requests
|
||||
.get(tooltip_request_index)
|
||||
.cloned()
|
||||
else {
|
||||
log::error!("Unexpectedly absent TooltipRequest");
|
||||
continue;
|
||||
};
|
||||
let mut element = tooltip_request.tooltip.view.clone().into_any();
|
||||
let mouse_position = tooltip_request.tooltip.mouse_position;
|
||||
let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self);
|
||||
|
||||
let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size);
|
||||
let window_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: self.viewport_size(),
|
||||
};
|
||||
let mut tooltip_bounds =
|
||||
Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size);
|
||||
let window_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: self.viewport_size(),
|
||||
};
|
||||
|
||||
if tooltip_bounds.right() > window_bounds.right() {
|
||||
let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.);
|
||||
if new_x >= Pixels::ZERO {
|
||||
tooltip_bounds.origin.x = new_x;
|
||||
} else {
|
||||
tooltip_bounds.origin.x = cmp::max(
|
||||
Pixels::ZERO,
|
||||
tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(),
|
||||
);
|
||||
if tooltip_bounds.right() > window_bounds.right() {
|
||||
let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.);
|
||||
if new_x >= Pixels::ZERO {
|
||||
tooltip_bounds.origin.x = new_x;
|
||||
} else {
|
||||
tooltip_bounds.origin.x = cmp::max(
|
||||
Pixels::ZERO,
|
||||
tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tooltip_bounds.bottom() > window_bounds.bottom() {
|
||||
let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.);
|
||||
if new_y >= Pixels::ZERO {
|
||||
tooltip_bounds.origin.y = new_y;
|
||||
} else {
|
||||
tooltip_bounds.origin.y = cmp::max(
|
||||
Pixels::ZERO,
|
||||
tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(),
|
||||
);
|
||||
if tooltip_bounds.bottom() > window_bounds.bottom() {
|
||||
let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.);
|
||||
if new_y >= Pixels::ZERO {
|
||||
tooltip_bounds.origin.y = new_y;
|
||||
} else {
|
||||
tooltip_bounds.origin.y = cmp::max(
|
||||
Pixels::ZERO,
|
||||
tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible for an element to have an active tooltip while not being painted (e.g.
|
||||
// via the `visible_on_hover` method). Since mouse listeners are not active in this
|
||||
// case, instead update the tooltip's visibility here.
|
||||
let is_visible =
|
||||
(tooltip_request.tooltip.check_visible_and_update)(tooltip_bounds, self);
|
||||
if !is_visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx));
|
||||
|
||||
self.window.tooltip_bounds = Some(TooltipBounds {
|
||||
id: tooltip_request.id,
|
||||
bounds: tooltip_bounds,
|
||||
});
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
// Element's parent can get hidden (e.g. via the `visible_on_hover` method),
|
||||
// and element's `paint` won't be called (ergo, mouse listeners also won't be active) to detect that the tooltip has to be removed.
|
||||
// Ensure it's not stuck around in such cases.
|
||||
let invalidate_tooltip = !tooltip_request
|
||||
.tooltip
|
||||
.origin_bounds
|
||||
.contains(&self.mouse_position())
|
||||
&& (!tooltip_request.tooltip.hoverable
|
||||
|| !tooltip_bounds.contains(&self.mouse_position()));
|
||||
if invalidate_tooltip {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx));
|
||||
|
||||
self.window.tooltip_bounds = Some(TooltipBounds {
|
||||
id: tooltip_request.id,
|
||||
bounds: tooltip_bounds,
|
||||
});
|
||||
Some(element)
|
||||
None
|
||||
}
|
||||
|
||||
fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) {
|
||||
|
||||
Reference in New Issue
Block a user