use gpui::prelude::FluentBuilder;
use gpui::AppContext;
use gpui::{
Animation, AnimationExt, ClickEvent, ElementId, Hsla, InteractiveElement, IntoElement,
Bounds, ParentElement, Pixels, RenderOnce, Styled, div, px,
};
use crate::{animation::constants::duration, theme::ActiveTheme};
use crate::i18n::{I18n, TextDirection};
use crate::component::BoundsTrackerElement;
use crate::animation::ease_out_quint_clamped;
fn desired_menu_left(
trigger_bounds: Bounds<Pixels>,
menu_width: Pixels,
direction: TextDirection,
window: &gpui::Window,
) -> Pixels {
let desired_left = match direction {
TextDirection::Ltr => trigger_bounds.left(),
TextDirection::Rtl => trigger_bounds.right() - menu_width,
};
let window_bounds = window.bounds();
let min_left = window_bounds.left();
let max_left = (window_bounds.right() - menu_width).max(min_left);
desired_left.clamp(min_left, max_left)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PopoverPlacement {
BottomStart,
BottomEnd,
}
pub fn popover(id: impl Into<ElementId>) -> Popover {
Popover::new(id)
}
type CloseFn = Box<dyn Fn(&mut gpui::Window, &mut gpui::App)>;
#[derive(IntoElement)]
pub struct Popover {
element_id: ElementId,
base: gpui::Div,
open: bool,
placement: PopoverPlacement,
width: Option<gpui::Pixels>,
trigger: Option<gpui::AnyElement>,
content: Option<gpui::AnyElement>,
bg: Option<Hsla>,
border: Option<Hsla>,
on_close: Option<CloseFn>,
}
impl Default for Popover {
fn default() -> Self {
Self::new("ui:popover")
}
}
impl Popover {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
element_id: id.into(),
base: div(),
open: false,
placement: PopoverPlacement::BottomStart,
width: None,
trigger: None,
content: None,
bg: None,
border: None,
on_close: None,
}
}
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.element_id = id.into();
self
}
pub fn element_id(&self) -> &ElementId {
&self.element_id
}
pub fn child_id(&self, suffix: &str) -> ElementId {
(self.element_id.clone(), suffix.to_string()).into()
}
pub fn key(self, key: impl Into<ElementId>) -> Self {
self.id(key)
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn placement(mut self, placement: PopoverPlacement) -> Self {
self.placement = placement;
self
}
pub fn width(mut self, width: gpui::Pixels) -> Self {
self.width = Some(width);
self
}
pub fn bg(mut self, color: impl Into<Hsla>) -> Self {
self.bg = Some(color.into());
self
}
pub fn border(mut self, color: impl Into<Hsla>) -> Self {
self.border = Some(color.into());
self
}
pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
self.trigger = Some(trigger.into_any_element());
self
}
pub fn content(mut self, content: impl IntoElement) -> Self {
self.content = Some(content.into_any_element());
self
}
pub fn on_close<F>(mut self, f: F) -> Self
where
F: 'static + Fn(&mut gpui::Window, &mut gpui::App),
{
self.on_close = Some(Box::new(f));
self
}
}
impl ParentElement for Popover {
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.base.extend(elements);
}
}
impl Styled for Popover {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl RenderOnce for Popover {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
let element_id = self.element_id;
let id = element_id.clone();
let trigger_bounds_state = cx.new(|_| Bounds::<Pixels>::default());
let theme = cx.theme();
let bg = self.bg.unwrap_or(theme.surface.raised);
let border = self.border.unwrap_or(theme.border.default);
let is_open = self.open;
let placement = self.placement;
let width = self.width;
let on_close = self.on_close;
let trigger = self.trigger.unwrap_or_else(|| div().into_any_element());
let content = self.content.unwrap_or_else(|| div().into_any_element());
let trigger = self.base
.id(element_id)
.relative()
.child(BoundsTrackerElement {
bounds_state: trigger_bounds_state.clone(),
inner: trigger.into_any_element(),
})
.when(is_open, move |this| {
let direction = cx
.try_global::<I18n>()
.map(|i18n| i18n.text_direction())
.unwrap_or(TextDirection::Ltr);
let menu_width_px = width.unwrap_or(px(260.));
let trigger_bounds = *trigger_bounds_state.read(cx);
let menu_left = desired_menu_left(trigger_bounds, menu_width_px, direction, _window);
let relative_left = menu_left - trigger_bounds.left();
let menu = div()
.id((id.clone(), "ui:popover:menu"))
.absolute()
.when(placement == PopoverPlacement::BottomStart, |this| {
this.top_full().left_0()
})
.when(placement == PopoverPlacement::BottomEnd, |this| {
this.top_full().left_0()
})
.when(relative_left != Pixels::ZERO, |this| this.left(relative_left))
.mt(px(10.))
.rounded_md()
.overflow_hidden()
.border_1()
.border_color(border)
.bg(bg)
.shadow_md()
.py_1()
.w(menu_width_px)
.occlude()
.on_mouse_down_out(move |_ev, window, cx| {
if let Some(on_close) = &on_close {
on_close(window, cx);
}
})
.child(content);
let animated = menu.with_animation(
format!("ui:popover:menu:{}", is_open),
Animation::new(duration::MENU_OPEN).with_easing(ease_out_quint_clamped),
|this, value| this.opacity(value).mt(px(10.0 - 6.0 * value)),
);
this.child(gpui::deferred(animated).with_priority(100))
});
trigger
}
}
#[allow(dead_code)]
fn _click_passthrough(_ev: &ClickEvent) {}