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

Bug fix: arrow keys won't move focus away from TextEdit #3352

Merged
merged 3 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1276,7 +1276,7 @@ impl Context {
nodes,
tree: Some(accesskit::Tree::new(root_id)),
focus: has_focus.then(|| {
let focus_id = self.memory(|mem| mem.interaction.focus.id);
let focus_id = self.memory(|mem| mem.focus());
focus_id.map_or(root_id, |id| id.accesskit_id())
}),
});
Expand Down
59 changes: 59 additions & 0 deletions crates/egui/src/data/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,3 +1028,62 @@ impl From<u32> for TouchId {
Self(id as u64)
}
}

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

// TODO(emilk): generalize this to a proper event filter.
/// Controls which events that a focused widget will have exclusive access to.
///
/// Currently this only controls a few special keyboard events,
/// but in the future this `struct` should be extended into a full callback thing.
///
/// Any events not covered by the filter are given to the widget, but are not exclusive.
#[derive(Clone, Copy, Debug)]
pub struct EventFilter {
/// If `true`, pressing tab will act on the widget,
/// and NOT move focus away from the focused widget.
///
/// Default: `false`
pub tab: bool,

/// If `true`, pressing arrows will act on the widget,
/// and NOT move focus away from the focused widget.
///
/// Default: `false`
pub arrows: bool,

/// If `true`, pressing escape will act on the widget,
/// and NOT surrender focus from the focused widget.
///
/// Default: `false`
pub escape: bool,
}

#[allow(clippy::derivable_impls)] // let's be explicit
impl Default for EventFilter {
fn default() -> Self {
Self {
tab: false,
arrows: false,
escape: false,
}
}
}

impl EventFilter {
pub fn matches(&self, event: &Event) -> bool {
if let Event::Key { key, .. } = event {
match key {
crate::Key::Tab => self.tab,
crate::Key::ArrowUp
| crate::Key::ArrowRight
| crate::Key::ArrowDown
| crate::Key::ArrowLeft => self.arrows,
crate::Key::Escape => self.escape,
_ => true,
}
} else {
true
}
}
}
9 changes: 9 additions & 0 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,15 @@ impl InputState {
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
self.accesskit_action_requests(id, action).count()
}

/// Get all events that matches the given filter.
pub fn filtered_events(&self, filter: &EventFilter) -> Vec<Event> {
self.events
.iter()
.filter(|event| filter.matches(event))
.cloned()
.collect()
}
}

// ----------------------------------------------------------------------------
Expand Down
156 changes: 90 additions & 66 deletions crates/egui/src/memory.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use epaint::{emath::Rangef, vec2, Vec2};

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

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

