Give focus to any clickable widget with tab/shift-tab

Use space or enter to click the selected widget.
Use arrow keys to adjust sliders and `DragValue`s.

Closes https://github.com/emilk/egui/issues/31
This commit is contained in:
Emil Ernerfeldt 2021-03-07 18:15:57 +01:00
parent 6fd7c422ab
commit a370339db7
12 changed files with 130 additions and 52 deletions

View file

@ -9,6 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Added ⭐
* You can now give focus to any clickable widget with tab/shift-tab.
* Use space or enter to click it.
* Use arrow keys to adjust sliders and `DragValue`s.
### Fixed 🐛
* Fixed secondary-click to open a menu

View file

@ -198,9 +198,26 @@ impl CtxRef {
if !enabled || sense == Sense::hover() || !layer_id.allow_interaction() {
// Not interested or allowed input:
self.memory().surrender_kb_focus(id);
return response;
}
if sense.click {
self.memory().interested_in_kb_focus(id);
}
if response.has_kb_focus() && response.clicked_elsewhere() {
self.memory().surrender_kb_focus(id);
}
if sense.click
&& response.has_kb_focus()
&& (self.input().key_pressed(Key::Space) || self.input().key_pressed(Key::Enter))
{
// Space/enter works like a primary click for e.g. selected buttons
response.clicked[PointerButton::Primary as usize] = true;
}
self.register_interaction_id(id, rect.min);
let mut memory = self.memory();
@ -729,6 +746,16 @@ impl Context {
self.wants_keyboard_input()
))
.on_hover_text("Is egui currently listening for text input");
ui.label(format!(
"keyboard focus widget: {}",
self.memory()
.interaction
.kb_focus_id
.as_ref()
.map(Id::short_debug_format)
.unwrap_or_default()
))
.on_hover_text("Is egui currently listening for text input");
ui.advance_cursor(16.0);
CollapsingHeader::new("📥 Input")

View file

