1use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use zng_app::widget::info::WIDGET_TREE_CHANGED_EVENT;
8use zng_ext_input::focus::*;
9use zng_ext_input::gesture::{CLICK_EVENT, GESTURES};
10use zng_ext_window::WINDOWS;
11use zng_wgt::node::bind_state_init;
12use zng_wgt::prelude::*;
13
14#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
16pub fn focusable(child: impl IntoUiNode, focusable: impl IntoVar<bool>) -> UiNode {
17 let focusable = focusable.into_var();
18 match_node(child, move |_, op| match op {
19 UiNodeOp::Init => {
20 WIDGET.sub_var_info(&focusable);
21 }
22 UiNodeOp::Info { info } => {
23 FocusInfoBuilder::new(info).focusable(focusable.get());
24 }
25 _ => {}
26 })
27}
28
29#[property(CONTEXT, default(TabIndex::default()))]
31pub fn tab_index(child: impl IntoUiNode, tab_index: impl IntoVar<TabIndex>) -> UiNode {
32 let tab_index = tab_index.into_var();
33 match_node(child, move |_, op| match op {
34 UiNodeOp::Init => {
35 WIDGET.sub_var_info(&tab_index);
36 }
37 UiNodeOp::Info { info } => {
38 FocusInfoBuilder::new(info).tab_index(tab_index.get());
39 }
40 _ => {}
41 })
42}
43
44#[property(CONTEXT, default(false))]
46pub fn focus_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
47 focus_scope_impl(child, is_scope, false)
48}
49#[property(CONTEXT, default(false))]
58pub fn alt_focus_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
59 focus_scope_impl(child, is_scope, true)
60}
61
62fn focus_scope_impl(child: impl IntoUiNode, is_scope: impl IntoVar<bool>, is_alt: bool) -> UiNode {
63 let is_scope = is_scope.into_var();
64 match_node(child, move |_, op| match op {
65 UiNodeOp::Init => {
66 WIDGET.sub_var_info(&is_scope);
67 }
68 UiNodeOp::Info { info } => {
69 let mut info = FocusInfoBuilder::new(info);
70 if is_alt {
71 info.alt_scope(is_scope.get());
72 } else {
73 info.scope(is_scope.get());
74 }
75 }
76 UiNodeOp::Deinit if is_alt && FOCUS.is_focus_within(WIDGET.id()).get() => {
77 FOCUS.focus_exit(false);
79 }
80 _ => {}
81 })
82}
83
84#[property(CONTEXT, default(FocusScopeOnFocus::default()))]
86pub fn focus_scope_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> UiNode {
87 let behavior = behavior.into_var();
88 match_node(child, move |_, op| match op {
89 UiNodeOp::Init => {
90 WIDGET.sub_var_info(&behavior);
91 }
92 UiNodeOp::Info { info } => {
93 FocusInfoBuilder::new(info).on_focus(behavior.get());
94 }
95 _ => {}
96 })
97}
98
99#[property(CONTEXT, default(TabNav::Continue))]
101pub fn tab_nav(child: impl IntoUiNode, tab_nav: impl IntoVar<TabNav>) -> UiNode {
102 let tab_nav = tab_nav.into_var();
103 match_node(child, move |_, op| match op {
104 UiNodeOp::Init => {
105 WIDGET.sub_var_info(&tab_nav);
106 }
107 UiNodeOp::Info { info } => {
108 FocusInfoBuilder::new(info).tab_nav(tab_nav.get());
109 }
110 _ => {}
111 })
112}
113
114#[property(CONTEXT, default(DirectionalNav::Continue))]
116pub fn directional_nav(child: impl IntoUiNode, directional_nav: impl IntoVar<DirectionalNav>) -> UiNode {
117 let directional_nav = directional_nav.into_var();
118 match_node(child, move |_, op| match op {
119 UiNodeOp::Init => {
120 WIDGET.sub_var_info(&directional_nav);
121 }
122 UiNodeOp::Info { info } => {
123 FocusInfoBuilder::new(info).directional_nav(directional_nav.get());
124 }
125 _ => {}
126 })
127}
128
129#[property(CONTEXT, default(Shortcuts::default()))]
131pub fn focus_shortcut(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>) -> UiNode {
132 let shortcuts = shortcuts.into_var();
133 let mut _handle = None;
134 match_node(child, move |_, op| match op {
135 UiNodeOp::Init => {
136 WIDGET.sub_var(&shortcuts);
137 let s = shortcuts.get();
138 _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
139 }
140 UiNodeOp::Update { .. } => {
141 if let Some(s) = shortcuts.get_new() {
142 _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
143 }
144 }
145 _ => {}
146 })
147}
148
149#[property(CONTEXT, default(false))]
153pub fn skip_directional(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
154 let enabled = enabled.into_var();
155 match_node(child, move |_, op| match op {
156 UiNodeOp::Init => {
157 WIDGET.sub_var_info(&enabled);
158 }
159 UiNodeOp::Info { info } => {
160 FocusInfoBuilder::new(info).skip_directional(enabled.get());
161 }
162 _ => {}
163 })
164}
165
166#[derive(Clone, Copy, PartialEq, Eq)]
172pub enum FocusClickBehavior {
173 Ignore,
175 Exit {
177 recursive_alt: bool,
179 enabled: bool,
181 handled: bool,
183 },
184}
185impl std::fmt::Debug for FocusClickBehavior {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 if f.alternate() {
188 write!(f, "FocusClickBehavior::")?;
189 }
190 match &self {
191 Self::Ignore => write!(f, "Ignore"),
192 Self::Exit {
193 recursive_alt,
194 enabled,
195 handled,
196 } => f
197 .debug_struct("Exit")
198 .field("recursive_alt", recursive_alt)
199 .field("enabled", enabled)
200 .field("handled", handled)
201 .finish(),
202 }
203 }
204}
205impl FocusClickBehavior {
206 pub fn menu_item() -> Self {
210 FocusClickBehavior::Exit {
211 recursive_alt: true,
212 enabled: true,
213 handled: false,
214 }
215 }
216}
217
218#[property(CONTEXT, default(FocusClickBehavior::Ignore))]
227pub fn focus_click_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusClickBehavior>) -> UiNode {
228 let behavior = behavior.into_var();
229 match_node(child, move |c, op| {
230 if let UiNodeOp::Update { updates } = op {
231 let mut on_click = || {
232 if let Some(ctx) = &*FOCUS_CLICK_HANDLED_CTX.get() {
233 c.update(updates);
236
237 ctx.swap(true, Ordering::Relaxed)
239 } else {
240 let mut ctx = Some(Arc::new(Some(AtomicBool::new(false))));
243 FOCUS_CLICK_HANDLED_CTX.with_context(&mut ctx, || c.update(updates));
244
245 let ctx = ctx.unwrap();
247 (*ctx).as_ref().unwrap().load(Ordering::Relaxed)
248 }
249 };
250
251 CLICK_EVENT.each_update(true, |args| {
252 let focus_click_handled_by_inner = on_click();
253 if !focus_click_handled_by_inner
254 && let FocusClickBehavior::Exit {
255 recursive_alt,
256 enabled,
257 handled,
258 } = behavior.get()
259 {
260 if enabled && !args.target.interactivity().is_enabled() {
261 return;
262 }
263 if handled && !args.propagation.is_stopped() {
264 return;
265 }
266
267 tracing::trace!("focus_exit by focus_click_behavior");
268 FOCUS.focus_exit(recursive_alt);
269 }
270 });
271 }
272 })
273}
274context_local! {
275 static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
276}
277
278event_property! {
279 #[property(EVENT)]
281 pub fn on_focus_changed<on_pre_focus_changed>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
282 const PRE: bool;
283 EventNodeBuilder::new(FOCUS_CHANGED_EVENT).build::<PRE>(child, handler)
284 }
285
286 #[property(EVENT)]
288 pub fn on_focus<on_pre_focus>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
289 const PRE: bool;
290 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
291 .filter(|| {
292 let id = WIDGET.id();
293 move |args| args.is_focus(id)
294 })
295 .build::<PRE>(child, handler)
296 }
297
298 #[property(EVENT)]
300 pub fn on_blur<on_pre_blur>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
301 const PRE: bool;
302 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
303 .filter(|| {
304 let id = WIDGET.id();
305 move |args| args.is_blur(id)
306 })
307 .build::<PRE>(child, handler)
308 }
309
310 #[property(EVENT)]
312 pub fn on_focus_enter<on_pre_focus_enter>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
313 const PRE: bool;
314 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
315 .filter(|| {
316 let id = WIDGET.id();
317 move |args| args.is_focus_enter(id)
318 })
319 .build::<PRE>(child, handler)
320 }
321
322 #[property(EVENT)]
324 pub fn on_focus_leave<on_pre_focus_leave>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
325 const PRE: bool;
326 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
327 .filter(|| {
328 let id = WIDGET.id();
329 move |args| args.is_focus_leave(id)
330 })
331 .build::<PRE>(child, handler)
332 }
333}
334
335#[property(EVENT, widget_impl(FocusableMix<P>))]
357pub fn is_focused(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
358 bind_state_init(child, state, |s| {
359 let id = WIDGET.id();
360 s.set(FOCUS.focused().with(|p| matches!(p, Some(p) if p.widget_id() == id)));
361 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
362 if args.is_focus(id) {
363 Some(true)
364 } else if args.is_blur(id) {
365 Some(false)
366 } else {
367 None
368 }
369 })
370 })
371}
372
373#[property(EVENT, widget_impl(FocusableMix<P>))]
382pub fn is_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
383 bind_state_init(child, state, |s| {
384 let id = WIDGET.id();
385 s.set(FOCUS.focused().with(|p| matches!(p, Some(p) if p.contains(id))));
386 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
387 if args.is_focus_enter(id) {
388 Some(true)
389 } else if args.is_focus_leave(id) {
390 Some(false)
391 } else {
392 None
393 }
394 })
395 })
396}
397
398#[property(EVENT, widget_impl(FocusableMix<P>))]
411pub fn is_focused_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
412 bind_state_init(child, state, |s| {
413 let id = WIDGET.id();
414 s.set(FOCUS.is_highlighting().get() && FOCUS.focused().with(|p| matches!(p, Some(p) if p.widget_id() == id)));
415 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
416 if args.is_focus(id) {
417 Some(args.highlight)
418 } else if args.is_blur(id) {
419 Some(false)
420 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
421 Some(args.highlight)
422 } else {
423 None
424 }
425 })
426 })
427}
428
429#[property(EVENT, widget_impl(FocusableMix<P>))]
438pub fn is_focus_within_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
439 bind_state_init(child, state, |s| {
440 let id = WIDGET.id();
441 s.set(FOCUS.is_highlighting().get() && FOCUS.focused().with(|p| matches!(p, Some(p) if p.contains(id))));
442 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
443 if args.is_focus_enter(id) {
444 Some(args.highlight)
445 } else if args.is_focus_leave(id) {
446 Some(false)
447 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
448 Some(args.highlight)
449 } else {
450 None
451 }
452 })
453 })
454}
455
456#[property(EVENT, widget_impl(FocusableMix<P>))]
473pub fn is_return_focus(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
474 bind_state_init(child, state, |s| {
475 let id = WIDGET.id();
476 RETURN_FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
477 if args.is_return_focus(id) {
478 Some(true)
479 } else if args.was_return_focus(id) {
480 Some(false)
481 } else {
482 None
483 }
484 })
485 })
486}
487
488#[property(EVENT, widget_impl(FocusableMix<P>))]
494pub fn is_return_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
495 bind_state_init(child, state, |s| {
496 let id = WIDGET.id();
497 RETURN_FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
498 if args.is_return_focus_enter(id) {
499 Some(true)
500 } else if args.is_return_focus_leave(id) {
501 Some(false)
502 } else {
503 None
504 }
505 })
506 })
507}
508
509#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
515pub fn focus_on_init(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
516 let enabled = enabled.into_var();
517
518 enum State {
519 WaitInfo,
520 InfoInited,
521 Done,
522 }
523 let mut state = State::WaitInfo;
524
525 match_node(child, move |_, op| match op {
526 UiNodeOp::Init => {
527 if enabled.get() {
528 state = State::WaitInfo;
529 } else {
530 state = State::Done;
531 }
532 }
533 UiNodeOp::Info { .. } => {
534 if let State::WaitInfo = &state {
535 state = State::InfoInited;
536 WIDGET.update();
538 }
539 }
540 UiNodeOp::Update { .. } => {
541 if let State::InfoInited = &state {
542 state = State::Done;
543 FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
544 }
545 }
546 _ => {}
547 })
548}
549
550#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
560pub fn return_focus_on_deinit(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
561 let enabled = enabled.into_var();
562 let mut return_focus = None;
563 match_node(child, move |_, op| match op {
564 UiNodeOp::Init => {
565 return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
566 }
567 UiNodeOp::Deinit => {
568 if let Some(id) = return_focus.take()
569 && enabled.get()
570 {
571 if let Some(w) = zng_ext_window::WINDOWS.widget_info(id)
572 && w.into_focusable(false, false).is_some()
573 {
574 FOCUS.focus_widget(id, false);
576 return;
577 }
578 let win_id = WINDOW.id();
580 WIDGET_TREE_CHANGED_EVENT
581 .hook(move |args| {
582 if args.tree.window_id() != win_id {
583 return WINDOWS.widget_tree(win_id).is_some();
585 }
586 FOCUS.focus_widget(id, false);
587 false
588 })
589 .perm();
590
591 WIDGET.update_info();
593 }
594 }
595 _ => {}
596 })
597}
598
599#[widget_mixin]
601pub struct FocusableMix<P>(P);
602impl<P: WidgetImpl> FocusableMix<P> {
603 fn widget_intrinsic(&mut self) {
604 widget_set! {
605 self;
606 focusable = true;
607 when *#is_focused_hgl {
608 zng_wgt_fill::foreground_highlight = {
609 offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
610 widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
611 sides: FOCUS_HIGHLIGHT_SIDES_VAR,
612 };
613 }
614 }
615 }
616}
617
618context_var! {
619 pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
621 pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
623 pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
625}
626
627#[property(
629 CONTEXT,
630 default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
631 widget_impl(FocusableMix<P>)
632)]
633pub fn focus_highlight(
634 child: impl IntoUiNode,
635 offsets: impl IntoVar<SideOffsets>,
636 widths: impl IntoVar<SideOffsets>,
637 sides: impl IntoVar<BorderSides>,
638) -> UiNode {
639 let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
640 let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
641 with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
642}