Skip to content

Commit 737f617

Browse files
committed
Allow widget focus change with keyboard arrows
1 parent 2c7c598 commit 737f617

File tree

2 files changed

+188
-49
lines changed

2 files changed

+188
-49
lines changed

crates/egui/src/memory.rs

+167-39
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use epaint::Vec2;
2+
13
use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};
24

35
// ----------------------------------------------------------------------------
@@ -89,6 +91,24 @@ pub struct Memory {
8991
everything_is_visible: bool,
9092
}
9193

94+
#[derive(Clone, Copy, Debug, Default)]
95+
pub enum FocusDirection {
96+
// Select the widget closest above the current focused widget.
97+
Up,
98+
// Select the widget to the right of the current focused widget.
99+
Right,
100+
// Select the widget below the current focused widget.
101+
Down,
102+
// Select the widget to the left of the the current focused widget.
103+
Left,
104+
// Select the previous widget that had focus.
105+
Previous,
106+
// Select the next widget that wants focus.
107+
Next,
108+
#[default]
109+
None,
110+
}
111+
92112
// ----------------------------------------------------------------------------
93113

94114
/// Some global options that you can read and write.
@@ -200,11 +220,11 @@ pub(crate) struct Focus {
200220
/// If `true`, pressing tab will NOT move focus away from the current widget.
201221
is_focus_locked: bool,
202222

203-
/// Set at the beginning of the frame, set to `false` when "used".
204-
pressed_tab: bool,
223+
/// Set when looking for widget with navigational keys like arrows, tab, shift+tab
224+
focus_direction: FocusDirection,
205225

206-
/// Set at the beginning of the frame, set to `false` when "used".
207-
pressed_shift_tab: bool,
226+
/// A cache of widget ids that are interested in focus with their corresponding rectangles.
227+
focus_widgets_cache: IdMap<Rect>,
208228
}
209229

