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 UiNode, focusable: impl IntoVar<bool>) -> impl 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 UiNode, tab_index: impl IntoVar<TabIndex>) -> impl 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 UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
46 focus_scope_impl(child, is_scope, false)
47}
48#[property(CONTEXT, default(false))]
57pub fn alt_focus_scope(child: impl UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
58 focus_scope_impl(child, is_scope, true)
59}
60
61fn focus_scope_impl(child: impl UiNode, is_scope: impl IntoVar<bool>, is_alt: bool) -> impl 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 UiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> impl 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 UiNode, tab_nav: impl IntoVar<TabNav>) -> impl 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 UiNode, directional_nav: impl IntoVar<DirectionalNav>) -> impl 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 UiNode, shortcuts: impl IntoVar<Shortcuts>) -> impl 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 UiNode, enabled: impl IntoVar<bool>) -> impl 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 UiNode, behavior: impl IntoVar<FocusClickBehavior>) -> impl 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 if args.propagation().is_stopped() && !delegate() {
237 let exit = match behavior.get() {
240 FocusClickBehavior::Ignore => false,
241 FocusClickBehavior::Exit => true,
242 FocusClickBehavior::ExitEnabled => args.target.interactivity().is_enabled(),
243 FocusClickBehavior::ExitHandled => true,
244 };
245 if exit {
246 FOCUS.focus_exit();
247 }
248 }
249 }
250 }
251 })
252}
253context_local! {
254 static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
255}
256
257event_property! {
258 pub fn focus_changed {
260 event: FOCUS_CHANGED_EVENT,
261 args: FocusChangedArgs,
262 }
263
264 pub fn focus {
266 event: FOCUS_CHANGED_EVENT,
267 args: FocusChangedArgs,
268 filter: |args| args.is_focus(WIDGET.id()),
269 }
270
271 pub fn blur {
273 event: FOCUS_CHANGED_EVENT,
274 args: FocusChangedArgs,
275 filter: |args| args.is_blur(WIDGET.id()),
276 }
277
278 pub fn focus_enter {
280 event: FOCUS_CHANGED_EVENT,
281 args: FocusChangedArgs,
282 filter: |args| args.is_focus_enter(WIDGET.id()),
283 }
284
285 pub fn focus_leave {
287 event: FOCUS_CHANGED_EVENT,
288 args: FocusChangedArgs,
289 filter: |args| args.is_focus_leave(WIDGET.id()),
290 }
291}
292
293#[property(CONTEXT, widget_impl(FocusableMix<P>))]
315pub fn is_focused(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
316 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
317 let id = WIDGET.id();
318 if args.is_focus(id) {
319 Some(true)
320 } else if args.is_blur(id) {
321 Some(false)
322 } else {
323 None
324 }
325 })
326}
327
328#[property(CONTEXT, widget_impl(FocusableMix<P>))]
337pub fn is_focus_within(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
338 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
339 let id = WIDGET.id();
340 if args.is_focus_enter(id) {
341 Some(true)
342 } else if args.is_focus_leave(id) {
343 Some(false)
344 } else {
345 None
346 }
347 })
348}
349
350#[property(CONTEXT, widget_impl(FocusableMix<P>))]
363pub fn is_focused_hgl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
364 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
365 let id = WIDGET.id();
366 if args.is_focus(id) {
367 Some(args.highlight)
368 } else if args.is_blur(id) {
369 Some(false)
370 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
371 Some(args.highlight)
372 } else {
373 None
374 }
375 })
376}
377
378#[property(CONTEXT, widget_impl(FocusableMix<P>))]
387pub fn is_focus_within_hgl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
388 event_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
389 let id = WIDGET.id();
390 if args.is_focus_enter(id) {
391 Some(args.highlight)
392 } else if args.is_focus_leave(id) {
393 Some(false)
394 } else if args.is_highlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
395 Some(args.highlight)
396 } else {
397 None
398 }
399 })
400}
401
402#[property(CONTEXT, widget_impl(FocusableMix<P>))]
419pub fn is_return_focus(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
420 event_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
421 let id = WIDGET.id();
422 if args.is_return_focus(id) {
423 Some(true)
424 } else if args.was_return_focus(id) {
425 Some(false)
426 } else {
427 None
428 }
429 })
430}
431
432#[property(CONTEXT, widget_impl(FocusableMix<P>))]
438pub fn is_return_focus_within(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
439 event_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
440 let id = WIDGET.id();
441 if args.is_return_focus_enter(id) {
442 Some(true)
443 } else if args.is_return_focus_leave(id) {
444 Some(false)
445 } else {
446 None
447 }
448 })
449}
450
451#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
457pub fn focus_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
458 let enabled = enabled.into_var();
459
460 enum State {
461 WaitInfo,
462 InfoInited,
463 Done,
464 }
465 let mut state = State::WaitInfo;
466
467 match_node(child, move |_, op| match op {
468 UiNodeOp::Init => {
469 if enabled.get() {
470 state = State::WaitInfo;
471 } else {
472 state = State::Done;
473 }
474 }
475 UiNodeOp::Info { .. } => {
476 if let State::WaitInfo = &state {
477 state = State::InfoInited;
478 WIDGET.update();
480 }
481 }
482 UiNodeOp::Update { .. } => {
483 if let State::InfoInited = &state {
484 state = State::Done;
485 FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
486 }
487 }
488 _ => {}
489 })
490}
491
492#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
502pub fn return_focus_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
503 let enabled = enabled.into_var();
504 let mut return_focus = None;
505 match_node(child, move |_, op| match op {
506 UiNodeOp::Init => {
507 return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
508 }
509 UiNodeOp::Deinit => {
510 if let Some(id) = return_focus.take() {
511 if enabled.get() {
512 if let Some(w) = zng_ext_window::WINDOWS.widget_info(id) {
513 if w.into_focusable(false, false).is_some() {
514 FOCUS.focus_widget(id, false);
516 return;
517 }
518 }
519 WIDGET_INFO_CHANGED_EVENT
521 .on_pre_event(app_hn_once!(|_| {
522 FOCUS.focus_widget(id, false);
523 }))
524 .perm();
525 WIDGET.update_info();
527 }
528 }
529 }
530 _ => {}
531 })
532}
533
534#[widget_mixin]
536pub struct FocusableMix<P>(P);
537impl<P: WidgetImpl> FocusableMix<P> {
538 fn widget_intrinsic(&mut self) {
539 widget_set! {
540 self;
541 focusable = true;
542 when *#is_focused_hgl {
543 zng_wgt_fill::foreground_highlight = {
544 offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
545 widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
546 sides: FOCUS_HIGHLIGHT_SIDES_VAR,
547 };
548 }
549 }
550 }
551}
552
553context_var! {
554 pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
556 pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
558 pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
560}
561
562#[property(
564 CONTEXT,
565 default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
566 widget_impl(FocusableMix<P>)
567)]
568pub fn focus_highlight(
569 child: impl UiNode,
570 offsets: impl IntoVar<SideOffsets>,
571 widths: impl IntoVar<SideOffsets>,
572 sides: impl IntoVar<BorderSides>,
573) -> impl UiNode {
574 let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
575 let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
576 with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
577}