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
 27trait ModalViewHandle {
 28    fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
 29    fn view(&self) -> AnyView;
 30    fn fade_out_background(&self, cx: &mut App) -> bool;
 31}
 32
 33impl<V: ModalView> ModalViewHandle for Entity<V> {
 34    fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision {
 35        self.update(cx, |this, cx| this.on_before_dismiss(window, cx))
 36    }
 37
 38    fn view(&self) -> AnyView {
 39        self.clone().into()
 40    }
 41
 42    fn fade_out_background(&self, cx: &mut App) -> bool {
 43        self.read(cx).fade_out_background()
 44    }
 45}
 46
 47pub struct ActiveModal {
 48    modal: Box<dyn ModalViewHandle>,
 49    _subscriptions: [Subscription; 2],
 50    previous_focus_handle: Option<FocusHandle>,
 51    focus_handle: FocusHandle,
 52}
 53
 54pub struct ModalLayer {
 55    active_modal: Option<ActiveModal>,
 56    dismiss_on_focus_lost: bool,
 57}
 58
 59pub(crate) struct ModalOpenedEvent;
 60
 61impl EventEmitter<ModalOpenedEvent> for ModalLayer {}
 62
 63impl Default for ModalLayer {
 64    fn default() -> Self {
 65        Self::new()
 66    }
 67}
 68
 69impl ModalLayer {
 70    pub fn new() -> Self {
 71        Self {
 72            active_modal: None,
 73            dismiss_on_focus_lost: false,
 74        }
 75    }
 76
 77    pub fn toggle_modal<V, B>(&mut self, window: &mut Window, cx: &mut Context<Self>, build_view: B)
 78    where
 79        V: ModalView,
 80        B: FnOnce(&mut Window, &mut Context<V>) -> V,
 81    {
 82        if let Some(active_modal) = &self.active_modal {
 83            let is_close = active_modal.modal.view().downcast::<V>().is_ok();
 84            let did_close = self.hide_modal(window, cx);
 85            if is_close || !did_close {
 86                return;
 87            }
 88        }
 89        let new_modal = cx.new(|cx| build_view(window, cx));
 90        self.show_modal(new_modal, window, cx);
 91        cx.emit(ModalOpenedEvent);
 92    }
 93
 94    fn show_modal<V>(&mut self, new_modal: Entity<V>, window: &mut Window, cx: &mut Context<Self>)
 95    where
 96        V: ModalView,
 97    {
 98        let focus_handle = cx.focus_handle();
 99        self.active_modal = Some(ActiveModal {
100            modal: Box::new(new_modal.clone()),
101            _subscriptions: [
102                cx.subscribe_in(
103                    &new_modal,
104                    window,
105                    |this, _, _: &DismissEvent, window, cx| {
106                        this.hide_modal(window, cx);
107                    },
108                ),
109                cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
110                    if this.dismiss_on_focus_lost {
111                        this.hide_modal(window, cx);
112                    }
113                }),
114            ],
115            previous_focus_handle: window.focused(cx),
116            focus_handle,
117        });
118        cx.defer_in(window, move |_, window, cx| {
119            window.focus(&new_modal.focus_handle(cx));
120        });
121        cx.notify();
122    }
123
124    pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
125        let Some(active_modal) = self.active_modal.as_mut() else {
126            self.dismiss_on_focus_lost = false;
127            return false;
128        };
129
130        match active_modal.modal.on_before_dismiss(window, cx) {
131            DismissDecision::Dismiss(dismiss) => {
132                self.dismiss_on_focus_lost = !dismiss;
133                if !dismiss {
134                    return false;
135                }
136            }
137            DismissDecision::Pending => {
138                self.dismiss_on_focus_lost = false;
139                return false;
140            }
141        }
142
143        if let Some(active_modal) = self.active_modal.take() {
144            if let Some(previous_focus) = active_modal.previous_focus_handle
145                && active_modal.focus_handle.contains_focused(window, cx)
146            {
147                previous_focus.focus(window);
148            }
149            cx.notify();
150        }
151        true
152    }
153
154    pub fn active_modal<V>(&self) -> Option<Entity<V>>
155    where
156        V: 'static,
157    {
158        let active_modal = self.active_modal.as_ref()?;
159        active_modal.modal.view().downcast::<V>().ok()
160    }
161
162    pub fn has_active_modal(&self) -> bool {
163        self.active_modal.is_some()
164    }
165}
166
167impl Render for ModalLayer {
168    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
169        let Some(active_modal) = &self.active_modal else {
170            return div();
171        };
172
173        div()
174            .occlude()
175            .absolute()
176            .size_full()
177            .top_0()
178            .left_0()
179            .when(active_modal.modal.fade_out_background(cx), |el| {
180                let mut background = cx.theme().colors().elevated_surface_background;
181                background.fade_out(0.2);
182                el.bg(background)
183            })
184            .on_mouse_down(
185                MouseButton::Left,
186                cx.listener(|this, _, window, cx| {
187                    this.hide_modal(window, cx);
188                }),
189            )
190            .child(
191                v_flex()
192                    .h(px(0.0))
193                    .top_20()
194                    .flex()
195                    .flex_col()
196                    .items_center()
197                    .track_focus(&active_modal.focus_handle)
198                    .child(
199                        h_flex()
200                            .occlude()
201                            .child(active_modal.modal.view())
202                            .on_mouse_down(MouseButton::Left, |_, _, cx| {
203                                cx.stop_propagation();
204                            }),
205                    ),
206            )
207    }
208}