zng_wgt/
interactivity_props.rs

1use std::sync::Arc;
2
3use task::parking_lot::Mutex;
4use zng_app::{static_id, widget::info::WIDGET_TREE_CHANGED_EVENT};
5
6use crate::{event_property, node::bind_state_init, 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 IntoUiNode, enabled: impl IntoVar<bool>) -> 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 IntoUiNode, interactive: impl IntoVar<bool>) -> 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
93fn is_interactivity_state(child: UiNode, state: Var<bool>, check: fn(Interactivity) -> bool) -> UiNode {
94    bind_state_init(child, state, move |state| {
95        let win_id = WINDOW.id();
96        let wgt_id = WIDGET.id();
97        WIDGET_TREE_CHANGED_EVENT.var_bind(state, move |args| {
98            if args.tree.window_id() == win_id
99                && let Some(wgt) = args.tree.get(wgt_id)
100            {
101                Some(check(wgt.interactivity()))
102            } else {
103                None
104            }
105        })
106    })
107}
108
109/// If the widget is enabled 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_enabled(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
117    is_interactivity_state(child.into_node(), state.into_var(), Interactivity::is_vis_enabled)
118}
119/// If the widget is disabled for interaction.
120///
121/// This property is used only for probing the state. You can set the state using
122/// the [`enabled`] property.
123///
124/// [`enabled`]: fn@enabled
125#[property(EVENT)]
126pub fn is_disabled(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
127    fn check(i: Interactivity) -> bool {
128        !i.is_vis_enabled()
129    }
130    is_interactivity_state(child.into_node(), state.into_var(), check)
131}
132
133/// Get the widget interactivity value.
134///
135/// This property is used only for probing the state. You can set the state using
136/// the [`enabled`] and [`interactive`] properties.
137///
138/// [`enabled`]: fn@enabled
139/// [`interactive`]: fn@interactive
140#[property(EVENT)]
141pub fn get_interactivity(child: impl IntoUiNode, state: impl IntoVar<Interactivity>) -> UiNode {
142    bind_state_init(child, state, move |state| {
143        let win_id = WINDOW.id();
144        let wgt_id = WIDGET.id();
145        WIDGET_TREE_CHANGED_EVENT.var_bind(state, move |args| {
146            if args.tree.window_id() == win_id
147                && let Some(wgt) = args.tree.get(wgt_id)
148            {
149                Some(wgt.interactivity())
150            } else {
151                None
152            }
153        })
154    })
155}
156
157/// Only allow interaction inside the widget, descendants and ancestors.
158///
159/// When a widget is in modal mode, only it, descendants and ancestors are interactive. If [`modal_includes`]
160/// is set on the widget the ancestors and descendants of each include are also allowed.
161///
162/// 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.
163///
164/// This property also sets the accessibility modal flag.
165///
166/// [`modal_includes`]: fn@modal_includes
167#[property(CONTEXT, default(false))]
168pub fn modal(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
169    static_id! {
170        static ref MODAL_WIDGETS: StateId<Arc<Mutex<ModalWidgetsData>>>;
171    }
172    #[derive(Default)]
173    struct ModalWidgetsData {
174        widgets: IdSet<WidgetId>,
175        registrar: Option<WidgetId>,
176
177        last_in_tree: Option<WidgetInfo>,
178    }
179    let enabled = enabled.into_var();
180
181    match_node(child, move |_, op| match op {
182        UiNodeOp::Init => {
183            WIDGET.sub_var_info(&enabled);
184            WINDOW.init_state_default(*MODAL_WIDGETS); // insert window state
185        }
186        UiNodeOp::Deinit => {
187            let mws = WINDOW.req_state(*MODAL_WIDGETS);
188
189            // maybe unregister.
190            let mut mws = mws.lock();
191            let widget_id = WIDGET.id();
192            if mws.widgets.remove(&widget_id) {
193                if mws.registrar == Some(widget_id) {
194                    // change the existing modal that will re-register on info rebuild.
195                    mws.registrar = mws.widgets.iter().next().copied();
196                    if let Some(id) = mws.registrar {
197                        // ensure that the next registrar is not reused.
198                        UPDATES.update_info(id);
199                    }
200                }
201
202                if mws.last_in_tree.as_ref().map(WidgetInfo::id) == Some(widget_id) {
203                    // will re-compute next time the filter is used.
204                    mws.last_in_tree = None;
205                }
206            }
207        }
208        UiNodeOp::Info { info } => {
209            let mws = WINDOW.req_state(*MODAL_WIDGETS);
210
211            if enabled.get() {
212                if let Some(mut a) = info.access() {
213                    a.flag_modal();
214                }
215
216                let insert_filter = {
217                    let mut mws = mws.lock();
218                    let widget_id = WIDGET.id();
219                    if mws.widgets.insert(widget_id) {
220                        mws.last_in_tree = None;
221                        let r = mws.registrar.is_none();
222                        if r {
223                            mws.registrar = Some(widget_id);
224                        }
225                        r
226                    } else {
227                        mws.registrar == Some(widget_id)
228                    }
229                };
230                if insert_filter {
231                    // just registered and we are the first, insert the filter:
232
233                    info.push_interactivity_filter(clmv!(mws, |a| {
234                        let mut mws = mws.lock();
235
236                        // caches the top-most modal.
237                        if mws.last_in_tree.is_none() {
238                            match mws.widgets.len() {
239                                0 => unreachable!(),
240                                1 => {
241                                    // only one modal
242                                    mws.last_in_tree = a.info.tree().get(*mws.widgets.iter().next().unwrap());
243                                    assert!(mws.last_in_tree.is_some());
244                                }
245                                _ => {
246                                    // multiple modals, find the *top* one.
247                                    let mut found = 0;
248                                    for info in a.info.root().self_and_descendants() {
249                                        if mws.widgets.contains(&info.id()) {
250                                            mws.last_in_tree = Some(info);
251                                            found += 1;
252                                            if found == mws.widgets.len() {
253                                                break;
254                                            }
255                                        }
256                                    }
257                                }
258                            };
259                        }
260
261                        // filter, only allows inside self inclusive, and ancestors.
262                        // modal_includes checks if the id is modal or one of the includes.
263
264                        let modal = mws.last_in_tree.as_ref().unwrap();
265
266                        if a.info
267                            .self_and_ancestors()
268                            .any(|w| modal.modal_includes(w.id()) || w.modal_included(modal.id()))
269                        {
270                            // widget ancestor is modal, modal include or includes itself in modal
271                            return Interactivity::ENABLED;
272                        }
273                        if a.info
274                            .self_and_descendants()
275                            .any(|w| modal.modal_includes(w.id()) || w.modal_included(modal.id()))
276                        {
277                            // widget or descendant is modal, modal include or includes itself in modal
278                            return Interactivity::ENABLED;
279                        }
280                        Interactivity::BLOCKED
281                    }));
282                }
283            } else {
284                // maybe unregister.
285                let mut mws = mws.lock();
286                let widget_id = WIDGET.id();
287                if mws.widgets.remove(&widget_id) && mws.last_in_tree.as_ref().map(|w| w.id()) == Some(widget_id) {
288                    mws.last_in_tree = None;
289                }
290            }
291        }
292        _ => {}
293    })
294}
295
296/// Extra widgets that are allowed interaction by this widget when it is [`modal`].
297///
298/// Note that this is only needed for widgets that are not descendants nor ancestors of this widget, but
299/// still need to be interactive when the modal is active.
300///
301/// See also [`modal_included`] if you prefer setting the modal widget id on the included widget.
302///
303/// This property calls [`insert_modal_include`] on the widget.
304///
305/// [`modal`]: fn@modal
306/// [`insert_modal_include`]: WidgetInfoBuilderModalExt::insert_modal_include
307/// [`modal_included`]: fn@modal_included
308#[property(CONTEXT, default(IdSet::new()))]
309pub fn modal_includes(child: impl IntoUiNode, includes: impl IntoVar<IdSet<WidgetId>>) -> UiNode {
310    let includes = includes.into_var();
311    match_node(child, move |_, op| match op {
312        UiNodeOp::Init => {
313            WIDGET.sub_var_info(&includes);
314        }
315        UiNodeOp::Info { info } => includes.with(|w| {
316            for id in w {
317                info.insert_modal_include(*id);
318            }
319        }),
320        _ => (),
321    })
322}
323
324/// Include itself in the allow list of another widget that is [`modal`] or descendant of modal.
325///
326/// Note that this is only needed for widgets that are not descendants nor ancestors of the modal widget, but
327/// still need to be interactive when the modal is active.
328///
329/// See also [`modal_includes`] if you prefer setting the included widget id on the modal widget.
330///
331/// This property calls [`set_modal_included`] on the widget.
332///
333/// [`modal`]: fn@modal
334/// [`set_modal_included`]: WidgetInfoBuilderModalExt::set_modal_included
335/// [`modal_includes`]: fn@modal_includes
336#[property(CONTEXT)]
337pub fn modal_included(child: impl IntoUiNode, modal_or_descendant: impl IntoVar<WidgetId>) -> UiNode {
338    let modal = modal_or_descendant.into_var();
339    match_node(child, move |_, op| match op {
340        UiNodeOp::Init => {
341            WIDGET.sub_var_info(&modal);
342        }
343        UiNodeOp::Info { info } => {
344            info.set_modal_included(modal.get());
345        }
346        _ => {}
347    })
348}
349
350/// Widget info builder extensions for [`modal`] control.
351///
352/// [`modal`]: fn@modal
353pub trait WidgetInfoBuilderModalExt {
354    /// Include an extra widget in the modal filter of this widget.
355    fn insert_modal_include(&mut self, include: WidgetId);
356    /// Register a modal widget that must include this widget in its modal filter.
357    fn set_modal_included(&mut self, modal: WidgetId);
358}
359impl WidgetInfoBuilderModalExt for WidgetInfoBuilder {
360    fn insert_modal_include(&mut self, include: WidgetId) {
361        self.with_meta(|mut m| m.entry(*MODAL_INCLUDES).or_default().insert(include));
362    }
363
364    fn set_modal_included(&mut self, modal: WidgetId) {
365        self.set_meta(*MODAL_INCLUDED, modal);
366    }
367}
368
369trait WidgetInfoModalExt {
370    fn modal_includes(&self, id: WidgetId) -> bool;
371    fn modal_included(&self, modal: WidgetId) -> bool;
372}
373impl WidgetInfoModalExt for WidgetInfo {
374    fn modal_includes(&self, id: WidgetId) -> bool {
375        self.id() == id || self.meta().get(*MODAL_INCLUDES).map(|i| i.contains(&id)).unwrap_or(false)
376    }
377
378    fn modal_included(&self, modal: WidgetId) -> bool {
379        if let Some(id) = self.meta().get_clone(*MODAL_INCLUDED) {
380            if id == modal {
381                return true;
382            }
383            if let Some(id) = self.tree().get(id) {
384                return id.ancestors().any(|w| w.id() == modal);
385            }
386        }
387        false
388    }
389}
390
391static_id! {
392    static ref MODAL_INCLUDES: StateId<IdSet<WidgetId>>;
393    static ref MODAL_INCLUDED: StateId<WidgetId>;
394}
395
396macro_rules! interactivity_var_event_source {
397    (|$interactivity:ident| $map:expr, $default:expr) => {
398        $crate::node::VarEventNodeBuilder::new(|| {
399            let win_id = WINDOW.id();
400            let wgt_id = WIDGET.id();
401            WIDGET_TREE_CHANGED_EVENT.var_map(
402                move |a| {
403                    if a.tree.window_id() == win_id
404                        && let Some(w) = a.tree.get(wgt_id)
405                    {
406                        Some({
407                            let $interactivity = w.interactivity();
408                            $map
409                        })
410                    } else {
411                        None
412                    }
413                },
414                || $default,
415            )
416        })
417    };
418}
419
420event_property! {
421    /// Widget interactivity changed.
422    ///
423    /// Note that there are multiple specific events for interactivity changes, [`on_enabled`], [`on_disabled`], [`on_blocked`] and [`on_unblocked`]
424    /// are some of then.
425    ///
426    /// [`on_enabled`]: fn@on_enabled
427    /// [`on_disabled`]: fn@on_disabled
428    /// [`on_blocked`]: fn@on_blocked
429    /// [`on_unblocked`]: fn@on_unblocked
430    #[property(EVENT)]
431    pub fn on_interactivity_changed(child: impl IntoUiNode, handler: Handler<Interactivity>) -> UiNode {
432        interactivity_var_event_source!(|i| i, Interactivity::ENABLED).build::<false>(child, handler)
433    }
434
435    /// Widget was enabled or disabled.
436    ///
437    /// Note that this event tracks the actual enabled status of the widget, not the visually enabled status,
438    /// see [`Interactivity`] for more details.
439    ///
440    /// See [`on_interactivity_changed`] for a more general interactivity event.
441    ///
442    /// [`on_interactivity_changed`]: fn@on_interactivity_changed
443    /// [`Interactivity`]: zng_app::widget::info::Interactivity
444    pub fn on_enabled_changed(child: impl IntoUiNode, handler: Handler<bool>) -> UiNode {
445        interactivity_var_event_source!(|i| i.is_enabled(), true).build::<false>(child, handler)
446    }
447
448    /// Widget changed to enabled or disabled visuals.
449    ///
450    /// Note that this event tracks the visual enabled status of the widget, not the actual status, the widget may
451    /// still be blocked, see [`Interactivity`] for more details.
452    ///
453    /// See [`on_interactivity_changed`] for a more general interactivity event.
454    ///
455    /// [`on_interactivity_changed`]: fn@on_interactivity_changed
456    /// [`Interactivity`]: zng_app::widget::info::Interactivity
457    pub fn on_vis_enabled_changed(child: impl IntoUiNode, handler: Handler<bool>) -> UiNode {
458        interactivity_var_event_source!(|i| i.is_vis_enabled(), true).build::<false>(child, handler)
459    }
460
461    /// Widget interactions where blocked or unblocked.
462    ///
463    /// Note that blocked widgets may still be visually enabled, see [`Interactivity`] for more details.
464    ///
465    /// See [`on_interactivity_changed`] for a more general interactivity event.
466    ///
467    /// [`on_interactivity_changed`]: fn@on_interactivity_changed
468    /// [`Interactivity`]: zng_app::widget::info::Interactivity
469    pub fn on_blocked_changed(child: impl IntoUiNode, handler: Handler<bool>) -> UiNode {
470        interactivity_var_event_source!(|i| i.is_blocked(), false).build::<false>(child, handler)
471    }
472
473    /// Widget normal interactions now enabled.
474    ///
475    /// Note that this event tracks the actual enabled status of the widget, not the visually enabled status,
476    /// see [`Interactivity`] for more details.
477    ///
478    /// Note that widgets are enabled by default, so this will not notify on init.
479    ///
480    /// See [`on_enabled_changed`] for a more general event.
481    ///
482    /// [`on_enabled_changed`]: fn@on_enabled_changed
483    /// [`Interactivity`]: zng_app::widget::info::Interactivity
484    pub fn on_enabled(child: impl IntoUiNode, handler: Handler<()>) -> UiNode {
485        interactivity_var_event_source!(|i| i.is_enabled(), true)
486            .filter(|| |e| *e)
487            .map_args(|_| ())
488            .build::<false>(child, handler)
489    }
490
491    /// Widget normal interactions now disabled.
492    ///
493    /// Note that this event tracks the actual enabled status of the widget, not the visually enabled status,
494    /// see [`Interactivity`] for more details.
495    ///
496    /// See [`on_enabled_changed`] for a more general event.
497    ///
498    /// [`on_enabled_changed`]: fn@on_enabled_changed
499    /// [`Interactivity`]: zng_app::widget::info::Interactivity
500    pub fn on_disabled(child: impl IntoUiNode, handler: Handler<()>) -> UiNode {
501        interactivity_var_event_source!(|i| i.is_disabled(), false)
502            .filter(|| |d| *d)
503            .map_args(|_| ())
504            .build::<false>(child, handler)
505    }
506
507    /// Widget now looks enabled.
508    ///
509    /// Note that this event tracks the visual enabled status of the widget, not the actual status, the widget may
510    /// still be blocked, see [`Interactivity`] for more details.
511    ///
512    /// Note that widgets are enabled by default, so this will not notify on init.
513    ///
514    /// See [`on_vis_enabled_changed`] for a more general event.
515    ///
516    /// [`on_vis_enabled_changed`]: fn@on_vis_enabled_changed
517    /// [`Interactivity`]: zng_app::widget::info::Interactivity
518    pub fn on_vis_enabled(child: impl IntoUiNode, handler: Handler<()>) -> UiNode {
519        interactivity_var_event_source!(|i| i.is_vis_enabled(), true)
520            .filter(|| |e| *e)
521            .map_args(|_| ())
522            .build::<false>(child, handler)
523    }
524
525    /// Widget now looks disabled.
526    ///
527    /// Note that this event tracks the visual enabled status of the widget, not the actual status, the widget may
528    /// still be blocked, see [`Interactivity`] for more details.
529    ///
530    /// See [`on_vis_enabled_changed`] for a more general event.
531    ///
532    /// [`on_vis_enabled_changed`]: fn@on_vis_enabled_changed
533    /// [`Interactivity`]: zng_app::widget::info::Interactivity
534    pub fn on_vis_disabled(child: impl IntoUiNode, handler: Handler<()>) -> UiNode {
535        interactivity_var_event_source!(|i| i.is_vis_disabled(), false)
536            .filter(|| |d| *d)
537            .map_args(|_| ())
538            .build::<false>(child, handler)
539    }
540
541    /// Widget interactions now blocked.
542    ///
543    /// Note that blocked widgets may still be visually enabled, see [`Interactivity`] for more details.
544    ///
545    /// See [`on_blocked_changed`] for a more general event.
546    ///
547    /// [`on_blocked_changed`]: fn@on_blocked_changed
548    /// [`Interactivity`]: zng_app::widget::info::Interactivity
549    pub fn on_blocked(child: impl IntoUiNode, handler: Handler<()>) -> UiNode {
550        interactivity_var_event_source!(|i| i.is_blocked(), false)
551            .filter(|| |b| *b)
552            .map_args(|_| ())
553            .build::<false>(child, handler)
554    }
555
556    /// Widget interactions now unblocked.
557    ///
558    /// Note that the widget may still be disabled.
559    ///
560    /// See [`on_blocked_changed`] for a more general event.
561    ///
562    /// [`on_blocked_changed`]: fn@on_blocked_changed
563    /// [`Interactivity`]: zng_app::widget::info::Interactivity
564    pub fn on_unblocked(child: impl IntoUiNode, handler: Handler<()>) -> UiNode {
565        interactivity_var_event_source!(|i| !i.is_blocked(), true)
566            .filter(|| |u| *u)
567            .map_args(|_| ())
568            .build::<false>(child, handler)
569    }
570}