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:
parent
6fd7c422ab
commit
a370339db7
12 changed files with 130 additions and 52 deletions
|
@ -9,6 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## 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 🐛
|
||||||
|
|
||||||
* Fixed secondary-click to open a menu
|
* Fixed secondary-click to open a menu
|
||||||
|
|
|
@ -198,9 +198,26 @@ impl CtxRef {
|
||||||
|
|
||||||
if !enabled || sense == Sense::hover() || !layer_id.allow_interaction() {
|
if !enabled || sense == Sense::hover() || !layer_id.allow_interaction() {
|
||||||
// Not interested or allowed input:
|
// Not interested or allowed input:
|
||||||
|
self.memory().surrender_kb_focus(id);
|
||||||
return response;
|
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);
|
self.register_interaction_id(id, rect.min);
|
||||||
|
|
||||||
let mut memory = self.memory();
|
let mut memory = self.memory();
|
||||||
|
@ -729,6 +746,16 @@ impl Context {
|
||||||
self.wants_keyboard_input()
|
self.wants_keyboard_input()
|
||||||
))
|
))
|
||||||
.on_hover_text("Is egui currently listening for text 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);
|
ui.advance_cursor(16.0);
|
||||||
|
|
||||||
CollapsingHeader::new("📥 Input")
|
CollapsingHeader::new("📥 Input")
|
||||||
|
|
|
@ -120,16 +120,24 @@ impl InputState {
|
||||||
|
|
||||||
/// Was the given key pressed this frame?
|
/// Was the given key pressed this frame?
|
||||||
pub fn key_pressed(&self, desired_key: Key) -> bool {
|
pub fn key_pressed(&self, desired_key: Key) -> bool {
|
||||||
self.events.iter().any(|event| {
|
self.num_presses(desired_key) > 0
|
||||||
matches!(
|
}
|
||||||
event,
|
|
||||||
Event::Key {
|
/// How many times were the given key pressed this frame?
|
||||||
key,
|
pub fn num_presses(&self, desired_key: Key) -> usize {
|
||||||
pressed: true,
|
self.events
|
||||||
..
|
.iter()
|
||||||
} if *key == desired_key
|
.filter(|event| {
|
||||||
)
|
matches!(
|
||||||
})
|
event,
|
||||||
|
Event::Key {
|
||||||
|
key,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
} if *key == desired_key
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the given key currently held down?
|
/// Is the given key currently held down?
|
||||||
|
|
|
@ -153,6 +153,18 @@ impl Interaction {
|
||||||
self.pressed_tab = false;
|
self.pressed_tab = false;
|
||||||
self.pressed_shift_tab = false;
|
self.pressed_shift_tab = false;
|
||||||
for event in &new_input.events {
|
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 {
|
if let crate::Event::Key {
|
||||||
key: crate::Key::Tab,
|
key: crate::Key::Tab,
|
||||||
pressed: true,
|
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 {
|
impl Memory {
|
||||||
|
@ -238,21 +275,7 @@ impl Memory {
|
||||||
/// Register this widget as being interested in getting keyboard focus.
|
/// Register this widget as being interested in getting keyboard focus.
|
||||||
/// This will allow the user to select it with tab and shift-tab.
|
/// This will allow the user to select it with tab and shift-tab.
|
||||||
pub fn interested_in_kb_focus(&mut self, id: Id) {
|
pub fn interested_in_kb_focus(&mut self, id: Id) {
|
||||||
if self.interaction.kb_focus_give_to_next {
|
self.interaction.interested_in_kb_focus(id);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop editing of active `TextEdit` (if any).
|
/// Stop editing of active `TextEdit` (if any).
|
||||||
|
|
|
@ -421,6 +421,7 @@ impl std::ops::BitOrAssign for Response {
|
||||||
/// inner_resp.response.on_hover_text("You hovered the horizontal layout");
|
/// inner_resp.response.on_hover_text("You hovered the horizontal layout");
|
||||||
/// assert_eq!(inner_resp.inner, 42);
|
/// assert_eq!(inner_resp.inner, 42);
|
||||||
/// ```
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct InnerResponse<R> {
|
pub struct InnerResponse<R> {
|
||||||
pub inner: R,
|
pub inner: R,
|
||||||
pub response: Response,
|
pub response: Response,
|
||||||
|
|
|
@ -228,6 +228,7 @@ impl<'a> Widget for Checkbox<'a> {
|
||||||
desired_size = desired_size.at_least(spacing.interact_size);
|
desired_size = desired_size.at_least(spacing.interact_size);
|
||||||
desired_size.y = desired_size.y.max(icon_width);
|
desired_size.y = desired_size.y.max(icon_width);
|
||||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
*checked = !*checked;
|
*checked = !*checked;
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
|
|
|
@ -281,7 +281,20 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
drag_state.last_dragged_value = Some(stored_value);
|
drag_state.last_dragged_value = Some(stored_value);
|
||||||
ui.memory().drag_value = drag_state;
|
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
|
response
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ impl Widget for Hyperlink {
|
||||||
let color = ui.visuals().hyperlink_color;
|
let color = ui.visuals().hyperlink_color;
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
||||||
if response.hovered() {
|
if response.hovered() || response.has_kb_focus() {
|
||||||
// Underline:
|
// Underline:
|
||||||
for row in &galley.rows {
|
for row in &galley.rows {
|
||||||
let rect = row.rect().translate(rect.min.to_vec2());
|
let rect = row.rect().translate(rect.min.to_vec2());
|
||||||
|
|
|
@ -283,7 +283,7 @@ impl Widget for Label {
|
||||||
total_response
|
total_response
|
||||||
} else {
|
} else {
|
||||||
let galley = self.layout(ui);
|
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);
|
self.paint_galley(ui, rect.min, galley);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ impl Widget for SelectableLabel {
|
||||||
|
|
||||||
let visuals = ui.style().interact_selectable(&response, selected);
|
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 rect = rect.expand(visuals.expansion);
|
||||||
|
|
||||||
let corner_radius = 2.0;
|
let corner_radius = 2.0;
|
||||||
|
|
|
@ -322,6 +322,27 @@ impl<'a> Slider<'a> {
|
||||||
self.set_value(new_value);
|
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:
|
// Paint it:
|
||||||
{
|
{
|
||||||
let value = self.get_value();
|
let value = self.get_value();
|
||||||
|
|
|
@ -243,7 +243,7 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
|
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
|
||||||
let response = self.content_ui(&mut content_ui);
|
let response = self.content_ui(&mut content_ui);
|
||||||
let frame_rect = response.rect.expand2(margin);
|
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 {
|
if frame {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
@ -322,10 +322,6 @@ impl<'t> TextEdit<'t> {
|
||||||
};
|
};
|
||||||
let mut response = ui.interact(rect, id, sense);
|
let mut response = ui.interact(rect, id, sense);
|
||||||
|
|
||||||
if enabled {
|
|
||||||
ui.memory().interested_in_kb_focus(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
|
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
|
||||||
// TODO: triple-click to select whole paragraph
|
// 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 {
|
if response.hovered() && enabled {
|
||||||
ui.output().cursor_icon = CursorIcon::Text;
|
ui.output().cursor_icon = CursorIcon::Text;
|
||||||
}
|
}
|
||||||
|
@ -449,20 +437,10 @@ impl<'t> TextEdit<'t> {
|
||||||
insert_text(&mut ccursor, text, "\n");
|
insert_text(&mut ccursor, text, "\n");
|
||||||
Some(CCursorPair::one(ccursor))
|
Some(CCursorPair::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
// Common to end input with enter
|
ui.memory().surrender_kb_focus(id); // End input with enter
|
||||||
ui.memory().surrender_kb_focus(id);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Key {
|
|
||||||
key: Key::Escape,
|
|
||||||
pressed: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
ui.memory().surrender_kb_focus(id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key: Key::Z,
|
key: Key::Z,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
|
|
Loading…
Reference in a new issue