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 => {
77 if is_alt && FOCUS.is_focus_within(WIDGET.id()).get() {
78 FOCUS.focus_exit(false);
80 }
81 }
82 _ => {}
83 })
84}
85
86#[property(CONTEXT, default(FocusScopeOnFocus::default()))]
88pub fn focus_scope_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> UiNode {
89 let behavior = behavior.into_var();
90 match_node(child, move |_, op| match op {
91 UiNodeOp::Init => {
92 WIDGET.sub_var_info(&behavior);
93 }
94 UiNodeOp::Info { info } => {
95 FocusInfoBuilder::new(info).on_focus(behavior.get());
96 }
97 _ => {}
98 })
99}
100
101#[property(CONTEXT, default(TabNav::Continue))]
103pub fn tab_nav(child: impl IntoUiNode, tab_nav: impl IntoVar<TabNav>) -> UiNode {
104 let tab_nav = tab_nav.into_var();
105 match_node(child, move |_, op| match op {
106 UiNodeOp::Init => {
107 WIDGET.sub_var_info(&tab_nav);
108 }
109 UiNodeOp::Info { info } => {
110 FocusInfoBuilder::new(info).tab_nav(tab_nav.get());
111 }
112 _ => {}
113 })
114}
115
116#[property(CONTEXT, default(DirectionalNav::Continue))]
118pub fn directional_nav(child: impl IntoUiNode, directional_nav: impl IntoVar<DirectionalNav>) -> UiNode {
119 let directional_nav = directional_nav.into_var();
120 match_node(child, move |_, op| match op {
121 UiNodeOp::Init => {
122 WIDGET.sub_var_info(&directional_nav);
123 }
124 UiNodeOp::Info { info } => {
125 FocusInfoBuilder::new(info).directional_nav(directional_nav.get());
126 }
127 _ => {}
128 })
129}
130
131#[property(CONTEXT, default(Shortcuts::default()))]
133pub fn focus_shortcut(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>) -> UiNode {
134 let shortcuts = shortcuts.into_var();
135 let mut _handle = None;
136 match_node(child, move |_, op| match op {
137 UiNodeOp::Init => {
138 WIDGET.sub_var(&shortcuts);
139 let s = shortcuts.get();
140 _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
141 }
142 UiNodeOp::Update { .. } => {
143 if let Some(s) = shortcuts.get_new() {
144 _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
145 }
146 }
147 _ => {}
148 })
149}
150
151#[property(CONTEXT, default(false))]
155pub fn skip_directional(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
156 let enabled = enabled.into_var();
157 match_node(child, move |_, op| match op {
158 UiNodeOp::Init => {
159 WIDGET.sub_var_info(&enabled);
160 }
161 UiNodeOp::Info { info } => {
162 FocusInfoBuilder::new(info).skip_directional(enabled.get());
163 }
164 _ => {}
165 })
166}
167
168#[derive(Clone, Copy, PartialEq, Eq)]
174pub enum FocusClickBehavior {
175 Ignore,
177 Exit {
179 recursive_alt: bool,
181 enabled: bool,
183 handled: bool,
185 },
186}
187impl std::fmt::Debug for FocusClickBehavior {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 if f.alternate() {
190 write!(f, "FocusClickBehavior::")?;
191 }
192 match &self {
193 Self::Ignore => write!(f, "Ignore"),
194 Self::Exit {
195 recursive_alt,
196 enabled,
197 handled,
198 } => f
199 .debug_struct("Exit")
200 .field("recursive_alt", recursive_alt)
201 .field("enabled", enabled)
202 .field("handled", handled)
203 .finish(),
204 }
205 }
206}
207impl FocusClickBehavior {
208 pub fn menu_item() -> Self {
212 FocusClickBehavior::Exit {
213 recursive_alt: true,
214 enabled: true,
215 handled: false,
216 }
217 }
218}
219
220#[property(CONTEXT, default(FocusClickBehavior::Ignore))]
229pub fn focus_click_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusClickBehavior>) -> UiNode {
230 let behavior = behavior.into_var();
231 match_node(child, move |c, op| {
232 if let UiNodeOp::Update { updates } = op {
233 let mut on_click = || {
234 if let Some(ctx) = &*FOCUS_CLICK_HANDLED_CTX.get() {
235 c.update(updates);
238
239 ctx.swap(true, Ordering::Relaxed)
241 } else {
242 let mut ctx = Some(Arc::new(Some(AtomicBool::new(false))));
245 FOCUS_CLICK_HANDLED_CTX.with_context(&mut ctx, || c.update(updates));
246
247 let ctx = ctx.unwrap();
249 (*ctx).as_ref().unwrap().load(Ordering::Relaxed)
250 }
251 };
252
253 CLICK_EVENT.each_update(true, |args| {
254 let focus_click_handled_by_inner = on_click();
255 if !focus_click_handled_by_inner
256 && let FocusClickBehavior::Exit {
257 recursive_alt,
258 enabled,
259 handled,
260 } = behavior.get()
261 {
262 if enabled && !args.target.interactivity().is_enabled() {
263 return;
264 }
265 if handled && !args.propagation.is_stopped() {
266 return;
267 }
268
269 tracing::trace!("focus_exit by focus_click_behavior");
270 FOCUS.focus_exit(recursive_alt);
271 }
272 });
273 }
274 })
275}
276context_local! {
277 static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
278}
279
280event_property! {
281 #[property(EVENT)]
283 pub fn on_focus_changed<on_pre_focus_changed>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
284 const PRE: bool;
285 EventNodeBuilder::new(FOCUS_CHANGED_EVENT).build::<PRE>(child, handler)
286 }
287
288 #[property(EVENT)]
290 pub fn on_focus<on_pre_focus>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
291 const PRE: bool;
292 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
293 .filter(|| {
294 let id = WIDGET.id();
295 move |args| args.is_focus(id)
296 })
297 .build::<PRE>(child, handler)
298 }
299
300 #[property(EVENT)]
302 pub fn on_blur<on_pre_blur>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
303 const PRE: bool;
304 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
305 .filter(|| {
306 let id = WIDGET.id();
307 move |args| args.is_blur(id)
308 })
309 .build::<PRE>(child, handler)
310 }
311
312 #[property(EVENT)]
314 pub fn on_focus_enter<on_pre_focus_enter>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
315 const PRE: bool;
316 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
317 .filter(|| {
318 let id = WIDGET.id();
319 move |args| args.is_focus_enter(id)
320 })
321 .build::<PRE>(child, handler)
322 }
323
324 #[property(EVENT)]
326 pub fn on_focus_leave<on_pre_focus_leave>(child: impl IntoUiNode, handler: Handler<FocusChangedArgs>) -> UiNode {
327 const PRE: bool;
328 EventNodeBuilder::new(FOCUS_CHANGED_EVENT)
329 .filter(|| {
330 let id = WIDGET.id();
331 move |args| args.is_focus_leave(id)
332 })
333 .build::<PRE>(child, handler)
334 }
335}
336
337#[property(EVENT, widget_impl(FocusableMix<P>))]
359pub fn is_focused(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
360 bind_state_init(child, state, |s| {
361 let id = WIDGET.id();
362 s.set(FOCUS.focused().with(|p| matches!(p, Some(p) if p.widget_id() == id)));
363 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
364 if args.is_focus(id) {
365 Some(true)
366 } else if args.is_blur(id) {
367 Some(false)
368 } else {
369 None
370 }
371 })
372 })
373}
374
375#[property(EVENT, widget_impl(FocusableMix<P>))]
384pub fn is_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
385 bind_state_init(child, state, |s| {
386 let id = WIDGET.id();
387 s.set(FOCUS.focused().with(|p| matches!(p, Some(p) if p.contains(id))));
388 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
389 if args.is_focus_enter(id) {
390 Some(true)
391 } else if args.is_focus_leave(id) {
392 Some(false)
393 } else {
394 None
395 }
396 })
397 })
398}
399
400#[property(EVENT, widget_impl(FocusableMix<P>))]
413pub fn is_focused_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
414 bind_state_init(child, state, |s| {
415 let id = WIDGET.id();
416 s.set(FOCUS.is_highlighting().get() && FOCUS.focused().with(|p| matches!(p, Some(p) if p.widget_id() == id)));
417 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
418 if args.is_focus(id) {
419 Some(args.highlight)
420 } else if args.is_blur(id) {
421 Some(false)
422 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
423 Some(args.highlight)
424 } else {
425 None
426 }
427 })
428 })
429}
430
431#[property(EVENT, widget_impl(FocusableMix<P>))]
440pub fn is_focus_within_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
441 bind_state_init(child, state, |s| {
442 let id = WIDGET.id();
443 s.set(FOCUS.is_highlighting().get() && FOCUS.focused().with(|p| matches!(p, Some(p) if p.contains(id))));
444 FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
445 if args.is_focus_enter(id) {
446 Some(args.highlight)
447 } else if args.is_focus_leave(id) {
448 Some(false)
449 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
450 Some(args.highlight)
451 } else {
452 None
453 }
454 })
455 })
456}
457
458#[property(EVENT, widget_impl(FocusableMix<P>))]
475pub fn is_return_focus(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
476 bind_state_init(child, state, |s| {
477 let id = WIDGET.id();
478 RETURN_FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
479 if args.is_return_focus(id) {
480 Some(true)
481 } else if args.was_return_focus(id) {
482 Some(false)
483 } else {
484 None
485 }
486 })
487 })
488}
489
490#[property(EVENT, widget_impl(FocusableMix<P>))]
496pub fn is_return_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
497 bind_state_init(child, state, |s| {
498 let id = WIDGET.id();
499 RETURN_FOCUS_CHANGED_EVENT.var_bind(s, move |args| {
500 if args.is_return_focus_enter(id) {
501 Some(true)
502 } else if args.is_return_focus_leave(id) {
503 Some(false)
504 } else {
505 None
506 }
507 })
508 })
509}
510
511#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
517pub fn focus_on_init(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
518 let enabled = enabled.into_var();
519
520 enum State {
521 WaitInfo,
522 InfoInited,
523 Done,
524 }
525 let mut state = State::WaitInfo;
526
527 match_node(child, move |_, op| match op {
528 UiNodeOp::Init => {
529 if enabled.get() {
530 state = State::WaitInfo;
531 } else {
532 state = State::Done;
533 }
534 }
535 UiNodeOp::Info { .. } => {
536 if let State::WaitInfo = &state {
537 state = State::InfoInited;
538 WIDGET.update();
540 }
541 }
542 UiNodeOp::Update { .. } => {
543 if let State::InfoInited = &state {
544 state = State::Done;
545 FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
546 }
547 }
548 _ => {}
549 })
550}
551
552#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
562pub fn return_focus_on_deinit(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
563 let enabled = enabled.into_var();
564 let mut return_focus = None;
565 match_node(child, move |_, op| match op {
566 UiNodeOp::Init => {
567 return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
568 }
569 UiNodeOp::Deinit => {
570 if let Some(id) = return_focus.take()
571 && enabled.get()
572 {
573 if let Some(w) = zng_ext_window::WINDOWS.widget_info(id)
574 && w.into_focusable(false, false).is_some()
575 {
576 FOCUS.focus_widget(id, false);
578 return;
579 }
580 let win_id = WINDOW.id();
582 WIDGET_TREE_CHANGED_EVENT
583 .hook(move |args| {
584 if args.tree.window_id() != win_id {
585 return WINDOWS.widget_tree(win_id).is_some();
587 }
588 FOCUS.focus_widget(id, false);
589 false
590 })
591 .perm();
592
593 WIDGET.update_info();
595 }
596 }
597 _ => {}
598 })
599}
600
601#[widget_mixin]
603pub struct FocusableMix<P>(P);
604impl<P: WidgetImpl> FocusableMix<P> {
605 fn widget_intrinsic(&mut self) {
606 widget_set! {
607 self;
608 focusable = true;
609 when *#is_focused_hgl {
610 zng_wgt_fill::foreground_highlight = {
611 offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
612 widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
613 sides: FOCUS_HIGHLIGHT_SIDES_VAR,
614 };
615 }
616 }
617 }
618}
619
620context_var! {
621 pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
623 pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
625 pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
627}
628
629#[property(
631 CONTEXT,
632 default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
633 widget_impl(FocusableMix<P>)
634)]
635pub fn focus_highlight(
636 child: impl IntoUiNode,
637 offsets: impl IntoVar<SideOffsets>,
638 widths: impl IntoVar<SideOffsets>,
639 sides: impl IntoVar<BorderSides>,
640) -> UiNode {
641 let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
642 let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
643 with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
644}