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}
48
49impl<Msg> Wrapper<Msg> {
50 fn new(child: Box<dyn Widget<Msg>>) -> Self {
51 Self {
52 child: vec![child],
53 focusable: false,
54 descendants_focusable: true,
55 skip_traversal: false,
56 order: None,
57 group: false,
58 }
59 }
60}
61
62impl<Msg: Clone> Widget<Msg> for Wrapper<Msg> {
63 fn style(&self) -> Style {
64 sizing_of(self.child[0].style())
65 }
66
67 fn style_themed(&self, theme: &Theme) -> Style {
68 sizing_of(self.child[0].style_themed(theme))
69 }
70
71 fn build_themed(&self, theme: &Theme) {
72 self.child[0].build_themed(theme);
73 }
74
75 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
76 &self.child
77 }
78
79 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
80
81 fn on_click(&self) -> Option<Msg> {
82 None
83 }
84
85 fn focusable(&self) -> bool {
86 self.focusable
87 }
88
89 fn descendants_focusable(&self) -> bool {
90 self.descendants_focusable
91 }
92
93 fn focus_skip_traversal(&self) -> bool {
94 self.skip_traversal
95 }
96
97 fn focus_order(&self) -> Option<f32> {
98 self.order
99 }
100
101 fn focus_group(&self) -> bool {
102 self.group
103 }
104}
105
106pub struct Focus<Msg = crate::callback::Callback>(Wrapper<Msg>);
109
110impl<Msg> Focus<Msg> {
111 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
113 let mut w = Wrapper::new(Box::new(child));
114 w.focusable = true;
115 Self(w)
116 }
117
118 pub fn can_request_focus(mut self, can: bool) -> Self {
121 self.0.focusable = can;
122 self
123 }
124
125 pub fn skip_traversal(mut self) -> Self {
127 self.0.skip_traversal = true;
128 self
129 }
130}
131
132pub struct ExcludeFocus<Msg = crate::callback::Callback>(Wrapper<Msg>);
135
136impl<Msg> ExcludeFocus<Msg> {
137 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
139 let mut w = Wrapper::new(Box::new(child));
140 w.descendants_focusable = false;
141 Self(w)
142 }
143}
144
145pub struct ExcludeFocusTraversal<Msg = crate::callback::Callback>(Wrapper<Msg>);
150
151impl<Msg> ExcludeFocusTraversal<Msg> {
152 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
154 let mut w = Wrapper::new(Box::new(child));
155 w.skip_traversal = true;
156 Self(w)
157 }
158}
159
160pub struct FocusTraversalOrder<Msg = crate::callback::Callback>(Wrapper<Msg>);
165
166impl<Msg> FocusTraversalOrder<Msg> {
167 pub fn new(order: f32, child: impl Widget<Msg> + 'static) -> Self {
169 let mut w = Wrapper::new(Box::new(child));
170 w.order = Some(order);
171 Self(w)
172 }
173}
174
175pub struct FocusTraversalGroup<Msg = crate::callback::Callback>(Wrapper<Msg>);
178
179impl<Msg> FocusTraversalGroup<Msg> {
180 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
182 let mut w = Wrapper::new(Box::new(child));
183 w.group = true;
184 Self(w)
185 }
186}
187
188macro_rules! delegate {
191 ($($ty:ident),* $(,)?) => {$(
192 impl<Msg: Clone> Widget<Msg> for $ty<Msg> {
193 fn style(&self) -> Style {
194 Widget::<Msg>::style(&self.0)
195 }
196 fn style_themed(&self, theme: &Theme) -> Style {
197 Widget::<Msg>::style_themed(&self.0, theme)
198 }
199 fn build_themed(&self, theme: &Theme) {
200 Widget::<Msg>::build_themed(&self.0, theme)
201 }
202 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
203 Widget::<Msg>::children(&self.0)
204 }
205 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
206 Widget::<Msg>::paint(&self.0, bounds, status, theme, scene)
207 }
208 fn on_click(&self) -> Option<Msg> {
209 Widget::<Msg>::on_click(&self.0)
210 }
211 fn focusable(&self) -> bool {
212 Widget::<Msg>::focusable(&self.0)
213 }
214 fn descendants_focusable(&self) -> bool {
215 Widget::<Msg>::descendants_focusable(&self.0)
216 }
217 fn focus_skip_traversal(&self) -> bool {
218 Widget::<Msg>::focus_skip_traversal(&self.0)
219 }
220 fn focus_order(&self) -> Option<f32> {
221 Widget::<Msg>::focus_order(&self.0)
222 }
223 fn focus_group(&self) -> bool {
224 Widget::<Msg>::focus_group(&self.0)
225 }
226 }
227 )*};
228}
229
230delegate!(
231 Focus,
232 ExcludeFocus,
233 ExcludeFocusTraversal,
234 FocusTraversalOrder,
235 FocusTraversalGroup,
236);
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::runtime::Runtime;
242 use crate::ui::build_ui;
243 use crate::{Container, TextField};
244 use frus_core::Size;
245
246 #[derive(Clone, Debug, PartialEq)]
247 enum Msg {
248 Edited(String),
249 }
250
251 fn field(label: &str) -> TextField<Msg> {
253 TextField::new(label.to_string())
254 .label(label.to_string())
255 .on_input(Msg::Edited)
256 }
257
258 fn order_of(root: &dyn Widget<Msg>) -> Vec<usize> {
259 let ui = build_ui(
260 root,
261 Size::new(400.0, 400.0),
262 &Runtime::default(),
263 &Theme::default(),
264 );
265 let ids: Vec<_> = ui.focusable_ids().collect();
266 ui.traversal_order()
267 .iter()
268 .map(|id| ids.iter().position(|x| x == id).expect("a known stop"))
269 .collect()
270 }
271
272 #[test]
274 fn tab_follows_the_tree_by_default() {
275 let root = crate::column![field("a"), field("b"), field("c")];
276 assert_eq!(order_of(&root), vec![0, 1, 2]);
277 }
278
279 #[test]
281 fn an_order_moves_only_what_it_names() {
282 let root = crate::column![
283 FocusTraversalOrder::new(2.0, field("a")),
284 FocusTraversalOrder::new(1.0, field("b")),
285 field("c"),
286 ];
287 assert_eq!(order_of(&root), vec![1, 0, 2]);
289 }
290
291 #[test]
293 fn skipping_traversal_leaves_the_stop_where_it_was() {
294 let root = crate::column![
295 field("a"),
296 ExcludeFocusTraversal::new(field("b")),
297 field("c"),
298 ];
299 assert_eq!(order_of(&root), vec![0, 2], "Tab passes b by");
300
301 let ui = build_ui(
302 &root,
303 Size::new(400.0, 400.0),
304 &Runtime::default(),
305 &Theme::default(),
306 );
307 assert_eq!(
308 ui.focusable_ids().count(),
309 3,
310 "but b is still somewhere a click can land"
311 );
312 }
313
314 #[test]
316 fn an_excluded_subtree_registers_nothing() {
317 let root = crate::column![field("a"), ExcludeFocus::new(field("b")), field("c")];
318 let ui = build_ui(
319 &root,
320 Size::new(400.0, 400.0),
321 &Runtime::default(),
322 &Theme::default(),
323 );
324 assert_eq!(ui.focusable_ids().count(), 2);
325 assert_eq!(order_of(&root), vec![0, 1]);
326 }
327
328 #[test]
332 fn the_flags_do_not_leak_to_what_comes_after() {
333 let root = crate::column![
334 ExcludeFocus::new(crate::column![field("a"), field("b")]),
335 field("c"),
336 field("d"),
337 ];
338 let ui = build_ui(
339 &root,
340 Size::new(400.0, 400.0),
341 &Runtime::default(),
342 &Theme::default(),
343 );
344 assert_eq!(ui.focusable_ids().count(), 2, "only c and d");
345 }
346
347 #[test]
349 fn a_group_resolves_its_order_among_its_own_members() {
350 let root = crate::column![
351 field("page-1"),
352 FocusTraversalGroup::new(crate::column![
353 FocusTraversalOrder::new(2.0, field("dialog-1")),
354 FocusTraversalOrder::new(1.0, field("dialog-2")),
355 ]),
356 field("page-2"),
357 ];
358 assert_eq!(order_of(&root), vec![0, 2, 1, 3]);
360 }
361
362 #[test]
364 fn it_makes_an_ordinary_box_a_focus_stop() {
365 let plain = Container::<Msg>::new().width(40.0).height(40.0);
366 assert!(!Widget::<Msg>::focusable(&plain));
367 let wrapped = Focus::new(Container::<Msg>::new().width(40.0).height(40.0));
368 assert!(Widget::<Msg>::focusable(&wrapped));
369 assert!(!Widget::<Msg>::focusable(
370 &Focus::new(Container::<Msg>::new()).can_request_focus(false)
371 ));
372 }
373}