diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 97e7e1bec2..6b0e2a1af6 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -31,7 +31,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, + json, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, + SizeConstraint, View, }; use core::panic; use json::ToJson; @@ -155,6 +156,18 @@ pub trait Element { { FlexItem::new(self.boxed()).float() } + + fn with_tooltip( + self, + id: usize, + tooltip: ElementBox, + cx: &mut RenderContext, + ) -> Tooltip + where + Self: 'static + Sized, + { + Tooltip::new(id, self.boxed(), tooltip, cx) + } } pub enum Lifecycle { diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 2ad6eaf028..1dea333400 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -25,6 +25,7 @@ pub struct MouseEventHandler { mouse_down_out: Option>, right_mouse_down_out: Option>, drag: Option>, + hover: Option>, padding: Padding, } @@ -47,6 +48,7 @@ impl MouseEventHandler { mouse_down_out: None, right_mouse_down_out: None, drag: None, + hover: None, padding: Default::default(), } } @@ -109,6 +111,14 @@ impl MouseEventHandler { self } + pub fn on_hover( + mut self, + handler: impl Fn(Vector2F, bool, &mut EventContext) + 'static, + ) -> Self { + self.hover = Some(Rc::new(handler)); + self + } + pub fn with_padding(mut self, padding: Padding) -> Self { self.padding = padding; self @@ -153,7 +163,7 @@ impl Element for MouseEventHandler { view_id: cx.current_view_id(), discriminant: Some((self.tag, self.id)), bounds: self.hit_bounds(bounds), - hover: None, + hover: self.hover.clone(), click: self.click.clone(), mouse_down: self.mouse_down.clone(), right_click: self.right_click.clone(), diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 3ab433f161..b56f269667 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -1,38 +1,77 @@ -use super::{ContainerStyle, Element, ElementBox}; -use crate::{ - geometry::{rect::RectF, vector::Vector2F}, - json::{json, ToJson}, - ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, View, +use std::{ + cell::{Cell, RefCell}, + rc::Rc, + time::Duration, }; +use super::{Element, ElementBox, MouseEventHandler}; +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json::json, + ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View, +}; + +const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500); + pub struct Tooltip { - state: ElementStateHandle, child: ElementBox, - style: ContainerStyle, - text: String, + tooltip: Option, + state: ElementStateHandle>, } #[derive(Default)] -struct TooltipState {} +struct TooltipState { + visible: Cell, + position: Cell, + debounce: RefCell>>, +} impl Tooltip { pub fn new( id: usize, child: ElementBox, - text: String, + tooltip: ElementBox, cx: &mut RenderContext, ) -> Self { - Self { - state: cx.element_state::(id), - child, - text, - style: Default::default(), - } - } + let state_handle = cx.element_state::>(id); + let state = state_handle.read(cx).clone(); + let tooltip = if state.visible.get() { + Some(tooltip) + } else { + None + }; + let child = MouseEventHandler::new::(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); - pub fn with_style(mut self, style: ContainerStyle) -> Self { - self.style = style; - self + 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(); + } + } + }) + .boxed(); + Self { + child, + tooltip, + state: state_handle, + } } } @@ -46,6 +85,9 @@ impl Element for Tooltip { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let size = self.child.layout(constraint, cx); + if let Some(tooltip) = self.tooltip.as_mut() { + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + } (size, ()) } @@ -57,6 +99,13 @@ impl Element for Tooltip { cx: &mut PaintContext, ) { self.child.paint(bounds.origin(), visible_bounds, cx); + if let Some(tooltip) = self.tooltip.as_mut() { + let origin = self.state.read(cx).position.get(); + let size = tooltip.size(); + cx.scene.push_stacking_context(None); + tooltip.paint(origin, RectF::new(origin, size), cx); + cx.scene.pop_stacking_context(); + } } fn dispatch_event( @@ -80,8 +129,7 @@ impl Element for Tooltip { ) -> serde_json::Value { json!({ "child": self.child.debug(cx), - "style": self.style.to_json(), - "text": &self.text, + "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)), }) } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 87efeb2e5f..6e29242b84 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -311,7 +311,7 @@ impl Presenter { if let Some(region_id) = region.id() { if !self.hovered_region_ids.contains(®ion_id) { invalidated_views.push(region.view_id); - hovered_regions.push(region.clone()); + hovered_regions.push((region.clone(), position)); self.hovered_region_ids.insert(region_id); } } @@ -319,7 +319,7 @@ impl Presenter { if let Some(region_id) = region.id() { if self.hovered_region_ids.contains(®ion_id) { invalidated_views.push(region.view_id); - unhovered_regions.push(region.clone()); + unhovered_regions.push((region.clone(), position)); self.hovered_region_ids.remove(®ion_id); } } @@ -348,20 +348,20 @@ impl Presenter { let mut event_cx = self.build_event_context(cx); let mut handled = false; - for unhovered_region in unhovered_regions { + for (unhovered_region, position) in unhovered_regions { handled = true; if let Some(hover_callback) = unhovered_region.hover { event_cx.with_current_view(unhovered_region.view_id, |event_cx| { - hover_callback(false, event_cx); + hover_callback(position, false, event_cx); }) } } - for hovered_region in hovered_regions { + for (hovered_region, position) in hovered_regions { handled = true; if let Some(hover_callback) = hovered_region.hover { event_cx.with_current_view(hovered_region.view_id, |event_cx| { - hover_callback(true, event_cx); + hover_callback(position, true, event_cx); }) } } @@ -449,6 +449,7 @@ impl Presenter { view_stack: Default::default(), invalidated_views: Default::default(), notify_count: 0, + window_id: self.window_id, app: cx, } } @@ -626,6 +627,7 @@ pub struct EventContext<'a> { pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, pub app: &'a mut MutableAppContext, + pub window_id: usize, pub notify_count: usize, view_stack: Vec, invalidated_views: HashSet, @@ -653,6 +655,14 @@ impl<'a> EventContext<'a> { result } + pub fn window_id(&self) -> usize { + self.window_id + } + + pub fn view_id(&self) -> Option { + self.view_stack.last().copied() + } + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { dispatcher_view_id: self.view_stack.last().copied(), diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 1f503c8bf7..ffe0fc76d1 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -49,7 +49,7 @@ pub struct MouseRegion { pub view_id: usize, pub discriminant: Option<(TypeId, usize)>, pub bounds: RectF, - pub hover: Option>, + pub hover: Option>, pub mouse_down: Option>, pub click: Option>, pub right_mouse_down: Option>,