Fix bugs in consume_key and improve Modifiers API

Improvements and fixes following https://github.com/emilk/egui/pull/1212
This commit is contained in:
Emil Ernerfeldt 2022-02-21 16:53:41 +01:00
parent 476a3057b0
commit fd3fb726c1
4 changed files with 129 additions and 44 deletions

View file

@ -254,18 +254,24 @@ pub enum PointerButton {
pub const NUM_POINTER_BUTTONS: usize = 3; pub const NUM_POINTER_BUTTONS: usize = 3;
/// State of the modifier keys. These must be fed to egui. /// State of the modifier keys. These must be fed to egui.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] ///
/// The best way to compare `Modifiers` is by using [`Modifiers::matches`].
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers { pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac). /// Either of the alt keys are down (option ⌥ on Mac).
pub alt: bool, pub alt: bool,
/// Either of the control keys are down. /// Either of the control keys are down.
/// When checking for keyboard shortcuts, consider using [`Self::command`] instead. /// When checking for keyboard shortcuts, consider using [`Self::command`] instead.
pub ctrl: bool, pub ctrl: bool,
/// Either of the shift keys are down. /// Either of the shift keys are down.
pub shift: bool, pub shift: bool,
/// The Mac ⌘ Command key. Should always be set to `false` on other platforms. /// The Mac ⌘ Command key. Should always be set to `false` on other platforms.
pub mac_cmd: bool, pub mac_cmd: bool,
/// On Windows and Linux, set this to the same value as `ctrl`. /// On Windows and Linux, set this to the same value as `ctrl`.
/// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`).
/// This is so that egui can, for instance, select all text by checking for `command + A` /// This is so that egui can, for instance, select all text by checking for `command + A`
@ -278,37 +284,51 @@ impl Modifiers {
Default::default() Default::default()
} }
pub fn alt(self, value: bool) -> Self { pub const NONE: Self = Self {
Self { alt: value, ..self } alt: false,
} ctrl: false,
shift: false,
mac_cmd: false,
command: false,
};
pub fn ctrl(self, value: bool) -> Self { pub const ALT: Self = Self {
Self { alt: true,
ctrl: value, ctrl: false,
..self shift: false,
} mac_cmd: false,
} command: false,
};
pub fn shift(self, value: bool) -> Self { pub const CTRL: Self = Self {
Self { alt: false,
shift: value, ctrl: true,
..self shift: false,
} mac_cmd: false,
} command: false,
};
pub fn mac_cmd(self, value: bool) -> Self { pub const SHIFT: Self = Self {
Self { alt: false,
mac_cmd: value, ctrl: false,
..self shift: true,
} mac_cmd: false,
} command: false,
};
pub fn command(self, value: bool) -> Self { /// The Mac ⌘ Command key
Self { pub const MAC_CMD: Self = Self {
command: value, alt: false,
..self ctrl: false,
} shift: false,
} mac_cmd: true,
command: false,
};
/// On Mac: ⌘ Command key, elsewhere: Ctrl key
pub const COMMAND: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: false,
command: true,
};
#[inline(always)] #[inline(always)]
pub fn is_none(&self) -> bool { pub fn is_none(&self) -> bool {
@ -320,6 +340,7 @@ impl Modifiers {
!self.is_none() !self.is_none()
} }
/// Is shift the only pressed button?
#[inline(always)] #[inline(always)]
pub fn shift_only(&self) -> bool { pub fn shift_only(&self) -> bool {
self.shift && !(self.alt || self.command) self.shift && !(self.alt || self.command)
@ -330,6 +351,66 @@ impl Modifiers {
pub fn command_only(&self) -> bool { pub fn command_only(&self) -> bool {
!self.alt && !self.shift && self.command !self.alt && !self.shift && self.command
} }
/// Check for equality but with proper handling of [`Self::command`].
///
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD));
/// ```
pub fn matches(&self, pattern: Modifiers) -> bool {
// alt and shift must always match the pattern:
if pattern.alt != self.alt || pattern.shift != self.shift {
return false;
}
if pattern.mac_cmd {
// Mac-specific match:
if !self.mac_cmd {
return false;
}
if pattern.ctrl != self.ctrl {
return false;
}
return true;
}
if !pattern.ctrl && !pattern.command {
// the pattern explicitly doesn't want any ctrl/command:
return !self.ctrl && !self.command;
}
// if the pattern is looking for command, then `ctrl` may or may not be set depending on platform.
// if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform.
if pattern.ctrl && !self.ctrl {
return false;
}
if pattern.command && !self.command {
return false;
}
true
}
}
impl std::ops::BitOr for Modifiers {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self {
alt: self.alt | rhs.alt,
ctrl: self.ctrl | rhs.ctrl,
shift: self.shift | rhs.shift,
mac_cmd: self.mac_cmd | rhs.mac_cmd,
command: self.command | rhs.command,
}
}
} }
/// Keyboard keys. /// Keyboard keys.

View file

@ -192,22 +192,26 @@ impl InputState {
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty() self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
} }
/// Ignore a key if it was pressed or released this frame. Useful for hotkeys. /// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
/// Matches on both key press and key release, consuming them and removing them from `self.events`.
/// Returns true if the key was pressed this frame (even if the key release was consumed).
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool { pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
let mut match_found = false;
self.events.retain(|event| { self.events.retain(|event| {
!matches!( let is_match = matches!(
event, event,
Event::Key { Event::Key {
key: ev_key, key: ev_key,
modifiers: ev_mods, modifiers: ev_mods,
.. pressed: true
} if *ev_key == key && *ev_mods == modifiers } if *ev_key == key && ev_mods.matches(modifiers)
) );
match_found |= is_match;
!is_match
}); });
self.keys_down.remove(&key) match_found
} }
/// Was the given key pressed this frame? /// Was the given key pressed this frame?

View file

@ -64,7 +64,10 @@ impl super::View for TextEdit {
anything_selected, anything_selected,
egui::Label::new("Press ctrl+T to toggle the case of selected text (cmd+T on Mac)"), egui::Label::new("Press ctrl+T to toggle the case of selected text (cmd+T on Mac)"),
); );
if ui.input().modifiers.command_only() && ui.input().key_pressed(egui::Key::T) { if ui
.input_mut()
.consume_key(egui::Modifiers::COMMAND, egui::Key::T)
{
if let Some(text_cursor_range) = output.cursor_range { if let Some(text_cursor_range) = output.cursor_range {
use egui::TextBuffer as _; use egui::TextBuffer as _;
let selected_chars = text_cursor_range.as_sorted_char_range(); let selected_chars = text_cursor_range.as_sorted_char_range();

View file

@ -126,10 +126,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
(Key::S, "~"), // ~strikethrough~ (Key::S, "~"), // ~strikethrough~
(Key::U, "_"), // _underline_ (Key::U, "_"), // _underline_
] { ] {
if ui if ui.input_mut().consume_key(egui::Modifiers::COMMAND, key) {
.input_mut()
.consume_key(egui::Modifiers::new().command(true), key)
{
toggle_surrounding(code, ccursor_range, surrounding); toggle_surrounding(code, ccursor_range, surrounding);
any_change = true; any_change = true;
}; };