Expand Down Expand Up @@ -219,7 +219,7 @@ pub(crate) struct Interaction {
#[derive(Clone, Debug, Default)]
pub(crate) struct Focus {
/// The widget with keyboard focus (i.e. a text input field).
pub(crate) id: Option<Id>,
focused_widget: Option<FocusWidget>,

/// What had keyboard focus previous frame?
id_previous_frame: Option<Id>,
Expand All @@ -237,16 +237,29 @@ pub(crate) struct Focus {
/// The last widget interested in focus.
last_interested: Option<Id>,

/// If `true`, pressing tab will NOT move focus away from the current widget.
is_focus_locked: bool,

/// Set when looking for widget with navigational keys like arrows, tab, shift+tab
focus_direction: FocusDirection,

/// A cache of widget ids that are interested in focus with their corresponding rectangles.
focus_widgets_cache: IdMap<Rect>,
}

/// The widget with focus.
#[derive(Clone, Copy, Debug)]
struct FocusWidget {
pub id: Id,
pub filter: EventFilter,
}

impl FocusWidget {
pub fn new(id: Id) -> Self {
Self {
id,
filter: Default::default(),
}
}
}

impl Interaction {
/// Are we currently clicking or dragging an egui widget?
pub fn is_using_pointer(&self) -> bool {
Expand Down Expand Up @@ -278,14 +291,15 @@ impl Interaction {
impl Focus {
/// Which widget currently has keyboard focus?
pub fn focused(&self) -> Option<Id> {
self.id
self.focused_widget.as_ref().map(|w| w.id)
}

fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) {
self.id_previous_frame = self.id;
self.id_previous_frame = self.focused();
if let Some(id) = self.id_next_frame.take() {
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
}
let event_filter = self.focused_widget.map(|w| w.filter).unwrap_or_default();

#[cfg(feature = "accesskit")]
{
Expand All @@ -295,37 +309,35 @@ impl Focus {
self.focus_direction = FocusDirection::None;

for event in &new_input.events {
if let crate::Event::Key {
key,
pressed: true,
modifiers,
..
} = event
{
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 !event_filter.matches(event) {
if let crate::Event::Key {
key,
pressed: true,
modifiers,
..
} = event
{
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 modifiers.shift {
Some(FocusDirection::Previous)
} else {
Some(FocusDirection::Next)
}
} else {
None
}
crate::Key::Escape => {
self.focused_widget = None;
Some(FocusDirection::None)
}
_ => None,
} {
self.focus_direction = cardinality;
}
crate::Key::Escape => {
self.id = None;
self.is_focus_locked = false;
Some(FocusDirection::None)
}
_ => None,
} {
self.focus_direction = cardinality;
}
}

Expand All @@ -346,17 +358,17 @@ 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);
self.focused_widget = Some(FocusWidget::new(found_widget));
}
}

if let Some(id) = self.id {
if let Some(focused_widget) = self.focused_widget {
// Allow calling `request_focus` one frame and not using it until next frame
let recently_gained_focus = self.id_previous_frame != Some(id);
let recently_gained_focus = self.id_previous_frame != Some(focused_widget.id);

if !recently_gained_focus && !used_ids.contains_key(&id) {
if !recently_gained_focus && !used_ids.contains_key(&focused_widget.id) {
// Dead-mans-switch: the widget with focus has disappeared!
self.id = None;
self.focused_widget = None;
}
}
}
Expand All @@ -369,7 +381,7 @@ impl Focus {
#[cfg(feature = "accesskit")]
{
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
self.id_requested_by_accesskit = None;
self.give_to_next = false;
self.reset_focus();
Expand All @@ -382,23 +394,23 @@ impl Focus {
.or_insert(Rect::EVERYTHING);

if self.give_to_next && !self.had_focus_last_frame(id) {
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
self.give_to_next = false;
} else if self.id == Some(id) {
if self.focus_direction == FocusDirection::Next && !self.is_focus_locked {
self.id = None;
} else if self.focused() == Some(id) {
if self.focus_direction == FocusDirection::Next {
self.focused_widget = None;
self.give_to_next = true;
self.reset_focus();
} else if self.focus_direction == FocusDirection::Previous && !self.is_focus_locked {
} else if self.focus_direction == FocusDirection::Previous {
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
self.reset_focus();
}
} else if self.focus_direction == FocusDirection::Next
&& self.id.is_none()
&& self.focused_widget.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.focused_widget = Some(FocusWidget::new(id));
self.reset_focus();
}

Expand All @@ -424,7 +436,7 @@ impl Focus {
}
}

let Some(focus_id) = self.id else {
let Some(current_focused) = self.focused_widget else {
return None;
};

Expand All @@ -449,15 +461,15 @@ impl Focus {
}
});

let Some(current_rect) = self.focus_widgets_cache.get(&focus_id) else {
let Some(current_rect) = self.focus_widgets_cache.get(&current_focused.id) else {
return None;
};

let mut best_score = std::f32::INFINITY;
let mut best_id = None;

for (candidate_id, candidate_rect) in &self.focus_widgets_cache {
if Some(*candidate_id) == self.id {
if *candidate_id == current_focused.id {
continue;
}

Expand Down Expand Up @@ -542,46 +554,58 @@ impl Memory {
/// from the window and back.
#[inline(always)]
pub fn has_focus(&self, id: Id) -> bool {
self.interaction.focus.id == Some(id)
self.interaction.focus.focused() == Some(id)
}

/// Which widget has keyboard focus?
pub fn focus(&self) -> Option<Id> {
self.interaction.focus.id
self.interaction.focus.focused()
}

/// Prevent keyboard focus from moving away from this widget even if users presses the tab key.
/// Set an event filter for a widget.
///
/// This allows you to control whether the widget will loose focus
/// when the user presses tab, arrow keys, or escape.
///
/// You must first give focus to the widget before calling this.
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked = lock_focus;
if let Some(focused) = &mut self.interaction.focus.focused_widget {
if focused.id == id {
focused.filter = event_filter;
}
}
}
}

/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
pub fn has_lock_focus(&self, id: Id) -> bool {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked
} else {
false
}
/// Set an event filter for a widget.
///
/// You must first give focus to the widget before calling this.
#[deprecated = "Use set_focus_lock_filter instead"]
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
self.set_focus_lock_filter(
id,
EventFilter {
tab: lock_focus,
arrows: lock_focus,
escape: false,
},
);
}

/// Give keyboard focus to a specific widget.
/// See also [`crate::Response::request_focus`].
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.interaction.focus.id = Some(id);
self.interaction.focus.is_focus_locked = false;
self.interaction.focus.focused_widget = Some(FocusWidget::new(id));
}

/// Surrender keyboard focus for a specific widget.
/// See also [`crate::Response::surrender_focus`].
#[inline(always)]
pub fn surrender_focus(&mut self, id: Id) {
if self.interaction.focus.id == Some(id) {
self.interaction.focus.id = None;
self.interaction.focus.is_focus_locked = false;
if self.interaction.focus.focused() == Some(id) {
self.interaction.focus.focused_widget = None;
}
}

Expand All @@ -600,7 +624,7 @@ impl Memory {
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)]
pub fn stop_text_input(&mut self) {
self.interaction.focus.id = None;
self.interaction.focus.focused_widget = None;
}

#[inline(always)]
Expand Down
Loading