1use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use zng_app::widget::info::WIDGET_INFO_CHANGED_EVENT;
8use zng_ext_input::focus::*;
9use zng_ext_input::gesture::{CLICK_EVENT, GESTURES};
10use zng_ext_input::mouse::MOUSE_INPUT_EVENT;
11use zng_wgt::prelude::*;
12
13#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
15pub fn focusable(child: impl IntoUiNode, focusable: impl IntoVar<bool>) -> UiNode {
16 let focusable = focusable.into_var();
17 match_node(child, move |_, op| match op {
18 UiNodeOp::Init => {
19 WIDGET.sub_var_info(&focusable);
20 }
21 UiNodeOp::Info { info } => {
22 FocusInfoBuilder::new(info).focusable(focusable.get());
23 }
24 _ => {}
25 })
26}
27
28#[property(CONTEXT, default(TabIndex::default()))]
30pub fn tab_index(child: impl IntoUiNode, tab_index: impl IntoVar<TabIndex>) -> UiNode {
31 let tab_index = tab_index.into_var();
32 match_node(child, move |_, op| match op {
33 UiNodeOp::Init => {
34 WIDGET.sub_var_info(&tab_index);
35 }
36 UiNodeOp::Info { info } => {
37 FocusInfoBuilder::new(info).tab_index(tab_index.get());
38 }
39 _ => {}
40 })
41}
42
43#[property(CONTEXT, default(false))]
45pub fn focus_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
46 focus_scope_impl(child, is_scope, false)
47}
48#[property(CONTEXT, default(false))]
57pub fn alt_focus_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
58 focus_scope_impl(child, is_scope, true)
59}
60
61fn focus_scope_impl(child: impl IntoUiNode, is_scope: impl IntoVar<bool>, is_alt: bool) -> UiNode {
62 let is_scope = is_scope.into_var();
63 match_node(child, move |_, op| match op {
64 UiNodeOp::Init => {
65 WIDGET.sub_var_info(&is_scope);
66 }
67 UiNodeOp::Info { info } => {
68 let mut info = FocusInfoBuilder::new(info);
69 if is_alt {
70 info.alt_scope(is_scope.get());
71 } else {
72 info.scope(is_scope.get());
73 }
74 }
75 UiNodeOp::Deinit => {
76 if is_alt && FOCUS.is_focus_within(WIDGET.id()).get() {
77 FOCUS.focus_exit();
79 }
80 }
81 _ => {}
82 })
83}
84
85#[property(CONTEXT, default(FocusScopeOnFocus::default()))]
87pub fn focus_scope_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> UiNode {
88 let behavior = behavior.into_var();
89 match_node(child, move |_, op| match op {
90 UiNodeOp::Init => {
91 WIDGET.sub_var_info(&behavior);
92 }
93 UiNodeOp::Info { info } => {
94 FocusInfoBuilder::new(info).on_focus(behavior.get());
95 }
96 _ => {}
97 })
98}
99
100#[property(CONTEXT, default(TabNav::Continue))]
102pub fn tab_nav(child: impl IntoUiNode, tab_nav: impl IntoVar<TabNav>) -> UiNode {
103 let tab_nav = tab_nav.into_var();
104 match_node(child, move |_, op| match op {
105 UiNodeOp::Init => {
106 WIDGET.sub_var_info(&tab_nav);
107 }
108 UiNodeOp::Info { info } => {
109 FocusInfoBuilder::new(info).tab_nav(tab_nav.get());
110 }
111 _ => {}
112 })
113}
114
115#[property(CONTEXT, default(DirectionalNav::Continue))]
117pub fn directional_nav(child: impl IntoUiNode, directional_nav: impl IntoVar<DirectionalNav>) -> UiNode {
118 let directional_nav = directional_nav.into_var();
119 match_node(child, move |_, op| match op {
120 UiNodeOp::Init => {
121 WIDGET.sub_var_info(&directional_nav);
122 }
123 UiNodeOp::Info { info } => {
124 FocusInfoBuilder::new(info).directional_nav(directional_nav.get());
125 }
126 _ => {}
127 })
128}
129
130#[property(CONTEXT, default(Shortcuts::default()))]
132pub fn focus_shortcut(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>) -> UiNode {
133 let shortcuts = shortcuts.into_var();
134 let mut _handle = None;
135 match_node(child, move |_, op| match op {
136 UiNodeOp::Init => {
137 WIDGET.sub_var(&shortcuts);
138 let s = shortcuts.get();
139 _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
140 }
141 UiNodeOp::Update { .. } => {
142 if let Some(s) = shortcuts.get_new() {
143 _handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
144 }
145 }
146 _ => {}
147 })
148}
149
150#[property(CONTEXT, default(false))]
154pub fn skip_directional(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
155 let enabled = enabled.into_var();
156 match_node(child, move |_, op| match op {
157 UiNodeOp::Init => {
158 WIDGET.sub_var_info(&enabled);
159 }
160 UiNodeOp::Info { info } => {
161 FocusInfoBuilder::new(info).skip_directional(enabled.get());
162 }
163 _ => {}
164 })
165}
166
167#[derive(Clone, Copy, PartialEq, Eq)]
173pub enum FocusClickBehavior {
174 Ignore,
176 Exit,
178 ExitEnabled,
180 ExitHandled,
182}
183
184impl std::fmt::Debug for FocusClickBehavior {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 if f.alternate() {
187 write!(f, "FocusClickBehavior::")?;
188 }
189 match self {
190 Self::Ignore => write!(f, "Ignore"),
191 Self::Exit => write!(f, "Exit"),
192 Self::ExitEnabled => write!(f, "ExitEnabled"),
193 Self::ExitHandled => write!(f, "ExitHandled"),
194 }
195 }
196}
197
198#[property(CONTEXT, default(FocusClickBehavior::Ignore))]
207pub fn focus_click_behavior(child: impl IntoUiNode, behavior: impl IntoVar<FocusClickBehavior>) -> UiNode {
208 let behavior = behavior.into_var();
209 match_node(child, move |c, op| {
210 if let UiNodeOp::Event { update } = op {
211 let mut delegate = || {
212 if let Some(ctx) = &*FOCUS_CLICK_HANDLED_CTX.get() {
213 c.event(update);
214 ctx.swap(true, Ordering::Relaxed)
215 } else {
216 let mut ctx = Some(Arc::new(Some(AtomicBool::new(false))));
217 FOCUS_CLICK_HANDLED_CTX.with_context(&mut ctx, || c.event(update));
218 let ctx = ctx.unwrap();
219 (*ctx).as_ref().unwrap().load(Ordering::Relaxed)
220 }
221 };
222
223 if let Some(args) = CLICK_EVENT.on(update) {
224 if !delegate() {
225 let exit = match behavior.get() {
226 FocusClickBehavior::Ignore => false,
227 FocusClickBehavior::Exit => true,
228 FocusClickBehavior::ExitEnabled => args.target.interactivity().is_enabled(),
229 FocusClickBehavior::ExitHandled => args.propagation().is_stopped(),
230 };
231 if exit {
232 FOCUS.focus_exit();
233 }
234 }
235 } else if let Some(args) = MOUSE_INPUT_EVENT.on_unhandled(update)
236 && args.propagation().is_stopped()
237 && !delegate()
238 {
239 let exit = match behavior.get() {
242 FocusClickBehavior::Ignore => false,
243 FocusClickBehavior::Exit => true,
244 FocusClickBehavior::ExitEnabled => args.target.interactivity().is_enabled(),
245 FocusClickBehavior::ExitHandled => true,
246 };
247 if exit {
248 FOCUS.focus_exit();
249 }
250 }
251 }
252 })
253}
254context_local! {
255 static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
256}
257
258event_property! {
259 pub fn focus_changed {
261 event: FOCUS_CHANGED_EVENT,
262 args: FocusChangedArgs,
263 }
264
265 pub fn focus {
267 event: FOCUS_CHANGED_EVENT,
268 args: FocusChangedArgs,
269 filter: |args| args.is_focus(WIDGET.id()),
270 }
271
272 pub fn blur {
274 event: FOCUS_CHANGED_EVENT,
275 args: FocusChangedArgs,
276 filter: |args| args.is_blur(WIDGET.id()),
277 }
278
279 pub fn focus_enter {
281 event: FOCUS_CHANGED_EVENT,
282 args: FocusChangedArgs,
283 filter: |args| args.is_focus_enter(WIDGET.id()),
284 }
285
286 pub fn focus_leave {
288 event: FOCUS_CHANGED_EVENT,
289 args: FocusChangedArgs,
290 filter: |args| args.is_focus_leave(WIDGET.id()),
291 }
292}
293
294#[property(EVENT, widget_impl(FocusableMix<P>))]
316pub fn is_focused(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
317 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
318 let id = WIDGET.id();
319 if args.is_focus(id) {
320 Some(true)
321 } else if args.is_blur(id) {
322 Some(false)
323 } else {
324 None
325 }
326 })
327}
328
329#[property(EVENT, widget_impl(FocusableMix<P>))]
338pub fn is_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
339 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
340 let id = WIDGET.id();
341 if args.is_focus_enter(id) {
342 Some(true)
343 } else if args.is_focus_leave(id) {
344 Some(false)
345 } else {
346 None
347 }
348 })
349}
350
351#[property(EVENT, widget_impl(FocusableMix<P>))]
364pub fn is_focused_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
365 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
366 let id = WIDGET.id();
367 if args.is_focus(id) {
368 Some(args.highlight)
369 } else if args.is_blur(id) {
370 Some(false)
371 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
372 Some(args.highlight)
373 } else {
374 None
375 }
376 })
377}
378
379#[property(EVENT, widget_impl(FocusableMix<P>))]
388pub fn is_focus_within_hgl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
389 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
390 let id = WIDGET.id();
391 if args.is_focus_enter(id) {
392 Some(args.highlight)
393 } else if args.is_focus_leave(id) {
394 Some(false)
395 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
396 Some(args.highlight)
397 } else {
398 None
399 }
400 })
401}
402
403#[property(EVENT, widget_impl(FocusableMix<P>))]
420pub fn is_return_focus(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
421 event_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
422 let id = WIDGET.id();
423 if args.is_return_focus(id) {
424 Some(true)
425 } else if args.was_return_focus(id) {
426 Some(false)
427 } else {
428 None
429 }
430 })
431}
432
433#[property(EVENT, widget_impl(FocusableMix<P>))]
439pub fn is_return_focus_within(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
440 event_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
441 let id = WIDGET.id();
442 if args.is_return_focus_enter(id) {
443 Some(true)
444 } else if args.is_return_focus_leave(id) {
445 Some(false)
446 } else {
447 None
448 }
449 })
450}
451
452#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
458pub fn focus_on_init(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
459 let enabled = enabled.into_var();
460
461 enum State {
462 WaitInfo,
463 InfoInited,
464 Done,
465 }
466 let mut state = State::WaitInfo;
467
468 match_node(child, move |_, op| match op {
469 UiNodeOp::Init => {
470 if enabled.get() {
471 state = State::WaitInfo;
472 } else {
473 state = State::Done;
474 }
475 }
476 UiNodeOp::Info { .. } => {
477 if let State::WaitInfo = &state {
478 state = State::InfoInited;
479 WIDGET.update();
481 }
482 }
483 UiNodeOp::Update { .. } => {
484 if let State::InfoInited = &state {
485 state = State::Done;
486 FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
487 }
488 }
489 _ => {}
490 })
491}
492
493#[property(EVENT, default(false), widget_impl(FocusableMix<P>))]
503pub fn return_focus_on_deinit(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
504 let enabled = enabled.into_var();
505 let mut return_focus = None;
506 match_node(child, move |_, op| match op {
507 UiNodeOp::Init => {
508 return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
509 }
510 UiNodeOp::Deinit => {
511 if let Some(id) = return_focus.take()
512 && enabled.get()
513 {
514 if let Some(w) = zng_ext_window::WINDOWS.widget_info(id)
515 && w.into_focusable(false, false).is_some()
516 {
517 FOCUS.focus_widget(id, false);
519 return;
520 }
521 WIDGET_INFO_CHANGED_EVENT
523 .on_pre_event(hn_once!(|_| {
524 FOCUS.focus_widget(id, false);
525 }))
526 .perm();
527 WIDGET.update_info();
529 }
530 }
531 _ => {}
532 })
533}
534
535#[widget_mixin]
537pub struct FocusableMix<P>(P);
538impl<P: WidgetImpl> FocusableMix<P> {
539 fn widget_intrinsic(&mut self) {
540 widget_set! {
541 self;
542 focusable = true;
543 when *#is_focused_hgl {
544 zng_wgt_fill::foreground_highlight = {
545 offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
546 widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
547 sides: FOCUS_HIGHLIGHT_SIDES_VAR,
548 };
549 }
550 }
551 }
552}
553
554context_var! {
555 pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
557 pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
559 pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
561}
562
563#[property(
565 CONTEXT,
566 default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
567 widget_impl(FocusableMix<P>)
568)]
569pub fn focus_highlight(
570 child: impl IntoUiNode,
571 offsets: impl IntoVar<SideOffsets>,
572 widths: impl IntoVar<SideOffsets>,
573 sides: impl IntoVar<BorderSides>,
574) -> UiNode {
575 let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
576 let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
577 with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
578}