1use 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 AtPosition(DVec2),
53 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn close(&self, cx: &mut Cx) {
334 if let Some(mut inner) = self.borrow_mut() {
335 inner.close(cx);
336 }
337 }
338
339 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 pub fn is_open(&self) -> bool {
351 self.borrow().map_or(false, |inner| inner.is_open())
352 }
353}