1use std::time::Duration;
4
5use zng_ext_input::focus::{DirectionalNav, FOCUS_CHANGED_EVENT, FocusScopeOnFocus, TabNav};
6use zng_ext_window::WINDOWS;
7use zng_wgt::{modal_included, prelude::*};
8use zng_wgt_container::Container;
9use zng_wgt_fill::background_color;
10use zng_wgt_filter::drop_shadow;
11use zng_wgt_input::focus::{
12 FocusClickBehavior, FocusableMix, alt_focus_scope, directional_nav, focus_click_behavior, focus_scope_behavior, tab_nav,
13};
14use zng_wgt_style::{Style, StyleMix, impl_style_fn};
15
16use crate::{AnchorMode, AnchorOffset, LAYERS, LayerIndex};
17
18#[widget($crate::popup::Popup {
35 ($child:expr) => {
36 child = $child;
37 }
38})]
39pub struct Popup(FocusableMix<StyleMix<Container>>);
40impl Popup {
41 fn widget_intrinsic(&mut self) {
42 self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
43
44 widget_set! {
45 self;
46
47 alt_focus_scope = true;
48 directional_nav = DirectionalNav::Cycle;
49 tab_nav = TabNav::Cycle;
50 focus_scope_behavior = FocusScopeOnFocus::FirstDescendantIgnoreBounds;
51 focus_click_behavior = FocusClickBehavior::menu_item();
52 focus_on_init = true;
53 modal_included = POPUP.anchor_id();
54 }
55 }
56
57 widget_impl! {
58 pub focus_click_behavior(behavior: impl IntoVar<FocusClickBehavior>);
64 }
65}
66impl_style_fn!(Popup, DefaultStyle);
67
68context_var! {
69 pub static CLOSE_ON_FOCUS_LEAVE_VAR: bool = true;
73
74 pub static ANCHOR_MODE_VAR: AnchorMode = AnchorMode::popup(AnchorOffset::out_bottom());
78
79 pub static CONTEXT_CAPTURE_VAR: ContextCapture = ContextCapture::default();
81}
82
83#[property(CONTEXT, default(CLOSE_ON_FOCUS_LEAVE_VAR))]
91pub fn close_on_focus_leave(child: impl IntoUiNode, close: impl IntoVar<bool>) -> UiNode {
92 with_context_var(child, CLOSE_ON_FOCUS_LEAVE_VAR, close)
93}
94
95#[property(CONTEXT, default(ANCHOR_MODE_VAR))]
101pub fn anchor_mode(child: impl IntoUiNode, mode: impl IntoVar<AnchorMode>) -> UiNode {
102 with_context_var(child, ANCHOR_MODE_VAR, mode)
103}
104
105#[property(CONTEXT, default(CONTEXT_CAPTURE_VAR))]
116pub fn context_capture(child: impl IntoUiNode, capture: impl IntoVar<ContextCapture>) -> UiNode {
117 with_context_var(child, CONTEXT_CAPTURE_VAR, capture)
118}
119
120pub struct POPUP;
122impl POPUP {
123 pub fn open(&self, popup: impl IntoUiNode) -> Var<PopupState> {
128 self.open_impl(popup.into_node(), ANCHOR_MODE_VAR.into(), CONTEXT_CAPTURE_VAR.get())
129 }
130
131 pub fn open_config(
136 &self,
137 popup: impl IntoUiNode,
138 anchor_mode: impl IntoVar<AnchorMode>,
139 context_capture: impl IntoValue<ContextCapture>,
140 ) -> Var<PopupState> {
141 self.open_impl(popup.into_node(), anchor_mode.into_var(), context_capture.into())
142 }
143
144 fn open_impl(&self, mut popup: UiNode, anchor_mode: Var<AnchorMode>, context_capture: ContextCapture) -> Var<PopupState> {
145 let state = var(PopupState::Opening);
146 let mut _close_handle = CommandHandle::dummy();
147
148 let anchor_id = WIDGET.id();
149
150 popup = match_widget(
151 popup,
152 clmv!(state, |c, op| match op {
153 UiNodeOp::Init => {
154 c.init();
155
156 if let Some(mut wgt) = c.node().as_widget() {
157 wgt.with_context(WidgetUpdateMode::Bubble, || {
158 let id = WIDGET.id();
159 WIDGET.sub_event_when(&FOCUS_CHANGED_EVENT, move |args| args.is_focus_leave(id));
160 });
161 let id = wgt.id();
162 state.set(PopupState::Open(id));
163 _close_handle = POPUP_CLOSE_CMD.scoped(id).subscribe(true);
164 } else {
165 c.deinit();
171
172 let not_widget = std::mem::replace(c.node(), UiNode::nil());
173 let not_widget = match_node(not_widget, |c, op| match op {
174 UiNodeOp::Init => {
175 WIDGET.sub_event(&FOCUS_CHANGED_EVENT).sub_event(&POPUP_CLOSE_REQUESTED_EVENT);
176 }
177 UiNodeOp::Update { .. } => {
178 POPUP_CLOSE_REQUESTED_EVENT.latest_update(false, |args| {
179 if let Some(mut now_is_widget) = c.node().as_widget() {
180 let now_is_widget = now_is_widget.with_context(WidgetUpdateMode::Ignore, || WIDGET.info().path());
181 if !args.is_in_target(now_is_widget.widget_id()) {
182 POPUP_CLOSE_REQUESTED_EVENT.notify(PopupCloseRequestedArgs::new(
184 args.timestamp,
185 args.propagation.clone(),
186 now_is_widget,
187 ));
188 }
189 }
190 });
191 }
192 _ => {}
193 });
194
195 *c.node() = not_widget.into_widget();
196
197 c.init();
198 let id = c.node().as_widget().unwrap().id();
199
200 state.set(PopupState::Open(id));
201 _close_handle = POPUP_CLOSE_CMD.scoped(id).subscribe(true);
202 }
203 }
204 UiNodeOp::Deinit => {
205 state.set(PopupState::Closed);
206 _close_handle = CommandHandle::dummy();
207 }
208 UiNodeOp::Update { .. } => {
209 c.node().as_widget().unwrap().with_context(WidgetUpdateMode::Bubble, || {
210 let id = WIDGET.id();
211
212 FOCUS_CHANGED_EVENT.each_update(true, |args| {
213 if args.is_focus_leave(id) && CLOSE_ON_FOCUS_LEAVE_VAR.get() {
214 POPUP.close_id(id);
215 }
216 });
217 POPUP_CLOSE_CMD
218 .scoped(id)
219 .latest_update(true, false, |args| match args.param::<PopupCloseMode>() {
220 Some(s) => match s {
221 PopupCloseMode::Request => POPUP.close_id(id),
222 PopupCloseMode::Force => LAYERS.remove(id),
223 },
224 None => POPUP.close_id(id),
225 });
226 });
227 }
228 _ => {}
229 }),
230 );
231
232 let (filter, over) = match context_capture {
233 ContextCapture::NoCapture => {
234 let filter = CaptureFilter::Include({
235 let mut set = ContextValueSet::new();
236 set.insert(&CLOSE_ON_FOCUS_LEAVE_VAR);
237 set.insert(&ANCHOR_MODE_VAR);
238 set.insert(&CONTEXT_CAPTURE_VAR);
239 set
240 });
241 (filter, false)
242 }
243 ContextCapture::CaptureBlend { filter, over } => (filter, over),
244 };
245 if filter != CaptureFilter::None {
246 popup = with_context_blend(LocalContext::capture_filtered(filter), over, popup);
247 }
248 LAYERS.insert_anchored(LayerIndex::TOP_MOST, anchor_id, anchor_mode, popup);
249
250 state.read_only()
251 }
252
253 pub fn close(&self, state: &Var<PopupState>) {
257 match state.get() {
258 PopupState::Opening => state
259 .hook(|a| {
260 if let PopupState::Open(id) = a.downcast_value::<PopupState>().unwrap() {
261 POPUP_CLOSE_CMD.scoped(*id).notify_param(PopupCloseMode::Request);
262 }
263 false
264 })
265 .perm(),
266 PopupState::Open(id) => self.close_id(id),
267 PopupState::Closed => {}
268 }
269 }
270
271 pub fn force_close(&self, state: &Var<PopupState>) {
273 match state.get() {
274 PopupState::Opening => state
275 .hook(|a| {
276 if let PopupState::Open(id) = a.downcast_value::<PopupState>().unwrap() {
277 POPUP_CLOSE_CMD.scoped(*id).notify_param(PopupCloseMode::Force);
278 }
279 false
280 })
281 .perm(),
282 PopupState::Open(id) => self.force_close_id(id),
283 PopupState::Closed => {}
284 }
285 }
286
287 pub fn close_id(&self, widget_id: WidgetId) {
293 setup_popup_close_service();
294 if let Some(wgt) = WINDOWS.widget_info(widget_id) {
295 POPUP_CLOSE_REQUESTED_EVENT.notify(PopupCloseRequestedArgs::now(wgt.path()));
296 } else {
297 tracing::debug!("cannot close {widget_id}, not found");
298 }
299 }
300
301 pub fn force_close_id(&self, widget_id: WidgetId) {
303 POPUP_CLOSE_CMD.scoped(widget_id).notify_param(PopupCloseMode::Force);
304 }
305
306 pub fn anchor_id(&self) -> Var<WidgetId> {
308 LAYERS.anchor_id().map(|id| id.expect("POPUP layers are always anchored"))
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq)]
314pub enum PopupState {
315 Opening,
317 Open(WidgetId),
319 Closed,
321}
322
323#[widget($crate::popup::DefaultStyle)]
325pub struct DefaultStyle(Style);
326impl DefaultStyle {
327 fn widget_intrinsic(&mut self) {
328 widget_set! {
329 self;
330
331 replace = true;
332
333 background_color = light_dark(rgb(0.9, 0.9, 0.9), rgb(0.1, 0.1, 0.1));
335 drop_shadow = {
336 offset: 2,
337 blur_radius: 2,
338 color: colors::BLACK.with_alpha(50.pct()),
339 };
340 }
341 }
342}
343
344#[derive(Clone, PartialEq, Eq, Debug)]
351pub enum ContextCapture {
352 NoCapture,
358 CaptureBlend {
362 filter: CaptureFilter,
364
365 over: bool,
369 },
370}
371impl Default for ContextCapture {
372 fn default() -> Self {
374 Self::CaptureBlend {
375 filter: CaptureFilter::context_vars(),
376 over: true,
377 }
378 }
379}
380impl_from_and_into_var! {
381 fn from(capture_vars_blend_over: bool) -> ContextCapture {
382 if capture_vars_blend_over {
383 ContextCapture::CaptureBlend {
384 filter: CaptureFilter::ContextVars {
385 exclude: ContextValueSet::new(),
386 },
387 over: true,
388 }
389 } else {
390 ContextCapture::NoCapture
391 }
392 }
393
394 fn from(filter_over: CaptureFilter) -> ContextCapture {
395 ContextCapture::CaptureBlend {
396 filter: filter_over,
397 over: true,
398 }
399 }
400}
401
402event_args! {
403 pub struct PopupCloseRequestedArgs {
405 pub popup: WidgetPath,
407
408 ..
409
410 fn is_in_target(&self, id: WidgetId) -> bool {
414 self.popup.contains(id)
415 }
416 }
417}
418
419event! {
420 pub static POPUP_CLOSE_REQUESTED_EVENT: PopupCloseRequestedArgs;
426}
427event_property! {
428 #[property(EVENT)]
434 pub fn on_popup_close_requested<on_pre_popup_close_requested>(
435 child: impl IntoUiNode,
436 handler: Handler<PopupCloseRequestedArgs>,
437 ) -> UiNode {
438 const PRE: bool;
439 EventNodeBuilder::new(POPUP_CLOSE_REQUESTED_EVENT).build::<PRE>(child, handler)
440 }
441}
442
443command! {
444 pub static POPUP_CLOSE_CMD;
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
457pub enum PopupCloseMode {
458 #[default]
462 Request,
463 Force,
467}
468
469fn setup_popup_close_service() {
470 app_local! {
471 static POPUP_SETUP: bool = false;
472 }
473
474 if !std::mem::replace(&mut *POPUP_SETUP.write(), true) {
475 POPUP_CLOSE_REQUESTED_EVENT
476 .on_event(
477 false,
478 hn!(|args| {
479 if !args.propagation.is_stopped() {
480 POPUP_CLOSE_CMD.scoped(args.popup.widget_id()).notify_param(PopupCloseMode::Force);
481 }
482 }),
483 )
484 .perm();
485 }
486}
487
488#[property(EVENT, default(Duration::ZERO), widget_impl(Popup, DefaultStyle))]
495pub fn close_delay(child: impl IntoUiNode, delay: impl IntoVar<Duration>) -> UiNode {
496 let delay = delay.into_var();
497 let mut timer = None::<DeadlineHandle>;
498
499 let child = match_node(child, move |c, op| match op {
500 UiNodeOp::Init => {
501 let id = WIDGET.id();
502 WIDGET.sub_event_when(&POPUP_CLOSE_REQUESTED_EVENT, move |args| args.popup.widget_id() == id);
503 }
504 UiNodeOp::Deinit => {
505 timer = None;
506 }
507 UiNodeOp::Update { updates } => {
508 c.update(updates);
509 POPUP_CLOSE_REQUESTED_EVENT.each_update(false, |args| {
510 if args.popup.widget_id() != WIDGET.id() {
511 return;
512 }
513
514 if let Some(timer) = &timer {
515 if timer.has_executed() {
516 return;
518 } else {
519 args.propagation.stop();
520 return;
522 }
523 }
524
525 let delay = delay.get();
526 if delay != Duration::ZERO {
527 args.propagation.stop();
528
529 IS_CLOSE_DELAYED_VAR.set(true);
530 let cmd = POPUP_CLOSE_CMD.scoped(args.popup.widget_id());
531 timer = Some(TIMERS.on_deadline(
532 delay,
533 hn_once!(|_| {
534 cmd.notify_param(PopupCloseMode::Force);
535 }),
536 ));
537 }
538 });
539 }
540 _ => {}
541 });
542 with_context_var(child, IS_CLOSE_DELAYED_VAR, var(false))
543}
544
545#[property(EVENT+1, widget_impl(Popup, DefaultStyle))]
549pub fn is_close_delaying(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
550 bind_state(child, IS_CLOSE_DELAYED_VAR, state)
551}
552
553context_var! {
554 static IS_CLOSE_DELAYED_VAR: bool = false;
555}