diff --git a/CHANGELOG.md b/CHANGELOG.md index d236a615..091b7c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Pressing enter in a single-line `TextEdit` will now surrender keyboard focus for it. * You must now be explicit when creating a `TextEdit` if you want it to be singeline or multiline. * Improved automatic `Id` generation, making `Id` clashes less likely. +* Egui now requires modifier key state from the integration +* Renamed and removed some keys in the `Key` enum. ### Fixed 🐛 diff --git a/egui/src/input.rs b/egui/src/input.rs index 64002a1e..740ea911 100644 --- a/egui/src/input.rs +++ b/egui/src/input.rs @@ -151,7 +151,7 @@ impl Default for MouseInput { } /// An input event. Only covers events used by Egui. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { Copy, Cut, @@ -161,33 +161,45 @@ pub enum Event { Key { key: Key, pressed: bool, + modifiers: Modifiers, }, } -/// Keyboard key name. Only covers keys used by Egui. +/// State of the modifier keys. These must be fed to Egui. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Modifiers { + /// Either of the alt keys are down (option ⌥ on Mac) + pub alt: bool, + /// Either of the control keys are down + pub ctrl: bool, + /// Either of the shift keys are down + pub shift: bool, + /// The Mac ⌘ Command key. Should always be set to `false` on other platforms. + pub mac_cmd: bool, + /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). + /// On Windows and Linux, set this to the same value as `ctrl`. + /// This is so that Egui can, for instance, select all text by checking for `command + A` + /// and it will work on both Mac and Windows. + pub command: bool, +} + +/// Keyboard key name. Only covers keys used by Egui (mostly for text editing). #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum Key { - Alt, + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, Backspace, - Control, Delete, - Down, End, + Enter, Escape, Home, Insert, - Left, - /// Windows key or Mac Command key - Logo, PageDown, PageUp, - /// Enter/Return key - Enter, - Right, - Shift, - // Space, Tab, - Up, } impl InputState { @@ -227,7 +239,8 @@ impl InputState { event, Event::Key { key, - pressed: true + pressed: true, + .. } if *key == desired_key ) }) @@ -240,7 +253,8 @@ impl InputState { event, Event::Key { key, - pressed: false + pressed: false, + .. } if *key == desired_key ) }) diff --git a/egui/src/paint/galley.rs b/egui/src/paint/galley.rs index 5b6d9bb6..32ed3c34 100644 --- a/egui/src/paint/galley.rs +++ b/egui/src/paint/galley.rs @@ -621,6 +621,39 @@ impl Galley { column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), }) } + + pub fn cursor_next_word(&self, cursor: &Cursor) -> Cursor { + self.from_ccursor(CCursor { + index: next_word(self.text.chars(), cursor.ccursor.index), + prefer_next_row: true, + }) + } + + pub fn cursor_previous_word(&self, cursor: &Cursor) -> Cursor { + let num_chars = self.text.chars().count(); + self.from_ccursor(CCursor { + index: num_chars - next_word(self.text.chars().rev(), num_chars - cursor.ccursor.index), + prefer_next_row: true, + }) + } +} + +fn next_word(it: impl Iterator, mut index: usize) -> usize { + let mut it = it.skip(index); + if let Some(_first) = it.next() { + index += 1; + + if let Some(second) = it.next() { + index += 1; + for next in it { + if next.is_alphabetic() != second.is_alphabetic() { + break; + } + index += 1; + } + } + } + index } // ---------------------------------------------------------------------------- diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 23311433..a6ae9f7e 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -176,7 +176,7 @@ impl<'t> Widget for TextEdit<'t> { } else { Sense::nothing() }; - let response = ui.interact(rect, id, sense); // TODO: implement drag-select + let response = ui.interact(rect, id, sense); if response.clicked && enabled { ui.memory().request_kb_focus(id); @@ -228,6 +228,7 @@ impl<'t> Widget for TextEdit<'t> { Event::Key { key: Key::Enter, pressed: true, + .. } => { if multiline { let mut ccursor = cursor.ccursor; @@ -242,13 +243,16 @@ impl<'t> Widget for TextEdit<'t> { Event::Key { key: Key::Escape, pressed: true, + .. } => { ui.memory().surrender_kb_focus(id); break; } - Event::Key { key, pressed: true } => { - on_key_press(&mut cursor, text, &galley, *key) - } + Event::Key { + key, + pressed: true, + modifiers, + } => on_key_press(&mut cursor, text, &galley, *key, modifiers), Event::Key { .. } => None, }; @@ -334,18 +338,35 @@ fn on_key_press( text: &mut String, galley: &Galley, key: Key, + modifiers: &Modifiers, ) -> Option { + // TODO: cursor position preview on mouse hover + // TODO: drag-select + // TODO: double-click to select whole word + // TODO: triple-click to select whole paragraph + // TODO: drag selected text to either move or clone (ctrl on windows, alt on mac) + // TODO: ctrl-U to clear paragraph before the cursor + // TODO: ctrl-W to delete previous word + // TODO: alt/ctrl + backspace to delete previous word (alt on mac, ctrl on windows) + // TODO: alt/ctrl + delete to delete next word (alt on mac, ctrl on windows) + // TODO: cmd-A to select all + // TODO: shift modifier to only move half of the cursor to select things + match key { - Key::Backspace if cursor.ccursor.index > 0 => { - *cursor = galley.from_ccursor(cursor.ccursor - 1); - let mut char_it = text.chars(); - let mut new_text = String::with_capacity(text.capacity()); - for _ in 0..cursor.ccursor.index { - new_text.push(char_it.next().unwrap()) + Key::Backspace => { + if cursor.ccursor.index > 0 { + *cursor = galley.from_ccursor(cursor.ccursor - 1); + let mut char_it = text.chars(); + let mut new_text = String::with_capacity(text.capacity()); + for _ in 0..cursor.ccursor.index { + new_text.push(char_it.next().unwrap()) + } + new_text.extend(char_it.skip(1)); + *text = new_text; + Some(cursor.ccursor) + } else { + None } - new_text.extend(char_it.skip(1)); - *text = new_text; - Some(cursor.ccursor) } Key::Delete => { let mut char_it = text.chars(); @@ -357,32 +378,69 @@ fn on_key_press( *text = new_text; Some(cursor.ccursor) } - Key::Enter => unreachable!("Should have been handled earlier"), + + Key::ArrowLeft => { + if modifiers.alt || modifiers.ctrl { + // alt on mac, ctrl on windows + *cursor = galley.cursor_previous_word(cursor); + } else if modifiers.mac_cmd { + *cursor = galley.cursor_begin_of_row(cursor); + } else { + *cursor = galley.cursor_left_one_character(cursor); + } + None + } + Key::ArrowRight => { + if modifiers.alt || modifiers.ctrl { + // alt on mac, ctrl on windows + *cursor = galley.cursor_next_word(cursor); + } else if modifiers.mac_cmd { + *cursor = galley.cursor_end_of_row(cursor); + } else { + *cursor = galley.cursor_right_one_character(cursor); + } + None + } + Key::ArrowUp => { + if modifiers.command { + // mac and windows behavior + *cursor = Cursor::default(); + } else { + *cursor = galley.cursor_up_one_row(cursor); + } + None + } + Key::ArrowDown => { + if modifiers.command { + // mac and windows behavior + *cursor = galley.end(); + } else { + *cursor = galley.cursor_down_one_row(cursor); + } + None + } Key::Home => { - *cursor = galley.cursor_begin_of_row(cursor); + if modifiers.ctrl { + // windows behavior + *cursor = Cursor::default(); + } else { + *cursor = galley.cursor_begin_of_row(cursor); + } None } Key::End => { - *cursor = galley.cursor_end_of_row(cursor); + if modifiers.ctrl { + // windows behavior + *cursor = galley.end(); + } else { + *cursor = galley.cursor_end_of_row(cursor); + } None } - Key::Left => { - *cursor = galley.cursor_left_one_character(cursor); - None - } - Key::Right => { - *cursor = galley.cursor_right_one_character(cursor); - None - } - Key::Up => { - *cursor = galley.cursor_up_one_row(cursor); - None - } - Key::Down => { - *cursor = galley.cursor_down_one_row(cursor); - None - } - _ => None, + + Key::Enter | Key::Escape => unreachable!("Handled outside this function"), + + Key::Insert | Key::PageDown | Key::PageUp | Key::Tab => None, } } diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 4a3c1333..a4bab2b0 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -20,10 +20,7 @@ pub use clipboard::ClipboardContext; // TODO: remove pub struct GliumInputState { raw: egui::RawInput, - - /// Command modifier key. - /// Mac command key on Mac, ctrl on Window/Linux. - cmd: bool, + modifiers: egui::Modifiers, } impl GliumInputState { @@ -33,7 +30,7 @@ impl GliumInputState { pixels_per_point: Some(pixels_per_point), ..Default::default() }, - cmd: false, + modifiers: Default::default(), // cmd: false, } } } @@ -63,40 +60,48 @@ pub fn input_to_egui( input_state.raw.mouse_pos = None; } ReceivedCharacter(ch) => { - if !input_state.cmd && printable_char(ch) { + if printable_char(ch) && !input_state.modifiers.ctrl && !input_state.modifiers.mac_cmd { input_state.raw.events.push(Event::Text(ch.to_string())); } } KeyboardInput { input, .. } => { - if let Some(virtual_keycode) = input.virtual_keycode { - let is_command_key = if cfg!(target_os = "macos") { - matches!(virtual_keycode, VirtualKeyCode::LWin | VirtualKeyCode::RWin) - } else { - matches!( - virtual_keycode, - VirtualKeyCode::LControl | VirtualKeyCode::RControl - ) - }; + if let Some(keycode) = input.virtual_keycode { + let pressed = input.state == glutin::event::ElementState::Pressed; - if is_command_key { - input_state.cmd = input.state == glutin::event::ElementState::Pressed; + if matches!(keycode, VirtualKeyCode::LAlt | VirtualKeyCode::RAlt) { + input_state.modifiers.alt = pressed; + } + if matches!(keycode, VirtualKeyCode::LControl | VirtualKeyCode::RControl) { + input_state.modifiers.ctrl = pressed; + if !cfg!(target_os = "macos") { + input_state.modifiers.command = pressed; + } + } + if matches!(keycode, VirtualKeyCode::LShift | VirtualKeyCode::RShift) { + input_state.modifiers.shift = pressed; + } + if cfg!(target_os = "macos") + && matches!(keycode, VirtualKeyCode::LWin | VirtualKeyCode::RWin) + { + input_state.modifiers.mac_cmd = pressed; + input_state.modifiers.command = pressed; } - if input.state == glutin::event::ElementState::Pressed { + if pressed { if cfg!(target_os = "macos") - && input_state.cmd - && virtual_keycode == VirtualKeyCode::Q + && input_state.modifiers.mac_cmd + && keycode == VirtualKeyCode::Q { *control_flow = ControlFlow::Exit; } // VirtualKeyCode::Paste etc in winit are broken/untrustworthy, // so we detect these things manually: - if input_state.cmd && virtual_keycode == VirtualKeyCode::X { + if input_state.modifiers.command && keycode == VirtualKeyCode::X { input_state.raw.events.push(Event::Cut); - } else if input_state.cmd && virtual_keycode == VirtualKeyCode::C { + } else if input_state.modifiers.command && keycode == VirtualKeyCode::C { input_state.raw.events.push(Event::Copy); - } else if input_state.cmd && virtual_keycode == VirtualKeyCode::V { + } else if input_state.modifiers.command && keycode == VirtualKeyCode::V { if let Some(clipboard) = clipboard { match clipboard.get_contents() { Ok(contents) => { @@ -107,10 +112,11 @@ pub fn input_to_egui( } } } - } else if let Some(key) = translate_virtual_key_code(virtual_keycode) { + } else if let Some(key) = translate_virtual_key_code(keycode) { input_state.raw.events.push(Event::Key { key, - pressed: input.state == glutin::event::ElementState::Pressed, + pressed, + modifiers: input_state.modifiers, }); } } @@ -157,20 +163,13 @@ pub fn translate_virtual_key_code(key: VirtualKeyCode) -> Option { End => Key::End, PageDown => Key::PageDown, PageUp => Key::PageUp, - Left => Key::Left, - Up => Key::Up, - Right => Key::Right, - Down => Key::Down, + Left => Key::ArrowLeft, + Up => Key::ArrowUp, + Right => Key::ArrowRight, + Down => Key::ArrowDown, Back => Key::Backspace, Return => Key::Enter, - // Space => Key::Space, Tab => Key::Tab, - - LAlt | RAlt => Key::Alt, - LShift | RShift => Key::Shift, - LControl | RControl => Key::Control, - LWin | RWin => Key::Logo, - _ => { return None; } diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 6d6fc5ca..f4ac01fa 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -187,14 +187,22 @@ pub fn location_hash() -> Option { web_sys::window()?.location().hash().ok() } -/// Web sends all all keys as strings, so it is up to us to figure out if it is +/// Web sends all keys as strings, so it is up to us to figure out if it is /// a real text input or the name of a key. fn should_ignore_key(key: &str) -> bool { let is_function_key = key.starts_with('F') && key.len() > 1; is_function_key || matches!( key, - "CapsLock" | "ContextMenu" | "NumLock" | "Pause" | "ScrollLock" + "Alt" + | "CapsLock" + | "ContextMenu" + | "Control" + | "Meta" + | "NumLock" + | "Pause" + | "ScrollLock" + | "Shift" ) } @@ -202,24 +210,20 @@ fn should_ignore_key(key: &str) -> bool { /// a real text input or the name of a key. pub fn translate_key(key: &str) -> Option { match key { - "Alt" => Some(egui::Key::Alt), + "ArrowDown" => Some(egui::Key::ArrowDown), + "ArrowLeft" => Some(egui::Key::ArrowLeft), + "ArrowRight" => Some(egui::Key::ArrowRight), + "ArrowUp" => Some(egui::Key::ArrowUp), "Backspace" => Some(egui::Key::Backspace), - "Control" => Some(egui::Key::Control), "Delete" => Some(egui::Key::Delete), - "ArrowDown" => Some(egui::Key::Down), "End" => Some(egui::Key::End), + "Enter" => Some(egui::Key::Enter), "Esc" | "Escape" => Some(egui::Key::Escape), - "Home" => Some(egui::Key::Home), "Help" | "Insert" => Some(egui::Key::Insert), - "ArrowLeft" => Some(egui::Key::Left), - "Meta" => Some(egui::Key::Logo), + "Home" => Some(egui::Key::Home), "PageDown" => Some(egui::Key::PageDown), "PageUp" => Some(egui::Key::PageUp), - "Enter" => Some(egui::Key::Enter), - "ArrowRight" => Some(egui::Key::Right), - "Shift" => Some(egui::Key::Shift), "Tab" => Some(egui::Key::Tab), - "ArrowUp" => Some(egui::Key::Up), _ => None, } } @@ -269,15 +273,16 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { } let mut runner_lock = runner_ref.0.lock(); let key = event.key(); - if !should_ignore_key(&key) { - if let Some(key) = translate_key(&key) { - runner_lock - .web_input - .events - .push(egui::Event::Key { key, pressed: true }); - } else { - runner_lock.web_input.events.push(egui::Event::Text(key)); - } + + if let Some(key) = translate_key(&key) { + runner_lock.web_input.events.push(egui::Event::Key { + key, + pressed: true, + modifiers: modifiers_from_event(&event), + }); + runner_lock.needs_repaint = true; + } else if !should_ignore_key(&key) { + runner_lock.web_input.events.push(egui::Event::Text(key)); runner_lock.needs_repaint = true; } }) as Box); @@ -295,6 +300,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { runner_lock.web_input.events.push(egui::Event::Key { key, pressed: false, + modifiers: modifiers_from_event(&event), }); runner_lock.needs_repaint = true; } @@ -315,6 +321,22 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { Ok(()) } +fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { + egui::Modifiers { + alt: event.alt_key(), + ctrl: event.ctrl_key(), + shift: event.shift_key(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + mac_cmd: event.meta_key(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + command: event.ctrl_key() || event.meta_key(), + } +} + fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { use wasm_bindgen::JsCast; let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap();