use std::rc::Rc;
use gpui::{
Anchor, AnyElement, App, ClickEvent, Div, ElementId, IntoElement, Pixels, Point, RenderOnce,
Stateful, Window, div, prelude::*, px,
};
use gpui_kit_theme::{Elevation, Layer, Theme};
use crate::foundation::{ActiveTheme, Ident, StyledExt};
type DismissHandler = Rc<dyn Fn(&mut Window, &mut App)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Edge {
Left,
Right,
Top,
Bottom,
}
impl Edge {
pub fn is_horizontal(self) -> bool {
matches!(self, Self::Left | Self::Right)
}
pub fn is_leading(self) -> bool {
matches!(self, Self::Left | Self::Top)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Placement {
Below,
Above,
At(Point<Pixels>),
Center,
Edge(Edge),
}
#[derive(IntoElement)]
pub struct Overlay {
ident: Ident,
layer: Layer,
placement: Placement,
window_snap_margin: Option<Pixels>,
scrim: bool,
content: Option<AnyElement>,
on_dismiss: Option<DismissHandler>,
}
impl std::fmt::Debug for Overlay {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Overlay")
.field("ident", &self.ident)
.field("layer", &self.layer)
.field("placement", &self.placement)
.field("window_snap_margin", &self.window_snap_margin)
.field("scrim", &self.scrim)
.field("dismissible", &self.on_dismiss.is_some())
.finish()
}
}
impl Overlay {
pub fn new(ident: impl Into<Ident>) -> Self {
Self {
ident: ident.into(),
layer: Layer::Popover,
placement: Placement::Below,
window_snap_margin: None,
scrim: false,
content: None,
on_dismiss: None,
}
}
pub fn modal(ident: impl Into<Ident>) -> Self {
Self::new(ident)
.layer(Layer::Modal)
.placement(Placement::Center)
.scrim(true)
}
pub fn edge(ident: impl Into<Ident>, edge: Edge) -> Self {
Self::new(ident)
.layer(Layer::Modal)
.placement(Placement::Edge(edge))
.scrim(true)
}
pub fn layer(mut self, layer: Layer) -> Self {
self.layer = layer;
self
}
pub fn placement(mut self, placement: Placement) -> Self {
self.placement = placement;
self
}
pub(crate) fn window_snap_margin(mut self, margin: Pixels) -> Self {
self.window_snap_margin = Some(margin);
self
}
pub fn scrim(mut self, scrim: bool) -> Self {
self.scrim = scrim;
self
}
pub fn child(mut self, content: impl IntoElement) -> Self {
self.content = Some(content.into_any_element());
self
}
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler));
self
}
fn anchor(&self) -> Anchor {
match self.placement {
Placement::Above => Anchor::BottomLeft,
_ => Anchor::TopLeft,
}
}
}
impl RenderOnce for Overlay {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let viewport = window.viewport_size();
let element_id: ElementId = self.ident.element_id();
let anchor = self.anchor();
let content = self.content.unwrap_or_else(|| div().into_any_element());
let surface = div()
.id(element_id)
.occlude()
.child(content)
.into_any_element();
let mut anchored = gpui::anchored().anchor(anchor);
if let Placement::At(position) = self.placement {
anchored = anchored.position(position);
}
if let Some(margin) = self.window_snap_margin {
anchored = anchored.snap_to_window_with_margin(margin);
} else if self.placement == Placement::Center {
anchored = anchored.snap_to_window_with_margin(px(theme.spacing.sm));
}
let placed = match self.placement {
Placement::Center => scrim_frame(&theme, viewport, self.scrim, self.on_dismiss.clone())
.items_center()
.justify_center()
.child(surface)
.into_any_element(),
Placement::Edge(edge) => {
scrim_frame(&theme, viewport, self.scrim, self.on_dismiss.clone())
.map(|frame| {
if edge.is_horizontal() {
frame.flex_row()
} else {
frame.flex_col()
}
})
.map(|frame| {
if edge.is_leading() {
frame.justify_start()
} else {
frame.justify_end()
}
})
.child(surface)
.into_any_element()
}
_ if self.scrim => scrim_frame(&theme, viewport, true, self.on_dismiss.clone())
.child(anchored.child(surface))
.into_any_element(),
_ => anchored.child(surface).into_any_element(),
};
pinned(
gpui::deferred(placed)
.priority(priority(&theme, self.layer))
.into_any_element(),
)
}
}
pub fn surface(theme: &Theme, elevation: Elevation) -> Div {
div()
.column()
.bg(theme.colors.overlay)
.radius(theme, gpui_kit_theme::Radius::Card)
.elevation(theme, elevation)
.overflow_hidden()
.text_color(theme.colors.text)
}
pub fn priority(theme: &Theme, layer: Layer) -> usize {
theme.layer(layer).max(0) as usize
}
fn scrim_frame(
theme: &Theme,
viewport: gpui::Size<Pixels>,
visible: bool,
on_dismiss: Option<DismissHandler>,
) -> Stateful<Div> {
let mut frame = div()
.id("overlay.scrim")
.occlude()
.absolute()
.top_0()
.left_0()
.w(viewport.width)
.h(viewport.height)
.flex();
if visible {
frame = frame.bg(gpui::black().opacity(theme.opacity.scrim));
}
if let Some(handler) = on_dismiss {
frame = frame.on_click(move |_: &ClickEvent, window, cx| handler(window, cx));
}
frame
}
pub(crate) fn pinned(layer: AnyElement) -> AnyElement {
div()
.absolute()
.top_0()
.left_0()
.size_0()
.child(layer)
.into_any_element()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layers_paint_in_token_order() {
let theme = Theme::studio_dark();
assert!(priority(&theme, Layer::Tooltip) > priority(&theme, Layer::Popover));
assert!(priority(&theme, Layer::Toast) > priority(&theme, Layer::Modal));
assert_eq!(priority(&theme, Layer::Content), 0);
}
#[test]
fn a_modal_defaults_to_a_centered_scrimmed_dialog() {
let overlay = Overlay::modal("confirm");
assert_eq!(overlay.layer, Layer::Modal);
assert_eq!(overlay.placement, Placement::Center);
assert!(overlay.scrim);
}
#[test]
fn placement_decides_which_edge_the_surface_hangs_from() {
assert_eq!(
Overlay::new("menu").placement(Placement::Above).anchor(),
Anchor::BottomLeft
);
assert_eq!(
Overlay::new("menu").placement(Placement::Below).anchor(),
Anchor::TopLeft
);
}
}