use crate::element_id::for_entity;
use crate::theme::{ActiveTheme, Themeable};
use gpui::{
AnyElement, App, Context, DismissEvent, ElementId, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Pixels, Point, Render, Styled, Window, anchored,
deferred, div, point, prelude::*, px,
};
use std::rc::Rc;
pub type RenderCallback = Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>;
pub struct PopoverChanged {
pub open: bool,
}
pub struct PopoverPanel {
content_render: RenderCallback,
focus_handle: FocusHandle,
}
impl EventEmitter<DismissEvent> for PopoverPanel {}
impl Focusable for PopoverPanel {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl PopoverPanel {
pub fn build(
content_render: RenderCallback,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
window.focus(&focus_handle, cx);
Self {
content_render,
focus_handle,
}
})
}
fn dismiss(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl Render for PopoverPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let theme = cx.theme();
let surface_color = theme.surface();
let border_color = theme.border();
let content = (self.content_render)(window, cx);
div()
.id(for_entity("popover-panel", cx.entity_id()))
.track_focus(&focus_handle)
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.dismiss(window, cx);
}))
.on_key_down(cx.listener(|this, event: &gpui::KeyDownEvent, window, cx| {
if event.keystroke.key == "escape" {
this.dismiss(window, cx);
}
}))
.flex()
.flex_col()
.bg(surface_color)
.border_1()
.border_color(border_color)
.rounded_md()
.shadow_lg()
.overflow_hidden()
.child(content)
}
}
pub struct Popover {
id: ElementId,
trigger_render: Option<RenderCallback>,
content_render: Option<RenderCallback>,
offset: Point<Pixels>,
}
pub fn popover(id: impl Into<ElementId>) -> Popover {
Popover::new(id)
}
impl Popover {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
trigger_render: None,
content_render: None,
offset: point(px(0.), px(4.)),
}
}
pub fn trigger(
mut self,
render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.trigger_render = Some(Rc::new(render));
self
}
pub fn content(
mut self,
render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.content_render = Some(Rc::new(render));
self
}
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
self.offset = offset;
self
}
}
pub struct PopoverState {
id: ElementId,
trigger_render: Option<RenderCallback>,
content_render: Option<RenderCallback>,
offset: Point<Pixels>,
open: bool,
panel: Option<Entity<PopoverPanel>>,
}
impl EventEmitter<PopoverChanged> for PopoverState {}
impl PopoverState {
pub fn new(popover: Popover) -> Self {
Self {
id: popover.id,
trigger_render: popover.trigger_render,
content_render: popover.content_render,
offset: popover.offset,
open: false,
panel: None,
}
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.open {
return;
}
self.show_panel(window, cx);
}
pub fn close(&mut self, cx: &mut Context<Self>) {
if !self.open {
return;
}
self.open = false;
self.panel = None;
cx.emit(PopoverChanged { open: false });
cx.notify();
}
pub fn toggle(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.open {
self.close(cx);
} else {
self.show_panel(window, cx);
}
}
fn show_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(content_render) = self.content_render.clone() else {
return;
};
let panel = PopoverPanel::build(content_render, window, cx);
cx.subscribe_in(
&panel,
window,
|this, _, _event: &DismissEvent, _window, cx| {
this.open = false;
this.panel = None;
cx.emit(PopoverChanged { open: false });
cx.notify();
},
)
.detach();
self.panel = Some(panel);
self.open = true;
cx.emit(PopoverChanged { open: true });
cx.notify();
}
}
impl Render for PopoverState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let trigger_element = self
.trigger_render
.as_ref()
.map(|render| render(window, cx));
let offset = self.offset;
div()
.relative()
.child(
div()
.id(self.id.clone())
.cursor_pointer()
.on_click(cx.listener(|this, _, window, cx| {
this.toggle(window, cx);
}))
.when_some(trigger_element, |this, trigger| this.child(trigger)),
)
.when_some(self.panel.clone(), |this, panel| {
this.child(
deferred(
anchored()
.offset(offset)
.child(div().occlude().child(panel)),
)
.with_priority(1),
)
})
}
}