moly_kit/widgets/
moly_modal.rs

1//! Copy of the original modal from the main Moly app which draws its content
2//! over the whole app (from its root).
3
4use makepad_widgets::*;
5
6script_mod! {
7    use mod.prelude.widgets.*
8
9    mod.widgets.MolyModalBase = #(MolyModal::register_widget(vm))
10    mod.widgets.MolyModal = set_type_default() do mod.widgets.MolyModalBase {
11        width: Fill
12        height: Fill
13        flow: Overlay
14        align: Align { x: 0.5, y: 0.5 }
15
16        draw_bg +: {
17            pixel: fn() -> vec4 {
18                return vec4(0. 0. 0. 0.0)
19            }
20        }
21
22        bg_view := View {
23            width: Fill
24            height: Fill
25            show_bg: true
26            draw_bg +: {
27                pixel: fn() -> vec4 {
28                    return vec4(0. 0. 0. 0.7)
29                }
30            }
31        }
32
33        content := View {
34            flow: Overlay
35            width: Fit
36            height: Fit
37        }
38    }
39}
40
41#[derive(Clone, Debug, Default)]
42pub enum MolyModalAction {
43    #[default]
44    None,
45    Dismissed,
46}
47
48#[derive(Clone, Copy, Debug)]
49enum PopupPlacement {
50    /// Position the popup at the given top-left coordinate, clamped to
51    /// screen bounds.
52    AtPosition(DVec2),
53    /// Position the popup above the given anchor point with a gap. The
54    /// anchor is the top-left of the reference widget (e.g., a button).
55    /// After the content is measured, `pos.y = anchor.y - content_height
56    /// - gap`.
57    Above { anchor: DVec2, gap: f64 },
58}
59
60#[derive(Script, Widget)]
61pub struct MolyModal {
62    #[source]
63    source: ScriptObjectRef,
64
65    #[deref]
66    view: View,
67
68    #[rust]
69    draw_list: Option<DrawList2d>,
70
71    #[live]
72    draw_bg: DrawQuad,
73
74    #[live(true)]
75    dismiss_on_focus_lost: bool,
76
77    #[rust]
78    opened: bool,
79
80    #[rust]
81    desired_popup_placement: Option<PopupPlacement>,
82}
83
84impl ScriptHook for MolyModal {
85    fn on_after_new(&mut self, vm: &mut ScriptVm) {
86        self.draw_list = Some(DrawList2d::script_new(vm));
87    }
88
89    fn on_after_apply(
90        &mut self,
91        vm: &mut ScriptVm,
92        _apply: &Apply,
93        _scope: &mut Scope,
94        _value: ScriptValue,
95    ) {
96        vm.with_cx_mut(|cx| {
97            if let Some(draw_list) = &self.draw_list {
98                draw_list.redraw(cx);
99            }
100        });
101    }
102}
103
104impl Widget for MolyModal {
105    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
106        if !self.opened {
107            return;
108        }
109
110        // When passing down events we need to suspend the sweep lock
111        // because regular View instances won't respond to events if
112        // the sweep lock is active.
113        cx.sweep_unlock(self.draw_bg.area());
114        let content = self.view.widget(cx, ids!(content));
115        content.handle_event(cx, event, scope);
116        cx.sweep_lock(self.draw_bg.area());
117
118        if self.dismiss_on_focus_lost {
119            let content_rec = content.area().rect(cx);
120            if let Hit::FingerUp(fe) =
121                event.hits_with_sweep_area(cx, self.draw_bg.area(), self.draw_bg.area())
122            {
123                if !content_rec.contains(fe.abs) {
124                    cx.widget_action(self.widget_uid(), MolyModalAction::Dismissed);
125                    self.close(cx);
126                }
127            }
128        }
129
130        self.ui_runner().handle(cx, event, scope, self);
131    }
132
133    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
134        let draw_list = self.draw_list.as_mut().unwrap();
135        draw_list.begin_overlay_reuse(cx);
136
137        cx.begin_root_turtle_for_pass(self.view.layout);
138        self.draw_bg.begin(cx, self.view.walk, self.view.layout);
139
140        if self.opened {
141            let bg_view = self.view.widget(cx, ids!(bg_view));
142            let _ = bg_view.draw_walk(cx, scope, walk.with_abs_pos(DVec2 { x: 0., y: 0. }));
143            let content = self.view.widget(cx, ids!(content));
144            content.draw_all(cx, scope);
145        }
146
147        self.draw_bg.end(cx);
148
149        cx.end_pass_sized_turtle();
150        self.draw_list.as_mut().unwrap().end(cx);
151
152        if let Some(placement) = self.desired_popup_placement.take() {
153            self.ui_runner().defer(move |me, cx, _| {
154                me.correct_popup_position(cx, placement);
155            });
156        }
157
158        DrawStep::done()
159    }
160}
161
162impl MolyModal {
163    #[deprecated(note = "Use open_as_dialog or open_as_popup instead")]
164    pub fn open(&mut self, cx: &mut Cx) {
165        self.opened = true;
166        self.draw_bg.redraw(cx);
167        cx.sweep_lock(self.draw_bg.area());
168    }
169
170    /// Opens the modal as a centered dialog.
171    pub fn open_as_dialog(&mut self, cx: &mut Cx) {
172        self.view.layout.align = Align { x: 0.5, y: 0.5 };
173
174        let mut content = self.view.widget(cx, ids!(content));
175        script_apply_eval!(cx, content, { margin: 0 });
176
177        let mut bg_view = self.view.widget(cx, ids!(bg_view));
178        script_apply_eval!(cx, bg_view, { visible: true });
179
180        #[allow(deprecated)]
181        self.open(cx);
182    }
183
184    /// Opens the modal as a bottom sheet anchored to the bottom of the screen.
185    pub fn open_as_bottom_sheet(&mut self, cx: &mut Cx) {
186        self.view.layout.align = Align { x: 0.0, y: 1.0 };
187
188        let mut content = self.view.widget(cx, ids!(content));
189        script_apply_eval!(cx, content, { margin: 0 });
190
191        let mut bg_view = self.view.widget(cx, ids!(bg_view));
192        script_apply_eval!(cx, bg_view, { visible: true });
193
194        #[allow(deprecated)]
195        self.open(cx);
196    }
197
198    /// Opens the modal as a popup at the given position.
199    pub fn open_as_popup(&mut self, cx: &mut Cx, pos: DVec2) {
200        self.desired_popup_placement = Some(PopupPlacement::AtPosition(pos));
201        self.open_popup_common(cx);
202    }
203
204    /// Opens the modal as a popup positioned above the given anchor
205    /// point. The anchor is typically the top-left of a button. After
206    /// the content is drawn and measured, the popup is placed so its
207    /// bottom edge is `gap` pixels above the anchor's y coordinate.
208    pub fn open_as_popup_above(&mut self, cx: &mut Cx, anchor: DVec2, gap: f64) {
209        self.desired_popup_placement = Some(PopupPlacement::Above { anchor, gap });
210        self.open_popup_common(cx);
211    }
212
213    fn open_popup_common(&mut self, cx: &mut Cx) {
214        self.view.layout.align = Align { x: 0.0, y: 0.0 };
215
216        let screen_size = cx.display_context.screen_size;
217        let margin = Inset {
218            left: screen_size.x,
219            top: screen_size.y,
220            right: 0.0,
221            bottom: 0.0,
222        };
223
224        let mut content = self.view.widget(cx, ids!(content));
225        script_apply_eval!(cx, content, { margin: #(margin) });
226
227        let mut bg_view = self.view.widget(cx, ids!(bg_view));
228        script_apply_eval!(cx, bg_view, { visible: false });
229
230        #[allow(deprecated)]
231        self.open(cx);
232    }
233
234    /// Closes the modal.
235    pub fn close(&mut self, cx: &mut Cx) {
236        self.opened = false;
237        self.draw_bg.redraw(cx);
238        cx.sweep_unlock(self.draw_bg.area())
239    }
240
241    /// Returns whether this modal was dismissed by the given
242    /// actions.
243    pub fn dismissed(&self, actions: &Actions) -> bool {
244        matches!(
245            actions.find_widget_action(self.widget_uid()).cast(),
246            MolyModalAction::Dismissed
247        )
248    }
249
250    /// Returns whether this modal is currently open.
251    pub fn is_open(&self) -> bool {
252        self.opened
253    }
254
255    fn correct_popup_position(&mut self, cx: &mut Cx, placement: PopupPlacement) {
256        let content = self.view.widget(cx, ids!(content));
257        let content_size = content.area().rect(cx).size;
258        let screen_size = cx.display_context.screen_size;
259
260        let pos = match placement {
261            PopupPlacement::AtPosition(pos) => pos,
262            PopupPlacement::Above { anchor, gap } => DVec2 {
263                x: anchor.x,
264                y: anchor.y - content_size.y - gap,
265            },
266        };
267
268        let pos_x = if pos.x + content_size.x > screen_size.x {
269            screen_size.x - content_size.x - 10.0
270        } else {
271            pos.x
272        };
273
274        let pos_y = if pos.y + content_size.y > screen_size.y {
275            screen_size.y - content_size.y - 10.0
276        } else if pos.y < 0.0 {
277            10.0
278        } else {
279            pos.y
280        };
281
282        let margin = Inset {
283            left: pos_x,
284            top: pos_y,
285            right: 0.0,
286            bottom: 0.0,
287        };
288        let mut content = self.view.widget(cx, ids!(content));
289        script_apply_eval!(cx, content, { margin: #(margin) });
290
291        self.redraw(cx);
292    }
293}
294
295impl MolyModalRef {
296    #[deprecated(note = "Use open_as_dialog or open_as_popup instead")]
297    pub fn open(&self, cx: &mut Cx) {
298        if let Some(mut inner) = self.borrow_mut() {
299            #[allow(deprecated)]
300            inner.open(cx);
301        }
302    }
303
304    /// Opens the modal as a centered dialog.
305    pub fn open_as_dialog(&self, cx: &mut Cx) {
306        if let Some(mut inner) = self.borrow_mut() {
307            inner.open_as_dialog(cx);
308        }
309    }
310
311    /// Opens the modal as a bottom sheet anchored to the bottom of the screen.
312    pub fn open_as_bottom_sheet(&self, cx: &mut Cx) {
313        if let Some(mut inner) = self.borrow_mut() {
314            inner.open_as_bottom_sheet(cx);
315        }
316    }
317
318    /// Opens the modal as a popup at the given position.
319    pub fn open_as_popup(&self, cx: &mut Cx, pos: DVec2) {
320        if let Some(mut inner) = self.borrow_mut() {
321            inner.open_as_popup(cx, pos);
322        }
323    }
324
325    /// Opens the modal as a popup positioned above the given anchor.
326    pub fn open_as_popup_above(&self, cx: &mut Cx, anchor: DVec2, gap: f64) {
327        if let Some(mut inner) = self.borrow_mut() {
328            inner.open_as_popup_above(cx, anchor, gap);
329        }
330    }
331
332    /// Closes the modal.
333    pub fn close(&self, cx: &mut Cx) {
334        if let Some(mut inner) = self.borrow_mut() {
335            inner.close(cx);
336        }
337    }
338
339    /// Returns whether this modal was dismissed by the given
340    /// actions.
341    pub fn dismissed(&self, actions: &Actions) -> bool {
342        if let Some(inner) = self.borrow() {
343            inner.dismissed(actions)
344        } else {
345            false
346        }
347    }
348
349    /// Returns whether this modal is currently open.
350    pub fn is_open(&self) -> bool {
351        self.borrow().map_or(false, |inner| inner.is_open())
352    }
353}