@@ -21,7 +21,7 @@ use crate::{
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
- StyleRefinement, Styled, Task, View, Visibility, WindowContext,
+ StyleRefinement, Styled, Task, TooltipId, View, Visibility, WindowContext,
};
use collections::HashMap;
use refineable::Refineable;
@@ -483,7 +483,29 @@ impl Interactivity {
self.tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
- self.tooltip_builder = Some(Rc::new(build_tooltip));
+ self.tooltip_builder = Some(TooltipBuilder {
+ build: Rc::new(build_tooltip),
+ hoverable: false,
+ });
+ }
+
+ /// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
+ /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
+ /// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`]
+ pub fn hoverable_tooltip(
+ &mut self,
+ build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static,
+ ) where
+ Self: Sized,
+ {
+ debug_assert!(
+ self.tooltip_builder.is_none(),
+ "calling tooltip more than once on the same element is not supported"
+ );
+ self.tooltip_builder = Some(TooltipBuilder {
+ build: Rc::new(build_tooltip),
+ hoverable: true,
+ });
}
/// Block the mouse from interacting with this element or any of its children
@@ -973,6 +995,20 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self.interactivity().tooltip(build_tooltip);
self
}
+
+ /// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
+ /// 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(
+ mut self,
+ build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ self.interactivity().hoverable_tooltip(build_tooltip);
+ self
+ }
}
/// A trait for providing focus related APIs to interactive elements
@@ -1015,7 +1051,10 @@ type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
type CanDropPredicate = Box<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>;
-pub(crate) type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>;
+pub(crate) struct TooltipBuilder {
+ build: Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
+ hoverable: bool,
+}
pub(crate) type KeyDownListener =
Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowContext) + 'static>;
@@ -1188,6 +1227,7 @@ pub struct Interactivity {
/// Whether the element was hovered. This will only be present after paint if an hitbox
/// was created for the interactive element.
pub hovered: Option<bool>,
+ pub(crate) tooltip_id: Option<TooltipId>,
pub(crate) content_size: Size<Pixels>,
pub(crate) key_context: Option<KeyContext>,
pub(crate) focusable: bool,
@@ -1321,7 +1361,7 @@ impl Interactivity {
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() {
- cx.set_tooltip(tooltip);
+ self.tooltip_id = Some(cx.set_tooltip(tooltip));
}
}
}
@@ -1806,6 +1846,7 @@ impl Interactivity {
}
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)
@@ -1818,11 +1859,17 @@ impl Interactivity {
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
let hitbox = hitbox.clone();
+ let tooltip_id = self.tooltip_id;
move |_: &MouseMoveEvent, phase, cx| {
let is_hovered =
pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx);
- if !is_hovered {
- active_tooltip.borrow_mut().take();
+ 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;
}
@@ -1833,15 +1880,14 @@ impl Interactivity {
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
- let tooltip_builder = tooltip_builder.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: tooltip_builder(cx),
- cursor_offset: cx.mouse_position(),
+ view: build_tooltip(cx),
+ mouse_position: cx.mouse_position(),
}),
_task: None,
});
@@ -1860,15 +1906,30 @@ impl Interactivity {
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
- move |_: &MouseDownEvent, _, _| {
- active_tooltip.borrow_mut().take();
+ 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 {
+ if active_tooltip.borrow_mut().take().is_some() {
+ cx.refresh();
+ }
+ }
}
});
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
- move |_: &ScrollWheelEvent, _, _| {
- active_tooltip.borrow_mut().take();
+ 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 {
+ if active_tooltip.borrow_mut().take().is_some() {
+ cx.refresh();
+ }
+ }
}
})
}
@@ -15,7 +15,7 @@
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut, Cow},
- mem,
+ cmp, mem,
ops::Range,
rc::Rc,
sync::Arc,
@@ -28,17 +28,18 @@ use futures::{future::Shared, FutureExt};
#[cfg(target_os = "macos")]
use media::core_video::CVImageBuffer;
use smallvec::SmallVec;
+use util::post_inc;
use crate::{
- hash, prelude::*, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace, Bounds,
- BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase,
- DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId,
- GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent,
- LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad,
- Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
- RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle,
- Style, Task, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window,
- WindowContext, SUBPIXEL_VARIANTS,
+ hash, point, prelude::*, px, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace,
+ Bounds, BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId,
+ DispatchPhase, DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle,
+ FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext,
+ KeyEvent, LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent,
+ PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
+ RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
+ StrikethroughStyle, Style, Task, TextStyleRefinement, TransformationMatrix, Underline,
+ UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
};
pub(crate) type AnyMouseListener =
@@ -84,6 +85,33 @@ impl Hitbox {
#[derive(Default, Eq, PartialEq)]
pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>);
+/// An identifier for a tooltip.
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub struct TooltipId(usize);
+
+impl TooltipId {
+ /// Checks if the tooltip is currently hovered.
+ pub fn is_hovered(&self, cx: &WindowContext) -> bool {
+ cx.window
+ .tooltip_bounds
+ .as_ref()
+ .map_or(false, |tooltip_bounds| {
+ tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&cx.mouse_position())
+ })
+ }
+}
+
+pub(crate) struct TooltipBounds {
+ id: TooltipId,
+ bounds: Bounds<Pixels>,
+}
+
+#[derive(Clone)]
+pub(crate) struct TooltipRequest {
+ id: TooltipId,
+ tooltip: AnyTooltip,
+}
+
pub(crate) struct DeferredDraw {
priority: usize,
parent_node: DispatchNodeId,
@@ -108,7 +136,7 @@ pub(crate) struct Frame {
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) input_handlers: Vec<Option<PlatformInputHandler>>,
- pub(crate) tooltip_requests: Vec<Option<AnyTooltip>>,
+ pub(crate) tooltip_requests: Vec<Option<TooltipRequest>>,
pub(crate) cursor_styles: Vec<CursorStyleRequest>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
@@ -364,6 +392,7 @@ impl<'a> VisualContext for ElementContext<'a> {
impl<'a> ElementContext<'a> {
pub(crate) fn draw_roots(&mut self) {
self.window.draw_phase = DrawPhase::Layout;
+ self.window.tooltip_bounds.take();
// Layout all root elements.
let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any();
@@ -388,14 +417,8 @@ impl<'a> ElementContext<'a> {
element.layout(offset, AvailableSpace::min_size(), self);
active_drag_element = Some(element);
self.app.active_drag = Some(active_drag);
- } else if let Some(tooltip_request) =
- self.window.next_frame.tooltip_requests.last().cloned()
- {
- let tooltip_request = tooltip_request.unwrap();
- let mut element = tooltip_request.view.clone().into_any();
- let offset = tooltip_request.cursor_offset;
- element.layout(offset, AvailableSpace::min_size(), self);
- tooltip_element = Some(element);
+ } else {
+ tooltip_element = self.layout_tooltip();
}
self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position);
@@ -415,6 +438,52 @@ impl<'a> ElementContext<'a> {
}
}
+ fn layout_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.measure(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(),
+ };
+
+ 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(),
+ );
+ }
+ }
+
+ self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.after_layout(cx));
+
+ self.window.tooltip_bounds = Some(TooltipBounds {
+ id: tooltip_request.id,
+ bounds: tooltip_bounds,
+ });
+ Some(element)
+ }
+
fn layout_deferred_draws(&mut self, deferred_draw_indices: &[usize]) {
assert_eq!(self.window.element_id_stack.len(), 0);
@@ -604,8 +673,13 @@ impl<'a> ElementContext<'a> {
}
/// Sets a tooltip to be rendered for the upcoming frame
- pub fn set_tooltip(&mut self, tooltip: AnyTooltip) {
- self.window.next_frame.tooltip_requests.push(Some(tooltip));
+ pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId {
+ let id = TooltipId(post_inc(&mut self.window.next_tooltip_id.0));
+ self.window
+ .next_frame
+ .tooltip_requests
+ .push(Some(TooltipRequest { id, tooltip }));
+ id
}
/// Pushes the given element id onto the global stack and invokes the given closure