use std::time::Duration;
use gpui::prelude::*;
use gpui::{deferred, div, px, relative, AnyElement, App, Context, IntoElement, Task, Window};
use super::Placement;
use crate::theme::theme;
type Builder = Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
pub struct HoverCard {
open: bool,
trigger: Builder,
content: Builder,
placement: Placement,
width: Option<f32>,
open_delay: Duration,
close_delay: Duration,
over_trigger: bool,
over_card: bool,
pending: Option<Task<()>>,
}
impl HoverCard {
pub fn new(
_cx: &mut Context<Self>,
trigger: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
content: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
HoverCard {
open: false,
trigger: Box::new(trigger),
content: Box::new(content),
placement: Placement::Bottom,
width: None,
open_delay: Duration::from_millis(300),
close_delay: Duration::from_millis(150),
over_trigger: false,
over_card: false,
pending: None,
}
}
pub fn placement(mut self, placement: Placement) -> Self {
self.placement = placement;
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn open_delay(mut self, delay: Duration) -> Self {
self.open_delay = delay;
self
}
pub fn close_delay(mut self, delay: Duration) -> Self {
self.close_delay = delay;
self
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn close(&mut self, cx: &mut Context<Self>) {
self.pending = None;
self.over_trigger = false;
self.over_card = false;
self.open = false;
cx.notify();
}
fn hover_trigger(&mut self, hovered: bool, cx: &mut Context<Self>) {
self.over_trigger = hovered;
if hovered {
self.schedule_open(cx);
} else {
self.schedule_close(cx);
}
}
fn hover_card(&mut self, hovered: bool, cx: &mut Context<Self>) {
self.over_card = hovered;
if hovered {
self.pending = None;
} else {
self.schedule_close(cx);
}
}
fn schedule_open(&mut self, cx: &mut Context<Self>) {
if self.open {
self.pending = None;
return;
}
let delay = self.open_delay;
self.pending = Some(cx.spawn(async move |this, cx| {
cx.background_executor().timer(delay).await;
this.update(cx, |this, cx| {
if this.over_trigger && !this.open {
this.open = true;
cx.notify();
}
})
.ok();
}));
}
fn schedule_close(&mut self, cx: &mut Context<Self>) {
if !self.open {
self.pending = None;
return;
}
let delay = self.close_delay;
self.pending = Some(cx.spawn(async move |this, cx| {
cx.background_executor().timer(delay).await;
this.update(cx, |this, cx| {
if !this.over_trigger && !this.over_card && this.open {
this.open = false;
cx.notify();
}
})
.ok();
}));
}
}
impl Render for HoverCard {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let radius = t.radius(t.default_radius);
let surface = t.surface().hsla();
let border = t.border().hsla();
let trigger_el = (self.trigger)(window, cx);
let mut wrap = div().relative().child(
div()
.id("guise-hovercard-trigger")
.on_hover(cx.listener(|this, hovered: &bool, _window, cx| {
this.hover_trigger(*hovered, cx);
}))
.child(trigger_el),
);
if self.open {
let content_el = (self.content)(window, cx);
let base = div()
.id("guise-hovercard-card")
.on_hover(cx.listener(|this, hovered: &bool, _window, cx| {
this.hover_card(*hovered, cx);
}))
.absolute()
.flex()
.flex_col()
.p(px(12.0))
.rounded(px(radius))
.border_1()
.border_color(border)
.bg(surface)
.shadow_md()
.child(content_el);
let placed = match self.placement {
Placement::Bottom => base.top(relative(1.0)).mt(px(6.0)).left(px(0.0)),
Placement::BottomEnd => base.top(relative(1.0)).mt(px(6.0)).right(px(0.0)),
Placement::Top => base.bottom(relative(1.0)).mb(px(6.0)).left(px(0.0)),
Placement::TopEnd => base.bottom(relative(1.0)).mb(px(6.0)).right(px(0.0)),
};
let placed = match self.width {
Some(w) => placed.w(px(w)),
None => placed.min_w(px(180.0)),
};
wrap = wrap.child(deferred(placed));
}
wrap
}
}