Skip to main content

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