From f5bee06b7bc1c9c4e5882c646174f647ebf50531 Mon Sep 17 00:00:00 2001 From: Victor Berger Date: Sun, 9 Apr 2017 16:01:00 +0200 Subject: [PATCH 1/3] keyboard: first draft of xkbcommon handling --- Cargo.toml | 8 +- src/keyboard/mod.rs | 263 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 3 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 src/keyboard/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 4ec15c0..218116d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,14 @@ authors = ["Victor Berger "] license = "MIT" [dependencies] -wayland-server = "0.8.6" +wayland-server = "0.9.1" nix = "0.7.0" -glutin = { version = "~0.7.4", optional = true } -glium = { version = "~0.16.0", optional = true } +xkbcommon = "0.2.1" +tempfile = "2.1.5" slog = { version = "~1.5.2", features = ["max_level_trace", "release_max_level_info"] } slog-stdlog = "~1.1.0" +glutin = { version = "~0.7.4", optional = true } +glium = { version = "~0.16.0", optional = true } clippy = { version = "*", optional = true } [dev-dependencies] diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs new file mode 100644 index 0000000..3c0d189 --- /dev/null +++ b/src/keyboard/mod.rs @@ -0,0 +1,263 @@ +//! Utilities for keyboard handling +//! +//! This module provides utilities for keyboardand keymap handling: keymap interpretation +//! and forwarding keystrokes to clients using xkbcommon. +//! +//! You can first create a `KbdHandle` using the `create_keyboard_handler` function in this module. +//! The handle you obtained can be cloned to access this keyboard state from different places. It is +//! expected that such a context is created for each keyboard the compositor has access to. +//! +//! This handle gives you 3 main way to interact with the keymap handling: +//! +//! - send the keymap information to a client using the `KbdHandle::send_keymap` method. +//! - set the current focus for this keyboard: designing the client that will receive the key inputs +//! using the `KbdHandle::set_focus` method. +//! - process key inputs from the input backend, allowing them to be catched at the compositor-level +//! or forwarded to the client. See the documentation of the `KbdHandle::input` method for +//! details. + + +use backend::input::KeyState; +use std::io::Write; +use std::os::unix::io::AsRawFd; +use std::sync::{Arc, Mutex}; + +use tempfile::tempfile; + +use wayland_server::{Liveness, Resource}; +use wayland_server::protocol::{wl_keyboard, wl_surface}; + +use xkbcommon::xkb; + +pub use xkbcommon::xkb::{Keysym, keysyms}; + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled +/// (like caps lock). +#[derive(Copy,Clone,Debug)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, +} + +impl ModifiersState { + fn new() -> ModifiersState { + ModifiersState { + ctrl: false, + alt: false, + shift: false, + caps_lock: false, + logo: false, + num_lock: false, + } + } + + fn update_with(&mut self, state: &xkb::State) { + self.ctrl = state.mod_name_is_active(&xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); + self.alt = state.mod_name_is_active(&xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); + self.shift = state.mod_name_is_active(&xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); + self.caps_lock = state.mod_name_is_active(&xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE); + self.logo = state.mod_name_is_active(&xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + self.num_lock = state.mod_name_is_active(&xkb::MOD_NAME_NUM, xkb::STATE_MODS_EFFECTIVE); + } +} + +struct KbdInternal { + focus: Option<(wl_surface::WlSurface, wl_keyboard::WlKeyboard)>, + pressed_keys: Vec, + mods_state: ModifiersState, + _context: xkb::Context, + keymap: xkb::Keymap, + state: xkb::State, +} + +impl KbdInternal { + fn new() -> Result { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + // TODO: api to choose the keymap to load + let keymap = xkb::Keymap::new_from_names(&context, &"", &"", &"fr", &"oss", None, 0).ok_or(())?; + let state = xkb::State::new(&keymap); + Ok(KbdInternal { + focus: None, + pressed_keys: Vec::new(), + mods_state: ModifiersState::new(), + _context: context, + keymap: keymap, + state: state, + }) + } + + // return true if modifier state has changed + fn key_input(&mut self, keycode: u32, state: KeyState) -> bool { + // track pressed keys as xkbcommon does not seem to expose it :( + let direction = match state { + KeyState::Pressed => { + self.pressed_keys.push(keycode); + xkb::KeyDirection::Down + } + KeyState::Released => { + self.pressed_keys.retain(|&k| k != keycode); + xkb::KeyDirection::Up + } + }; + + // update state + let state_components = self.state.update_key(keycode, direction); + if state_components != 0 { + self.mods_state.update_with(&self.state); + true + } else { + false + } + } + + fn serialize_modifiers(&self) -> (u32, u32, u32, u32) { + let mods_depressed = self.state.serialize_mods(xkb::STATE_MODS_DEPRESSED); + let mods_latched = self.state.serialize_mods(xkb::STATE_MODS_LATCHED); + let mods_locked = self.state.serialize_mods(xkb::STATE_MODS_LOCKED); + let layout_locked = self.state.serialize_layout(xkb::STATE_LAYOUT_LOCKED); + + return (mods_depressed, mods_latched, mods_locked, layout_locked); + } + + fn serialize_pressed_keys(&self) -> Vec { + let serialized = unsafe { + ::std::slice::from_raw_parts(self.pressed_keys.as_ptr() as *const u8, + self.pressed_keys.len() * 4) + }; + serialized.into() + } +} + +pub fn create_keyboard_handler() -> Result { + let internal = KbdInternal::new()?; + + // prepare a tempfile with the keymap, to send it to clients + // TODO: better error handling + let mut keymap_file = tempfile().unwrap(); + let keymap_data = internal.keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1); + keymap_file.write_all(keymap_data.as_bytes()).unwrap(); + keymap_file.flush().unwrap(); + + Ok(KbdHandle { + internal: Arc::new((Mutex::new(internal), (keymap_file, keymap_data.as_bytes().len() as u32))), + }) +} + +/// An handle to a keyboard handler +/// +/// It can be cloned and all clones manipulate the same internal state. Clones +/// can also be sent across threads. +/// +/// See module-level documentation for details of its use. +#[derive(Clone)] +pub struct KbdHandle { + internal: Arc<(Mutex, (::std::fs::File, u32))>, +} + +impl KbdHandle { + /// Handle a keystroke + /// + /// All keystrokes from the input backend should be fed _in order_ to this method of the + /// keyboard handler. It will internally track the state of the keymap. + /// + /// The `filter` argument is expected to be a closure which will peek at the generated input + /// as interpreted by the keymap befor it is forwarded to the focused client. If this closure + /// returns false, the input will not be sent to the client. This mechanism can be used to + /// implement compositor-level key bindings for example. + /// + /// The module `smithay::keyboard::keysyms` exposes definitions of all possible keysyms + /// to be compared against. This includes non-characted keysyms, such as XF86 special keys. + pub fn input(&self, keycode: u32, state: KeyState, serial: u32, filter: F) + where F: FnOnce(&ModifiersState, Keysym) -> bool + { + let mut guard = self.internal + .0 + .lock() + .unwrap(); + let mods_changed = guard.key_input(keycode, state); + + if !filter(&guard.mods_state, guard.state.key_get_one_sym(keycode)) { + // the filter returned false, we do not forward to client + return; + } + + // forward to client if no keybinding is triggered + if let Some((_, ref kbd)) = guard.focus { + if mods_changed { + let (dep, la, lo, gr) = guard.serialize_modifiers(); + kbd.modifiers(serial, dep, la, lo, gr); + } + let wl_state = match state { + KeyState::Pressed => wl_keyboard::KeyState::Pressed, + KeyState::Released => wl_keyboard::KeyState::Released, + }; + kbd.key(serial, 0, keycode, wl_state); + } + } + + /// Set the current focus of this keyboard + /// + /// Any previous focus will be sent a `wl_keyboard::leave` event, and if the new focus + /// is not `None`, a `wl_keyboard::enter` event will be sent. + pub fn set_focus(&self, focus: Option<(wl_surface::WlSurface, wl_keyboard::WlKeyboard)>, serial: u32) { + // TODO: check surface and keyboard are from the same client + + let mut guard = self.internal + .0 + .lock() + .unwrap(); + + // remove current focus + let old_kbd = if let Some((old_surface, old_kbd)) = guard.focus.take() { + if old_surface.status() != Liveness::Dead { + old_kbd.leave(serial, &old_surface); + } + Some(old_kbd) + } else { + None + }; + + // set new focus + if let Some((surface, kbd)) = focus { + if surface.status() != Liveness::Dead { + // send new mods status if client instance changed + match old_kbd { + Some(ref okbd) if okbd.equals(&kbd) => {} + _ => { + let (dep, la, lo, gr) = guard.serialize_modifiers(); + kbd.modifiers(serial, dep, la, lo, gr); + } + } + // send enter event + kbd.enter(serial, &surface, guard.serialize_pressed_keys()); + } + guard.focus = Some((surface, kbd)) + } + } + + /// Send the keymap to this keyboard + /// + /// This should be done first, before anything else is done with this keyboard. + pub fn send_keymap(&self, kbd: &wl_keyboard::WlKeyboard) { + let keymap_data = &self.internal.1; + kbd.keymap(wl_keyboard::KeymapFormat::XkbV1, + keymap_data.0.as_raw_fd(), + keymap_data.1); + } +} diff --git a/src/lib.rs b/src/lib.rs index 56498d3..5958779 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ #[macro_use] extern crate wayland_server; extern crate nix; +extern crate xkbcommon; +extern crate tempfile; #[cfg(feature = "backend_glutin")] extern crate glutin; @@ -19,3 +21,4 @@ extern crate slog_stdlog; pub mod shm; pub mod backend; +pub mod keyboard; From a7117369a2ba15ecfba23ec0ace32544fa833155 Mon Sep 17 00:00:00 2001 From: Victor Berger Date: Sun, 9 Apr 2017 16:43:10 +0200 Subject: [PATCH 2/3] cargo fmt --- src/backend/glutin.rs | 31 +++++++++++++++++++++++++------ src/keyboard/mod.rs | 13 ++++--------- src/shm/mod.rs | 8 ++++++-- src/shm/pool.rs | 14 +++++++------- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/backend/glutin.rs b/src/backend/glutin.rs index ff97418..c545d52 100644 --- a/src/backend/glutin.rs +++ b/src/backend/glutin.rs @@ -218,7 +218,9 @@ impl InputBackend for GlutinInputBackend { } fn get_handler(&mut self) -> Option<&mut InputHandler> { - self.handler.as_mut().map(|handler| handler as &mut InputHandler) + self.handler + .as_mut() + .map(|handler| handler as &mut InputHandler) } fn clear_handler(&mut self) { @@ -233,7 +235,8 @@ impl InputBackend for GlutinInputBackend { fn set_cursor_position(&mut self, x: u32, y: u32) -> Result<(), ()> { if let Some((win_x, win_y)) = self.window.get_position() { - self.window.set_cursor_position(win_x + x as i32, win_y + y as i32) + self.window + .set_cursor_position(win_x + x as i32, win_y + y as i32) } else { Err(()) } @@ -304,7 +307,11 @@ impl InputBackend for GlutinInputBackend { Event::MouseInput(state, button) => { handler.on_pointer_button(&self.seat, self.time_counter, button.into(), state.into()) } - Event::Touch(Touch { phase: TouchPhase::Started, location: (x, y), id }) => { + Event::Touch(Touch { + phase: TouchPhase::Started, + location: (x, y), + id, + }) => { handler.on_touch(&self.seat, self.time_counter, TouchEvent::Down { @@ -313,7 +320,11 @@ impl InputBackend for GlutinInputBackend { y: y, }) } - Event::Touch(Touch { phase: TouchPhase::Moved, location: (x, y), id }) => { + Event::Touch(Touch { + phase: TouchPhase::Moved, + location: (x, y), + id, + }) => { handler.on_touch(&self.seat, self.time_counter, TouchEvent::Motion { @@ -322,7 +333,11 @@ impl InputBackend for GlutinInputBackend { y: y, }) } - Event::Touch(Touch { phase: TouchPhase::Ended, location: (x, y), id }) => { + Event::Touch(Touch { + phase: TouchPhase::Ended, + location: (x, y), + id, + }) => { handler.on_touch(&self.seat, self.time_counter, TouchEvent::Motion { @@ -334,7 +349,11 @@ impl InputBackend for GlutinInputBackend { self.time_counter, TouchEvent::Up { slot: Some(TouchSlot::new(id as u32)) }); } - Event::Touch(Touch { phase: TouchPhase::Cancelled, id, .. }) => { + Event::Touch(Touch { + phase: TouchPhase::Cancelled, + id, + .. + }) => { handler.on_touch(&self.seat, self.time_counter, TouchEvent::Cancel { slot: Some(TouchSlot::new(id as u32)) }) diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index 3c0d189..7961088 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -90,7 +90,8 @@ impl KbdInternal { fn new() -> Result { let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); // TODO: api to choose the keymap to load - let keymap = xkb::Keymap::new_from_names(&context, &"", &"", &"fr", &"oss", None, 0).ok_or(())?; + let keymap = xkb::Keymap::new_from_names(&context, &"", &"", &"fr", &"oss", None, 0) + .ok_or(())?; let state = xkb::State::new(&keymap); Ok(KbdInternal { focus: None, @@ -186,10 +187,7 @@ impl KbdHandle { pub fn input(&self, keycode: u32, state: KeyState, serial: u32, filter: F) where F: FnOnce(&ModifiersState, Keysym) -> bool { - let mut guard = self.internal - .0 - .lock() - .unwrap(); + let mut guard = self.internal.0.lock().unwrap(); let mods_changed = guard.key_input(keycode, state); if !filter(&guard.mods_state, guard.state.key_get_one_sym(keycode)) { @@ -218,10 +216,7 @@ impl KbdHandle { pub fn set_focus(&self, focus: Option<(wl_surface::WlSurface, wl_keyboard::WlKeyboard)>, serial: u32) { // TODO: check surface and keyboard are from the same client - let mut guard = self.internal - .0 - .lock() - .unwrap(); + let mut guard = self.internal.0.lock().unwrap(); // remove current focus let old_kbd = if let Some((old_surface, old_kbd)) = guard.focus.take() { diff --git a/src/shm/mod.rs b/src/shm/mod.rs index 2a2bcc6..0660410 100644 --- a/src/shm/mod.rs +++ b/src/shm/mod.rs @@ -94,7 +94,9 @@ impl ShmGlobal { where L: Into> { use slog::DrainExt; - let log = logger.into().unwrap_or_else(|| ::slog::Logger::root(::slog_stdlog::StdLog.fuse(), o!())); + let log = logger + .into() + .unwrap_or_else(|| ::slog::Logger::root(::slog_stdlog::StdLog.fuse(), o!())); // always add the mandatory formats formats.push(wl_shm::Format::Argb8888); @@ -157,7 +159,9 @@ impl ShmGlobalToken { } let data = unsafe { &*(buffer.get_user_data() as *mut InternalBufferData) }; - if data.pool.with_data_slice(|slice| f(slice, data.data)).is_err() { + if data.pool + .with_data_slice(|slice| f(slice, data.data)) + .is_err() { // SIGBUS error occured buffer.post_error(wl_shm::Error::InvalidFd as u32, "Bad pool size.".into()); return Err(BufferAccessError::BadMap); diff --git a/src/shm/pool.rs b/src/shm/pool.rs index 08321db..0056cc3 100644 --- a/src/shm/pool.rs +++ b/src/shm/pool.rs @@ -60,13 +60,13 @@ impl Pool { // Prepare the access SIGBUS_GUARD.with(|guard| { - let (p, _) = guard.get(); - if !p.is_null() { - // Recursive call of this method is not supported - panic!("Recursive access to a SHM pool content is not supported."); - } - guard.set((&*pool_guard as *const MemMap, false)) - }); + let (p, _) = guard.get(); + if !p.is_null() { + // Recursive call of this method is not supported + panic!("Recursive access to a SHM pool content is not supported."); + } + guard.set((&*pool_guard as *const MemMap, false)) + }); let slice = pool_guard.get_slice(); f(slice); From 4adcadd9376eda4ae061304a8de24392ed32f6c7 Mon Sep 17 00:00:00 2001 From: Victor Berger Date: Tue, 11 Apr 2017 08:41:33 +0200 Subject: [PATCH 3/3] Create kdb handler from RMLVO rules --- src/keyboard/mod.rs | 51 ++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index 7961088..1085f04 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -7,7 +7,7 @@ //! The handle you obtained can be cloned to access this keyboard state from different places. It is //! expected that such a context is created for each keyboard the compositor has access to. //! -//! This handle gives you 3 main way to interact with the keymap handling: +//! This handle gives you 3 main ways to interact with the keymap handling: //! //! - send the keymap information to a client using the `KbdHandle::send_keymap` method. //! - set the current focus for this keyboard: designing the client that will receive the key inputs @@ -18,7 +18,7 @@ use backend::input::KeyState; -use std::io::Write; +use std::io::{Error as IoError, Write}; use std::os::unix::io::AsRawFd; use std::sync::{Arc, Mutex}; @@ -81,23 +81,33 @@ struct KbdInternal { focus: Option<(wl_surface::WlSurface, wl_keyboard::WlKeyboard)>, pressed_keys: Vec, mods_state: ModifiersState, - _context: xkb::Context, keymap: xkb::Keymap, state: xkb::State, } impl KbdInternal { - fn new() -> Result { + fn new(rules: &str, model: &str, layout: &str, variant: &str, options: Option) + -> Result { + // we create a new contex for each keyboard because libxkbcommon is actually NOT threadsafe + // so confining it inside the KbdInternal allows us to use Rusts mutability rules to make + // sure nothing goes wrong. + // + // FIXME: This is an issue with the xkbcommon-rs crate that does not reflect this + // non-threadsafety properly. let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); - // TODO: api to choose the keymap to load - let keymap = xkb::Keymap::new_from_names(&context, &"", &"", &"fr", &"oss", None, 0) - .ok_or(())?; + let keymap = xkb::Keymap::new_from_names(&context, + &rules, + &model, + &layout, + &variant, + options, + xkb::KEYMAP_COMPILE_NO_FLAGS) + .ok_or(())?; let state = xkb::State::new(&keymap); Ok(KbdInternal { focus: None, pressed_keys: Vec::new(), mods_state: ModifiersState::new(), - _context: context, keymap: keymap, state: state, }) @@ -145,15 +155,28 @@ impl KbdInternal { } } -pub fn create_keyboard_handler() -> Result { - let internal = KbdInternal::new()?; +/// Errors that can be encountered when creating a keyboard handler +pub enum Error { + /// libxkbcommon could not load the specified keymap + BadKeymap, + /// Smithay could not create a tempfile to share the keymap with clients + IoError(IoError), +} + +/// Create a keyboard handler from a set of RMLVO rules +pub fn create_keyboard_handler(rules: &str, model: &str, layout: &str, variant: &str, + options: Option) + -> Result { + let internal = KbdInternal::new(rules, model, layout, variant, options) + .map_err(|_| Error::BadKeymap)?; // prepare a tempfile with the keymap, to send it to clients - // TODO: better error handling - let mut keymap_file = tempfile().unwrap(); + let mut keymap_file = tempfile().map_err(Error::IoError)?; let keymap_data = internal.keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1); - keymap_file.write_all(keymap_data.as_bytes()).unwrap(); - keymap_file.flush().unwrap(); + keymap_file + .write_all(keymap_data.as_bytes()) + .map_err(Error::IoError)?; + keymap_file.flush().map_err(Error::IoError)?; Ok(KbdHandle { internal: Arc::new((Mutex::new(internal), (keymap_file, keymap_data.as_bytes().len() as u32))),