diff --git a/CHANGELOG.md b/CHANGELOG.md index c75f38bd..6c91b251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/egui/src/context.rs b/egui/src/context.rs index 4f34c6bb..fb0692ee 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -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") diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index d454edb9..706bec79 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -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? diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 8190c2c1..6b8cef37 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -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). diff --git a/egui/src/response.rs b/egui/src/response.rs index 83eae239..3ab4aa38 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -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 { pub inner: R, pub response: Response, diff --git a/egui/src/widgets/button.rs b/egui/src/widgets/button.rs index 2bfb3ae2..0058c685 100644 --- a/egui/src/widgets/button.rs +++ b/egui/src/widgets/button.rs @@ -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(); diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index 9a4ad54f..77817343 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -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 }; diff --git a/egui/src/widgets/hyperlink.rs b/egui/src/widgets/hyperlink.rs index c975cab6..b0ff07e5 100644 --- a/egui/src/widgets/hyperlink.rs +++ b/egui/src/widgets/hyperlink.rs @@ -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()); diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index 5a3ce82f..58daa7f0 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -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 } diff --git a/egui/src/widgets/selected_label.rs b/egui/src/widgets/selected_label.rs index db3e84b3..c1d678dc 100644 --- a/egui/src/widgets/selected_label.rs +++ b/egui/src/widgets/selected_label.rs @@ -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; diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 35a0092b..3f52e034 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -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(); diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 96bbe817..70c46ba4 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -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,