1use std::rc::Rc;
28
29use gpui::prelude::*;
30use gpui::{
31 div, AnyElement, App, Context, Entity, FocusHandle, IntoElement, KeyDownEvent, SharedString,
32 Window,
33};
34
35use crate::devtools::Probed;
36use crate::feedback::ToastStack;
37use crate::theme::ColorName;
38
39pub type ModalCloser = Rc<dyn Fn(&mut Window, &mut App) + 'static>;
42
43type ModalBuilder = Rc<dyn Fn(ModalCloser, &mut Window, &mut App) -> AnyElement + 'static>;
44
45struct ModalEntry {
46 id: usize,
47 builder: ModalBuilder,
48 previous_focus: Option<FocusHandle>,
49}
50
51pub struct OverlayHost {
54 modals: Vec<ModalEntry>,
55 toasts: Entity<ToastStack>,
56 next_id: usize,
57}
58
59impl OverlayHost {
60 pub fn new(cx: &mut Context<Self>) -> Self {
61 OverlayHost {
62 modals: Vec::new(),
63 toasts: cx.new(|_| ToastStack::new()),
64 next_id: 0,
65 }
66 }
67
68 pub fn toast_stack(&self) -> Entity<ToastStack> {
70 self.toasts.clone()
71 }
72
73 pub fn toast(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
75 let message = message.into();
76 self.toasts.update(cx, |toasts, cx| {
77 toasts.push(message, cx);
78 });
79 }
80
81 pub fn toast_titled(
83 &mut self,
84 title: impl Into<SharedString>,
85 message: impl Into<SharedString>,
86 color: ColorName,
87 cx: &mut Context<Self>,
88 ) {
89 let (title, message) = (title.into(), message.into());
90 self.toasts.update(cx, |toasts, cx| {
91 toasts.push_titled(title, message, color, cx);
92 });
93 }
94
95 pub fn open_modal<E>(
103 &mut self,
104 window: &mut Window,
105 cx: &mut Context<Self>,
106 builder: impl Fn(ModalCloser, &mut Window, &mut App) -> E + 'static,
107 ) -> usize
108 where
109 E: IntoElement,
110 {
111 let id = self.next_id;
112 self.next_id += 1;
113 self.modals.push(ModalEntry {
114 id,
115 builder: Rc::new(move |close, window, cx| builder(close, window, cx).into_any_element()),
116 previous_focus: window.focused(cx),
117 });
118 cx.notify();
119 id
120 }
121
122 pub fn close_modal(&mut self, id: usize, window: &mut Window, cx: &mut Context<Self>) {
124 let Some(index) = self.modals.iter().position(|m| m.id == id) else {
125 return;
126 };
127 let entry = self.modals.remove(index);
128 if let Some(focus) = entry.previous_focus {
129 window.focus(&focus);
130 }
131 cx.notify();
132 }
133
134 pub fn close_top(&mut self, window: &mut Window, cx: &mut Context<Self>) {
136 if let Some(id) = self.modals.last().map(|m| m.id) {
137 self.close_modal(id, window, cx);
138 }
139 }
140
141 pub fn modal_count(&self) -> usize {
142 self.modals.len()
143 }
144
145 fn closer(&self, id: usize, cx: &mut Context<Self>) -> ModalCloser {
146 let host = cx.entity().downgrade();
147 Rc::new(move |window, cx| {
148 host
149 .update(cx, |host, cx| host.close_modal(id, window, cx))
150 .ok();
151 })
152 }
153}
154
155impl Render for OverlayHost {
156 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
157 let mut root = div().child(self.toasts.clone());
158
159 let top_id = self.modals.last().map(|m| m.id);
160 let entries: Vec<(usize, ModalBuilder)> = self
161 .modals
162 .iter()
163 .map(|m| (m.id, m.builder.clone()))
164 .collect();
165
166 for (id, builder) in entries {
167 let close = self.closer(id, cx);
168 let content = builder(close, window, cx);
169 let is_top = top_id == Some(id);
170 root = root.child(
171 div()
172 .on_key_down(cx.listener(move |this, event: &KeyDownEvent, window, cx| {
173 if is_top && event.keystroke.key == "escape" {
174 this.close_modal(id, window, cx);
175 cx.stop_propagation();
176 }
177 }))
178 .child(content),
179 );
180 }
181 root.probe("OverlayHost")
182 }
183}