modal_layer.rs

  1use gpui::{
  2    AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView,
  3    MouseButton, Subscription,
  4};
  5use ui::prelude::*;
  6
  7#[derive(Debug)]
  8pub enum DismissDecision {
  9    Dismiss(bool),
 10    Pending,
 11}
 12
 13pub trait ModalView: ManagedView {
 14    fn on_before_dismiss(
 15        &mut self,
 16        _window: &mut Window,
 17        _: &mut Context<Self>,
 18    ) -> DismissDecision {
 19        DismissDecision::Dismiss(true)
 20    }
 21
 22    fn fade_out_background(&self) -> bool {
 23        false
 24    }
 25
 26    fn render_bare(&self) -> bool {
 27        false
 28    }
 29}
 30
 31trait ModalViewHandle {
 32    fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
 33    fn view(&self) -> AnyView;
 34    fn fade_out_background(&self, cx: &mut App) -> bool;
 35    fn render_bare(&self, cx: &mut App) -> bool;
 36}
 37
 38impl<V: ModalView> ModalViewHandle for Entity<V> {
 39    fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision {
 40        self.update(cx, |this, cx| this.on_before_dismiss(window, cx))
 41    }
 42
 43    fn view(&self) -> AnyView {
 44        self.clone().into()
 45    }
 46
 47    fn fade_out_background(&self, cx: &mut App) -> bool {
 48        self.read(cx).fade_out_background()
 49    }
 50
 51    fn render_bare(&self, cx: &mut App) -> bool {
 52        self.read(cx).render_bare()
 53    }
 54}
 55
 56pub struct ActiveModal {
 57    modal: Box<dyn ModalViewHandle>,
 58    _subscriptions: [Subscription; 2],
 59    previous_focus_handle: Option<FocusHandle>,
 60    focus_handle: FocusHandle,
 61}
 62
 63pub struct ModalLayer {
 64    active_modal: Option<ActiveModal>,
 65    dismiss_on_focus_lost: bool,
 66}
 67
 68pub(crate) struct ModalOpenedEvent;
 69
 70impl EventEmitter<ModalOpenedEvent> for ModalLayer {}
 71
 72impl Default for ModalLayer {
 73    fn default() -> Self {
 74        Self::new()
 75    }
 76}
 77
 78impl ModalLayer {
 79    pub fn new() -> Self {
 80        Self {
 81            active_modal: None,
 82            dismiss_on_focus_lost: false,
 83        }
 84    }
 85
 86    /// Toggles a modal of type `V`. If a modal of the same type is currently active,
 87    /// it will be hidden. If a different modal is active, it will be replaced with the new one.
 88    /// If no modal is active, the new modal will be shown.
 89    ///
 90    /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
 91    /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
 92    /// will not be shown.
 93    pub fn toggle_modal<V, B>(&mut self, window: &mut Window, cx: &mut Context<Self>, build_view: B)
 94    where
 95        V: ModalView,
 96        B: FnOnce(&mut Window, &mut Context<V>) -> V,
 97    {
 98        if let Some(active_modal) = &self.active_modal {
 99            let should_close = active_modal.modal.view().downcast::<V>().is_ok();
100            let did_close = self.hide_modal(window, cx);
101            if should_close || !did_close {
102                return;
103            }
104        }
105        let new_modal = cx.new(|cx| build_view(window, cx));
106        self.show_modal(new_modal, window, cx);
107        cx.emit(ModalOpenedEvent);
108    }
109
110    /// Shows a modal and sets up subscriptions for dismiss events and focus tracking.
111    /// The modal is automatically focused after being shown.
112    fn show_modal<V>(&mut self, new_modal: Entity<V>, window: &mut Window, cx: &mut Context<Self>)
113    where
114        V: ModalView,
115    {
116        let focus_handle = cx.focus_handle();
117        self.active_modal = Some(ActiveModal {
118            modal: Box::new(new_modal.clone()),
119            _subscriptions: [
120                cx.subscribe_in(
121                    &new_modal,
122                    window,
123                    |this, _, _: &DismissEvent, window, cx| {
124                        this.hide_modal(window, cx);
125                    },
126                ),
127                cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
128                    if this.dismiss_on_focus_lost {
129                        this.hide_modal(window, cx);
130                    }
131                }),
132            ],
133            previous_focus_handle: window.focused(cx),
134            focus_handle,
135        });
136        cx.defer_in(window, move |_, window, cx| {
137            window.focus(&new_modal.focus_handle(cx), cx);
138        });
139        cx.notify();
140    }
141
142    /// Attempts to hide the currently active modal.
143    ///
144    /// The modal's `on_before_dismiss` method is called to determine if dismissal should proceed.
145    /// If dismissal is allowed, the modal is removed and focus is restored to the previously
146    /// focused element.
147    ///
148    /// Returns `true` if the modal was successfully hidden, `false` otherwise.
149    pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
150        let Some(active_modal) = self.active_modal.as_mut() else {
151            self.dismiss_on_focus_lost = false;
152            return false;
153        };
154
155        match active_modal.modal.on_before_dismiss(window, cx) {
156            DismissDecision::Dismiss(should_dismiss) => {
157                if !should_dismiss {
158                    self.dismiss_on_focus_lost = !should_dismiss;
159                    return false;
160                }
161            }
162            DismissDecision::Pending => {
163                self.dismiss_on_focus_lost = false;
164                return false;
165            }
166        }
167
168        if let Some(active_modal) = self.active_modal.take() {
169            if let Some(previous_focus) = active_modal.previous_focus_handle
170                && active_modal.focus_handle.contains_focused(window, cx)
171            {
172                previous_focus.focus(window, cx);
173            }
174            cx.notify();
175        }
176        self.dismiss_on_focus_lost = false;
177        true
178    }
179
180    /// Returns the currently active modal if it is of type `V`.
181    pub fn active_modal<V>(&self) -> Option<Entity<V>>
182    where
183        V: 'static,
184    {
185        let active_modal = self.active_modal.as_ref()?;
186        active_modal.modal.view().downcast::<V>().ok()
187    }
188
189    pub fn has_active_modal(&self) -> bool {
190        self.active_modal.is_some()
191    }
192}
193
194impl Render for ModalLayer {
195    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
196        let Some(active_modal) = &self.active_modal else {
197            return div().into_any_element();
198        };
199
200        if active_modal.modal.render_bare(cx) {
201            return active_modal.modal.view().into_any_element();
202        }
203
204        div()
205            .absolute()
206            .size_full()
207            .inset_0()
208            .occlude()
209            .when(active_modal.modal.fade_out_background(cx), |this| {
210                let mut background = cx.theme().colors().elevated_surface_background;
211                background.fade_out(0.2);
212                this.bg(background)
213            })
214            .on_mouse_down(
215                MouseButton::Left,
216                cx.listener(|this, _, window, cx| {
217                    this.hide_modal(window, cx);
218                }),
219            )
220            .child(
221                v_flex()
222                    .h(px(0.0))
223                    .top_20()
224                    .items_center()
225                    .track_focus(&active_modal.focus_handle)
226                    .child(
227                        h_flex()
228                            .occlude()
229                            .child(active_modal.modal.view())
230                            .on_mouse_down(MouseButton::Left, |_, _, cx| {
231                                cx.stop_propagation();
232                            }),
233                    ),
234            )
235            .into_any_element()
236    }
237}