@ -120,16 +120,24 @@ impl InputState {
/// Was the given key pressed this frame?
pub fn key_pressed(&self, desired_key: Key) -> bool {
self.events.iter().any(|event| {
matches!(
event,
Event::Key {
key,
pressed: true,
..
} if *key == desired_key
)
})
self.num_presses(desired_key) > 0
}
/// How many times were the given key pressed this frame?
pub fn num_presses(&self, desired_key: Key) -> usize {
self.events
.iter()
.filter(|event| {
matches!(
event,
Event::Key {
key,
pressed: true,
..
} if *key == desired_key
)
})
.count()
}
/// Is the given key currently held down?

View file

@ -153,6 +153,18 @@ impl Interaction {
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.kb_focus_id = None;
break;
}
if let crate::Event::Key {
key: crate::Key::Tab,
pressed: true,
@ -167,6 +179,31 @@ impl Interaction {
}
}
}
pub(crate) fn had_kb_focus_last_frame(&self, id: Id) -> bool {
self.kb_focus_id_previous_frame == Some(id)
}
fn interested_in_kb_focus(&mut self, id: Id) {
if self.kb_focus_give_to_next && !self.had_kb_focus_last_frame(id) {
self.kb_focus_id = Some(id);
self.kb_focus_give_to_next = false;
} else if self.kb_focus_id == Some(id) {
if self.pressed_tab {
self.kb_focus_id = None;
self.kb_focus_give_to_next = true;
self.pressed_tab = false;
} else if self.pressed_shift_tab {
self.kb_focus_id = self.kb_focus_last_interested;
self.pressed_shift_tab = false;
}
} else if self.pressed_tab && self.kb_focus_id == None && !self.kb_focus_give_to_next {
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
self.kb_focus_id = Some(id);
}
self.kb_focus_last_interested = Some(id);
}
}
impl Memory {
@ -238,21 +275,7 @@ impl Memory {
/// Register this widget as being interested in getting keyboard focus.
/// This will allow the user to select it with tab and shift-tab.
pub fn interested_in_kb_focus(&mut self, id: Id) {
if self.interaction.kb_focus_give_to_next {
self.interaction.kb_focus_id = Some(id);
self.interaction.kb_focus_give_to_next = false;
} else if self.has_kb_focus(id) {
if self.interaction.pressed_tab {
self.interaction.kb_focus_id = None;
self.interaction.kb_focus_give_to_next = true;
self.interaction.pressed_tab = false;
} else if self.interaction.pressed_shift_tab {
self.interaction.kb_focus_id = self.interaction.kb_focus_last_interested;
self.interaction.pressed_shift_tab = false;
}
}
self.interaction.kb_focus_last_interested = Some(id);
self.interaction.interested_in_kb_focus(id);
}
/// Stop editing of active `TextEdit` (if any).

View file

@ -421,6 +421,7 @@ impl std::ops::BitOrAssign for Response {
/// inner_resp.response.on_hover_text("You hovered the horizontal layout");
/// assert_eq!(inner_resp.inner, 42);
/// ```
#[derive(Debug)]
pub struct InnerResponse<R> {
pub inner: R,
pub response: Response,

View file

@ -228,6 +228,7 @@ impl<'a> Widget for Checkbox<'a> {
desired_size = desired_size.at_least(spacing.interact_size);
desired_size.y = desired_size.y.max(icon_width);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.clicked() {
*checked = !*checked;
response.mark_changed();

View file

@ -281,7 +281,20 @@ impl<'a> Widget for DragValue<'a> {
drag_state.last_dragged_value = Some(stored_value);
ui.memory().drag_value = drag_state;
}
} else if response.has_kb_focus() {
let change = ui.input().num_presses(Key::ArrowUp) as f64
+ ui.input().num_presses(Key::ArrowRight) as f64
- ui.input().num_presses(Key::ArrowDown) as f64
- ui.input().num_presses(Key::ArrowLeft) as f64;
if change != 0.0 {
let new_value = value + speed * change;
let new_value = emath::round_to_decimals(new_value, auto_decimals);
let new_value = clamp(new_value, clamp_range);
set(&mut get_set_value, new_value);
}
}
response
};

View file

@ -64,7 +64,7 @@ impl Widget for Hyperlink {
let color = ui.visuals().hyperlink_color;
let visuals = ui.style().interact(&response);
if response.hovered() {
if response.hovered() || response.has_kb_focus() {
// Underline:
for row in &galley.rows {
let rect = row.rect().translate(rect.min.to_vec2());

View file

@ -283,7 +283,7 @@ impl Widget for Label {
total_response
} else {
let galley = self.layout(ui);
let (rect, response) = ui.allocate_exact_size(galley.size, Sense::click());
let (rect, response) = ui.allocate_exact_size(galley.size, Sense::hover());
self.paint_galley(ui, rect.min, galley);
response
}

View file

@ -63,7 +63,7 @@ impl Widget for SelectableLabel {
let visuals = ui.style().interact_selectable(&response, selected);
if selected || response.hovered() {
if selected || response.hovered() || response.has_kb_focus() {
let rect = rect.expand(visuals.expansion);
let corner_radius = 2.0;

View file

@ -322,6 +322,27 @@ impl<'a> Slider<'a> {
self.set_value(new_value);
}
if response.has_kb_focus() {
let kb_step = ui.input().num_presses(Key::ArrowRight) as f32
- ui.input().num_presses(Key::ArrowLeft) as f32;
if kb_step != 0.0 {
let prev_value = self.get_value();
let prev_x = self.x_from_value(prev_value, x_range.clone());
let new_x = prev_x + kb_step;
let new_value = if self.smart_aim {
let aim_radius = ui.input().aim_radius();
emath::smart_aim::best_in_range_f64(
self.value_from_x(new_x - aim_radius, x_range.clone()),
self.value_from_x(new_x + aim_radius, x_range.clone()),
)
} else {
self.value_from_x(new_x, x_range.clone())
};
self.set_value(new_value);
}
}
// Paint it:
{
let value = self.get_value();

View file

@ -243,7 +243,7 @@ impl<'t> Widget for TextEdit<'t> {
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
let response = self.content_ui(&mut content_ui);
let frame_rect = response.rect.expand2(margin);
let response = response | ui.allocate_rect(frame_rect, Sense::click());
let response = response | ui.allocate_rect(frame_rect, Sense::hover());
if frame {
let visuals = ui.style().interact(&response);
@ -322,10 +322,6 @@ impl<'t> TextEdit<'t> {
};
let mut response = ui.interact(rect, id, sense);
if enabled {
ui.memory().interested_in_kb_focus(id);
}
if enabled {
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
// TODO: triple-click to select whole paragraph
@ -371,14 +367,6 @@ impl<'t> TextEdit<'t> {
}
}
if response.clicked_elsewhere() {
ui.memory().surrender_kb_focus(id);
}
if !enabled {
ui.memory().surrender_kb_focus(id);
}
if response.hovered() && enabled {
ui.output().cursor_icon = CursorIcon::Text;
}
@ -449,20 +437,10 @@ impl<'t> TextEdit<'t> {
insert_text(&mut ccursor, text, "\n");
Some(CCursorPair::one(ccursor))
} else {
// Common to end input with enter
ui.memory().surrender_kb_focus(id);
ui.memory().surrender_kb_focus(id); // End input with enter
break;
}
}
Event::Key {
key: Key::Escape,
pressed: true,
..
} => {
ui.memory().surrender_kb_focus(id);
break;
}
Event::Key {
key: Key::Z,
pressed: true,