use std::{cell::Cell, rc::Rc, time::Duration};
use crate::{
Anchor, AnyElement, App, Bounds, Context, ElementExt, ElementId, InteractiveElement as _,
IntoElement, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement,
StyleRefinement, Styled, StyledExt as _, Task, Window, div, prelude::FluentBuilder as _,
};
use super::Popover;
#[derive(IntoElement)]
pub struct HoverCard {
id: ElementId,
style: StyleRefinement,
anchor: Anchor,
trigger: Option<Box<dyn FnOnce(&mut Window, &App) -> AnyElement + 'static>>,
content: Option<
Rc<
dyn Fn(&mut HoverCardState, &mut Window, &mut Context<HoverCardState>) -> AnyElement
+ 'static,
>,
>,
children: Vec<AnyElement>,
open_delay: Duration,
close_delay: Duration,
appearance: bool,
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
}
impl HoverCard {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: StyleRefinement::default(),
anchor: Anchor::TopCenter,
trigger: None,
content: None,
children: vec![],
open_delay: Duration::from_secs_f64(0.6),
close_delay: Duration::from_secs_f64(0.3),
appearance: true,
on_open_change: None,
}
}
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
self.anchor = anchor.into();
self
}
pub fn trigger<T>(mut self, trigger: T) -> Self
where
T: IntoElement + 'static,
{
self.trigger = Some(Box::new(|_, _| trigger.into_any_element()));
self
}
pub fn content<F, E>(mut self, content: F) -> Self
where
F: Fn(&mut HoverCardState, &mut Window, &mut Context<HoverCardState>) -> E + 'static,
E: IntoElement + 'static,
{
self.content = Some(Rc::new(move |state, window, cx| {
content(state, window, cx).into_any_element()
}));
self
}
pub fn open_delay(mut self, duration: Duration) -> Self {
self.open_delay = duration;
self
}
pub fn close_delay(mut self, duration: Duration) -> Self {
self.close_delay = duration;
self
}
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
pub fn on_open_change<F>(mut self, callback: F) -> Self
where
F: Fn(&bool, &mut Window, &mut App) + 'static,
{
self.on_open_change = Some(Rc::new(callback));
self
}
}
impl Styled for HoverCard {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl ParentElement for HoverCard {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
pub struct HoverCardState {
open: bool,
trigger_bounds: Bounds<Pixels>,
open_delay: Duration,
close_delay: Duration,
open_task: Option<Task<()>>,
close_task: Option<Task<()>>,
epoch: usize,
is_hovering_trigger: bool,
is_hovering_content: bool,
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
}
impl HoverCardState {
fn new(open_delay: Duration, close_delay: Duration) -> Self {
Self {
open: false,
trigger_bounds: Bounds::default(),
open_delay,
close_delay,
open_task: None,
close_task: None,
epoch: 0,
is_hovering_trigger: false,
is_hovering_content: false,
on_open_change: None,
}
}
pub fn is_open(&self) -> bool {
self.open
}
fn schedule_open(&mut self, cx: &mut Context<Self>) {
self.cancel_tasks();
let epoch = self.next_epoch();
let delay = self.open_delay;
self.open_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor().timer(delay).await;
let _ = this.update(cx, |state, cx| {
if state.epoch == epoch {
state.set_open(true, cx);
}
});
}));
}
fn schedule_close(&mut self, cx: &mut Context<Self>) {
self.cancel_tasks();
let epoch = self.next_epoch();
let delay = self.close_delay;
self.close_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor().timer(delay).await;
let _ = this.update(cx, |state, cx| {
if state.epoch == epoch && !state.is_hovering_trigger && !state.is_hovering_content
{
state.set_open(false, cx);
}
});
}));
}
fn cancel_tasks(&mut self) {
self.epoch += 1; self.open_task = None;
self.close_task = None;
}
fn next_epoch(&mut self) -> usize {
self.epoch += 1;
self.epoch
}
fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
if self.open == open {
return;
}
self.open = open;
cx.notify();
}
fn on_trigger_hover(&mut self, hovering: bool, cx: &mut Context<Self>) {
self.is_hovering_trigger = hovering;
if hovering {
self.schedule_open(cx);
} else {
if !self.is_hovering_content {
self.schedule_close(cx);
}
}
}
fn on_content_hover(&mut self, hovered: bool, cx: &mut Context<Self>) {
self.is_hovering_content = hovered;
if hovered {
self.cancel_tasks();
} else {
if !self.is_hovering_trigger {
self.schedule_close(cx);
}
}
}
}
impl Render for HoverCardState {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div() }
}
impl RenderOnce for HoverCard {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| {
HoverCardState::new(self.open_delay, self.close_delay)
});
let prev_open = state.read(cx).open;
state.update(cx, |state, _| {
state.open_delay = self.open_delay;
state.close_delay = self.close_delay;
state.on_open_change = self.on_open_change.clone();
});
let open = state.read(cx).open;
let trigger_bounds = state.read(cx).trigger_bounds;
if prev_open != open {
if let Some(ref callback) = self.on_open_change {
callback(&open, window, cx);
}
}
let Some(trigger) = self.trigger else {
return div().id("empty");
};
let anchor = self.anchor;
let position = Rc::new(Cell::new(Popover::resolved_corner(anchor, trigger_bounds)));
let root = div().id(self.id).child(
div()
.id("trigger")
.child((trigger)(window, cx))
.on_hover(window.listener_for(&state, |state, hovered, _, cx| {
state.on_trigger_hover(*hovered, cx);
}))
.on_prepaint({
let state = state.clone();
let position = position.clone();
move |bounds, _, cx| {
position.set(Popover::resolved_corner(anchor, bounds));
state.update(cx, |state, _| {
state.trigger_bounds = bounds;
});
}
}),
);
if !open {
return root;
}
let popover_content =
Popover::render_popover_content(self.anchor, self.appearance, window, cx)
.overflow_hidden()
.on_hover(window.listener_for(&state, |state, hovered, _, cx| {
state.on_content_hover(*hovered, cx);
}))
.when_some(self.content, |this, content| {
this.child(state.update(cx, |state, cx| (content)(state, window, cx)))
})
.children(self.children)
.refine_style(&self.style);
root.child(Popover::render_popover(
self.anchor,
position,
popover_content,
window,
cx,
))
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::{ParentElement as _, TestAppContext, div, rgpui};
use super::{HoverCard, HoverCardState};
#[rgpui::test]
fn test_hover_card_state_initial_closed(cx: &mut TestAppContext) {
cx.update(|_cx| {
let state = HoverCardState::new(Duration::from_millis(100), Duration::from_millis(50));
assert!(!state.is_open());
});
}
#[rgpui::test]
fn test_hover_card_builder(cx: &mut TestAppContext) {
cx.update(|_cx| {
let card = HoverCard::new("hover")
.trigger(div().child("trigger"))
.content(|_, _, _| div().child("content"))
.open_delay(Duration::from_millis(200))
.close_delay(Duration::from_millis(100));
let _ = card;
});
}
}