use std::rc::Rc;
use gpui::prelude::*;
use gpui::{
div, AnyElement, App, Context, Entity, FocusHandle, IntoElement, KeyDownEvent, SharedString,
Window,
};
use crate::feedback::ToastStack;
use crate::theme::ColorName;
pub type ModalCloser = Rc<dyn Fn(&mut Window, &mut App) + 'static>;
type ModalBuilder = Rc<dyn Fn(ModalCloser, &mut Window, &mut App) -> AnyElement + 'static>;
struct ModalEntry {
id: usize,
builder: ModalBuilder,
previous_focus: Option<FocusHandle>,
}
pub struct OverlayHost {
modals: Vec<ModalEntry>,
toasts: Entity<ToastStack>,
next_id: usize,
}
impl OverlayHost {
pub fn new(cx: &mut Context<Self>) -> Self {
OverlayHost {
modals: Vec::new(),
toasts: cx.new(|_| ToastStack::new()),
next_id: 0,
}
}
pub fn toast_stack(&self) -> Entity<ToastStack> {
self.toasts.clone()
}
pub fn toast(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
let message = message.into();
self.toasts.update(cx, |toasts, cx| {
toasts.push(message, cx);
});
}
pub fn toast_titled(
&mut self,
title: impl Into<SharedString>,
message: impl Into<SharedString>,
color: ColorName,
cx: &mut Context<Self>,
) {
let (title, message) = (title.into(), message.into());
self.toasts.update(cx, |toasts, cx| {
toasts.push_titled(title, message, color, cx);
});
}
pub fn open_modal<E>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
builder: impl Fn(ModalCloser, &mut Window, &mut App) -> E + 'static,
) -> usize
where
E: IntoElement,
{
let id = self.next_id;
self.next_id += 1;
self.modals.push(ModalEntry {
id,
builder: Rc::new(move |close, window, cx| {
builder(close, window, cx).into_any_element()
}),
previous_focus: window.focused(cx),
});
cx.notify();
id
}
pub fn close_modal(&mut self, id: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(index) = self.modals.iter().position(|m| m.id == id) else {
return;
};
let entry = self.modals.remove(index);
if let Some(focus) = entry.previous_focus {
window.focus(&focus);
}
cx.notify();
}
pub fn close_top(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(id) = self.modals.last().map(|m| m.id) {
self.close_modal(id, window, cx);
}
}
pub fn modal_count(&self) -> usize {
self.modals.len()
}
fn closer(&self, id: usize, cx: &mut Context<Self>) -> ModalCloser {
let host = cx.entity().downgrade();
Rc::new(move |window, cx| {
host.update(cx, |host, cx| host.close_modal(id, window, cx))
.ok();
})
}
}
impl Render for OverlayHost {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mut root = div().child(self.toasts.clone());
let top_id = self.modals.last().map(|m| m.id);
let entries: Vec<(usize, ModalBuilder)> = self
.modals
.iter()
.map(|m| (m.id, m.builder.clone()))
.collect();
for (id, builder) in entries {
let close = self.closer(id, cx);
let content = builder(close, window, cx);
let is_top = top_id == Some(id);
root = root.child(
div()
.on_key_down(cx.listener(move |this, event: &KeyDownEvent, window, cx| {
if is_top && event.keystroke.key == "escape" {
this.close_modal(id, window, cx);
cx.stop_propagation();
}
}))
.child(content),
);
}
root
}
}