Browse Source

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

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

* Fix using arrow-keys to move slider

* Typo
pull/3354/head
Emil Ernerfeldt 1 year ago
committed by GitHub
parent
commit
ceb8723c5e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      crates/egui/src/context.rs
  2. 59
      crates/egui/src/data/input.rs
  3. 9
      crates/egui/src/input_state.rs
  4. 120
      crates/egui/src/memory.rs
  5. 10
      crates/egui/src/widgets/slider.rs
  6. 30
      crates/egui/src/widgets/text_edit/builder.rs

2
crates/egui/src/context.rs

@ -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())
}),
});

59
crates/egui/src/data/input.rs

@ -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
crates/egui/src/input_state.rs

@ -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()
}
}
// ----------------------------------------------------------------------------

120
crates/egui/src/memory.rs

@ -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};
// ----------------------------------------------------------------------------
@ -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>,
@ -237,9 +237,6 @@ 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,
@ -247,6 +244,22 @@ pub(crate) struct Focus {
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 {
@ -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")]
{
@ -295,6 +309,7 @@ impl Focus {
self.focus_direction = FocusDirection::None;
for event in &new_input.events {
if !event_filter.matches(event) {
if let crate::Event::Key {
key,
pressed: true,
@ -307,20 +322,16 @@ impl Focus {
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;
self.focused_widget = None;
Some(FocusDirection::None)
}
_ => None,
@ -328,6 +339,7 @@ impl Focus {
self.focus_direction = cardinality;
}
}
}
#[cfg(feature = "accesskit")]
{
@ -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;
}
}
}
@ -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();
@ -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();
}
@ -424,7 +436,7 @@ impl Focus {
}
}
let Some(focus_id) = self.id else {
let Some(current_focused) = self.focused_widget else {
return None;
};
@ -449,7 +461,7 @@ 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;
};
@ -457,7 +469,7 @@ impl Focus {
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;
}
@ -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;
}
}
@ -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)]

10
crates/egui/src/widgets/slider.rs

@ -570,6 +570,16 @@ impl<'a> Slider<'a> {
let mut increment = 0usize;
if response.has_focus() {
ui.ctx().memory_mut(|m| {
m.set_focus_lock_filter(
response.id,
EventFilter {
arrows: true, // pressing arrows should not move focus to next widget
..Default::default()
},
);
});
let (dec_key, inc_key) = match self.orientation {
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
// Note that this is for moving the slider position,

30
crates/egui/src/widgets/text_edit/builder.rs

@ -65,7 +65,7 @@ pub struct TextEdit<'t> {
interactive: bool,
desired_width: Option<f32>,
desired_height_rows: usize,
lock_focus: bool,
event_filter: EventFilter,
cursor_at_end: bool,
min_size: Vec2,
align: Align2,
@ -115,7 +115,11 @@ impl<'t> TextEdit<'t> {
interactive: true,
desired_width: None,
desired_height_rows: 4,
lock_focus: false,
event_filter: EventFilter {
arrows: true, // moving the cursor is really important
tab: false, // tab is used to change focus, not to insert a tab character
..Default::default()
},
cursor_at_end: true,
min_size: Vec2::ZERO,
align: Align2::LEFT_TOP,
@ -127,7 +131,7 @@ impl<'t> TextEdit<'t> {
/// Build a [`TextEdit`] focused on code editing.
/// By default it comes with:
/// - monospaced font
/// - focus lock
/// - focus lock (tab will insert a tab character instead of moving focus)
pub fn code_editor(self) -> Self {
self.font(TextStyle::Monospace).lock_focus(true)
}
@ -266,8 +270,8 @@ impl<'t> TextEdit<'t> {
///
/// When `true`, the widget will keep the focus and pressing TAB
/// will insert the `'\t'` character.
pub fn lock_focus(mut self, b: bool) -> Self {
self.lock_focus = b;
pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
self.event_filter.tab = tab_will_indent;
self
}
@ -352,7 +356,9 @@ impl<'t> TextEdit<'t> {
let margin = self.margin;
let max_rect = ui.available_rect_before_wrap().shrink2(margin);
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
let mut output = self.show_content(&mut content_ui);
let id = output.response.id;
let frame_rect = output.response.rect.expand2(margin);
ui.allocate_space(frame_rect.size());
@ -413,7 +419,7 @@ impl<'t> TextEdit<'t> {
interactive,
desired_width,
desired_height_rows,
lock_focus,
event_filter,
cursor_at_end,
min_size,
align,
@ -569,7 +575,7 @@ impl<'t> TextEdit<'t> {
let mut cursor_range = None;
let prev_cursor_range = state.cursor_range(&galley);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CursorRange::one(galley.end())
@ -589,6 +595,7 @@ impl<'t> TextEdit<'t> {
password,
default_cursor_range,
char_limit,
event_filter,
);
if changed {
@ -880,6 +887,7 @@ fn events(
password: bool,
default_cursor_range: CursorRange,
char_limit: usize,
event_filter: EventFilter,
) -> (bool, CursorRange) {
let mut cursor_range = state.cursor_range(galley).unwrap_or(default_cursor_range);
@ -898,7 +906,7 @@ fn events(
let mut any_change = false;
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
let events = ui.input(|i| i.filtered_events(&event_filter));
for event in &events {
let did_mutate_text = match event {
Event::Copy => {
@ -946,8 +954,7 @@ fn events(
pressed: true,
modifiers,
..
} => {
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
} if multiline => {
let mut ccursor = delete_selected(text, &cursor_range);
if modifiers.shift {
// TODO(emilk): support removing indentation over a selection?
@ -956,9 +963,6 @@ fn events(
insert_text(&mut ccursor, text, "\t", char_limit);
}
Some(CCursorRange::one(ccursor))
} else {
None
}
}
Event::Key {
key: Key::Enter,

Loading…
Cancel
Save