210230
impl Interaction {
@@ -236,6 +256,10 @@ impl Interaction {
236256
}
237257

238258
impl Focus {
259+
pub fn set_id(&mut self, id: Id) {
260+
self.id = Some(id);
261+
}
262+
239263
/// Which widget currently has keyboard focus?
240264
pub fn focused(&self) -> Option<Id> {
241265
self.id
@@ -244,44 +268,48 @@ impl Focus {
244268
fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) {
245269
self.id_previous_frame = self.id;
246270
if let Some(id) = self.id_next_frame.take() {
247-
self.id = Some(id);
271+
self.set_id(id);
248272
}
249273

250274
#[cfg(feature = "accesskit")]
251275
{
252276
self.id_requested_by_accesskit = None;
253277
}
254278

255-
self.pressed_tab = false;
256-
self.pressed_shift_tab = false;
257-
for event in &new_input.events {
258-
if matches!(
259-
event,
260-
crate::Event::Key {
261-
key: crate::Key::Escape,
262-
pressed: true,
263-
modifiers: _,
264-
..
265-
}
266-
) {
267-
self.id = None;
268-
self.is_focus_locked = false;
269-
break;
270-
}
279+
self.focus_direction = FocusDirection::None;
271280

281+
for event in &new_input.events {
272282
if let crate::Event::Key {
273-
key: crate::Key::Tab,
283+
key,
274284
pressed: true,
275285
modifiers,
276286
..
277287
} = event
278288
{
279-
if !self.is_focus_locked {
280-
if modifiers.shift {
281-
self.pressed_shift_tab = true;
282-
} else {
283-
self.pressed_tab = true;
289+
if let Some(cardinality) = match key {
290+
crate::Key::ArrowUp => Some(FocusDirection::Up),
291+
crate::Key::ArrowRight => Some(FocusDirection::Right),
292+
crate::Key::ArrowDown => Some(FocusDirection::Down),
293+
crate::Key::ArrowLeft => Some(FocusDirection::Left),
294+
crate::Key::Tab => {
295+
if !self.is_focus_locked {
296+
if modifiers.shift {
297+
Some(FocusDirection::Previous)
298+
} else {
299+
Some(FocusDirection::Next)
300+
}
301+
} else {
302+
None
303+
}
284304
}
305+
crate::Key::Escape => {
306+
self.id = None;
307+
self.is_focus_locked = false;
308+
Some(FocusDirection::None)
309+
}
310+
_ => None,
311+
} {
312+
self.focus_direction = cardinality;
285313
}
286314
}
287315

@@ -300,6 +328,15 @@ impl Focus {
300328
}
301329

302330
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
331+
if !matches!(
332+
self.focus_direction,
333+
FocusDirection::None | FocusDirection::Previous | FocusDirection::Next
334+
) {
335+
if let Some(found_widget) = self.find_widget_in_direction(used_ids) {
336+
self.set_id(found_widget);
337+
}
338+
}
339+
303340
if let Some(id) = self.id {
304341
// Allow calling `request_focus` one frame and not using it until next frame
305342
let recently_gained_focus = self.id_previous_frame != Some(id);
@@ -319,34 +356,125 @@ impl Focus {
319356
#[cfg(feature = "accesskit")]
320357
{
321358
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
322-
self.id = Some(id);
359+
self.set_id(id);
323360
self.id_requested_by_accesskit = None;
324361
self.give_to_next = false;
325-
self.pressed_tab = false;
326-
self.pressed_shift_tab = false;
362+
self.reset_focus();
327363
}
328364
}
329365

366+
// The rect is updated at the end of the frame.
367+
self.focus_widgets_cache
368+
.entry(id)
369+
.or_insert(Rect::EVERYTHING);
370+
330371
if self.give_to_next && !self.had_focus_last_frame(id) {
331-
self.id = Some(id);
372+
self.set_id(id);
332373
self.give_to_next = false;
333374
} else if self.id == Some(id) {
334-
if self.pressed_tab && !self.is_focus_locked {
375+
if matches!(self.focus_direction, FocusDirection::Next) && !self.is_focus_locked {
335376
self.id = None;
336377
self.give_to_next = true;
337-
self.pressed_tab = false;
338-
} else if self.pressed_shift_tab && !self.is_focus_locked {
378+
self.reset_focus();
379+
} else if matches!(self.focus_direction, FocusDirection::Previous)
380+
&& !self.is_focus_locked
381+
{
339382
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
340-
self.pressed_shift_tab = false;
383+
self.reset_focus();
341384
}
342-
} else if self.pressed_tab && self.id.is_none() && !self.give_to_next {
385+
} else if matches!(self.focus_direction, FocusDirection::Next)
386+
&& self.id.is_none()
387+
&& !self.give_to_next
388+
{
343389
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
344-
self.id = Some(id);
345-
self.pressed_tab = false;
390+
self.set_id(id);
391+
self.reset_focus();
346392
}
347393

348394
self.last_interested = Some(id);
349395
}
396+
397+
pub fn reset_focus(&mut self) {
398+
self.focus_direction = FocusDirection::None;
399+
}
400+
401+
pub fn find_widget_in_direction(&mut self, new_rects: &IdMap<Rect>) -> Option<Id> {
402+
let Some(focus_id) = self.id else {return None;};
403+
404+
// Update cache with new rects
405+
self.focus_widgets_cache.retain(|id, old_rect| {
406+
if let Some(new_rect) = new_rects.get(id) {
407+
*old_rect = *new_rect;
408+
true // Keep the item
409+
} else {
410+
false // Remove the item
411+
}
412+
});
413+
414+
let current_focus_widget_rect = self.focus_widgets_cache.get(&focus_id).unwrap();
415+
let current_widget_pos = current_focus_widget_rect.left_center();
416+
417+
let mut focus_candidates: Vec<(&Id, f32, f32)> =
418+
Vec::with_capacity(self.focus_widgets_cache.len());
419+
420+
// The local cache is lagging behind the new rectangles so update them
421+
for (widget_id, widget_rect) in &mut self.focus_widgets_cache {
422+
if Some(*widget_id) == self.id {
423+
continue;
424+
}
425+
426+
let candidate_widget_pos = widget_rect.left_center();
427+
428+
let current_to_candidate = candidate_widget_pos - current_widget_pos;
429+
430+
// Early out if widget is not aligned with the focus direction
431+
let aligns_with_focus_direction = match self.focus_direction {
432+
FocusDirection::Up => current_to_candidate.y < 0.0,
433+
FocusDirection::Right => current_to_candidate.x > 0.0,
434+
FocusDirection::Down => current_to_candidate.y > 0.0,
435+
FocusDirection::Left => current_to_candidate.x < 0.0,
436+
_ => false,
437+
};
438+
439+
if !aligns_with_focus_direction {
440+
continue;
441+
}
442+
443+
// Check if the widget has the right dot product and length.
444+
let focus_direction = match self.focus_direction {
445+
FocusDirection::Up => Vec2::UP,
446+
FocusDirection::Right => Vec2::RIGHT,
447+
FocusDirection::Down => Vec2::DOWN,
448+
FocusDirection::Left => Vec2::LEFT,
449+
_ => {
450+
continue;
451+
}
452+
};
453+
454+
let dot_current_candidate = current_to_candidate.dot(focus_direction);
455+
let distance_current_candidate = current_to_candidate.length();
456+
457+
// Only interested in widgets that fall in 90 degrees to right or left from focus vector.
458+
if dot_current_candidate >= 0.0 {
459+
focus_candidates.push((
460+
widget_id,
461+
dot_current_candidate,
462+
distance_current_candidate,
463+
));
464+
}
465+
}
466+
467+
if !focus_candidates.is_empty() {
468+
// Select widget based on highest dot product and than lowest distance.
469+
focus_candidates.sort_by(|(_, dot1, _), (_, dot2, _)| dot2.partial_cmp(dot1).unwrap());
470+
focus_candidates.sort_by(|(_, _, len1), (_, _, len2)| len1.partial_cmp(len2).unwrap());
471+
472+
let (id, _, _) = focus_candidates[0];
473+
return Some(*id);
474+
}
475+
476+
None
477+
}
350478
}
351479

352480
impl Memory {
@@ -430,7 +558,7 @@ impl Memory {
430558
/// See also [`crate::Response::request_focus`].
431559
#[inline(always)]
432560
pub fn request_focus(&mut self, id: Id) {
433-
self.interaction.focus.id = Some(id);
561+
self.interaction.focus.set_id(id);
434562
self.interaction.focus.is_focus_locked = false;
435563
}
436564

examples/hello_world/src/main.rs

+21-10
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ fn main() -> Result<(), eframe::Error> {
1717

1818
struct MyApp {
1919
name: String,
20+
name2: String,
2021
age: u32,
22+
age2: u32,
2123
}
2224

2325
impl Default for MyApp {
2426
fn default() -> Self {
2527
Self {
2628
name: "Arthur".to_owned(),
29+
name2: "Afadsf".to_owned(),
2730
age: 42,
31+
age2: 42,
2832
}
2933
}
3034
}
@@ -33,16 +37,23 @@ impl eframe::App for MyApp {
3337
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
3438
egui::CentralPanel::default().show(ctx, |ui| {
3539
ui.heading("My egui Application");
36-
ui.horizontal(|ui| {
37-
let name_label = ui.label("Your name: ");
38-
ui.text_edit_singleline(&mut self.name)
39-
.labelled_by(name_label.id);
40-
});
41-
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
42-
if ui.button("Click each year").clicked() {
43-
self.age += 1;
44-
}
45-
ui.label(format!("Hello '{}', age {}", self.name, self.age));
40+
41+
ui.vertical(|ui| {
42+
ui.horizontal(|ui| {
43+
ui.text_edit_singleline(&mut self.name);
44+
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age 1"));
45+
if ui.button("First button").clicked() {
46+
self.age += 1;
47+
}
48+
});
49+
ui.horizontal(|ui| {
50+
ui.text_edit_singleline(&mut self.name2);
51+
ui.add(egui::Slider::new(&mut self.age2, 0..=120).text("age 2"));
52+
if ui.button("Second button").clicked() {
53+
self.age += 1;
54+
}
55+
})
56+
})
4657
});
4758
}
4859
}

0 commit comments

Comments
 (0)