use std::rc::Rc;
use gpui_kit::{
AnyElement, App, ElementId, Entity, FocusHandle, FontWeight, InteractiveElement as _,
IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window,
assets::IconName,
base::{self, StyledExt as _, TestSupportExt as _, actions::Cancel, h_flex, v_flex},
div,
};
use crate::{
ActiveTheme as _, Button, ButtonSize, Icon,
menu::{MenuLook, MenuMotion},
};
type OpenChange = Rc<dyn Fn(bool, &mut Window, &mut App)>;
struct Focus {
surface: FocusHandle,
previous: Option<FocusHandle>,
opened: bool,
}
#[derive(IntoElement)]
pub struct Dialog {
id: ElementId,
open: bool,
title: Option<SharedString>,
description: Option<SharedString>,
children: Vec<AnyElement>,
footer: Option<AnyElement>,
show_close_button: bool,
on_open_change: Option<OpenChange>,
focus: Option<FocusHandle>,
top: Option<Pixels>,
style: StyleRefinement,
}
impl Dialog {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
open: false,
title: None,
description: None,
children: Vec::new(),
footer: None,
show_close_button: true,
on_open_change: None,
focus: None,
top: None,
style: StyleRefinement::default(),
}
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn on_open_change(
mut self,
handler: impl Fn(bool, &mut Window, &mut App) + 'static,
) -> Self {
self.on_open_change = Some(Rc::new(handler));
self
}
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn footer(mut self, footer: impl IntoElement) -> Self {
self.footer = Some(footer.into_any_element());
self
}
pub fn show_close_button(mut self, show: bool) -> Self {
self.show_close_button = show;
self
}
pub(crate) fn top(mut self, offset: Pixels) -> Self {
self.top = Some(offset);
self
}
pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
self.focus = Some(focus.clone());
self
}
}
impl Styled for Dialog {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl ParentElement for Dialog {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for Dialog {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let child = |name: &'static str| ElementId::NamedChild(self.id.clone().into(), name.into());
let focus: Entity<Focus> = window.use_keyed_state(child("focus"), cx, |_, cx| Focus {
surface: cx.focus_handle(),
previous: None,
opened: false,
});
let surface = focus.read(cx).surface.clone();
move_focus(&focus, self.open, self.focus.as_ref(), window, cx);
let theme = cx.theme();
let look = MenuLook::of(theme, window.rem_size());
let overlay = theme.dialog_overlay;
let width = theme.metrics.dialog_width;
let padding = theme.metrics.dialog_padding;
let title_style = theme.base.typography.lg;
let Some(motion) =
MenuMotion::sample(child("presence"), self.open, false, &look, window, cx)
else {
return div().into_any_element();
};
let close = self.show_close_button.then(|| {
let target = surface.clone();
div().absolute().top_3().right_3().child(
Button::new(child("close"))
.ghost()
.size(ButtonSize::Sm)
.icon(Icon::from(IconName::Close))
.accessibility_label("Close")
.on_click(move |_, window, cx| {
target.dispatch_action(&Cancel, window, cx);
}),
)
});
let header = (self.title.is_some() || self.description.is_some()).then(|| {
v_flex()
.gap_2()
.children(self.title.map(|title| {
div()
.text_size(title_style.size)
.line_height(title_style.line_height)
.font_weight(FontWeight::SEMIBOLD)
.child(title)
}))
.children(
self.description
.map(|text| div().text_color(look.muted_foreground).child(text)),
)
});
let popup = v_flex()
.id(child("popup"))
.test_support()
.relative()
.occlude()
.w(width)
.max_w_full()
.gap_4()
.p(padding)
.rounded(look.radius)
.bg(look.surface)
.border_1()
.border_color(look.border)
.shadow(look.shadow(motion.shadow_strength))
.opacity(motion.opacity)
.text_size(look.text_size)
.line_height(look.line_height)
.text_color(look.foreground)
.refine_style(&self.style)
.children(close)
.children(header)
.children(self.children)
.children(
self.footer
.map(|footer| h_flex().justify_end().gap_2().child(footer)),
);
let on_open_change = self.on_open_change;
let mut dialog = base::Dialog::new(cx)
.focus_handle(surface)
.open(true)
.backdrop(
div()
.id(child("backdrop"))
.test_support()
.absolute()
.size_full()
.bg(overlay.opacity(motion.opacity)),
)
.popup(popup)
.p_4();
if let Some(offset) = self.top {
dialog = dialog.items_start().pt(offset);
}
if let Some(handler) = on_open_change {
dialog = dialog.on_open_change(move |open, _, window, cx| handler(open, window, cx));
}
dialog.into_any_element()
}
}
fn move_focus(
focus: &Entity<Focus>,
open: bool,
target: Option<&FocusHandle>,
window: &mut Window,
cx: &mut App,
) {
let (surface, previous, opened) = {
let focus = focus.read(cx);
(focus.surface.clone(), focus.previous.clone(), focus.opened)
};
if open && !opened {
let before = window.focused(cx);
target.unwrap_or(&surface).focus(window, cx);
focus.update(cx, |focus, _| {
focus.previous = before;
focus.opened = true;
});
} else if !open && opened {
if surface.contains_focused(window, cx)
&& let Some(previous) = previous
{
previous.focus(window, cx);
}
focus.update(cx, |focus, _| {
focus.previous = None;
focus.opened = false;
});
}
}