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}