Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change focused widget with arrow keys #3272

Merged
merged 13 commits into from
Aug 30, 2023
197 changes: 163 additions & 34 deletions crates/egui/src/memory.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use epaint::{vec2, Vec2};

use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -89,6 +91,41 @@ pub struct Memory {
everything_is_visible: bool,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FocusDirection {
// Select the widget closest above the current focused widget.
Up,

// Select the widget to the right of the current focused widget.
Right,

// Select the widget below the current focused widget.
Down,

// Select the widget to the left of the the current focused widget.
Left,

// Select the previous widget that had focus.
Previous,

// Select the next widget that wants focus.
Next,

/// Don't change focus.

#[default]
None,
}

impl FocusDirection {
fn is_cardinal(&self) -> bool {
!matches!(
self,
FocusDirection::None | FocusDirection::Previous | FocusDirection::Next
)
}
}

// ----------------------------------------------------------------------------

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

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

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

impl Interaction {
Expand Down Expand Up @@ -252,36 +289,40 @@ impl Focus {
self.id_requested_by_accesskit = None;
}

self.pressed_tab = false;
self.pressed_shift_tab = false;
for event in &new_input.events {
if matches!(
event,
crate::Event::Key {
key: crate::Key::Escape,
pressed: true,
modifiers: _,
..
}
) {
self.id = None;
self.is_focus_locked = false;
break;
}
self.focus_direction = FocusDirection::None;

for event in &new_input.events {
if let crate::Event::Key {
key: crate::Key::Tab,
key,
pressed: true,
modifiers,
..
} = event
{
if !self.is_focus_locked {
if modifiers.shift {
self.pressed_shift_tab = true;
} else {
self.pressed_tab = true;
if let Some(cardinality) = match key {
crate::Key::ArrowUp => Some(FocusDirection::Up),
crate::Key::ArrowRight => Some(FocusDirection::Right),
crate::Key::ArrowDown => Some(FocusDirection::Down),
crate::Key::ArrowLeft => Some(FocusDirection::Left),
crate::Key::Tab => {
if !self.is_focus_locked {
if modifiers.shift {
Some(FocusDirection::Previous)
} else {
Some(FocusDirection::Next)
}
} else {
None
}
}
crate::Key::Escape => {
self.id = None;
self.is_focus_locked = false;
Some(FocusDirection::None)
}
_ => None,
} {
self.focus_direction = cardinality;
}
}

Expand All @@ -300,6 +341,12 @@ impl Focus {
}

pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
if self.focus_direction.is_cardinal() {
if let Some(found_widget) = self.find_widget_in_direction(used_ids) {
self.id = Some(found_widget);
}
}

if let Some(id) = self.id {
// Allow calling `request_focus` one frame and not using it until next frame
let recently_gained_focus = self.id_previous_frame != Some(id);
Expand All @@ -322,31 +369,113 @@ impl Focus {
self.id = Some(id);
self.id_requested_by_accesskit = None;
self.give_to_next = false;
self.pressed_tab = false;
self.pressed_shift_tab = false;
self.reset_focus();
}
}

// The rect is updated at the end of the frame.
self.focus_widgets_cache
.entry(id)
.or_insert(Rect::EVERYTHING);
Comment on lines +379 to +382
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why insert it in focus_widgets_cache here at all? Cannot we just assign self.focus_widgets_cache = new_rect.clone(); in end_frame?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that because I was under the belive that used_ids: &IdMap<Rect> was not directly identical to the widgets id's passed into interested_in_focus. So I am inserting them such that I know which widgets to keep track of. Is that assumption correct?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no that makes sense 👍


if self.give_to_next && !self.had_focus_last_frame(id) {
self.id = Some(id);
self.give_to_next = false;
} else if self.id == Some(id) {
if self.pressed_tab && !self.is_focus_locked {
if self.focus_direction == FocusDirection::Next && !self.is_focus_locked {
self.id = None;
self.give_to_next = true;
self.pressed_tab = false;
} else if self.pressed_shift_tab && !self.is_focus_locked {
self.reset_focus();
} else if self.focus_direction == FocusDirection::Previous && !self.is_focus_locked {
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
self.pressed_shift_tab = false;
self.reset_focus();
}
} else if self.pressed_tab && self.id.is_none() && !self.give_to_next {
} else if self.focus_direction == FocusDirection::Next
&& self.id.is_none()
&& !self.give_to_next
{
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
self.id = Some(id);
self.pressed_tab = false;
self.reset_focus();
}

self.last_interested = Some(id);
}

pub fn reset_focus(&mut self) {
self.focus_direction = FocusDirection::None;
}

pub fn find_widget_in_direction(&mut self, new_rects: &IdMap<Rect>) -> Option<Id> {
let Some(focus_id) = self.id else {return None;};

// Check if the widget has the right dot product and length.
let focus_direction = match self.focus_direction {
FocusDirection::Up => Vec2::UP,
FocusDirection::Right => Vec2::RIGHT,
FocusDirection::Down => Vec2::DOWN,
FocusDirection::Left => Vec2::LEFT,
_ => {
return None;
}
};

// Update cache with new rects
self.focus_widgets_cache.retain(|id, old_rect| {
if let Some(new_rect) = new_rects.get(id) {
*old_rect = *new_rect;
true // Keep the item
} else {
false // Remove the item
}
});

let current_focus_widget_rect = *self.focus_widgets_cache.get(&focus_id).unwrap();

let mut focus_candidates: Vec<(&Id, f32, f32)> =
Vec::with_capacity(self.focus_widgets_cache.len());

for (widget_id, widget_rect) in &mut self.focus_widgets_cache {
if Some(*widget_id) == self.id {
continue;
}

let x_overlaps = -current_focus_widget_rect
.x_range()
.overlaps(widget_rect.x_range());
let y_overlaps = -current_focus_widget_rect
.y_range()
.overlaps(widget_rect.y_range());

let current_to_candidate = vec2(x_overlaps, y_overlaps);

let dot_current_candidate = current_to_candidate.normalized().dot(focus_direction);
let distance_current_candidate = current_to_candidate.length();

// Only interested in widgets that fall in 90 degrees to right or left from focus vector.
if dot_current_candidate > 0.0 {
focus_candidates.push((
widget_id,
dot_current_candidate,
distance_current_candidate,
));
}
}

if !focus_candidates.is_empty() {
// The ratio that decides what widget is closed in the desired direction.
// A high dot product or a small distance are individually not good metrics.
focus_candidates.sort_by(|(_, dot1, len1), (_, dot2, len2)| {
(len1 / dot1).partial_cmp(&(len2 / dot2)).unwrap()
});
focus_candidates.sort_by(|(_, dot1, _), (_, dot2, _)| dot2.partial_cmp(dot1).unwrap());

let (id, _, _) = focus_candidates[0];
return Some(*id);
}

None
}
}

impl Memory {
Expand Down
36 changes: 36 additions & 0 deletions crates/emath/src/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,48 @@ impl Rangef {
self.max - self.min
}

/// The length of the range, i.e. `max - min`.
#[inline]
pub fn center(self) -> f32 {
self.min + (self.span() / 2.0)
}

/// Returns the similarity between `self` and `other`.
///
/// * negative if `a` is left of `b`
/// * positive if `a` is right of `b`
/// * zero if the ranges overlap significantly
#[inline]
pub fn similarity(self, other: Rangef) -> f32 {
TimonPost marked this conversation as resolved.
Show resolved Hide resolved
if self.contains_range(other) || other.contains_range(self) {
0.0
} else {
-(other.center() - self.center())
}
}

#[inline]
#[must_use]
pub fn contains(self, x: f32) -> bool {
self.min <= x && x <= self.max
}

/// Returns if `self` contains `other` for at least 50%.
#[inline]
pub fn contains_range(&self, other: Rangef) -> bool {
emilk marked this conversation as resolved.
Show resolved Hide resolved
let overlap_start = self.min.max(other.min);
let overlap_end = self.max.min(other.max);

if overlap_start >= overlap_end {
return false;
}

let overlap_length = overlap_end - overlap_start;
let other_length = other.max - other.min;

overlap_length >= 0.5 * other_length
}

/// Equivalent to `x.clamp(min, max)`
#[inline]
#[must_use]
Expand Down
32 changes: 22 additions & 10 deletions examples/hello_world/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,44 @@ fn main() -> Result<(), eframe::Error> {

struct MyApp {
name: String,
name2: String,
age: u32,
age2: u32,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please revert the changes in this file. Use egui_demo_app (cargo r) for a better test of your code!

}

impl Default for MyApp {
fn default() -> Self {
Self {
name: "Arthur".to_owned(),
name2: "Afadsf".to_owned(),
age: 42,
age2: 42,
}
}
}

impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.set_debug_on_hover(true);
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
let name_label = ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name)
.labelled_by(name_label.id);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
self.age += 1;
}
ui.label(format!("Hello '{}', age {}", self.name, self.age));

ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut self.name);
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age 1"));
if ui.button("First button").clicked() {
self.age += 1;
}
});
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut self.name2);
ui.add(egui::Slider::new(&mut self.age2, 0..=120).text("age 2"));
if ui.button("Second button").clicked() {
self.age += 1;
}
})
})
});
}
}
Loading