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}