use std::{cell::Cell, rc::Rc};
use gpui_kit::{
AnyElement, App, Bounds, ElementId, FocusHandle, InteractiveElement as _, IntoElement,
ParentElement as _, RenderOnce, StyleRefinement, Styled, Window,
base::{self, Align, Placement, Selectable, StyledExt as _, TestSupportExt as _},
div,
};
use crate::{
ActiveTheme as _,
menu::{MenuLook, MenuMotion, corner, measure, placement},
};
type TriggerBuilder = Box<dyn FnOnce(bool) -> AnyElement>;
type ContentBuilder = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
type OpenChange = Box<dyn Fn(bool, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct Popover {
id: ElementId,
trigger: Option<TriggerBuilder>,
content: Option<ContentBuilder>,
align: Align,
open: Option<bool>,
on_open_change: Option<OpenChange>,
focus: Option<FocusHandle>,
style: StyleRefinement,
}
impl Popover {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
trigger: None,
content: None,
align: Align::Start,
open: None,
on_open_change: None,
focus: None,
style: StyleRefinement::default(),
}
}
pub fn trigger(mut self, trigger: impl Selectable + IntoElement + 'static) -> Self {
self.trigger = Some(Box::new(move |open| trigger.open(open).into_any_element()));
self
}
pub fn content<E: IntoElement>(
mut self,
content: impl FnOnce(&mut Window, &mut App) -> E + 'static,
) -> Self {
self.content = Some(Box::new(move |window, cx| {
content(window, cx).into_any_element()
}));
self
}
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
pub fn open(mut self, open: bool) -> Self {
self.open = Some(open);
self
}
pub fn on_open_change(
mut self,
handler: impl Fn(bool, &mut Window, &mut App) + 'static,
) -> Self {
self.on_open_change = Some(Box::new(handler));
self
}
pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
self.focus = Some(focus.clone());
self
}
}
impl Styled for Popover {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Popover {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let look = MenuLook::of(cx.theme(), window.rem_size());
let trigger_bounds = window
.use_keyed_state(
ElementId::NamedChild(self.id.clone().into(), "trigger-bounds".into()),
cx,
|_, _| Rc::new(Cell::new(Bounds::default())),
)
.read(cx)
.clone();
let side = placement(
trigger_bounds.get(),
window.viewport_size().height,
look.max_height,
look.gap,
);
let opens_above = side == Placement::Top;
let (anchor, _) = corner(side, self.align, trigger_bounds.get(), look.gap);
let panel_id = ElementId::NamedChild(self.id.clone().into(), "panel".into());
let content = self.content;
let style = self.style;
let mut popover = base::Popover::new(self.id)
.anchor(anchor)
.offset(look.gap)
.trigger_with(move |open, _, _| {
div()
.relative()
.child(measure(move |bounds| trigger_bounds.set(bounds)))
.children(self.trigger.map(|trigger| trigger(open)))
.into_any_element()
})
.content(move |_, window, cx| {
let Some(motion) = MenuMotion::sample(
ElementId::NamedChild(panel_id.clone().into(), "presence".into()),
true,
true,
&look,
window,
cx,
) else {
return div().into_any_element();
};
div()
.id(panel_id)
.test_support()
.relative()
.top(if opens_above {
-motion.offset
} else {
motion.offset
})
.opacity(motion.opacity)
.p(look.padding)
.rounded(look.radius)
.bg(look.surface)
.border_1()
.border_color(look.border)
.shadow(look.shadow(motion.shadow_strength))
.text_size(look.text_size)
.line_height(look.line_height)
.text_color(look.foreground)
.refine_style(&style)
.children(content.map(|content| content(window, cx)))
.into_any_element()
});
if let Some(open) = self.open {
popover = popover.open(open);
}
if let Some(handler) = self.on_open_change {
popover = popover.on_open_change(move |open, window, cx| handler(*open, window, cx));
}
if let Some(focus) = &self.focus {
popover = popover.track_focus(focus);
}
popover
}
}