use crate::elements::icon_button::icon_button;
use crate::icons::Icons;
use crate::theme::{ActiveTheme, Themeable};
use gpui::{
actions, deferred, div, prelude::*, px, AnyElement, App, Context, DismissEvent, ElementId,
EventEmitter, FocusHandle, Focusable, IntoElement, KeyBinding, ParentElement, Render,
SharedString, Styled, Window,
};
use std::rc::Rc;
actions!(dialog, [Close]);
pub const DIALOG_CONTEXT: &str = "Dialog";
pub struct DialogOpened;
pub struct DialogClosed;
pub struct Dialog {
id: ElementId,
title: Option<SharedString>,
description: Option<SharedString>,
content: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
footer: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
close_on_escape: bool,
close_on_backdrop_click: bool,
show_close_button: bool,
}
pub fn dialog(id: impl Into<ElementId>) -> Dialog {
Dialog::new(id)
}
impl Dialog {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
title: None,
description: None,
content: None,
footer: None,
close_on_escape: true,
close_on_backdrop_click: true,
show_close_button: true,
}
}
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 content(
mut self,
content: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.content = Some(Rc::new(content));
self
}
pub fn footer(
mut self,
footer: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.footer = Some(Rc::new(footer));
self
}
pub fn close_on_escape(mut self, close: bool) -> Self {
self.close_on_escape = close;
self
}
pub fn close_on_backdrop_click(mut self, close: bool) -> Self {
self.close_on_backdrop_click = close;
self
}
pub fn show_close_button(mut self, show: bool) -> Self {
self.show_close_button = show;
self
}
}
pub struct DialogState {
id: ElementId,
title: Option<SharedString>,
description: Option<SharedString>,
content: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
footer: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
close_on_escape: bool,
close_on_backdrop_click: bool,
show_close_button: bool,
is_open: bool,
focus_handle: Option<FocusHandle>,
}
impl EventEmitter<DialogOpened> for DialogState {}
impl EventEmitter<DialogClosed> for DialogState {}
impl EventEmitter<DismissEvent> for DialogState {}
impl DialogState {
pub fn new(dialog: Dialog) -> Self {
Self {
id: dialog.id,
title: dialog.title,
description: dialog.description,
content: dialog.content,
footer: dialog.footer,
close_on_escape: dialog.close_on_escape,
close_on_backdrop_click: dialog.close_on_backdrop_click,
show_close_button: dialog.show_close_button,
is_open: false,
focus_handle: None,
}
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.is_open {
return;
}
self.is_open = true;
let focus_handle = cx.focus_handle();
window.focus(&focus_handle, cx);
self.focus_handle = Some(focus_handle);
cx.emit(DialogOpened);
cx.notify();
}
pub fn close(&mut self, cx: &mut Context<Self>) {
if !self.is_open {
return;
}
self.is_open = false;
self.focus_handle = None;
cx.emit(DialogClosed);
cx.emit(DismissEvent);
cx.notify();
}
pub fn toggle(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.is_open {
self.close(cx);
} else {
self.open(window, cx);
}
}
fn handle_close(&mut self, _: &Close, _window: &mut Window, cx: &mut Context<Self>) {
if self.close_on_escape {
self.close(cx);
}
}
}
impl Focusable for DialogState {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.focus_handle
.clone()
.unwrap_or_else(|| cx.focus_handle())
}
}
impl Render for DialogState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
if !self.is_open {
return div().into_any_element();
}
let focus_handle = self.focus_handle.clone();
let close_on_backdrop_click = self.close_on_backdrop_click;
let show_close_button = self.show_close_button;
let title = self.title.clone();
let description = self.description.clone();
let content = self.content.clone();
let footer = self.footer.clone();
let backdrop_color = theme.overlay();
let surface_color = theme.surface();
let border_color = theme.border();
let fg_color = theme.fg();
let fg_muted_color = theme.fg_muted();
deferred(
div()
.id(self.id.clone())
.key_context(DIALOG_CONTEXT)
.when_some(focus_handle, |this, handle| {
this.track_focus(&handle)
.on_action(cx.listener(Self::handle_close))
})
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.size_full()
.flex()
.items_center()
.justify_center()
.bg(backdrop_color)
.when(close_on_backdrop_click, |this| {
this.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|this, _event, _window, cx| {
this.close(cx);
}),
)
})
.child(
div()
.id("dialog-panel")
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
.min_w(px(320.))
.max_w(px(500.))
.bg(surface_color)
.border_1()
.border_color(border_color)
.rounded_lg()
.shadow_xl()
.flex()
.flex_col()
.when(title.is_some() || show_close_button, |this| {
this.child(
div()
.flex()
.items_start()
.justify_between()
.p_4()
.when(
description.is_some()
|| content.is_some()
|| footer.is_some(),
|this| this.pb_0(),
)
.when_some(title.clone(), |this, title| {
this.child(
div()
.flex_1()
.text_base()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(fg_color)
.child(title),
)
})
.when(show_close_button, |this| {
this.child(
icon_button("dialog-close", Icons::cross_1())
.on_click(cx.listener(|this, _, _window, cx| {
this.close(cx);
})),
)
}),
)
})
.when_some(description, |this, desc| {
this.child(
div()
.px_4()
.pt_2()
.when(content.is_none() && footer.is_none(), |this| {
this.pb_4()
})
.text_sm()
.text_color(fg_muted_color)
.child(desc),
)
})
.when_some(content, |this, content| {
this.child(
div()
.px_4()
.pt_2()
.when(footer.is_none(), |this| this.pb_4())
.child(content(window, cx)),
)
})
.when_some(footer, |this, footer| {
this.child(
div()
.flex()
.justify_end()
.gap_2()
.p_4()
.child(footer(window, cx)),
)
}),
),
)
.with_priority(10)
.into_any_element()
}
}
pub fn bind_dialog_keys(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Close, Some(DIALOG_CONTEXT))]);
}