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
|
||||
|
||||
### 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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue