zng_wgt/
interactivity_props.rs

1use std::sync::Arc;
2
3use task::parking_lot::Mutex;
4use zng_app::{static_id, widget::info};
5
6use crate::prelude::*;
7
8context_var! {
9    static IS_ENABLED_VAR: bool = true;
10}
11
12/// Defines if default interaction is allowed in the widget and its descendants.
13///
14/// This property sets the interactivity of the widget to [`ENABLED`] or [`DISABLED`], to probe the enabled state in `when` clauses
15/// use [`is_enabled`] or [`is_disabled`]. To probe the a widget's info state use [`WidgetInfo::interactivity`] value.
16///
17/// # Interactivity
18///
19/// Every widget has an interactivity state, it defines two tiers of disabled, the normal disabled blocks the default actions
20/// of the widget, but still allows some interactions, such as a different cursor on hover or event an error tooltip on click, the
21/// second tier blocks all interaction with the widget. This property controls the normal disabled, to fully block interaction use
22/// the [`interactive`] property.
23///
24/// # Disabled Visual
25///
26/// Widgets that are interactive should visually indicate when the normal interactions are disabled, you can use the [`is_disabled`]
27/// state property in a when block to implement the visually disabled appearance of a widget.
28///
29/// The visual cue for the disabled state is usually a reduced contrast from content and background by graying-out the text and applying a
30/// grayscale filter for images. Also consider adding disabled interactions, such as a different cursor or a tooltip that explains why the button
31/// is disabled.
32///
33/// [`ENABLED`]: zng_app::widget::info::Interactivity::ENABLED
34/// [`DISABLED`]: zng_app::widget::info::Interactivity::DISABLED
35/// [`WidgetInfo::interactivity`]: zng_app::widget::info::WidgetInfo::interactivity
36/// [`interactive`]: fn@interactive
37/// [`is_enabled`]: fn@is_enabled
38/// [`is_disabled`]: fn@is_disabled
39#[property(CONTEXT, default(true))]
40pub fn enabled(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
41    let enabled = enabled.into_var();
42
43    let child = match_node(
44        child,
45        clmv!(enabled, |_, op| match op {
46            UiNodeOp::Init => {
47                WIDGET.sub_var_info(&enabled);
48            }
49            UiNodeOp::Info { info } => {
50                if !enabled.get() {
51                    info.push_interactivity(Interactivity::DISABLED);
52                }
53            }
54            _ => {}
55        }),
56    );
57
58    with_context_var(child, IS_ENABLED_VAR, merge_var!(IS_ENABLED_VAR, enabled, |&a, &b| a && b))
59}
60
61/// Defines if any interaction is allowed in the widget and its descendants.
62///
63/// This property sets the interactivity of the widget to [`BLOCKED`] when `false`, widgets with blocked interactivity do not
64/// receive any interaction event and behave like a background visual. To probe the widget's info state use [`WidgetInfo::interactivity`] value.
65///
66/// This property *enables* and *disables* interaction with the widget and its descendants without causing
67/// a visual change like [`enabled`], it also blocks "disabled" interactions such as a different cursor or tooltip for disabled buttons.
68///
69/// Note that this affects the widget where it is set and descendants, to disable interaction only in the widgets
70/// inside `child` use the [`node::interactive_node`].
71///
72/// [`enabled`]: fn@enabled
73/// [`BLOCKED`]: Interactivity::BLOCKED
74/// [`WidgetInfo::interactivity`]: zng_app::widget::info::WidgetInfo::interactivity
75/// [`node::interactive_node`]: crate::node::interactive_node
76#[property(CONTEXT, default(true))]
77pub fn interactive(child: impl UiNode, interactive: impl IntoVar<bool>) -> impl UiNode {
78    let interactive = interactive.into_var();
79
80    match_node(child, move |_, op| match op {
81        UiNodeOp::Init => {
82            WIDGET.sub_var_info(&interactive);
83        }
84        UiNodeOp::Info { info } => {
85            if !interactive.get() {
86                info.push_interactivity(Interactivity::BLOCKED);
87            }
88        }
89        _ => {}
90    })
91}
92
93/// If the widget is enabled for interaction.
94///
95/// This property is used only for probing the state. You can set the state using
96/// the [`enabled`] property.
97///
98/// [`enabled`]: fn@enabled
99#[property(EVENT)]
100pub fn is_enabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
101    event_state(child, state, true, info::INTERACTIVITY_CHANGED_EVENT, move |args| {
102        if let Some((_, new)) = args.vis_enabled_change(WIDGET.id()) {
103            Some(new.is_vis_enabled())
104        } else {
105            None
106        }
107    })
108}
109/// If the widget is disabled for interaction.
110///
111/// This property is used only for probing the state. You can set the state using
112/// the [`enabled`] property.
113///
114/// [`enabled`]: fn@enabled
115#[property(EVENT)]
116pub fn is_disabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
117    event_state(child, state, false, info::INTERACTIVITY_CHANGED_EVENT, move |args| {
118        if let Some((_, new)) = args.vis_enabled_change(WIDGET.id()) {
119            Some(!new.is_vis_enabled())
120        } else {
121            None
122        }
123    })
124}
125
126event_property! {
127    /// Widget interactivity changed.
128    ///
129    /// Note that there are multiple specific events for interactivity changes, [`on_enable`], [`on_disable`], [`on_block`] and [`on_unblock`]
130    /// are some of then.
131    ///
132    /// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
133    /// from `None`, this initial event can be detected using the [`is_new`] method in the args.
134    ///
135    /// [`on_enable`]: fn@on_enable
136    /// [`on_disable`]: fn@on_disable
137    /// [`on_block`]: fn@on_block
138    /// [`on_unblock`]: fn@on_unblock
139    /// [`is_new`]: info::InteractivityChangedArgs::is_new
140    pub fn interactivity_changed {
141        event: info::INTERACTIVITY_CHANGED_EVENT,
142        args: info::InteractivityChangedArgs,
143    }
144
145    /// Widget was enabled or disabled.
146    ///
147    /// Note that this event tracks the actual enabled status of the widget, not the visually enabled status,
148    /// see [`Interactivity`] for more details.
149    ///
150    /// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
151    /// from `None`, this initial event can be detected using the [`is_new`] method in the args.
152    ///
153    /// See [`on_interactivity_changed`] for a more general interactivity event.
154    ///
155    /// [`on_interactivity_changed`]: fn@on_interactivity_changed
156    /// [`Interactivity`]: zng_app::widget::info::Interactivity
157    /// [`is_new`]: info::InteractivityChangedArgs::is_new
158    pub fn enabled_changed {
159        event: info::INTERACTIVITY_CHANGED_EVENT,
160        args: info::InteractivityChangedArgs,
161        filter: |a| a.enabled_change(WIDGET.id()).is_some(),
162    }
163
164    /// Widget changed to enabled or disabled visuals.
165    ///
166    /// Note that this event tracks the visual enabled status of the widget, not the actual status, the widget may
167    /// still be blocked, see [`Interactivity`] for more details.
168    ///
169    /// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
170    /// from `None`, this initial event can be detected using the [`is_new`] method in the args.
171    ///
172    /// See [`on_interactivity_changed`] for a more general interactivity event.
173    ///
174    /// [`on_interactivity_changed`]: fn@on_interactivity_changed
175    /// [`Interactivity`]: zng_app::widget::info::Interactivity
176    /// [`is_new`]: info::InteractivityChangedArgs::is_new
177    pub fn vis_enabled_changed {
178        event: info::INTERACTIVITY_CHANGED_EVENT,
179        args: info::InteractivityChangedArgs,
180        filter: |a| a.vis_enabled_change(WIDGET.id()).is_some(),
181    }
182
183    /// Widget interactions where blocked or unblocked.
184    ///
185    /// Note that blocked widgets may still be visually enabled, see [`Interactivity`] for more details.
186    ///
187    /// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
188    /// from `None`, this initial event can be detected using the [`is_new`] method in the args.
189    ///
190    /// See [`on_interactivity_changed`] for a more general interactivity event.
191    ///
192    /// [`on_interactivity_changed`]: fn@on_interactivity_changed
193    /// [`Interactivity`]: zng_app::widget::info::Interactivity
194    /// [`is_new`]: info::InteractivityChangedArgs::is_new
195    pub fn blocked_changed {
196        event: info::INTERACTIVITY_CHANGED_EVENT,
197        args: info::InteractivityChangedArgs,
198        filter: |a| a.blocked_change(WIDGET.id()).is_some(),
199    }
200
201    /// Widget normal interactions now enabled.
202    ///
203    /// Note that this event tracks the actual enabled status of the widget, not the visually enabled status,
204    /// see [`Interactivity`] for more details.
205    ///
206    /// Note that an event is received when the widget first initializes in the widget info tree if it starts enabled,
207    /// this initial event can be detected using the [`is_new`] method in the args.
208    ///
209    /// See [`on_enabled_changed`] for a more general event.
210    ///
211    /// [`on_enabled_changed`]: fn@on_enabled_changed
212    /// [`Interactivity`]: zng_app::widget::info::Interactivity
213    /// [`is_new`]: info::InteractivityChangedArgs::is_new
214    pub fn enable {
215        event: info::INTERACTIVITY_CHANGED_EVENT,
216        args: info::InteractivityChangedArgs,
217        filter: |a| a.is_enable(WIDGET.id()),
218    }
219
220    /// Widget normal interactions now disabled.
221    ///
222    /// Note that this event tracks the actual enabled status of the widget, not the visually enabled status,
223    /// see [`Interactivity`] for more details.
224    ///
225    /// Note that an event is received when the widget first initializes in the widget info tree if it starts disabled,
226    /// this initial event can be detected using the [`is_new`] method in the args.
227    ///
228    /// See [`on_enabled_changed`] for a more general event.
229    ///
230    /// [`on_enabled_changed`]: fn@on_enabled_changed
231    /// [`Interactivity`]: zng_app::widget::info::Interactivity
232    /// [`is_new`]: info::InteractivityChangedArgs::is_new
233    pub fn disable {
234        event: info::INTERACTIVITY_CHANGED_EVENT,
235        args: info::InteractivityChangedArgs,
236        filter: |a| a.is_disable(WIDGET.id()),
237    }
238
239    /// Widget now looks enabled.
240    ///
241    /// Note that this event tracks the visual enabled status of the widget, not the actual status, the widget may
242    /// still be blocked, see [`Interactivity`] for more details.
243    ///
244    /// Note that an event is received when the widget first initializes in the widget info tree if it starts visually enabled,
245    /// this initial event can be detected using the [`is_new`] method in the args.
246    ///
247    /// See [`on_vis_enabled_changed`] for a more general event.
248    ///
249    /// [`on_vis_enabled_changed`]: fn@on_vis_enabled_changed
250    /// [`Interactivity`]: zng_app::widget::info::Interactivity
251    /// [`is_new`]: info::InteractivityChangedArgs::is_new
252    pub fn vis_enable {
253        event: info::INTERACTIVITY_CHANGED_EVENT,
254        args: info::InteractivityChangedArgs,
255        filter: |a| a.is_vis_enable(WIDGET.id()),
256    }
257
258    /// Widget now looks disabled.
259    ///
260    /// Note that this event tracks the visual enabled status of the widget, not the actual status, the widget may
261    /// still be blocked, see [`Interactivity`] for more details.
262    ///
263    /// Note that an event is received when the widget first initializes in the widget info tree if it starts visually disabled,
264    /// this initial event can be detected using the [`is_new`] method in the args.
265    ///
266    /// See [`on_vis_enabled_changed`] for a more general event.
267    ///
268    /// [`on_vis_enabled_changed`]: fn@on_vis_enabled_changed
269    /// [`Interactivity`]: zng_app::widget::info::Interactivity
270    /// [`is_new`]: info::InteractivityChangedArgs::is_new
271    pub fn vis_disable {
272        event: info::INTERACTIVITY_CHANGED_EVENT,
273        args: info::InteractivityChangedArgs,
274        filter: |a| a.is_vis_disable(WIDGET.id()),
275    }
276
277    /// Widget interactions now blocked.
278    ///
279    /// Note that blocked widgets may still be visually enabled, see [`Interactivity`] for more details.
280    ///
281    /// Note that an event is received when the widget first initializes in the widget info tree if it starts blocked,
282    /// this initial event can be detected using the [`is_new`] method in the args.
283    ///
284    /// See [`on_blocked_changed`] for a more general event.
285    ///
286    /// [`on_blocked_changed`]: fn@on_blocked_changed
287    /// [`Interactivity`]: zng_app::widget::info::Interactivity
288    /// [`is_new`]: info::InteractivityChangedArgs::is_new
289    pub fn block {
290        event: info::INTERACTIVITY_CHANGED_EVENT,
291        args: info::InteractivityChangedArgs,
292        filter: |a| a.is_block(WIDGET.id()),
293    }
294
295    /// Widget interactions now unblocked.
296    ///
297    /// Note that the widget may still be disabled.
298    ///
299    /// Note that an event is received when the widget first initializes in the widget info tree if it starts unblocked,
300    /// this initial event can be detected using the [`is_new`] method in the args.
301    ///
302    /// See [`on_blocked_changed`] for a more general event.
303    ///
304    /// [`on_blocked_changed`]: fn@on_blocked_changed
305    /// [`Interactivity`]: zng_app::widget::info::Interactivity
306    /// [`is_new`]: info::InteractivityChangedArgs::is_new
307    pub fn unblock {
308        event: info::INTERACTIVITY_CHANGED_EVENT,
309        args: info::InteractivityChangedArgs,
310        filter: |a| a.is_unblock(WIDGET.id()),
311    }
312}
313
314/// Only allow interaction inside the widget, descendants and ancestors.
315///
316/// When a widget is in modal mode, only it, descendants and ancestors are interactive. If [`modal_includes`]
317/// is set on the widget the ancestors and descendants of each include are also allowed.
318///
319/// Only one widget can be the modal at a time, if multiple widgets set `modal = true` only the last one by traversal order is actually modal.
320///
321/// This property also sets the accessibility modal flag.
322///
323/// [`modal_includes`]: fn@modal_includes
324#[property(CONTEXT, default(false))]
325pub fn modal(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
326    static_id! {
327        static ref MODAL_WIDGETS: StateId<Arc<Mutex<ModalWidgetsData>>>;
328    }
329    #[derive(Default)]
330    struct ModalWidgetsData {
331        widgets: IdSet<WidgetId>,
332        registrar: Option<WidgetId>,
333
334        last_in_tree: Option<WidgetInfo>,
335    }
336    let enabled = enabled.into_var();
337
338    match_node(child, move |_, op| match op {
339        UiNodeOp::Init => {
340            WIDGET.sub_var_info(&enabled);
341            WINDOW.init_state_default(*MODAL_WIDGETS); // insert window state
342        }
343        UiNodeOp::Deinit => {
344            let mws = WINDOW.req_state(*MODAL_WIDGETS);
345
346            // maybe unregister.
347            let mut mws = mws.lock();
348            let widget_id = WIDGET.id();
349            if mws.widgets.remove(&widget_id) {
350                if mws.registrar == Some(widget_id) {
351                    // change the existing modal that will re-register on info rebuild.
352                    mws.registrar = mws.widgets.iter().next().copied();
353                    if let Some(id) = mws.registrar {
354                        // ensure that the next registrar is not reused.
355                        UPDATES.update_info(id);
356                    }
357                }
358
359                if mws.last_in_tree.as_ref().map(WidgetInfo::id) == Some(widget_id) {
360                    // will re-compute next time the filter is used.
361                    mws.last_in_tree = None;
362                }
363            }
364        }
365        UiNodeOp::Info { info } => {
366            let mws = WINDOW.req_state(*MODAL_WIDGETS);
367
368            if enabled.get() {
369                if let Some(mut a) = info.access() {
370                    a.flag_modal();
371                }
372
373                let insert_filter = {
374                    let mut mws = mws.lock();
375                    let widget_id = WIDGET.id();
376                    if mws.widgets.insert(widget_id) {
377                        mws.last_in_tree = None;
378                        let r = mws.registrar.is_none();
379                        if r {
380                            mws.registrar = Some(widget_id);
381                        }
382                        r
383                    } else {
384                        mws.registrar == Some(widget_id)
385                    }
386                };
387                if insert_filter {
388                    // just registered and we are the first, insert the filter:
389
390                    info.push_interactivity_filter(clmv!(mws, |a| {
391                        let mut mws = mws.lock();
392
393                        // caches the top-most modal.
394                        if mws.last_in_tree.is_none() {
395                            match mws.widgets.len() {
396                                0 => unreachable!(),
397                                1 => {
398                                    // only one modal
399                                    mws.last_in_tree = a.info.tree().get(*mws.widgets.iter().next().unwrap());
400                                    assert!(mws.last_in_tree.is_some());
401                                }
402                                _ => {
403                                    // multiple modals, find the *top* one.
404                                    let mut found = 0;
405                                    for info in a.info.root().self_and_descendants() {
406                                        if mws.widgets.contains(&info.id()) {
407                                            mws.last_in_tree = Some(info);
408                                            found += 1;
409                                            if found == mws.widgets.len() {
410                                                break;
411                                            }
412                                        }
413                                    }
414                                }
415                            };
416                        }
417
418                        // filter, only allows inside self inclusive, and ancestors.
419                        // modal_includes checks if the id is modal or one of the includes.
420
421                        let modal = mws.last_in_tree.as_ref().unwrap();
422
423                        if a.info
424                            .self_and_ancestors()
425                            .any(|w| modal.modal_includes(w.id()) || w.modal_included(modal.id()))
426                        {
427                            // widget ancestor is modal, modal include or includes itself in modal
428                            return Interactivity::ENABLED;
429                        }
430                        if a.info
431                            .self_and_descendants()
432                            .any(|w| modal.modal_includes(w.id()) || w.modal_included(modal.id()))
433                        {
434                            // widget or descendant is modal, modal include or includes itself in modal
435                            return Interactivity::ENABLED;
436                        }
437                        Interactivity::BLOCKED
438                    }));
439                }
440            } else {
441                // maybe unregister.
442                let mut mws = mws.lock();
443                let widget_id = WIDGET.id();
444                if mws.widgets.remove(&widget_id) && mws.last_in_tree.as_ref().map(|w| w.id()) == Some(widget_id) {
445                    mws.last_in_tree = None;
446                }
447            }
448        }
449        _ => {}
450    })
451}
452
453/// Extra widgets that are allowed interaction by this widget when it is [`modal`].
454///
455/// Note that this is only needed for widgets that are not descendants nor ancestors of this widget, but
456/// still need to be interactive when the modal is active.
457///
458/// See also [`modal_included`] if you prefer setting the modal widget id on the included widget.
459///
460/// This property calls [`insert_modal_include`] on the widget.
461///
462/// [`modal`]: fn@modal
463/// [`insert_modal_include`]: WidgetInfoBuilderModalExt::insert_modal_include
464/// [`modal_included`]: fn@modal_included
465#[property(CONTEXT, default(IdSet::new()))]
466pub fn modal_includes(child: impl UiNode, includes: impl IntoVar<IdSet<WidgetId>>) -> impl UiNode {
467    let includes = includes.into_var();
468    match_node(child, move |_, op| match op {
469        UiNodeOp::Init => {
470            WIDGET.sub_var_info(&includes);
471        }
472        UiNodeOp::Info { info } => includes.with(|w| {
473            for id in w {
474                info.insert_modal_include(*id);
475            }
476        }),
477        _ => (),
478    })
479}
480
481/// Include itself in the allow list of another widget that is [`modal`] or descendant of modal.
482///
483/// Note that this is only needed for widgets that are not descendants nor ancestors of the modal widget, but
484/// still need to be interactive when the modal is active.
485///
486/// See also [`modal_includes`] if you prefer setting the included widget id on the modal widget.
487///
488/// This property calls [`set_modal_included`] on the widget.
489///
490/// [`modal`]: fn@modal
491/// [`set_modal_included`]: WidgetInfoBuilderModalExt::set_modal_included
492/// [`modal_includes`]: fn@modal_includes
493#[property(CONTEXT)]
494pub fn modal_included(child: impl UiNode, modal_or_descendant: impl IntoVar<WidgetId>) -> impl UiNode {
495    let modal = modal_or_descendant.into_var();
496    match_node(child, move |_, op| match op {
497        UiNodeOp::Init => {
498            WIDGET.sub_var_info(&modal);
499        }
500        UiNodeOp::Info { info } => {
501            info.set_modal_included(modal.get());
502        }
503        _ => {}
504    })
505}
506
507/// Widget info builder extensions for [`modal`] control.
508///
509/// [`modal`]: fn@modal
510pub trait WidgetInfoBuilderModalExt {
511    /// Include an extra widget in the modal filter of this widget.
512    fn insert_modal_include(&mut self, include: WidgetId);
513    /// Register a modal widget that must include this widget in its modal filter.
514    fn set_modal_included(&mut self, modal: WidgetId);
515}
516impl WidgetInfoBuilderModalExt for WidgetInfoBuilder {
517    fn insert_modal_include(&mut self, include: WidgetId) {
518        self.with_meta(|mut m| m.entry(*MODAL_INCLUDES).or_default().insert(include));
519    }
520
521    fn set_modal_included(&mut self, modal: WidgetId) {
522        self.set_meta(*MODAL_INCLUDED, modal);
523    }
524}
525
526trait WidgetInfoModalExt {
527    fn modal_includes(&self, id: WidgetId) -> bool;
528    fn modal_included(&self, modal: WidgetId) -> bool;
529}
530impl WidgetInfoModalExt for WidgetInfo {
531    fn modal_includes(&self, id: WidgetId) -> bool {
532        self.id() == id || self.meta().get(*MODAL_INCLUDES).map(|i| i.contains(&id)).unwrap_or(false)
533    }
534
535    fn modal_included(&self, modal: WidgetId) -> bool {
536        if let Some(id) = self.meta().get_clone(*MODAL_INCLUDED) {
537            if id == modal {
538                return true;
539            }
540            if let Some(id) = self.tree().get(id) {
541                return id.ancestors().any(|w| w.id() == modal);
542            }
543        }
544        false
545    }
546}
547
548static_id! {
549    static ref MODAL_INCLUDES: StateId<IdSet<WidgetId>>;
550    static ref MODAL_INCLUDED: StateId<WidgetId>;
551}