use std::rc::Rc;
use gpui::{
Anchor, AnyElement, App, Context, ElementId, InteractiveElement as _, IntoElement,
ParentElement, RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
};
use gpui_base::HoverCard as BaseHoverCard;
pub use gpui_base::HoverCardState;
use instant::Duration;
use crate::{StyledExt as _, popover::Popover};
#[derive(IntoElement)]
pub struct HoverCard {
id: ElementId,
style: StyleRefinement,
trigger_style: Option<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(),
trigger_style: None,
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 trigger_style(mut self, style: StyleRefinement) -> Self {
self.trigger_style = Some(style);
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);
}
}
impl RenderOnce for HoverCard {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let Some(trigger) = self.trigger else {
return div().id("empty").into_any_element();
};
let anchor = self.anchor;
let appearance = self.appearance;
let content = self.content;
let children = self.children;
let style = self.style;
let trigger_style = self.trigger_style;
BaseHoverCard::new(self.id)
.anchor(anchor)
.open_delay(self.open_delay)
.close_delay(self.close_delay)
.trigger((trigger)(window, cx))
.content(move |state, window, cx| {
Popover::render_popover_content(anchor, appearance, window, cx)
.overflow_hidden()
.when_some(content, |this, content| {
this.child((content)(state, window, cx))
})
.children(children)
.refine_style(&style)
})
.when_some(self.on_open_change, |this, callback| {
this.on_open_change(move |open, window, cx| callback(open, window, cx))
})
.when_some(trigger_style, |this, style| this.refine_style(&style))
.into_any_element()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{button::Button, h_flex};
use gpui::{Context, Render, point, px};
struct TriggerStyleHarness {
styled: bool,
}
impl Render for TriggerStyleHarness {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
h_flex()
.absolute()
.left(px(100.))
.top(px(100.))
.w(px(200.))
.child(
HoverCard::new("trigger-style-hover-card")
.appearance(false)
.open_delay(Duration::ZERO)
.anchor(Anchor::TopRight)
.when(self.styled, |this| {
this.trigger_style(StyleRefinement::default().w_full())
})
.trigger(Button::new("styled-trigger").size(px(40.)))
.child(
div()
.debug_selector(|| "styled-content".into())
.size(px(20.)),
),
),
)
}
}
#[gpui::test]
fn trigger_style_is_applied_to_the_trigger_container(cx: &mut gpui::TestAppContext) {
cx.update(crate::init);
let (view, cx) = cx.add_window_view(|_, _| TriggerStyleHarness { styled: false });
cx.update(|window, cx| window.draw(cx).clear(cx));
cx.simulate_mouse_move(point(px(110.), px(110.)), None, Default::default());
cx.run_until_parked();
cx.update(|window, cx| {
window.draw(cx).clear(cx);
window.draw(cx).clear(cx);
});
assert_eq!(
cx.debug_bounds("styled-content").unwrap().top_right().x,
px(140.)
);
cx.update(|window, cx| {
view.update(cx, |view, cx| {
view.styled = true;
cx.notify();
});
window.draw(cx).clear(cx);
window.draw(cx).clear(cx);
});
assert_eq!(
cx.debug_bounds("styled-content").unwrap().top_right().x,
px(300.)
);
}
}