1use frus_core::{Rect, Scene};
33use frus_layout::Style;
34
35use crate::interaction::Status;
36use crate::theme::Theme;
37use crate::widget::{sizing_of, Widget};
38
39struct Wrapper<Msg> {
41 child: Vec<Box<dyn Widget<Msg>>>,
42 focusable: bool,
43 descendants_focusable: bool,
44 skip_traversal: bool,
45 order: Option<f32>,
46 group: bool,
47 on_focus_change: Option<Box<dyn Fn(bool) -> Msg>>,
49}
50
51impl<Msg> Wrapper<Msg> {
52 fn new(child: Box<dyn Widget<Msg>>) -> Self {
53 Self {
54 child: vec![child],
55 focusable: false,
56 descendants_focusable: true,
57 skip_traversal: false,
58 order: None,
59 group: false,
60 on_focus_change: None,
61 }
62 }
63}
64
65impl<Msg: Clone> Widget<Msg> for Wrapper<Msg> {
66 fn style(&self) -> Style {
67 sizing_of(self.child[0].style())
68 }
69
70 fn style_themed(&self, theme: &Theme) -> Style {
71 sizing_of(self.child[0].style_themed(theme))
72 }
73
74 fn build_themed(&self, theme: &Theme) {
75 self.child[0].build_themed(theme);
76 }
77
78 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
79 &self.child
80 }
81
82 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
83
84 fn on_click(&self) -> Option<Msg> {
85 None
86 }
87
88 fn focusable(&self) -> bool {
89 self.focusable
90 }
91
92 fn descendants_focusable(&self) -> bool {
93 self.descendants_focusable
94 }
95
96 fn focus_skip_traversal(&self) -> bool {
97 self.skip_traversal
98 }
99
100 fn focus_order(&self) -> Option<f32> {
101 self.order
102 }
103
104 fn focus_group(&self) -> bool {
105 self.group
106 }
107
108 fn on_focus_change(&self, focused: bool) -> Option<Msg> {
109 self.on_focus_change.as_ref().map(|make| make(focused))
110 }
111}
112
113pub struct Focus<Msg = crate::callback::Callback>(Wrapper<Msg>);
116
117impl<Msg> Focus<Msg> {
118 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
120 let mut w = Wrapper::new(Box::new(child));
121 w.focusable = true;
122 Self(w)
123 }
124
125 pub fn can_request_focus(mut self, can: bool) -> Self {
128 self.0.focusable = can;
129 self
130 }
131
132 pub fn skip_traversal(mut self) -> Self {
134 self.0.skip_traversal = true;
135 self
136 }
137
138 pub fn on_focus_change<R: crate::callback::IntoMsg<Msg>>(
143 mut self,
144 on_change: impl Fn(bool) -> R + 'static,
145 ) -> Self {
146 self.0.on_focus_change = Some(Box::new(crate::callback::handler1(on_change)));
147 self
148 }
149}
150
151pub struct ExcludeFocus<Msg = crate::callback::Callback>(Wrapper<Msg>);
154
155impl<Msg> ExcludeFocus<Msg> {
156 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
158 let mut w = Wrapper::new(Box::new(child));
159 w.descendants_focusable = false;
160 Self(w)
161 }
162}
163
164pub struct ExcludeFocusTraversal<Msg = crate::callback::Callback>(Wrapper<Msg>);
169
170impl<Msg> ExcludeFocusTraversal<Msg> {
171 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
173 let mut w = Wrapper::new(Box::new(child));
174 w.skip_traversal = true;
175 Self(w)
176 }
177}
178
179pub struct FocusTraversalOrder<Msg = crate::callback::Callback>(Wrapper<Msg>);
184
185impl<Msg> FocusTraversalOrder<Msg> {
186 pub fn new(order: f32, child: impl Widget<Msg> + 'static) -> Self {
188 let mut w = Wrapper::new(Box::new(child));
189 w.order = Some(order);
190 Self(w)
191 }
192}
193
194pub struct FocusTraversalGroup<Msg = crate::callback::Callback>(Wrapper<Msg>);
197
198impl<Msg> FocusTraversalGroup<Msg> {
199 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
201 let mut w = Wrapper::new(Box::new(child));
202 w.group = true;
203 Self(w)
204 }
205}
206
207macro_rules! delegate {
210 ($($ty:ident),* $(,)?) => {$(
211 impl<Msg: Clone> Widget<Msg> for $ty<Msg> {
212 fn style(&self) -> Style {
213 Widget::<Msg>::style(&self.0)
214 }
215 fn style_themed(&self, theme: &Theme) -> Style {
216 Widget::<Msg>::style_themed(&self.0, theme)
217 }
218 fn build_themed(&self, theme: &Theme) {
219 Widget::<Msg>::build_themed(&self.0, theme)
220 }
221 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
222 Widget::<Msg>::children(&self.0)
223 }
224 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
225 Widget::<Msg>::paint(&self.0, bounds, status, theme, scene)
226 }
227 fn on_click(&self) -> Option<Msg> {
228 Widget::<Msg>::on_click(&self.0)
229 }
230 fn focusable(&self) -> bool {
231 Widget::<Msg>::focusable(&self.0)
232 }
233 fn descendants_focusable(&self) -> bool {
234 Widget::<Msg>::descendants_focusable(&self.0)
235 }
236 fn focus_skip_traversal(&self) -> bool {
237 Widget::<Msg>::focus_skip_traversal(&self.0)
238 }
239 fn focus_order(&self) -> Option<f32> {
240 Widget::<Msg>::focus_order(&self.0)
241 }
242 fn focus_group(&self) -> bool {
243 Widget::<Msg>::focus_group(&self.0)
244 }
245 fn on_focus_change(&self, focused: bool) -> Option<Msg> {
246 Widget::<Msg>::on_focus_change(&self.0, focused)
247 }
248 }
249 )*};
250}
251
252delegate!(
253 Focus,
254 ExcludeFocus,
255 ExcludeFocusTraversal,
256 FocusTraversalOrder,
257 FocusTraversalGroup,
258);
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::runtime::Runtime;
264 use crate::ui::build_ui;
265 use crate::{Container, TextField};
266 use frus_core::Size;
267
268 #[derive(Clone, Debug, PartialEq)]
269 enum Msg {
270 Edited(String),
271 }
272
273 fn field(label: &str) -> TextField<Msg> {
275 TextField::new(label.to_string())
276 .label(label.to_string())
277 .on_input(Msg::Edited)
278 }
279
280 fn order_of(root: &dyn Widget<Msg>) -> Vec<usize> {
281 let ui = build_ui(
282 root,
283 Size::new(400.0, 400.0),
284 &Runtime::default(),
285 &Theme::default(),
286 );
287 let ids: Vec<_> = ui.focusable_ids().collect();
288 ui.traversal_order()
289 .iter()
290 .map(|id| ids.iter().position(|x| x == id).expect("a known stop"))
291 .collect()
292 }
293
294 #[test]
296 fn tab_follows_the_tree_by_default() {
297 let root = crate::column![field("a"), field("b"), field("c")];
298 assert_eq!(order_of(&root), vec![0, 1, 2]);
299 }
300
301 #[test]
303 fn an_order_moves_only_what_it_names() {
304 let root = crate::column![
305 FocusTraversalOrder::new(2.0, field("a")),
306 FocusTraversalOrder::new(1.0, field("b")),
307 field("c"),
308 ];
309 assert_eq!(order_of(&root), vec![1, 0, 2]);
311 }
312
313 #[test]
315 fn skipping_traversal_leaves_the_stop_where_it_was() {
316 let root = crate::column![
317 field("a"),
318 ExcludeFocusTraversal::new(field("b")),
319 field("c"),
320 ];
321 assert_eq!(order_of(&root), vec![0, 2], "Tab passes b by");
322
323 let ui = build_ui(
324 &root,
325 Size::new(400.0, 400.0),
326 &Runtime::default(),
327 &Theme::default(),
328 );
329 assert_eq!(
330 ui.focusable_ids().count(),
331 3,
332 "but b is still somewhere a click can land"
333 );
334 }
335
336 #[test]
338 fn an_excluded_subtree_registers_nothing() {
339 let root = crate::column![field("a"), ExcludeFocus::new(field("b")), field("c")];
340 let ui = build_ui(
341 &root,
342 Size::new(400.0, 400.0),
343 &Runtime::default(),
344 &Theme::default(),
345 );
346 assert_eq!(ui.focusable_ids().count(), 2);
347 assert_eq!(order_of(&root), vec![0, 1]);
348 }
349
350 #[test]
354 fn the_flags_do_not_leak_to_what_comes_after() {
355 let root = crate::column![
356 ExcludeFocus::new(crate::column![field("a"), field("b")]),
357 field("c"),
358 field("d"),
359 ];
360 let ui = build_ui(
361 &root,
362 Size::new(400.0, 400.0),
363 &Runtime::default(),
364 &Theme::default(),
365 );
366 assert_eq!(ui.focusable_ids().count(), 2, "only c and d");
367 }
368
369 #[test]
371 fn a_group_resolves_its_order_among_its_own_members() {
372 let root = crate::column![
373 field("page-1"),
374 FocusTraversalGroup::new(crate::column![
375 FocusTraversalOrder::new(2.0, field("dialog-1")),
376 FocusTraversalOrder::new(1.0, field("dialog-2")),
377 ]),
378 field("page-2"),
379 ];
380 assert_eq!(order_of(&root), vec![0, 2, 1, 3]);
382 }
383
384 #[test]
386 fn it_makes_an_ordinary_box_a_focus_stop() {
387 let plain = Container::<Msg>::new().width(40.0).height(40.0);
388 assert!(!Widget::<Msg>::focusable(&plain));
389 let wrapped = Focus::new(Container::<Msg>::new().width(40.0).height(40.0));
390 assert!(Widget::<Msg>::focusable(&wrapped));
391 assert!(!Widget::<Msg>::focusable(
392 &Focus::new(Container::<Msg>::new()).can_request_focus(false)
393 ));
394 }
395}
396
397#[cfg(test)]
399mod focus_change_tests {
400 use super::*;
401 use crate::WidgetId;
402 use crate::{build_ui, focus_changes, Flex, Runtime, Size, TextField};
403
404 #[derive(Clone, Debug, PartialEq)]
405 enum Msg {
406 Group(bool),
407 Typed,
408 }
409
410 fn page() -> Flex<Msg> {
412 Flex::column()
413 .width(300.0)
414 .height(300.0)
415 .child(
416 Focus::new(
417 Flex::column()
418 .child(TextField::new("a").on_input(|_| Msg::Typed))
419 .child(TextField::new("b").on_input(|_| Msg::Typed)),
420 )
421 .skip_traversal()
422 .on_focus_change(Msg::Group),
423 )
424 .child(TextField::new("c").on_input(|_| Msg::Typed))
425 }
426
427 fn fields(page: &Flex<Msg>) -> Vec<WidgetId> {
429 let ui = build_ui(
430 page,
431 Size::new(300.0, 300.0),
432 &Runtime::default(),
433 &crate::Theme::default(),
434 );
435 let ids: Vec<WidgetId> = ui.focusable_ids().collect();
436 assert_eq!(ids.len(), 4, "{ids:?}");
438 ids[1..].to_vec()
439 }
440
441 #[test]
444 fn a_group_hears_the_focus_arrive_and_leave() {
445 let page = page();
446 let ids = fields(&page);
447 let (a, b, c) = (Some(ids[0]), Some(ids[1]), Some(ids[2]));
448 assert_eq!(
449 focus_changes(&page, None, a),
450 vec![Msg::Group(true)],
451 "arrived"
452 );
453 assert_eq!(focus_changes(&page, a, b), vec![], "moved inside");
454 assert_eq!(focus_changes(&page, b, c), vec![Msg::Group(false)], "left");
455 assert_eq!(focus_changes(&page, c, a), vec![Msg::Group(true)]);
456 assert_eq!(
457 focus_changes(&page, a, None),
458 vec![Msg::Group(false)],
459 "cleared"
460 );
461 assert_eq!(focus_changes(&page, a, a), vec![], "no move");
462 let gone = Some(WidgetId::ROOT.child(9));
463 assert_eq!(
464 focus_changes(&page, gone, a),
465 vec![Msg::Group(true)],
466 "a widget no longer there is not heard from"
467 );
468 }
469
470 #[test]
472 fn the_one_that_lost_it_hears_first() {
473 let nested = Flex::column()
474 .width(300.0)
475 .height(300.0)
476 .child(
477 Focus::new(TextField::new("a").on_input(|_| Msg::Typed))
478 .skip_traversal()
479 .on_focus_change(|f| if f { Msg::Group(true) } else { Msg::Typed }),
480 )
481 .child(
482 Focus::new(TextField::new("b").on_input(|_| Msg::Typed))
483 .skip_traversal()
484 .on_focus_change(Msg::Group),
485 );
486 let ui = build_ui(
487 &nested,
488 Size::new(300.0, 300.0),
489 &Runtime::default(),
490 &crate::Theme::default(),
491 );
492 let ids: Vec<WidgetId> = ui.focusable_ids().collect();
493 let (a, b) = (Some(ids[1]), Some(ids[3]));
495 assert_eq!(
496 focus_changes(&nested, a, b),
497 vec![Msg::Typed, Msg::Group(true)],
498 "the first group's `false`, then the second's `true`"
499 );
500 }
501
502 #[test]
504 fn nested_groups_hear_it_deepest_first() {
505 let nested = Flex::column()
506 .width(300.0)
507 .height(300.0)
508 .child(
509 Focus::new(
510 Focus::new(TextField::new("a").on_input(|_| Msg::Typed))
511 .skip_traversal()
512 .on_focus_change(|f| Msg::Group(!f)),
513 )
514 .skip_traversal()
515 .on_focus_change(Msg::Group),
516 )
517 .child(TextField::new("b").on_input(|_| Msg::Typed));
518 let ui = build_ui(
519 &nested,
520 Size::new(300.0, 300.0),
521 &Runtime::default(),
522 &crate::Theme::default(),
523 );
524 let ids: Vec<WidgetId> = ui.focusable_ids().collect();
525 let (a, b) = (Some(ids[2]), Some(ids[3]));
527 assert_eq!(
528 focus_changes(&nested, a, b),
529 vec![Msg::Group(true), Msg::Group(false)],
530 "the inner group (which says the opposite) first, then the outer"
531 );
532 assert_eq!(
533 focus_changes(&nested, b, a),
534 vec![Msg::Group(false), Msg::Group(true)],
535 "entering: the inner first, then the outer"
536 );
537 }
538}