modal_layer.rs

  1use gpui::{
  2    AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView,
  3    MouseButton, Pixels, Point, Subscription,
  4};
  5use ui::prelude::*;
  6
  7#[derive(Debug, Clone, Copy, Default)]
  8pub enum ModalPlacement {
  9    #[default]
 10    Centered,
 11    Anchored {
 12        position: Point<Pixels>,
 13    },
 14}
 15
 16#[derive(Debug)]
 17pub enum DismissDecision {
 18    Dismiss(bool),
 19    Pending,
 20}
 21
 22pub trait ModalView: ManagedView {
 23    fn on_before_dismiss(
 24        &mut self,
 25        _window: &mut Window,
 26        _: &mut Context<Self>,
 27    ) -> DismissDecision {
 28        DismissDecision::Dismiss(true)
 29    }
 30
 31    fn fade_out_background(&self) -> bool {
 32        false
 33    }
 34
 35    fn render_bare(&self) -> bool {
 36        false
 37    }
 38}
 39
 40trait ModalViewHandle {
 41    fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
 42    fn view(&self) -> AnyView;
 43    fn fade_out_background(&self, cx: &mut App) -> bool;
 44    fn render_bare(&self, cx: &mut App) -> bool;
 45}
 46
 47impl<V: ModalView> ModalViewHandle for Entity<V> {
 48    fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision {
 49        self.update(cx, |this, cx| this.on_before_dismiss(window, cx))
 50    }
 51
 52    fn view(&self) -> AnyView {
 53        self.clone().into()
 54    }
 55
 56    fn fade_out_background(&self, cx: &mut App) -> bool {
 57        self.read(cx).fade_out_background()
 58    }
 59
 60    fn render_bare(&self, cx: &mut App) -> bool {
 61        self.read(cx).render_bare()
 62    }
 63}
 64
 65pub struct ActiveModal {
 66    modal: Box<dyn ModalViewHandle>,
 67    _subscriptions: [Subscription; 2],
 68    previous_focus_handle: Option<FocusHandle>,
 69    focus_handle: FocusHandle,
 70    placement: ModalPlacement,
 71}
 72
 73pub struct ModalLayer {
 74    active_modal: Option<ActiveModal>,
 75    dismiss_on_focus_lost: bool,
 76}
 77
 78pub(crate) struct ModalOpenedEvent;
 79
 80impl EventEmitter<ModalOpenedEvent> for ModalLayer {}
 81
 82impl Default for ModalLayer {
 83    fn default() -> Self {
 84        Self::new()
 85    }
 86}
 87
 88impl ModalLayer {
 89    pub fn new() -> Self {
 90        Self {
 91            active_modal: None,
 92            dismiss_on_focus_lost: false,
 93        }
 94    }
 95
 96    pub fn toggle_modal<V, B>(&mut self, window: &mut Window, cx: &mut Context<Self>, build_view: B)
 97    where
 98        V: ModalView,
 99        B: FnOnce(&mut Window, &mut Context<V>) -> V,
100    {
101        self.toggle_modal_with_placement(window, cx, ModalPlacement::Centered, build_view);
102    }
103
104    pub fn toggle_modal_with_placement<V, B>(
105        &mut self,
106        window: &mut Window,
107        cx: &mut Context<Self>,
108        placement: ModalPlacement,
109        build_view: B,
110    ) where
111        V: ModalView,
112        B: FnOnce(&mut Window, &mut Context<V>) -> V,
113    {
114        if let Some(active_modal) = &self.active_modal {
115            let is_close = active_modal.modal.view().downcast::<V>().is_ok();
116            let did_close = self.hide_modal(window, cx);
117            if is_close || !did_close {
118                return;
119            }
120        }
121        let new_modal = cx.new(|cx| build_view(window, cx));
122        self.show_modal(new_modal, placement, window, cx);
123        cx.emit(ModalOpenedEvent);
124    }
125
126    fn show_modal<V>(
127        &mut self,
128        new_modal: Entity<V>,
129        placement: ModalPlacement,
130        window: &mut Window,
131        cx: &mut Context<Self>,
132    ) where
133        V: ModalView,
134    {
135        let focus_handle = cx.focus_handle();
136        self.active_modal = Some(ActiveModal {
137            modal: Box::new(new_modal.clone()),
138            _subscriptions: [
139                cx.subscribe_in(
140                    &new_modal,
141                    window,
142                    |this, _, _: &DismissEvent, window, cx| {
143                        this.hide_modal(window, cx);
144                    },
145                ),
146                cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
147                    if this.dismiss_on_focus_lost {
148                        this.hide_modal(window, cx);
149                    }
150                }),
151            ],
152            previous_focus_handle: window.focused(cx),
153            focus_handle,
154            placement,
155        });
156        cx.defer_in(window, move |_, window, cx| {
157            window.focus(&new_modal.focus_handle(cx), cx);
158        });
159        cx.notify();
160    }
161
162    pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
163        let Some(active_modal) = self.active_modal.as_mut() else {
164            self.dismiss_on_focus_lost = false;
165            return false;
166        };
167
168        match active_modal.modal.on_before_dismiss(window, cx) {
169            DismissDecision::Dismiss(dismiss) => {
170                self.dismiss_on_focus_lost = !dismiss;
171                if !dismiss {
172                    return false;
173                }
174            }
175            DismissDecision::Pending => {
176                self.dismiss_on_focus_lost = false;
177                return false;
178            }
179        }
180
181        if let Some(active_modal) = self.active_modal.take() {
182            if let Some(previous_focus) = active_modal.previous_focus_handle
183                && active_modal.focus_handle.contains_focused(window, cx)
184            {
185                previous_focus.focus(window, cx);
186            }
187            cx.notify();
188        }
189        true
190    }
191
192    pub fn active_modal<V>(&self) -> Option<Entity<V>>
193    where
194        V: 'static,
195    {
196        let active_modal = self.active_modal.as_ref()?;
197        active_modal.modal.view().downcast::<V>().ok()
198    }
199
200    pub fn has_active_modal(&self) -> bool {
201        self.active_modal.is_some()
202    }
203}
204
205impl Render for ModalLayer {
206    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
207        let Some(active_modal) = &self.active_modal else {
208            return div().into_any_element();
209        };
210
211        if active_modal.modal.render_bare(cx) {
212            return active_modal.modal.view().into_any_element();
213        }
214
215        let content = h_flex()
216            .occlude()
217            .child(active_modal.modal.view())
218            .on_mouse_down(MouseButton::Left, |_, _, cx| {
219                cx.stop_propagation();
220            });
221
222        let positioned = match active_modal.placement {
223            ModalPlacement::Centered => v_flex()
224                .h(px(0.0))
225                .top_20()
226                .items_center()
227                .track_focus(&active_modal.focus_handle)
228                .child(content)
229                .into_any_element(),
230            ModalPlacement::Anchored { position } => div()
231                .absolute()
232                .left(position.x)
233                .top(position.y - px(20.))
234                .track_focus(&active_modal.focus_handle)
235                .child(content)
236                .into_any_element(),
237        };
238
239        div()
240            .absolute()
241            .size_full()
242            .inset_0()
243            .occlude()
244            .when(active_modal.modal.fade_out_background(cx), |this| {
245                let mut background = cx.theme().colors().elevated_surface_background;
246                background.fade_out(0.2);
247                this.bg(background)
248            })
249            .on_mouse_down(
250                MouseButton::Left,
251                cx.listener(|this, _, window, cx| {
252                    this.hide_modal(window, cx);
253                }),
254            )
255            .child(positioned)
256            .into_any_element()
257    }
258}