diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb11632acc..bc32bc0760 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -360,7 +360,7 @@ jobs: - name: install libgtk-dev run: | sudo apt update - sudo apt install libgtk-3-dev + sudo apt install libgtk-3-dev libxkbcommon-dev libxkbcommon-x11-dev if: contains(matrix.os, 'ubuntu') - name: install stable toolchain diff --git a/CHANGELOG.md b/CHANGELOG.md index bf64ec7bd4..0cf5e3128f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ You can find its changes [documented below](#070---2021-01-01). - `Slider` widget now warns if max < min and swaps the values ([#1882] by [@Maan2003]) - Widget/Slider: Add stepping functionality ([#1875] by [@raymanfx]) - Add #[data(eq)] shorthand attribute for Data derive macro ([#1884] by [@Maan2003]) +- X11: detect keyboard layout ([#1779] by [@Maan2003]) ### Changed @@ -750,6 +751,7 @@ Last release without a changelog :( [#1761]: https://github.com/linebender/druid/pull/1761 [#1764]: https://github.com/linebender/druid/pull/1764 [#1772]: https://github.com/linebender/druid/pull/1772 +[#1779]: https://github.com/linebender/druid/pull/1779 [#1787]: https://github.com/linebender/druid/pull/1787 [#1801]: https://github.com/linebender/druid/pull/1800 [#1802]: https://github.com/linebender/druid/pull/1802 diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index bda6dbecee..e9f27bb905 100755 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -16,7 +16,7 @@ default-target = "x86_64-pc-windows-msvc" [features] default = ["gtk"] gtk = ["gio", "gdk", "gdk-sys", "glib", "glib-sys", "gtk-sys", "gtk-rs", "gdk-pixbuf"] -x11 = ["x11rb", "nix", "cairo-sys-rs"] +x11 = ["x11rb", "nix", "cairo-sys-rs", "bindgen", "pkg-config"] # Implement HasRawWindowHandle for WindowHandle raw-win-handle = ["raw-window-handle"] @@ -89,7 +89,7 @@ glib = { version = "0.10.1", optional = true } glib-sys = { version = "0.10.0", optional = true } gtk-sys = { version = "0.10.0", optional = true } nix = { version = "0.18.0", optional = true } -x11rb = { version = "0.8.0", features = ["allow-unsafe-code", "present", "render", "randr", "xfixes", "resource_manager", "cursor"], optional = true } +x11rb = { version = "0.8.0", features = ["allow-unsafe-code", "present", "render", "randr", "xfixes", "xkb", "resource_manager", "cursor"], optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] wasm-bindgen = "0.2.67" @@ -105,3 +105,7 @@ static_assertions = "1.1.0" test-env-log = { version = "0.2.5", features = ["trace"], default-features = false } tracing-subscriber = "0.2.15" unicode-segmentation = "1.7.0" + +[build-dependencies] +bindgen = {version = "0.58", optional = true} +pkg-config = { version = "0.3", optional = true } diff --git a/druid-shell/build.rs b/druid-shell/build.rs new file mode 100644 index 0000000000..287cfa7675 --- /dev/null +++ b/druid-shell/build.rs @@ -0,0 +1,51 @@ +#[cfg(not(feature = "x11"))] +fn main() {} + +#[cfg(feature = "x11")] +fn main() { + use pkg_config::probe_library; + use std::env; + use std::path::PathBuf; + + if env::var("CARGO_CFG_TARGET_OS").unwrap() != "linux" { + return; + } + + probe_library("xkbcommon").unwrap(); + probe_library("xkbcommon-x11").unwrap(); + + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header_contents( + "wrapper.h", + "\ +#include +#include +#include +#include ", + ) + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .prepend_enum_name(false) + .size_t_is_usize(true) + .allowlist_function("xkb_.*") + .allowlist_type("xkb_.*") + .allowlist_var("XKB_.*") + .allowlist_type("xcb_connection_t") + // this needs var args + .blocklist_function("xkb_context_set_log_fn") + // we use FILE from libc + .blocklist_type("FILE") + .blocklist_type("va_list") + .blocklist_type("_.*") + .generate() + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/xkbcommon.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("xkbcommon_sys.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/druid-shell/src/backend/gtk/keycodes.rs b/druid-shell/src/backend/gtk/keycodes.rs index 0987ab03b1..fc4400a061 100644 --- a/druid-shell/src/backend/gtk/keycodes.rs +++ b/druid-shell/src/backend/gtk/keycodes.rs @@ -23,21 +23,65 @@ pub type RawKey = gdk::keys::Key; #[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] pub fn raw_key_to_key(raw: RawKey) -> Option { + // changes from x11 backend keycodes: + // * XKB_KEY_ prefix removed + // * 3270 is replaced with _3270 + // * XF86 prefix is removed + // * Sun* Keys are gone Some(match raw { - Escape => Key::Escape, BackSpace => Key::Backspace, - Tab | ISO_Left_Tab => Key::Tab, - Return => Key::Enter, - Control_L | Control_R => Key::Control, - Alt_L | Alt_R => Key::Alt, - Shift_L | Shift_R => Key::Shift, - // TODO: investigate mapping. Map Meta_[LR]? - Super_L | Super_R => Key::Meta, - Caps_Lock => Key::CapsLock, - F1 => Key::F1, - F2 => Key::F2, - F3 => Key::F3, - F4 => Key::F4, + Tab | KP_Tab | ISO_Left_Tab => Key::Tab, + Clear | KP_Begin => Key::Clear, + Return | KP_Enter => Key::Enter, + Linefeed => Key::Enter, + Pause => Key::Pause, + Scroll_Lock => Key::ScrollLock, + Escape => Key::Escape, + Multi_key => Key::Compose, + Kanji => Key::KanjiMode, + Muhenkan => Key::NonConvert, + Henkan_Mode => Key::Convert, + Romaji => Key::Romaji, + Hiragana => Key::Hiragana, + Katakana => Key::Katakana, + Hiragana_Katakana => Key::HiraganaKatakana, + Zenkaku => Key::Zenkaku, + Hankaku => Key::Hankaku, + Zenkaku_Hankaku => Key::ZenkakuHankaku, + Kana_Lock => Key::KanaMode, + Eisu_Shift | Eisu_toggle => Key::Alphanumeric, + Hangul => Key::HangulMode, + Hangul_Hanja => Key::HanjaMode, + Codeinput => Key::CodeInput, + SingleCandidate => Key::SingleCandidate, + MultipleCandidate => Key::AllCandidates, + PreviousCandidate => Key::PreviousCandidate, + Home | KP_Home => Key::Home, + Left | KP_Left => Key::ArrowLeft, + Up | KP_Up => Key::ArrowUp, + Right | KP_Right => Key::ArrowRight, + Down | KP_Down => Key::ArrowDown, + Prior | KP_Prior => Key::PageUp, + Next | KP_Next => Key::PageDown, + End | KP_End => Key::End, + Select => Key::Select, + // Treat Print/PrintScreen as PrintScreen https://crbug.com/683097. + Print | _3270_PrintScreen => Key::PrintScreen, + Execute => Key::Execute, + Insert | KP_Insert => Key::Insert, + Undo => Key::Undo, + Redo => Key::Redo, + Menu => Key::ContextMenu, + Find => Key::Find, + Cancel => Key::Cancel, + Help => Key::Help, + Break | _3270_Attn => Key::Attn, + Mode_switch => Key::ModeChange, + Num_Lock => Key::NumLock, + F1 | KP_F1 => Key::F1, + F2 | KP_F2 => Key::F2, + F3 | KP_F3 => Key::F3, + F4 | KP_F4 => Key::F4, F5 => Key::F5, F6 => Key::F6, F7 => Key::F7, @@ -46,51 +90,126 @@ pub fn raw_key_to_key(raw: RawKey) -> Option { F10 => Key::F10, F11 => Key::F11, F12 => Key::F12, - - Print => Key::PrintScreen, - Scroll_Lock => Key::ScrollLock, - // Pause/Break not audio. - Pause => Key::Pause, - - Insert => Key::Insert, + // not available in keyboard-types + // Tools | F13 => Key::F13, + // F14 | Launch5 => Key::F14, + // F15 | Launch6 => Key::F15, + // F16 | Launch7 => Key::F16, + // F17 | Launch8 => Key::F17, + // F18 | Launch9 => Key::F18, + // F19 => Key::F19, + // F20 => Key::F20, + // F21 => Key::F21, + // F22 => Key::F22, + // F23 => Key::F23, + // F24 => Key::F24, + // Calculator => Key::LaunchCalculator, + // MyComputer | Explorer => Key::LaunchMyComputer, + // ISO_Level3_Latch => Key::AltGraphLatch, + // ISO_Level5_Shift => Key::ShiftLevel5, + Shift_L | Shift_R => Key::Shift, + Control_L | Control_R => Key::Control, + Caps_Lock => Key::CapsLock, + Meta_L | Meta_R => Key::Meta, + Alt_L | Alt_R => Key::Alt, + Super_L | Super_R => Key::Meta, + Hyper_L | Hyper_R => Key::Hyper, Delete => Key::Delete, - Home => Key::Home, - End => Key::End, - Page_Up => Key::PageUp, - Page_Down => Key::PageDown, - Num_Lock => Key::NumLock, - - Up => Key::ArrowUp, - Down => Key::ArrowDown, - Left => Key::ArrowLeft, - Right => Key::ArrowRight, - Clear => Key::Clear, - - Menu => Key::ContextMenu, + Next_VMode => Key::VideoModeNext, + MonBrightnessUp => Key::BrightnessUp, + MonBrightnessDown => Key::BrightnessDown, + Standby | Sleep | Suspend => Key::Standby, + AudioLowerVolume => Key::AudioVolumeDown, + AudioMute => Key::AudioVolumeMute, + AudioRaiseVolume => Key::AudioVolumeUp, + AudioPlay => Key::MediaPlayPause, + AudioStop => Key::MediaStop, + AudioPrev => Key::MediaTrackPrevious, + AudioNext => Key::MediaTrackNext, + HomePage => Key::BrowserHome, + Mail => Key::LaunchMail, + Search => Key::BrowserSearch, + AudioRecord => Key::MediaRecord, + Calendar => Key::LaunchCalendar, + Back => Key::BrowserBack, + Forward => Key::BrowserForward, + Stop => Key::BrowserStop, + Refresh | Reload => Key::BrowserRefresh, + PowerOff => Key::PowerOff, WakeUp => Key::WakeUp, - Launch0 => Key::LaunchApplication1, - Launch1 => Key::LaunchApplication2, + Eject => Key::Eject, + ScreenSaver => Key::LaunchScreenSaver, + WWW => Key::LaunchWebBrowser, + Favorites => Key::BrowserFavorites, + AudioPause => Key::MediaPause, + AudioMedia | Music => Key::LaunchMusicPlayer, + AudioRewind => Key::MediaRewind, + CD | Video => Key::LaunchMediaPlayer, + Close => Key::Close, + Copy => Key::Copy, + Cut => Key::Cut, + Display => Key::DisplaySwap, + Excel => Key::LaunchSpreadsheet, + LogOff => Key::LogOff, + New => Key::New, + Open => Key::Open, + Paste => Key::Paste, + Reply => Key::MailReply, + Save => Key::Save, + Send => Key::MailSend, + Spell => Key::SpellCheck, + SplitScreen => Key::SplitScreenToggle, + Word | OfficeHome => Key::LaunchWordProcessor, + ZoomIn => Key::ZoomIn, + ZoomOut => Key::ZoomOut, + WebCam => Key::LaunchWebCam, + MailForward => Key::MailForward, + AudioForward => Key::MediaFastForward, + AudioRandomPlay => Key::RandomToggle, + Subtitle => Key::Subtitle, + Hibernate => Key::Hibernate, + _3270_EraseEOF => Key::EraseEof, + _3270_Play => Key::Play, + _3270_ExSelect => Key::ExSel, + _3270_CursorSelect => Key::CrSel, ISO_Level3_Shift => Key::AltGraph, - - KP_Begin => Key::Clear, - KP_Delete => Key::Delete, - KP_Down => Key::ArrowDown, - KP_End => Key::End, - KP_Enter => Key::Enter, - KP_F1 => Key::F1, - KP_F2 => Key::F2, - KP_F3 => Key::F3, - KP_F4 => Key::F4, - KP_Home => Key::Home, - KP_Insert => Key::Insert, - KP_Left => Key::ArrowLeft, - KP_Page_Down => Key::PageDown, - KP_Page_Up => Key::PageUp, - KP_Right => Key::ArrowRight, - // KP_Separator? What does it map to? - KP_Tab => Key::Tab, - KP_Up => Key::ArrowUp, - // TODO: more mappings (media etc) + ISO_Next_Group => Key::GroupNext, + ISO_Prev_Group => Key::GroupPrevious, + ISO_First_Group => Key::GroupFirst, + ISO_Last_Group => Key::GroupLast, + dead_grave + | dead_acute + | dead_circumflex + | dead_tilde + | dead_macron + | dead_breve + | dead_abovedot + | dead_diaeresis + | dead_abovering + | dead_doubleacute + | dead_caron + | dead_cedilla + | dead_ogonek + | dead_iota + | dead_voiced_sound + | dead_semivoiced_sound + | dead_belowdot + | dead_hook + | dead_horn + | dead_stroke + | dead_abovecomma + | dead_abovereversedcomma + | dead_doublegrave + | dead_belowring + | dead_belowmacron + | dead_belowcircumflex + | dead_belowtilde + | dead_belowbreve + | dead_belowdiaeresis + | dead_invertedbreve + | dead_belowcomma + | dead_currency + | dead_greek => Key::Dead, _ => return None, }) } diff --git a/druid-shell/src/backend/x11/application.rs b/druid-shell/src/backend/x11/application.rs index a63a9debb5..8c7e9153c9 100644 --- a/druid-shell/src/backend/x11/application.rs +++ b/druid-shell/src/backend/x11/application.rs @@ -36,8 +36,8 @@ use x11rb::xcb_ffi::XCBConnection; use crate::application::AppHandler; use super::clipboard::Clipboard; -use super::util; use super::window::Window; +use super::{util, xkb}; // This creates a `struct WindowAtoms` containing the specified atoms as members (along with some // convenience methods to intern and query those atoms). We use the following atoms: @@ -175,6 +175,7 @@ pub(crate) struct Application { render_argb32_pictformat_cursor: Option, /// Newest timestamp that we received timestamp: Rc>, + xkb_context: xkb::Context, } /// The mutable `Application` state. @@ -183,6 +184,7 @@ struct State { quitting: bool, /// A collection of all the `Application` windows. windows: HashMap>, + xkb_state: xkb::State, } #[derive(Clone, Debug)] @@ -208,11 +210,27 @@ impl Application { // https://github.com/linebender/druid/pull/1025#discussion_r442777892 let (conn, screen_num) = XCBConnection::connect(None)?; let rdb = Rc::new(ResourceDb::new_from_default(&conn)?); + let xkb_context = xkb::Context::new(); + xkb_context.set_log_level(tracing::Level::DEBUG); + use x11rb::protocol::xkb::ConnectionExt; + conn.xkb_use_extension(1, 0)? + .reply() + .context("init xkb extension")?; + let device_id = xkb_context + .core_keyboard_device_id(&conn) + .context("get core keyboard device id")?; + + let keymap = xkb_context + .keymap_from_device(&conn, device_id) + .context("key map from device")?; + + let xkb_state = keymap.state(); let connection = Rc::new(conn); let window_id = Application::create_event_window(&connection, screen_num)?; let state = Rc::new(RefCell::new(State { quitting: false, windows: HashMap::new(), + xkb_state, })); let (idle_read, idle_write) = nix::unistd::pipe2(nix::fcntl::OFlag::O_NONBLOCK)?; @@ -331,6 +349,7 @@ impl Application { marker: std::marker::PhantomData, render_argb32_pictformat_cursor, timestamp, + xkb_context, }) } @@ -523,7 +542,25 @@ impl Application { let w = self .window(ev.event) .context("KEY_PRESS - failed to get window")?; - w.handle_key_press(ev); + let hw_keycode = ev.detail; + let mut state = borrow_mut!(self.state)?; + let key_event = state + .xkb_state + .key_event(hw_keycode as _, keyboard_types::KeyState::Down); + + w.handle_key_event(key_event); + } + Event::KeyRelease(ev) => { + let w = self + .window(ev.event) + .context("KEY_PRESS - failed to get window")?; + let hw_keycode = ev.detail; + let mut state = borrow_mut!(self.state)?; + let key_event = state + .xkb_state + .key_event(hw_keycode as _, keyboard_types::KeyState::Up); + + w.handle_key_event(key_event); } Event::ButtonPress(ev) => { let w = self diff --git a/druid-shell/src/backend/x11/keycodes.rs b/druid-shell/src/backend/x11/keycodes.rs index e171b52394..6bbb03068e 100644 --- a/druid-shell/src/backend/x11/keycodes.rs +++ b/druid-shell/src/backend/x11/keycodes.rs @@ -1,201 +1,195 @@ -// Copyright 2020 The Druid Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +#![allow(non_upper_case_globals)] -//! X11 keycode handling. +use keyboard_types::Key; -use super::super::shared; -pub use super::super::shared::code_to_location; -use crate::keyboard::{Code, KbKey, Modifiers}; -use x11rb::protocol::xproto::Keycode; +use super::xkbcommon_sys::*; -/// Convert a hardware scan code to a key. -/// -/// Note: this is a hardcoded layout. We need to detect the user's -/// layout from the system and apply it. -pub fn code_to_key(code: Code, m: Modifiers) -> KbKey { - fn a(s: &str) -> KbKey { - KbKey::Character(s.into()) +/// Map from an xkb_common key code to a key, if possible. +pub fn map_key(keysym: u32) -> Key { + use Key::*; + match keysym { + XKB_KEY_BackSpace => Backspace, + XKB_KEY_Tab | XKB_KEY_KP_Tab | XKB_KEY_ISO_Left_Tab => Tab, + XKB_KEY_Clear | XKB_KEY_KP_Begin | XKB_KEY_XF86Clear => Clear, + XKB_KEY_Return | XKB_KEY_KP_Enter => Enter, + XKB_KEY_Linefeed => Enter, + XKB_KEY_Pause => Pause, + XKB_KEY_Scroll_Lock => ScrollLock, + XKB_KEY_Escape => Escape, + XKB_KEY_Multi_key => Compose, + XKB_KEY_Kanji => KanjiMode, + XKB_KEY_Muhenkan => NonConvert, + XKB_KEY_Henkan_Mode => Convert, + XKB_KEY_Romaji => Romaji, + XKB_KEY_Hiragana => Hiragana, + XKB_KEY_Katakana => Katakana, + XKB_KEY_Hiragana_Katakana => HiraganaKatakana, + XKB_KEY_Zenkaku => Zenkaku, + XKB_KEY_Hankaku => Hankaku, + XKB_KEY_Zenkaku_Hankaku => ZenkakuHankaku, + XKB_KEY_Kana_Lock => KanaMode, + XKB_KEY_Eisu_Shift | XKB_KEY_Eisu_toggle => Alphanumeric, + XKB_KEY_Hangul => HangulMode, + XKB_KEY_Hangul_Hanja => HanjaMode, + XKB_KEY_Codeinput => CodeInput, + XKB_KEY_SingleCandidate => SingleCandidate, + XKB_KEY_MultipleCandidate => AllCandidates, + XKB_KEY_PreviousCandidate => PreviousCandidate, + XKB_KEY_Home | XKB_KEY_KP_Home => Home, + XKB_KEY_Left | XKB_KEY_KP_Left => ArrowLeft, + XKB_KEY_Up | XKB_KEY_KP_Up => ArrowUp, + XKB_KEY_Right | XKB_KEY_KP_Right => ArrowRight, + XKB_KEY_Down | XKB_KEY_KP_Down => ArrowDown, + XKB_KEY_Prior | XKB_KEY_KP_Prior => PageUp, + XKB_KEY_Next | XKB_KEY_KP_Next | XKB_KEY_XF86ScrollDown => PageDown, + XKB_KEY_End | XKB_KEY_KP_End | XKB_KEY_XF86ScrollUp => End, + XKB_KEY_Select => Select, + // Treat Print/PrintScreen as PrintScreen https://crbug.com/683097. + XKB_KEY_Print | XKB_KEY_3270_PrintScreen => PrintScreen, + XKB_KEY_Execute => Execute, + XKB_KEY_Insert | XKB_KEY_KP_Insert => Insert, + XKB_KEY_Undo => Undo, + XKB_KEY_Redo => Redo, + XKB_KEY_Menu => ContextMenu, + XKB_KEY_Find => Find, + XKB_KEY_Cancel => Cancel, + XKB_KEY_Help => Help, + XKB_KEY_Break | XKB_KEY_3270_Attn => Attn, + XKB_KEY_Mode_switch => ModeChange, + XKB_KEY_Num_Lock => NumLock, + XKB_KEY_F1 | XKB_KEY_KP_F1 => F1, + XKB_KEY_F2 | XKB_KEY_KP_F2 => F2, + XKB_KEY_F3 | XKB_KEY_KP_F3 => F3, + XKB_KEY_F4 | XKB_KEY_KP_F4 => F4, + XKB_KEY_F5 => F5, + XKB_KEY_F6 => F6, + XKB_KEY_F7 => F7, + XKB_KEY_F8 => F8, + XKB_KEY_F9 => F9, + XKB_KEY_F10 => F10, + XKB_KEY_F11 => F11, + XKB_KEY_F12 => F12, + // not available in keyboard-types + // XKB_KEY_XF86Tools | XKB_KEY_F13 => F13, + // XKB_KEY_F14 | XKB_KEY_XF86Launch5 => F14, + // XKB_KEY_F15 | XKB_KEY_XF86Launch6 => F15, + // XKB_KEY_F16 | XKB_KEY_XF86Launch7 => F16, + // XKB_KEY_F17 | XKB_KEY_XF86Launch8 => F17, + // XKB_KEY_F18 | XKB_KEY_XF86Launch9 => F18, + // XKB_KEY_F19 => F19, + // XKB_KEY_F20 => F20, + // XKB_KEY_F21 => F21, + // XKB_KEY_F22 => F22, + // XKB_KEY_F23 => F23, + // XKB_KEY_F24 => F24, + // XKB_KEY_XF86Calculator => LaunchCalculator, + // XKB_KEY_XF86MyComputer | XKB_KEY_XF86Explorer => LaunchMyComputer, + // XKB_KEY_ISO_Level3_Latch => AltGraphLatch, + // XKB_KEY_ISO_Level5_Shift => ShiftLevel5, + XKB_KEY_Shift_L | XKB_KEY_Shift_R => Shift, + XKB_KEY_Control_L | XKB_KEY_Control_R => Control, + XKB_KEY_Caps_Lock => CapsLock, + XKB_KEY_Meta_L | XKB_KEY_Meta_R => Meta, + XKB_KEY_Alt_L | XKB_KEY_Alt_R => Alt, + XKB_KEY_Super_L | XKB_KEY_Super_R => Meta, + XKB_KEY_Hyper_L | XKB_KEY_Hyper_R => Hyper, + XKB_KEY_Delete => Delete, + XKB_KEY_SunProps => Props, + XKB_KEY_XF86Next_VMode => VideoModeNext, + XKB_KEY_XF86MonBrightnessUp => BrightnessUp, + XKB_KEY_XF86MonBrightnessDown => BrightnessDown, + XKB_KEY_XF86Standby | XKB_KEY_XF86Sleep | XKB_KEY_XF86Suspend => Standby, + XKB_KEY_XF86AudioLowerVolume => AudioVolumeDown, + XKB_KEY_XF86AudioMute => AudioVolumeMute, + XKB_KEY_XF86AudioRaiseVolume => AudioVolumeUp, + XKB_KEY_XF86AudioPlay => MediaPlayPause, + XKB_KEY_XF86AudioStop => MediaStop, + XKB_KEY_XF86AudioPrev => MediaTrackPrevious, + XKB_KEY_XF86AudioNext => MediaTrackNext, + XKB_KEY_XF86HomePage => BrowserHome, + XKB_KEY_XF86Mail => LaunchMail, + XKB_KEY_XF86Search => BrowserSearch, + XKB_KEY_XF86AudioRecord => MediaRecord, + XKB_KEY_XF86Calendar => LaunchCalendar, + XKB_KEY_XF86Back => BrowserBack, + XKB_KEY_XF86Forward => BrowserForward, + XKB_KEY_XF86Stop => BrowserStop, + XKB_KEY_XF86Refresh | XKB_KEY_XF86Reload => BrowserRefresh, + XKB_KEY_XF86PowerOff => PowerOff, + XKB_KEY_XF86WakeUp => WakeUp, + XKB_KEY_XF86Eject => Eject, + XKB_KEY_XF86ScreenSaver => LaunchScreenSaver, + XKB_KEY_XF86WWW => LaunchWebBrowser, + XKB_KEY_XF86Favorites => BrowserFavorites, + XKB_KEY_XF86AudioPause => MediaPause, + XKB_KEY_XF86AudioMedia | XKB_KEY_XF86Music => LaunchMusicPlayer, + XKB_KEY_XF86AudioRewind => MediaRewind, + XKB_KEY_XF86CD | XKB_KEY_XF86Video => LaunchMediaPlayer, + XKB_KEY_XF86Close => Close, + XKB_KEY_XF86Copy | XKB_KEY_SunCopy => Copy, + XKB_KEY_XF86Cut | XKB_KEY_SunCut => Cut, + XKB_KEY_XF86Display => DisplaySwap, + XKB_KEY_XF86Excel => LaunchSpreadsheet, + XKB_KEY_XF86LogOff => LogOff, + XKB_KEY_XF86New => New, + XKB_KEY_XF86Open | XKB_KEY_SunOpen => Open, + XKB_KEY_XF86Paste | XKB_KEY_SunPaste => Paste, + XKB_KEY_XF86Reply => MailReply, + XKB_KEY_XF86Save => Save, + XKB_KEY_XF86Send => MailSend, + XKB_KEY_XF86Spell => SpellCheck, + XKB_KEY_XF86SplitScreen => SplitScreenToggle, + XKB_KEY_XF86Word | XKB_KEY_XF86OfficeHome => LaunchWordProcessor, + XKB_KEY_XF86ZoomIn => ZoomIn, + XKB_KEY_XF86ZoomOut => ZoomOut, + XKB_KEY_XF86WebCam => LaunchWebCam, + XKB_KEY_XF86MailForward => MailForward, + XKB_KEY_XF86AudioForward => MediaFastForward, + XKB_KEY_XF86AudioRandomPlay => RandomToggle, + XKB_KEY_XF86Subtitle => Subtitle, + XKB_KEY_XF86Hibernate => Hibernate, + XKB_KEY_3270_EraseEOF => EraseEof, + XKB_KEY_3270_Play => Play, + XKB_KEY_3270_ExSelect => ExSel, + XKB_KEY_3270_CursorSelect => CrSel, + XKB_KEY_ISO_Level3_Shift => AltGraph, + XKB_KEY_ISO_Next_Group => GroupNext, + XKB_KEY_ISO_Prev_Group => GroupPrevious, + XKB_KEY_ISO_First_Group => GroupFirst, + XKB_KEY_ISO_Last_Group => GroupLast, + XKB_KEY_dead_grave + | XKB_KEY_dead_acute + | XKB_KEY_dead_circumflex + | XKB_KEY_dead_tilde + | XKB_KEY_dead_macron + | XKB_KEY_dead_breve + | XKB_KEY_dead_abovedot + | XKB_KEY_dead_diaeresis + | XKB_KEY_dead_abovering + | XKB_KEY_dead_doubleacute + | XKB_KEY_dead_caron + | XKB_KEY_dead_cedilla + | XKB_KEY_dead_ogonek + | XKB_KEY_dead_iota + | XKB_KEY_dead_voiced_sound + | XKB_KEY_dead_semivoiced_sound + | XKB_KEY_dead_belowdot + | XKB_KEY_dead_hook + | XKB_KEY_dead_horn + | XKB_KEY_dead_stroke + | XKB_KEY_dead_abovecomma + | XKB_KEY_dead_abovereversedcomma + | XKB_KEY_dead_doublegrave + | XKB_KEY_dead_belowring + | XKB_KEY_dead_belowmacron + | XKB_KEY_dead_belowcircumflex + | XKB_KEY_dead_belowtilde + | XKB_KEY_dead_belowbreve + | XKB_KEY_dead_belowdiaeresis + | XKB_KEY_dead_invertedbreve + | XKB_KEY_dead_belowcomma + | XKB_KEY_dead_currency + | XKB_KEY_dead_greek => Dead, + _ => Unidentified, } - fn s(mods: Modifiers, base: &str, shifted: &str) -> KbKey { - if mods.shift() { - KbKey::Character(shifted.into()) - } else { - KbKey::Character(base.into()) - } - } - fn n(mods: Modifiers, base: KbKey, num: &str) -> KbKey { - if mods.contains(Modifiers::NUM_LOCK) != mods.shift() { - KbKey::Character(num.into()) - } else { - base - } - } - match code { - Code::KeyA => s(m, "a", "A"), - Code::KeyB => s(m, "b", "B"), - Code::KeyC => s(m, "c", "C"), - Code::KeyD => s(m, "d", "D"), - Code::KeyE => s(m, "e", "E"), - Code::KeyF => s(m, "f", "F"), - Code::KeyG => s(m, "g", "G"), - Code::KeyH => s(m, "h", "H"), - Code::KeyI => s(m, "i", "I"), - Code::KeyJ => s(m, "j", "J"), - Code::KeyK => s(m, "k", "K"), - Code::KeyL => s(m, "l", "L"), - Code::KeyM => s(m, "m", "M"), - Code::KeyN => s(m, "n", "N"), - Code::KeyO => s(m, "o", "O"), - Code::KeyP => s(m, "p", "P"), - Code::KeyQ => s(m, "q", "Q"), - Code::KeyR => s(m, "r", "R"), - Code::KeyS => s(m, "s", "S"), - Code::KeyT => s(m, "t", "T"), - Code::KeyU => s(m, "u", "U"), - Code::KeyV => s(m, "v", "V"), - Code::KeyW => s(m, "w", "W"), - Code::KeyX => s(m, "x", "X"), - Code::KeyY => s(m, "y", "Y"), - Code::KeyZ => s(m, "z", "Z"), - - Code::Digit0 => s(m, "0", ")"), - Code::Digit1 => s(m, "1", "!"), - Code::Digit2 => s(m, "2", "@"), - Code::Digit3 => s(m, "3", "#"), - Code::Digit4 => s(m, "4", "$"), - Code::Digit5 => s(m, "5", "%"), - Code::Digit6 => s(m, "6", "^"), - Code::Digit7 => s(m, "7", "&"), - Code::Digit8 => s(m, "8", "*"), - Code::Digit9 => s(m, "9", "("), - - Code::Backquote => s(m, "`", "~"), - Code::Minus => s(m, "-", "_"), - Code::Equal => s(m, "=", "+"), - Code::BracketLeft => s(m, "[", "{"), - Code::BracketRight => s(m, "]", "}"), - Code::Backslash => s(m, "\\", "|"), - Code::Semicolon => s(m, ";", ":"), - Code::Quote => s(m, "'", "\""), - Code::Comma => s(m, ",", "<"), - Code::Period => s(m, ".", ">"), - Code::Slash => s(m, "/", "?"), - - Code::Space => a(" "), - - Code::Escape => KbKey::Escape, - Code::Backspace => KbKey::Backspace, - Code::Tab => KbKey::Tab, - Code::Enter => KbKey::Enter, - Code::ControlLeft => KbKey::Control, - Code::ShiftLeft => KbKey::Shift, - Code::ShiftRight => KbKey::Shift, - Code::NumpadMultiply => a("*"), - Code::AltLeft => KbKey::Alt, - Code::CapsLock => KbKey::CapsLock, - Code::F1 => KbKey::F1, - Code::F2 => KbKey::F2, - Code::F3 => KbKey::F3, - Code::F4 => KbKey::F4, - Code::F5 => KbKey::F5, - Code::F6 => KbKey::F6, - Code::F7 => KbKey::F7, - Code::F8 => KbKey::F8, - Code::F9 => KbKey::F9, - Code::F10 => KbKey::F10, - Code::NumLock => KbKey::NumLock, - Code::ScrollLock => KbKey::ScrollLock, - Code::Numpad0 => n(m, KbKey::Insert, "0"), - Code::Numpad1 => n(m, KbKey::End, "1"), - Code::Numpad2 => n(m, KbKey::ArrowDown, "2"), - Code::Numpad3 => n(m, KbKey::PageDown, "3"), - Code::Numpad4 => n(m, KbKey::ArrowLeft, "4"), - Code::Numpad5 => n(m, KbKey::Clear, "5"), - Code::Numpad6 => n(m, KbKey::ArrowRight, "6"), - Code::Numpad7 => n(m, KbKey::Home, "7"), - Code::Numpad8 => n(m, KbKey::ArrowUp, "8"), - Code::Numpad9 => n(m, KbKey::PageUp, "9"), - Code::NumpadSubtract => a("-"), - Code::NumpadAdd => a("+"), - Code::NumpadDecimal => n(m, KbKey::Delete, "."), - Code::IntlBackslash => s(m, "\\", "|"), - Code::F11 => KbKey::F11, - Code::F12 => KbKey::F12, - // This mapping is based on the picture in the w3c spec. - Code::IntlRo => a("\\"), - Code::Convert => KbKey::Convert, - Code::KanaMode => KbKey::KanaMode, - Code::NonConvert => KbKey::NonConvert, - Code::NumpadEnter => KbKey::Enter, - Code::ControlRight => KbKey::Control, - Code::NumpadDivide => a("/"), - Code::PrintScreen => KbKey::PrintScreen, - Code::AltRight => KbKey::Alt, - Code::Home => KbKey::Home, - Code::ArrowUp => KbKey::ArrowUp, - Code::PageUp => KbKey::PageUp, - Code::ArrowLeft => KbKey::ArrowLeft, - Code::ArrowRight => KbKey::ArrowRight, - Code::End => KbKey::End, - Code::ArrowDown => KbKey::ArrowDown, - Code::PageDown => KbKey::PageDown, - Code::Insert => KbKey::Insert, - Code::Delete => KbKey::Delete, - Code::AudioVolumeMute => KbKey::AudioVolumeMute, - Code::AudioVolumeDown => KbKey::AudioVolumeDown, - Code::AudioVolumeUp => KbKey::AudioVolumeUp, - Code::NumpadEqual => a("="), - Code::Pause => KbKey::Pause, - Code::NumpadComma => a(","), - Code::Lang1 => KbKey::HangulMode, - Code::Lang2 => KbKey::HanjaMode, - Code::IntlYen => a("¥"), - Code::MetaLeft => KbKey::Meta, - Code::MetaRight => KbKey::Meta, - Code::ContextMenu => KbKey::ContextMenu, - Code::BrowserStop => KbKey::BrowserStop, - Code::Again => KbKey::Again, - Code::Props => KbKey::Props, - Code::Undo => KbKey::Undo, - Code::Select => KbKey::Select, - Code::Copy => KbKey::Copy, - Code::Open => KbKey::Open, - Code::Paste => KbKey::Paste, - Code::Find => KbKey::Find, - Code::Cut => KbKey::Cut, - Code::Help => KbKey::Help, - Code::LaunchApp2 => KbKey::LaunchApplication2, - Code::WakeUp => KbKey::WakeUp, - Code::LaunchApp1 => KbKey::LaunchApplication1, - Code::LaunchMail => KbKey::LaunchMail, - Code::BrowserFavorites => KbKey::BrowserFavorites, - Code::BrowserBack => KbKey::BrowserBack, - Code::BrowserForward => KbKey::BrowserForward, - Code::Eject => KbKey::Eject, - Code::MediaTrackNext => KbKey::MediaTrackNext, - Code::MediaPlayPause => KbKey::MediaPlayPause, - Code::MediaTrackPrevious => KbKey::MediaTrackPrevious, - Code::MediaStop => KbKey::MediaStop, - Code::MediaSelect => KbKey::LaunchMediaPlayer, - Code::BrowserHome => KbKey::BrowserHome, - Code::BrowserRefresh => KbKey::BrowserRefresh, - Code::BrowserSearch => KbKey::BrowserSearch, - - _ => KbKey::Unidentified, - } -} - -pub fn hardware_keycode_to_code(hw_keycode: Keycode) -> Code { - shared::hardware_keycode_to_code(hw_keycode as u16) } diff --git a/druid-shell/src/backend/x11/mod.rs b/druid-shell/src/backend/x11/mod.rs index 262e70a680..b51998b70e 100644 --- a/druid-shell/src/backend/x11/mod.rs +++ b/druid-shell/src/backend/x11/mod.rs @@ -32,11 +32,13 @@ #[macro_use] mod util; +mod xkb; pub mod application; pub mod clipboard; pub mod error; -pub mod keycodes; +mod keycodes; pub mod menu; pub mod screen; pub mod window; +mod xkbcommon_sys; diff --git a/druid-shell/src/backend/x11/window.rs b/druid-shell/src/backend/x11/window.rs index f85c774831..547f393c39 100644 --- a/druid-shell/src/backend/x11/window.rs +++ b/druid-shell/src/backend/x11/window.rs @@ -47,7 +47,7 @@ use raw_window_handle::{unix::XcbHandle, HasRawWindowHandle, RawWindowHandle}; use crate::common_util::IdleCallback; use crate::dialog::FileDialogOptions; use crate::error::Error as ShellError; -use crate::keyboard::{KeyEvent, KeyState, Modifiers}; +use crate::keyboard::{KeyState, Modifiers}; use crate::kurbo::{Insets, Point, Rect, Size, Vec2}; use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; use crate::piet::{Piet, PietText, RenderContext}; @@ -57,10 +57,9 @@ use crate::text::{simulate_input, Event}; use crate::window::{ FileDialogToken, IdleToken, TextFieldToken, TimerToken, WinHandler, WindowLevel, }; -use crate::{window, ScaledArea}; +use crate::{window, KeyEvent, ScaledArea}; use super::application::Application; -use super::keycodes; use super::menu::Menu; use super::util::Timer; @@ -1041,26 +1040,12 @@ impl Window { Ok(()) } - pub fn handle_key_press(&self, key_press: &xproto::KeyPressEvent) { - let hw_keycode = key_press.detail; - let code = keycodes::hardware_keycode_to_code(hw_keycode); - let mods = key_mods(key_press.state); - let key = keycodes::code_to_key(code, mods); - let location = keycodes::code_to_location(code); - let state = KeyState::Down; - let key_event = KeyEvent { - code, - key, - mods, - location, - state, - repeat: false, - is_composing: false, - }; - self.with_handler(|h| { - if !h.key_down(key_event.clone()) { - simulate_input(h, self.active_text_field.get(), key_event); + pub fn handle_key_event(&self, event: KeyEvent) { + self.with_handler(|h| match event.state { + KeyState::Down => { + simulate_input(h, self.active_text_field.get(), event); } + KeyState::Up => h.key_up(event), }); } diff --git a/druid-shell/src/backend/x11/xkb.rs b/druid-shell/src/backend/x11/xkb.rs new file mode 100644 index 0000000000..576d5fd3fa --- /dev/null +++ b/druid-shell/src/backend/x11/xkb.rs @@ -0,0 +1,266 @@ +// Copyright 2021 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A minimal wrapper around Xkb for our use. + +use super::keycodes; +use super::xkbcommon_sys::*; +use crate::{ + backend::shared::{code_to_location, hardware_keycode_to_code}, + KeyEvent, KeyState, Modifiers, +}; +use keyboard_types::{Code, Key}; +use std::convert::TryFrom; +use std::os::raw::{c_char, c_int}; +use std::ptr; +use x11rb::xcb_ffi::XCBConnection; + +pub struct DeviceId(c_int); + +/// A global xkb context object. +/// +/// Reference counted under the hood. +// Assume this isn't threadsafe unless proved otherwise. (e.g. don't implement Send/Sync) +pub struct Context(*mut xkb_context); + +impl Context { + /// Create a new xkb context. + /// + /// The returned object is lightweight and clones will point at the same context internally. + pub fn new() -> Self { + unsafe { Self(xkb_context_new(XKB_CONTEXT_NO_FLAGS)) } + } + + pub fn core_keyboard_device_id(&self, conn: &XCBConnection) -> Option { + let id = unsafe { + xkb_x11_get_core_keyboard_device_id( + conn.get_raw_xcb_connection() as *mut xcb_connection_t + ) + }; + if id != -1 { + Some(DeviceId(id)) + } else { + None + } + } + + pub fn keymap_from_device(&self, conn: &XCBConnection, device: DeviceId) -> Option { + let key_map = unsafe { + xkb_x11_keymap_new_from_device( + self.0, + conn.get_raw_xcb_connection() as *mut xcb_connection_t, + device.0, + XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + if key_map.is_null() { + return None; + } + Some(Keymap(key_map)) + } + + /// Set the log level using `tracing` levels. + /// + /// Because `xkb` has a `critical` error, each rust error maps to 1 above (e.g. error -> + /// critical, warn -> error etc.) + pub fn set_log_level(&self, level: tracing::Level) { + use tracing::Level; + let level = match level { + Level::ERROR => XKB_LOG_LEVEL_CRITICAL, + Level::WARN => XKB_LOG_LEVEL_ERROR, + Level::INFO => XKB_LOG_LEVEL_WARNING, + Level::DEBUG => XKB_LOG_LEVEL_INFO, + Level::TRACE => XKB_LOG_LEVEL_DEBUG, + }; + unsafe { + xkb_context_set_log_level(self.0, level); + } + } +} + +impl Clone for Context { + fn clone(&self) -> Self { + Self(unsafe { xkb_context_ref(self.0) }) + } +} + +impl Drop for Context { + fn drop(&mut self) { + unsafe { + xkb_context_unref(self.0); + } + } +} + +pub struct Keymap(*mut xkb_keymap); + +impl Keymap { + pub fn state(&self) -> State { + State::new(self) + } +} + +impl Clone for Keymap { + fn clone(&self) -> Self { + Self(unsafe { xkb_keymap_ref(self.0) }) + } +} + +impl Drop for Keymap { + fn drop(&mut self) { + unsafe { + xkb_keymap_unref(self.0); + } + } +} + +pub struct State { + state: *mut xkb_state, + mods: ModsIndices, +} + +#[derive(Clone, Copy)] +pub struct ModsIndices { + control: xkb_mod_index_t, + shift: xkb_mod_index_t, + alt: xkb_mod_index_t, + super_: xkb_mod_index_t, + caps_lock: xkb_mod_index_t, + num_lock: xkb_mod_index_t, +} + +impl State { + pub fn new(keymap: &Keymap) -> Self { + let keymap = keymap.0; + let state = unsafe { xkb_state_new(keymap) }; + let mod_idx = |str: &'static [u8]| unsafe { + xkb_keymap_mod_get_index(keymap, str.as_ptr() as *mut c_char) + }; + Self { + state, + mods: ModsIndices { + control: mod_idx(XKB_MOD_NAME_CTRL), + shift: mod_idx(XKB_MOD_NAME_SHIFT), + alt: mod_idx(XKB_MOD_NAME_ALT), + super_: mod_idx(XKB_MOD_NAME_LOGO), + caps_lock: mod_idx(XKB_MOD_NAME_CAPS), + num_lock: mod_idx(XKB_MOD_NAME_NUM), + }, + } + } + + pub fn key_event(&mut self, scancode: u32, state: KeyState) -> KeyEvent { + let code = u16::try_from(scancode) + .map(hardware_keycode_to_code) + .unwrap_or(Code::Unidentified); + let key = self.get_logical_key(scancode); + // TODO this is lazy - really should use xkb i.e. augment the get_logical_key method. + let location = code_to_location(code); + + let repeat = false; + // TODO not sure how to get this + let is_composing = false; + + let mut mods = Modifiers::empty(); + // Update xkb's state (e.g. return capitals if we've pressed shift) + unsafe { + xkb_state_update_key( + self.state, + scancode, + match state { + KeyState::Down => XKB_KEY_DOWN, + KeyState::Up => XKB_KEY_UP, + }, + ); + // compiler will unroll this loop + // FIXME(msrv): remove .iter().cloned() when msrv is >= 1.53 + for (idx, mod_) in [ + (self.mods.control, Modifiers::CONTROL), + (self.mods.shift, Modifiers::SHIFT), + (self.mods.super_, Modifiers::SUPER), + (self.mods.alt, Modifiers::ALT), + (self.mods.caps_lock, Modifiers::CAPS_LOCK), + (self.mods.num_lock, Modifiers::NUM_LOCK), + ] + .iter() + .cloned() + { + if xkb_state_mod_index_is_active(self.state, idx, XKB_STATE_MODS_EFFECTIVE) != 0 { + mods |= mod_; + } + } + } + KeyEvent { + state, + key, + code, + location, + mods, + repeat, + is_composing, + } + } + + fn get_logical_key(&mut self, scancode: u32) -> Key { + let mut key = keycodes::map_key(self.key_get_one_sym(scancode)); + if matches!(key, Key::Unidentified) { + if let Some(s) = self.key_get_utf8(scancode) { + key = Key::Character(s); + } + } + key + } + + fn key_get_one_sym(&mut self, scancode: u32) -> u32 { + unsafe { xkb_state_key_get_one_sym(self.state, scancode) } + } + + /// Get the string representation of a key. + // TODO `keyboard_types` forces us to return a String, but it would be nicer if we could stay + // on the stack, especially since we expect most results to be pretty small. + fn key_get_utf8(&mut self, scancode: u32) -> Option { + unsafe { + // First get the size we will need + let len = xkb_state_key_get_utf8(self.state, scancode, ptr::null_mut(), 0); + if len == 0 { + return None; + } + // add 1 because we will get a null-terminated string. + let len = usize::try_from(len).unwrap() + 1; + let mut buf: Vec = Vec::new(); + buf.resize(len, 0); + xkb_state_key_get_utf8(self.state, scancode, buf.as_mut_ptr() as *mut c_char, len); + assert!(buf[buf.len() - 1] == 0); + buf.pop(); + Some(String::from_utf8(buf).unwrap()) + } + } +} + +impl Clone for State { + fn clone(&self) -> Self { + Self { + state: unsafe { xkb_state_ref(self.state) }, + mods: self.mods, + } + } +} + +impl Drop for State { + fn drop(&mut self) { + unsafe { + xkb_state_unref(self.state); + } + } +} diff --git a/druid-shell/src/backend/x11/xkbcommon_sys.rs b/druid-shell/src/backend/x11/xkbcommon_sys.rs new file mode 100644 index 0000000000..ae3a360b69 --- /dev/null +++ b/druid-shell/src/backend/x11/xkbcommon_sys.rs @@ -0,0 +1,8 @@ +#![allow(unused, non_upper_case_globals, non_camel_case_types)] +// unknown lints to make compile on older rust versions +#![cfg_attr(test, allow(unknown_lints, deref_nullptr))] +// generated code has some redudant static lifetimes, I don't think we can change that. +#![allow(clippy::redundant_static_lifetimes)] + +use nix::libc::FILE; +include!(concat!(env!("OUT_DIR"), "/xkbcommon_sys.rs"));