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}