mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Auto merge of #21881 - pyfisch:keyboard-types, r=paulrouget
Use keyboard-types crate Have embedders send DOM keys to servo and use a strongly typed KeyboardEvent from the W3C UI Events spec. All keyboard handling now uses the new types. Introduce a ShortcutMatcher to recognize key bindings. Shortcuts are now recognized in a uniform way. Updated the winit port. Updated webdriver integration. part of #20331 What this PR does: * allow the use non-ASCII keyboards for text input * decouple keyboard event "key" from "code" (key meaning vs location) What this PR does not do: * completely improve keyboard events send from winit and webdriver * add support for CompositionEvent or IME Notes: * The winit embedder does not send keyup events for printable keys (this is a regression) * keyboard-types is on crates.io because I believe it to be useful outside of servo. If you prefer I can add a copy in this repo. <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/21881) <!-- Reviewable:end -->
This commit is contained in:
commit
9a0404ac5f
35 changed files with 747 additions and 1673 deletions
|
@ -408,7 +408,7 @@ impl ServoGlue {
|
|||
EmbedderMsg::SelectFiles(..) |
|
||||
EmbedderMsg::MoveTo(..) |
|
||||
EmbedderMsg::ResizeTo(..) |
|
||||
EmbedderMsg::KeyEvent(..) |
|
||||
EmbedderMsg::Keyboard(..) |
|
||||
EmbedderMsg::SetCursor(..) |
|
||||
EmbedderMsg::NewFavicon(..) |
|
||||
EmbedderMsg::HeadParsed |
|
||||
|
|
|
@ -40,6 +40,7 @@ crossbeam-channel = "0.2"
|
|||
euclid = "0.19"
|
||||
gleam = "0.6"
|
||||
glutin = "0.18"
|
||||
keyboard-types = {version = "0.4.2-servo", features = ["serde"]}
|
||||
lazy_static = "1"
|
||||
libservo = {path = "../../components/servo"}
|
||||
log = "0.4"
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
use euclid::{TypedPoint2D, TypedVector2D};
|
||||
use glutin_app::keyutils::{CMD_OR_CONTROL, CMD_OR_ALT};
|
||||
use glutin_app::window::{Window, LINE_HEIGHT};
|
||||
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
||||
use servo::compositing::windowing::{WebRenderDebugOption, WindowEvent};
|
||||
use servo::embedder_traits::{EmbedderMsg, FilterPattern};
|
||||
use servo::msg::constellation_msg::{Key, TopLevelBrowsingContextId as BrowserId};
|
||||
use servo::msg::constellation_msg::{KeyModifiers, KeyState, TraversalDirection};
|
||||
use servo::msg::constellation_msg::{TopLevelBrowsingContextId as BrowserId};
|
||||
use servo::msg::constellation_msg::TraversalDirection;
|
||||
use servo::net_traits::pub_domains::is_reg_domain;
|
||||
use servo::script_traits::TouchEventType;
|
||||
use servo::servo_config::opts;
|
||||
|
@ -70,8 +71,8 @@ impl Browser {
|
|||
pub fn handle_window_events(&mut self, events: Vec<WindowEvent>) {
|
||||
for event in events {
|
||||
match event {
|
||||
WindowEvent::KeyEvent(ch, key, state, mods) => {
|
||||
self.handle_key_from_window(ch, key, state, mods);
|
||||
WindowEvent::Keyboard(key_event) => {
|
||||
self.handle_key_from_window(key_event);
|
||||
},
|
||||
event => {
|
||||
self.event_queue.push(event);
|
||||
|
@ -85,22 +86,14 @@ impl Browser {
|
|||
}
|
||||
|
||||
/// Handle key events before sending them to Servo.
|
||||
fn handle_key_from_window(
|
||||
&mut self,
|
||||
ch: Option<char>,
|
||||
key: Key,
|
||||
state: KeyState,
|
||||
mods: KeyModifiers,
|
||||
) {
|
||||
let pressed = state == KeyState::Pressed;
|
||||
// We don't match the state in the parent `match` because we don't want to do anything
|
||||
// on KeyState::Released when it's a combo that we handle on Pressed. For example,
|
||||
// if we catch Alt-Left on pressed, we don't want the Release event to be sent to Servo.
|
||||
match (mods, ch, key, self.browser_id) {
|
||||
(CMD_OR_CONTROL, _, Key::R, Some(id)) => if pressed {
|
||||
self.event_queue.push(WindowEvent::Reload(id));
|
||||
},
|
||||
(CMD_OR_CONTROL, _, Key::L, Some(id)) => if pressed {
|
||||
fn handle_key_from_window(&mut self, key_event: KeyboardEvent) {
|
||||
ShortcutMatcher::from_event(key_event.clone())
|
||||
.shortcut(CMD_OR_CONTROL, 'R', || {
|
||||
if let Some(id) = self.browser_id {
|
||||
self.event_queue.push(WindowEvent::Reload(id));
|
||||
}
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'L', || {
|
||||
let url: String = if let Some(ref current_url) = self.current_url {
|
||||
current_url.to_string()
|
||||
} else {
|
||||
|
@ -110,163 +103,130 @@ impl Browser {
|
|||
let input = tinyfiledialogs::input_box(title, title, &url);
|
||||
if let Some(input) = input {
|
||||
if let Some(url) = sanitize_url(&input) {
|
||||
self.event_queue.push(WindowEvent::LoadUrl(id, url));
|
||||
if let Some(id) = self.browser_id {
|
||||
self.event_queue.push(WindowEvent::LoadUrl(id, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
(CMD_OR_CONTROL, _, Key::Q, _) => if pressed {
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'Q', || {
|
||||
self.event_queue.push(WindowEvent::Quit);
|
||||
},
|
||||
(_, Some('3'), _, _) if mods ^ KeyModifiers::CONTROL == KeyModifiers::SHIFT => {
|
||||
if pressed {
|
||||
self.event_queue.push(WindowEvent::CaptureWebRender);
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F9, || {
|
||||
self.event_queue.push(WindowEvent::CaptureWebRender)
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F10, || {
|
||||
self.event_queue.push(WindowEvent::ToggleWebRenderDebug(
|
||||
WebRenderDebugOption::RenderTargetDebug,
|
||||
));
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F11, || {
|
||||
self.event_queue.push(WindowEvent::ToggleWebRenderDebug(
|
||||
WebRenderDebugOption::TextureCacheDebug,
|
||||
));
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F12, || {
|
||||
self.event_queue.push(WindowEvent::ToggleWebRenderDebug(
|
||||
WebRenderDebugOption::Profiler,
|
||||
));
|
||||
})
|
||||
.shortcut(CMD_OR_ALT, Key::ArrowRight, || {
|
||||
if let Some(id) = self.browser_id {
|
||||
let event = WindowEvent::Navigation(id, TraversalDirection::Forward(1));
|
||||
self.event_queue.push(event);
|
||||
}
|
||||
},
|
||||
(KeyModifiers::CONTROL, None, Key::F10, _) => if pressed {
|
||||
let event =
|
||||
WindowEvent::ToggleWebRenderDebug(WebRenderDebugOption::RenderTargetDebug);
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
(KeyModifiers::CONTROL, None, Key::F11, _) => if pressed {
|
||||
let event =
|
||||
WindowEvent::ToggleWebRenderDebug(WebRenderDebugOption::TextureCacheDebug);
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
(KeyModifiers::CONTROL, None, Key::F12, _) => if pressed {
|
||||
let event = WindowEvent::ToggleWebRenderDebug(WebRenderDebugOption::Profiler);
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
(CMD_OR_ALT, None, Key::Right, Some(id)) |
|
||||
(KeyModifiers::NONE, None, Key::NavigateForward, Some(id)) => if pressed {
|
||||
let event = WindowEvent::Navigation(id, TraversalDirection::Forward(1));
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
(CMD_OR_ALT, None, Key::Left, Some(id)) |
|
||||
(KeyModifiers::NONE, None, Key::NavigateBackward, Some(id)) => if pressed {
|
||||
let event = WindowEvent::Navigation(id, TraversalDirection::Back(1));
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
(KeyModifiers::NONE, None, Key::Escape, _) => if pressed {
|
||||
})
|
||||
.shortcut(CMD_OR_ALT, Key::ArrowLeft, || {
|
||||
if let Some(id) = self.browser_id {
|
||||
let event = WindowEvent::Navigation(id, TraversalDirection::Back(1));
|
||||
self.event_queue.push(event);
|
||||
}
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Escape, || {
|
||||
self.event_queue.push(WindowEvent::Quit);
|
||||
},
|
||||
_ => {
|
||||
self.platform_handle_key(ch, key, mods, state);
|
||||
},
|
||||
}
|
||||
})
|
||||
.otherwise(|| self.platform_handle_key(key_event));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "win"))]
|
||||
fn platform_handle_key(
|
||||
&mut self,
|
||||
ch: Option<char>,
|
||||
key: Key,
|
||||
mods: KeyModifiers,
|
||||
state: KeyState,
|
||||
) {
|
||||
let pressed = state == KeyState::Pressed;
|
||||
match (mods, key, self.browser_id) {
|
||||
(CMD_OR_CONTROL, Key::LeftBracket, Some(id)) => if pressed {
|
||||
let event = WindowEvent::Navigation(id, TraversalDirection::Back(1));
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
(CMD_OR_CONTROL, Key::RightBracket, Some(id)) => if pressed {
|
||||
let event = WindowEvent::Navigation(id, TraversalDirection::Back(1));
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
_ => {
|
||||
self.event_queue
|
||||
.push(WindowEvent::KeyEvent(ch, key, state, mods));
|
||||
},
|
||||
fn platform_handle_key(&mut self, key_event: KeyboardEvent) {
|
||||
if let Some(id) = self.browser_id {
|
||||
if let Some(event) = ShortcutMatcher::from_event(key_event.clone())
|
||||
.shortcut(CMD_OR_CONTROL, '[', || {
|
||||
WindowEvent::Navigation(id, TraversalDirection::Back(1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, ']', || {
|
||||
WindowEvent::Navigation(id, TraversalDirection::Forward(1))
|
||||
})
|
||||
.otherwise(|| WindowEvent::Keyboard(key_event))
|
||||
{
|
||||
self.event_queue.push(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "win")]
|
||||
fn platform_handle_key(
|
||||
&mut self,
|
||||
_ch: Option<char>,
|
||||
_key: Key,
|
||||
_mods: KeyModifiers,
|
||||
_state: KeyState,
|
||||
) {
|
||||
}
|
||||
fn platform_handle_key(&mut self, _key_event: KeyboardEvent) {}
|
||||
|
||||
/// Handle key events after they have been handled by Servo.
|
||||
fn handle_key_from_servo(
|
||||
&mut self,
|
||||
_: Option<BrowserId>,
|
||||
ch: Option<char>,
|
||||
key: Key,
|
||||
state: KeyState,
|
||||
mods: KeyModifiers,
|
||||
) {
|
||||
if state == KeyState::Released {
|
||||
return;
|
||||
}
|
||||
|
||||
match (mods, ch, key) {
|
||||
(CMD_OR_CONTROL, Some('='), _) | (CMD_OR_CONTROL, Some('+'), _) => {
|
||||
self.event_queue.push(WindowEvent::Zoom(1.1));
|
||||
},
|
||||
(_, Some('='), _) if mods == (CMD_OR_CONTROL | KeyModifiers::SHIFT) => {
|
||||
self.event_queue.push(WindowEvent::Zoom(1.1));
|
||||
},
|
||||
(CMD_OR_CONTROL, Some('-'), _) => {
|
||||
self.event_queue.push(WindowEvent::Zoom(1.0 / 1.1));
|
||||
},
|
||||
(CMD_OR_CONTROL, Some('0'), _) => {
|
||||
self.event_queue.push(WindowEvent::ResetZoom);
|
||||
},
|
||||
|
||||
(KeyModifiers::NONE, None, Key::PageDown) => {
|
||||
fn handle_key_from_servo(&mut self, _: Option<BrowserId>, event: KeyboardEvent) {
|
||||
ShortcutMatcher::from_event(event)
|
||||
.shortcut(CMD_OR_CONTROL, '=', || {
|
||||
self.event_queue.push(WindowEvent::Zoom(1.1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '+', || {
|
||||
self.event_queue.push(WindowEvent::Zoom(1.1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '-', || {
|
||||
self.event_queue.push(WindowEvent::Zoom(1.0 / 1.1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '0', || {
|
||||
self.event_queue.push(WindowEvent::ResetZoom)
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::PageDown, || {
|
||||
let scroll_location = ScrollLocation::Delta(TypedVector2D::new(
|
||||
0.0,
|
||||
-self.window.page_height() + 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
self.scroll_window_from_key(scroll_location, TouchEventType::Move);
|
||||
},
|
||||
(KeyModifiers::NONE, None, Key::PageUp) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::PageUp, || {
|
||||
let scroll_location = ScrollLocation::Delta(TypedVector2D::new(
|
||||
0.0,
|
||||
self.window.page_height() - 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
self.scroll_window_from_key(scroll_location, TouchEventType::Move);
|
||||
},
|
||||
|
||||
(KeyModifiers::NONE, None, Key::Home) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Home, || {
|
||||
self.scroll_window_from_key(ScrollLocation::Start, TouchEventType::Move);
|
||||
},
|
||||
|
||||
(KeyModifiers::NONE, None, Key::End) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::End, || {
|
||||
self.scroll_window_from_key(ScrollLocation::End, TouchEventType::Move);
|
||||
},
|
||||
|
||||
(KeyModifiers::NONE, None, Key::Up) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
|
||||
self.scroll_window_from_key(
|
||||
ScrollLocation::Delta(TypedVector2D::new(0.0, 3.0 * LINE_HEIGHT)),
|
||||
TouchEventType::Move,
|
||||
);
|
||||
},
|
||||
(KeyModifiers::NONE, None, Key::Down) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
|
||||
self.scroll_window_from_key(
|
||||
ScrollLocation::Delta(TypedVector2D::new(0.0, -3.0 * LINE_HEIGHT)),
|
||||
TouchEventType::Move,
|
||||
);
|
||||
},
|
||||
(KeyModifiers::NONE, None, Key::Left) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
|
||||
self.scroll_window_from_key(
|
||||
ScrollLocation::Delta(TypedVector2D::new(LINE_HEIGHT, 0.0)),
|
||||
TouchEventType::Move,
|
||||
);
|
||||
},
|
||||
(KeyModifiers::NONE, None, Key::Right) => {
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
|
||||
self.scroll_window_from_key(
|
||||
ScrollLocation::Delta(TypedVector2D::new(-LINE_HEIGHT, 0.0)),
|
||||
TouchEventType::Move,
|
||||
);
|
||||
},
|
||||
|
||||
_ => {},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn scroll_window_from_key(&mut self, scroll_location: ScrollLocation, phase: TouchEventType) {
|
||||
|
@ -311,7 +271,8 @@ impl Browser {
|
|||
&message,
|
||||
MessageBoxIcon::Warning,
|
||||
);
|
||||
}).unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed");
|
||||
}
|
||||
|
@ -350,8 +311,8 @@ impl Browser {
|
|||
self.event_queue
|
||||
.push(WindowEvent::SelectBrowser(new_browser_id));
|
||||
},
|
||||
EmbedderMsg::KeyEvent(ch, key, state, modified) => {
|
||||
self.handle_key_from_servo(browser_id, ch, key, state, modified);
|
||||
EmbedderMsg::Keyboard(key_event) => {
|
||||
self.handle_key_from_servo(browser_id, key_event);
|
||||
},
|
||||
EmbedderMsg::SetCursor(cursor) => {
|
||||
self.window.set_cursor(cursor);
|
||||
|
@ -438,7 +399,8 @@ fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
|||
},
|
||||
None => None,
|
||||
}
|
||||
}).unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed")
|
||||
}
|
||||
|
@ -480,7 +442,8 @@ fn get_selected_files(patterns: Vec<FilterPattern>, multiple_files: bool) -> Opt
|
|||
let file = tinyfiledialogs::open_file_dialog(picker_name, "", filter_opt);
|
||||
file.map(|x| vec![x])
|
||||
}
|
||||
}).unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed")
|
||||
}
|
||||
|
@ -495,7 +458,8 @@ fn sanitize_url(request: &str) -> Option<ServoUrl> {
|
|||
} else {
|
||||
None
|
||||
}
|
||||
}).or_else(|| {
|
||||
})
|
||||
.or_else(|| {
|
||||
PREFS
|
||||
.get("shell.searchpage")
|
||||
.as_string()
|
||||
|
|
|
@ -2,215 +2,34 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use servo::msg::constellation_msg::{self, Key, KeyModifiers};
|
||||
use winit::VirtualKeyCode;
|
||||
use keyboard_types::{Code, Key, KeyboardEvent, KeyState, Modifiers, Location};
|
||||
use winit::{ElementState, KeyboardInput, ModifiersState, VirtualKeyCode};
|
||||
|
||||
// Some shortcuts use Cmd on Mac and Control on other systems.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const CMD_OR_CONTROL: KeyModifiers = KeyModifiers::SUPER;
|
||||
pub const CMD_OR_CONTROL: Modifiers = Modifiers::META;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const CMD_OR_CONTROL: KeyModifiers = KeyModifiers::CONTROL;
|
||||
pub const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
|
||||
|
||||
// Some shortcuts use Cmd on Mac and Alt on other systems.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const CMD_OR_ALT: KeyModifiers = KeyModifiers::SUPER;
|
||||
pub const CMD_OR_ALT: Modifiers = Modifiers::META;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const CMD_OR_ALT: KeyModifiers = KeyModifiers::ALT;
|
||||
pub const CMD_OR_ALT: Modifiers = Modifiers::ALT;
|
||||
|
||||
pub fn char_to_script_key(c: char) -> Option<constellation_msg::Key> {
|
||||
match c {
|
||||
' ' => Some(Key::Space),
|
||||
'"' => Some(Key::Apostrophe),
|
||||
'\'' => Some(Key::Apostrophe),
|
||||
'<' => Some(Key::Comma),
|
||||
',' => Some(Key::Comma),
|
||||
'_' => Some(Key::Minus),
|
||||
'-' => Some(Key::Minus),
|
||||
'>' => Some(Key::Period),
|
||||
'.' => Some(Key::Period),
|
||||
'?' => Some(Key::Slash),
|
||||
'/' => Some(Key::Slash),
|
||||
'~' => Some(Key::GraveAccent),
|
||||
'`' => Some(Key::GraveAccent),
|
||||
')' => Some(Key::Num0),
|
||||
'0' => Some(Key::Num0),
|
||||
'!' => Some(Key::Num1),
|
||||
'1' => Some(Key::Num1),
|
||||
'@' => Some(Key::Num2),
|
||||
'2' => Some(Key::Num2),
|
||||
'#' => Some(Key::Num3),
|
||||
'3' => Some(Key::Num3),
|
||||
'$' => Some(Key::Num4),
|
||||
'4' => Some(Key::Num4),
|
||||
'%' => Some(Key::Num5),
|
||||
'5' => Some(Key::Num5),
|
||||
'^' => Some(Key::Num6),
|
||||
'6' => Some(Key::Num6),
|
||||
'&' => Some(Key::Num7),
|
||||
'7' => Some(Key::Num7),
|
||||
'*' => Some(Key::Num8),
|
||||
'8' => Some(Key::Num8),
|
||||
'(' => Some(Key::Num9),
|
||||
'9' => Some(Key::Num9),
|
||||
':' => Some(Key::Semicolon),
|
||||
';' => Some(Key::Semicolon),
|
||||
'+' => Some(Key::Equal),
|
||||
'=' => Some(Key::Equal),
|
||||
'A' => Some(Key::A),
|
||||
'a' => Some(Key::A),
|
||||
'B' => Some(Key::B),
|
||||
'b' => Some(Key::B),
|
||||
'C' => Some(Key::C),
|
||||
'c' => Some(Key::C),
|
||||
'D' => Some(Key::D),
|
||||
'd' => Some(Key::D),
|
||||
'E' => Some(Key::E),
|
||||
'e' => Some(Key::E),
|
||||
'F' => Some(Key::F),
|
||||
'f' => Some(Key::F),
|
||||
'G' => Some(Key::G),
|
||||
'g' => Some(Key::G),
|
||||
'H' => Some(Key::H),
|
||||
'h' => Some(Key::H),
|
||||
'I' => Some(Key::I),
|
||||
'i' => Some(Key::I),
|
||||
'J' => Some(Key::J),
|
||||
'j' => Some(Key::J),
|
||||
'K' => Some(Key::K),
|
||||
'k' => Some(Key::K),
|
||||
'L' => Some(Key::L),
|
||||
'l' => Some(Key::L),
|
||||
'M' => Some(Key::M),
|
||||
'm' => Some(Key::M),
|
||||
'N' => Some(Key::N),
|
||||
'n' => Some(Key::N),
|
||||
'O' => Some(Key::O),
|
||||
'o' => Some(Key::O),
|
||||
'P' => Some(Key::P),
|
||||
'p' => Some(Key::P),
|
||||
'Q' => Some(Key::Q),
|
||||
'q' => Some(Key::Q),
|
||||
'R' => Some(Key::R),
|
||||
'r' => Some(Key::R),
|
||||
'S' => Some(Key::S),
|
||||
's' => Some(Key::S),
|
||||
'T' => Some(Key::T),
|
||||
't' => Some(Key::T),
|
||||
'U' => Some(Key::U),
|
||||
'u' => Some(Key::U),
|
||||
'V' => Some(Key::V),
|
||||
'v' => Some(Key::V),
|
||||
'W' => Some(Key::W),
|
||||
'w' => Some(Key::W),
|
||||
'X' => Some(Key::X),
|
||||
'x' => Some(Key::X),
|
||||
'Y' => Some(Key::Y),
|
||||
'y' => Some(Key::Y),
|
||||
'Z' => Some(Key::Z),
|
||||
'z' => Some(Key::Z),
|
||||
'{' => Some(Key::LeftBracket),
|
||||
'[' => Some(Key::LeftBracket),
|
||||
'|' => Some(Key::Backslash),
|
||||
'\\' => Some(Key::Backslash),
|
||||
'}' => Some(Key::RightBracket),
|
||||
']' => Some(Key::RightBracket),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn winit_key_to_script_key(key: VirtualKeyCode) -> Result<constellation_msg::Key, ()> {
|
||||
fn get_servo_key_from_winit_key(key: Option<VirtualKeyCode>) -> Key {
|
||||
use winit::VirtualKeyCode::*;
|
||||
// TODO(negge): add more key mappings
|
||||
Ok(match key {
|
||||
A => Key::A,
|
||||
B => Key::B,
|
||||
C => Key::C,
|
||||
D => Key::D,
|
||||
E => Key::E,
|
||||
F => Key::F,
|
||||
G => Key::G,
|
||||
H => Key::H,
|
||||
I => Key::I,
|
||||
J => Key::J,
|
||||
K => Key::K,
|
||||
L => Key::L,
|
||||
M => Key::M,
|
||||
N => Key::N,
|
||||
O => Key::O,
|
||||
P => Key::P,
|
||||
Q => Key::Q,
|
||||
R => Key::R,
|
||||
S => Key::S,
|
||||
T => Key::T,
|
||||
U => Key::U,
|
||||
V => Key::V,
|
||||
W => Key::W,
|
||||
X => Key::X,
|
||||
Y => Key::Y,
|
||||
Z => Key::Z,
|
||||
|
||||
Numpad0 => Key::Kp0,
|
||||
Numpad1 => Key::Kp1,
|
||||
Numpad2 => Key::Kp2,
|
||||
Numpad3 => Key::Kp3,
|
||||
Numpad4 => Key::Kp4,
|
||||
Numpad5 => Key::Kp5,
|
||||
Numpad6 => Key::Kp6,
|
||||
Numpad7 => Key::Kp7,
|
||||
Numpad8 => Key::Kp8,
|
||||
Numpad9 => Key::Kp9,
|
||||
|
||||
Key0 => Key::Num0,
|
||||
Key1 => Key::Num1,
|
||||
Key2 => Key::Num2,
|
||||
Key3 => Key::Num3,
|
||||
Key4 => Key::Num4,
|
||||
Key5 => Key::Num5,
|
||||
Key6 => Key::Num6,
|
||||
Key7 => Key::Num7,
|
||||
Key8 => Key::Num8,
|
||||
Key9 => Key::Num9,
|
||||
|
||||
Return => Key::Enter,
|
||||
Space => Key::Space,
|
||||
// TODO: figure out how to map NavigateForward, NavigateBackward
|
||||
// TODO: map the remaining keys if possible
|
||||
let key = if let Some(key) = key {
|
||||
key
|
||||
} else {
|
||||
return Key::Unidentified;
|
||||
};
|
||||
match key {
|
||||
// printable: Key1 to Key0
|
||||
// printable: A to Z
|
||||
Escape => Key::Escape,
|
||||
Equals => Key::Equal,
|
||||
Minus => Key::Minus,
|
||||
Back => Key::Backspace,
|
||||
PageDown => Key::PageDown,
|
||||
PageUp => Key::PageUp,
|
||||
|
||||
Insert => Key::Insert,
|
||||
Home => Key::Home,
|
||||
Delete => Key::Delete,
|
||||
End => Key::End,
|
||||
|
||||
Left => Key::Left,
|
||||
Up => Key::Up,
|
||||
Right => Key::Right,
|
||||
Down => Key::Down,
|
||||
|
||||
LShift => Key::LeftShift,
|
||||
LControl => Key::LeftControl,
|
||||
LAlt => Key::LeftAlt,
|
||||
LWin => Key::LeftSuper,
|
||||
RShift => Key::RightShift,
|
||||
RControl => Key::RightControl,
|
||||
RAlt => Key::RightAlt,
|
||||
RWin => Key::RightSuper,
|
||||
|
||||
Apostrophe => Key::Apostrophe,
|
||||
Backslash => Key::Backslash,
|
||||
Comma => Key::Comma,
|
||||
Grave => Key::GraveAccent,
|
||||
LBracket => Key::LeftBracket,
|
||||
Period => Key::Period,
|
||||
RBracket => Key::RightBracket,
|
||||
Semicolon => Key::Semicolon,
|
||||
Slash => Key::Slash,
|
||||
Tab => Key::Tab,
|
||||
Subtract => Key::Minus,
|
||||
|
||||
F1 => Key::F1,
|
||||
F2 => Key::F2,
|
||||
F3 => Key::F3,
|
||||
|
@ -223,23 +42,226 @@ pub fn winit_key_to_script_key(key: VirtualKeyCode) -> Result<constellation_msg:
|
|||
F10 => Key::F10,
|
||||
F11 => Key::F11,
|
||||
F12 => Key::F12,
|
||||
|
||||
NavigateBackward => Key::NavigateBackward,
|
||||
NavigateForward => Key::NavigateForward,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_printable(key_code: VirtualKeyCode) -> bool {
|
||||
use winit::VirtualKeyCode::*;
|
||||
match key_code {
|
||||
Escape | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 | F13 | F14 |
|
||||
F15 | Snapshot | Scroll | Pause | Insert | Home | Delete | End | PageDown | PageUp |
|
||||
Left | Up | Right | Down | Back | LAlt | LControl | LShift | LWin | Mail |
|
||||
MediaSelect | MediaStop | Mute | MyComputer | NavigateForward | NavigateBackward |
|
||||
NextTrack | NoConvert | PlayPause | Power | PrevTrack | RAlt | RControl | RShift |
|
||||
RWin | Sleep | Stop | VolumeDown | VolumeUp | Wake | WebBack | WebFavorites |
|
||||
WebForward | WebHome | WebRefresh | WebSearch | WebStop => false,
|
||||
_ => true,
|
||||
// F13 to F15 are not mapped
|
||||
Snapshot => Key::PrintScreen,
|
||||
// Scroll not mapped
|
||||
Pause => Key::Pause,
|
||||
Insert => Key::Insert,
|
||||
Home => Key::Home,
|
||||
Delete => Key::Delete,
|
||||
End => Key::End,
|
||||
PageDown => Key::PageDown,
|
||||
PageUp => Key::PageUp,
|
||||
Left => Key::ArrowLeft,
|
||||
Up => Key::ArrowUp,
|
||||
Right => Key::ArrowRight,
|
||||
Down => Key::ArrowDown,
|
||||
Back => Key::Backspace,
|
||||
Return => Key::Enter,
|
||||
// printable: Space
|
||||
Compose => Key::Compose,
|
||||
// Caret not mapped
|
||||
Numlock => Key::NumLock,
|
||||
// printable: Numpad0 to Numpad9
|
||||
// AbntC1 and AbntC2 not mapped
|
||||
// printable: Add, Apostrophe,
|
||||
// Apps, At, Ax not mapped
|
||||
// printable: Backslash,
|
||||
Calculator => Key::LaunchApplication2,
|
||||
Capital => Key::CapsLock,
|
||||
// printable: Colon, Comma,
|
||||
Convert => Key::Convert,
|
||||
// not mapped: Decimal,
|
||||
// printable: Divide, Equals, Grave,
|
||||
Kana => Key::KanaMode,
|
||||
Kanji => Key::KanjiMode,
|
||||
LAlt => Key::Alt,
|
||||
// printable: LBracket,
|
||||
LControl => Key::Control,
|
||||
LShift => Key::Shift,
|
||||
LWin => Key::Meta,
|
||||
Mail => Key::LaunchMail,
|
||||
// not mapped: MediaSelect,
|
||||
MediaStop => Key::MediaStop,
|
||||
// printable: Minus, Multiply,
|
||||
Mute => Key::AudioVolumeMute,
|
||||
MyComputer => Key::LaunchApplication1,
|
||||
// not mapped: NavigateForward, NavigateBackward
|
||||
NextTrack => Key::MediaTrackNext,
|
||||
NoConvert => Key::NonConvert,
|
||||
// printable: NumpadComma, NumpadEnter, NumpadEquals,
|
||||
// not mapped: OEM102,
|
||||
// printable: Period,
|
||||
PlayPause => Key::MediaPlayPause,
|
||||
Power => Key::Power,
|
||||
PrevTrack => Key::MediaTrackPrevious,
|
||||
RAlt => Key::Alt,
|
||||
// printable RBracket
|
||||
RControl => Key::Control,
|
||||
RShift => Key::Shift,
|
||||
RWin => Key::Meta,
|
||||
// printable Semicolon, Slash
|
||||
Sleep => Key::Standby,
|
||||
// not mapped: Stop,
|
||||
// printable Subtract,
|
||||
// not mapped: Sysrq,
|
||||
Tab => Key::Tab,
|
||||
// printable: Underline,
|
||||
// not mapped: Unlabeled,
|
||||
VolumeDown => Key::AudioVolumeDown,
|
||||
VolumeUp => Key::AudioVolumeUp,
|
||||
Wake => Key::WakeUp,
|
||||
WebBack => Key::BrowserBack,
|
||||
WebFavorites => Key::BrowserFavorites,
|
||||
WebForward => Key::BrowserForward,
|
||||
WebHome => Key::BrowserHome,
|
||||
WebRefresh => Key::BrowserRefresh,
|
||||
WebSearch => Key::BrowserSearch,
|
||||
WebStop => Key::BrowserStop,
|
||||
// printable Yen,
|
||||
Copy => Key::Copy,
|
||||
Paste => Key::Paste,
|
||||
Cut => Key::Cut,
|
||||
_ => Key::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_servo_location_from_winit_key(key: Option<VirtualKeyCode>) -> Location {
|
||||
use winit::VirtualKeyCode::*;
|
||||
// TODO: add more numpad keys
|
||||
let key = if let Some(key) = key {
|
||||
key
|
||||
} else {
|
||||
return Location::Standard;
|
||||
};
|
||||
match key {
|
||||
LShift | LControl | LAlt | LWin => Location::Left,
|
||||
RShift | RControl | RAlt | RWin => Location::Right,
|
||||
Numpad0 | Numpad1 | Numpad2 | Numpad3 | Numpad4 | Numpad5 | Numpad6 | Numpad7 |
|
||||
Numpad8 | Numpad9 => Location::Numpad,
|
||||
NumpadComma | NumpadEnter | NumpadEquals => Location::Numpad,
|
||||
_ => Location::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_servo_code_from_scancode(scancode: u32) -> Code {
|
||||
// TODO: Map more codes
|
||||
use keyboard_types::Code::*;
|
||||
match scancode {
|
||||
1 => Escape,
|
||||
2 => Digit1,
|
||||
3 => Digit2,
|
||||
4 => Digit3,
|
||||
5 => Digit4,
|
||||
6 => Digit5,
|
||||
7 => Digit6,
|
||||
8 => Digit7,
|
||||
9 => Digit8,
|
||||
10 => Digit9,
|
||||
11 => Digit0,
|
||||
|
||||
14 => Backspace,
|
||||
15 => Tab,
|
||||
16 => KeyQ,
|
||||
17 => KeyW,
|
||||
18 => KeyE,
|
||||
19 => KeyR,
|
||||
20 => KeyT,
|
||||
21 => KeyY,
|
||||
22 => KeyU,
|
||||
23 => KeyI,
|
||||
24 => KeyO,
|
||||
25 => KeyP,
|
||||
26 => BracketLeft,
|
||||
27 => BracketRight,
|
||||
28 => Enter,
|
||||
|
||||
30 => KeyA,
|
||||
31 => KeyS,
|
||||
32 => KeyD,
|
||||
33 => KeyF,
|
||||
34 => KeyG,
|
||||
35 => KeyH,
|
||||
36 => KeyJ,
|
||||
37 => KeyK,
|
||||
38 => KeyL,
|
||||
39 => Semicolon,
|
||||
40 => Quote,
|
||||
|
||||
42 => ShiftLeft,
|
||||
43 => Backslash,
|
||||
44 => KeyZ,
|
||||
45 => KeyX,
|
||||
46 => KeyC,
|
||||
47 => KeyV,
|
||||
48 => KeyB,
|
||||
49 => KeyN,
|
||||
50 => KeyM,
|
||||
51 => Comma,
|
||||
52 => Period,
|
||||
53 => Slash,
|
||||
54 => ShiftRight,
|
||||
|
||||
57 => Space,
|
||||
|
||||
59 => F1,
|
||||
60 => F2,
|
||||
61 => F3,
|
||||
62 => F4,
|
||||
63 => F5,
|
||||
64 => F6,
|
||||
65 => F7,
|
||||
66 => F8,
|
||||
67 => F9,
|
||||
68 => F10,
|
||||
|
||||
87 => F11,
|
||||
88 => F12,
|
||||
|
||||
103 => ArrowUp,
|
||||
104 => PageUp,
|
||||
105 => ArrowLeft,
|
||||
106 => ArrowRight,
|
||||
|
||||
102 => Home,
|
||||
107 => End,
|
||||
108 => ArrowDown,
|
||||
109 => PageDown,
|
||||
110 => Insert,
|
||||
111 => Delete,
|
||||
|
||||
_ => Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn get_servo_code_from_scancode(_scancode: u32) -> Code {
|
||||
// TODO: Implement for Windows and Mac OS
|
||||
Code::Unidentified
|
||||
}
|
||||
|
||||
fn get_modifiers(mods: ModifiersState) -> Modifiers {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
modifiers.set(Modifiers::CONTROL, mods.ctrl);
|
||||
modifiers.set(Modifiers::SHIFT, mods.shift);
|
||||
modifiers.set(Modifiers::ALT, mods.alt);
|
||||
modifiers.set(Modifiers::META, mods.logo);
|
||||
modifiers
|
||||
}
|
||||
|
||||
pub fn keyboard_event_from_winit(input: KeyboardInput) -> KeyboardEvent {
|
||||
info!("winit keyboard input: {:?}", input);
|
||||
KeyboardEvent {
|
||||
state: match input.state {
|
||||
ElementState::Pressed => KeyState::Down,
|
||||
ElementState::Released => KeyState::Up,
|
||||
},
|
||||
key: get_servo_key_from_winit_key(input.virtual_keycode),
|
||||
code: get_servo_code_from_scancode(input.scancode),
|
||||
location: get_servo_location_from_winit_key(input.virtual_keycode),
|
||||
modifiers: get_modifiers(input.modifiers),
|
||||
repeat: false,
|
||||
is_composing: false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,12 @@ use euclid::{Length, TypedPoint2D, TypedVector2D, TypedScale, TypedSize2D};
|
|||
use gdi32;
|
||||
use gleam::gl;
|
||||
use glutin::{Api, ContextBuilder, GlContext, GlRequest, GlWindow};
|
||||
use keyboard_types::{Key, KeyboardEvent, KeyState};
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use osmesa_sys;
|
||||
use servo::compositing::windowing::{AnimationState, MouseWindowEvent, WindowEvent};
|
||||
use servo::compositing::windowing::{EmbedderCoordinates, WindowMethods};
|
||||
use servo::embedder_traits::EventLoopWaker;
|
||||
use servo::msg::constellation_msg::{Key, KeyState, KeyModifiers};
|
||||
use servo::script_traits::TouchEventType;
|
||||
use servo::servo_config::opts;
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
|
@ -31,13 +31,13 @@ use std::rc::Rc;
|
|||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use super::keyutils;
|
||||
use super::keyutils::keyboard_event_from_winit;
|
||||
#[cfg(target_os = "windows")]
|
||||
use user32;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winapi;
|
||||
use winit;
|
||||
use winit::{ElementState, Event, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
|
||||
use winit::{ElementState, Event, MouseButton, MouseScrollDelta, TouchPhase, KeyboardInput};
|
||||
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::os::macos::{ActivationPolicy, WindowBuilderExt};
|
||||
|
@ -150,8 +150,7 @@ pub struct Window {
|
|||
mouse_down_point: Cell<TypedPoint2D<i32, DevicePixel>>,
|
||||
event_queue: RefCell<Vec<WindowEvent>>,
|
||||
mouse_pos: Cell<TypedPoint2D<i32, DevicePixel>>,
|
||||
key_modifiers: Cell<KeyModifiers>,
|
||||
last_pressed_key: Cell<Option<Key>>,
|
||||
last_pressed: Cell<Option<KeyboardEvent>>,
|
||||
animation_state: Cell<AnimationState>,
|
||||
fullscreen: Cell<bool>,
|
||||
gl: Rc<gl::Gl>,
|
||||
|
@ -276,11 +275,8 @@ impl Window {
|
|||
event_queue: RefCell::new(vec![]),
|
||||
mouse_down_button: Cell::new(None),
|
||||
mouse_down_point: Cell::new(TypedPoint2D::new(0, 0)),
|
||||
|
||||
mouse_pos: Cell::new(TypedPoint2D::new(0, 0)),
|
||||
key_modifiers: Cell::new(KeyModifiers::empty()),
|
||||
|
||||
last_pressed_key: Cell::new(None),
|
||||
last_pressed: Cell::new(None),
|
||||
gl: gl.clone(),
|
||||
animation_state: Cell::new(AnimationState::Idle),
|
||||
fullscreen: Cell::new(false),
|
||||
|
@ -408,56 +404,46 @@ impl Window {
|
|||
GlRequest::Specific(Api::OpenGlEs, (3, 0))
|
||||
}
|
||||
|
||||
fn handle_received_character(&self, ch: char) {
|
||||
let last_key = if let Some(key) = self.last_pressed_key.get() {
|
||||
key
|
||||
fn handle_received_character(&self, mut ch: char) {
|
||||
info!("winit received character: {:?}", ch);
|
||||
if ch.is_control() {
|
||||
if ch as u8 >= 32 {
|
||||
return;
|
||||
}
|
||||
// shift ASCII control characters to lowercase
|
||||
ch = (ch as u8 + 96) as char;
|
||||
}
|
||||
let mut event = if let Some(event) = self.last_pressed.replace(None) {
|
||||
event
|
||||
} else if ch.is_ascii() {
|
||||
// Some keys like Backspace emit a control character in winit
|
||||
// but they are already dealt with in handle_keyboard_input
|
||||
// so just ignore the character.
|
||||
return
|
||||
} else {
|
||||
return;
|
||||
// For combined characters like the letter e with an acute accent
|
||||
// no keyboard event is emitted. A dummy event is created in this case.
|
||||
KeyboardEvent::default()
|
||||
};
|
||||
|
||||
self.last_pressed_key.set(None);
|
||||
|
||||
let (key, ch) = if let Some(key) = keyutils::char_to_script_key(ch) {
|
||||
(key, Some(ch))
|
||||
} else {
|
||||
(last_key, None)
|
||||
};
|
||||
|
||||
let modifiers = self.key_modifiers.get();
|
||||
let event = WindowEvent::KeyEvent(ch, key, KeyState::Pressed, modifiers);
|
||||
self.event_queue.borrow_mut().push(event);
|
||||
}
|
||||
|
||||
fn toggle_keyboard_modifiers(&self, mods: ModifiersState) {
|
||||
self.toggle_modifier(KeyModifiers::CONTROL, mods.ctrl);
|
||||
self.toggle_modifier(KeyModifiers::SHIFT, mods.shift);
|
||||
self.toggle_modifier(KeyModifiers::ALT, mods.alt);
|
||||
self.toggle_modifier(KeyModifiers::SUPER, mods.logo);
|
||||
event.key = Key::Character(ch.to_string());
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(WindowEvent::Keyboard(event));
|
||||
}
|
||||
|
||||
fn handle_keyboard_input(
|
||||
&self,
|
||||
element_state: ElementState,
|
||||
code: VirtualKeyCode,
|
||||
mods: ModifiersState,
|
||||
input: KeyboardInput,
|
||||
) {
|
||||
self.toggle_keyboard_modifiers(mods);
|
||||
|
||||
if let Ok(key) = keyutils::winit_key_to_script_key(code) {
|
||||
let state = match element_state {
|
||||
ElementState::Pressed => KeyState::Pressed,
|
||||
ElementState::Released => KeyState::Released,
|
||||
};
|
||||
if element_state == ElementState::Pressed && keyutils::is_printable(code) {
|
||||
// If pressed and printable, we expect a ReceivedCharacter event.
|
||||
self.last_pressed_key.set(Some(key));
|
||||
} else {
|
||||
self.last_pressed_key.set(None);
|
||||
let modifiers = self.key_modifiers.get();
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(WindowEvent::KeyEvent(None, key, state, modifiers));
|
||||
}
|
||||
let event = keyboard_event_from_winit(input);
|
||||
if event.state == KeyState::Down && event.key == Key::Unidentified {
|
||||
// If pressed and probably printable, we expect a ReceivedCharacter event.
|
||||
self.last_pressed.set(Some(event));
|
||||
} else if event.key != Key::Unidentified {
|
||||
self.last_pressed.set(None);
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(WindowEvent::Keyboard(event));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -470,17 +456,11 @@ impl Window {
|
|||
Event::WindowEvent {
|
||||
event:
|
||||
winit::WindowEvent::KeyboardInput {
|
||||
input:
|
||||
winit::KeyboardInput {
|
||||
state,
|
||||
virtual_keycode: Some(virtual_keycode),
|
||||
modifiers,
|
||||
..
|
||||
},
|
||||
input,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => self.handle_keyboard_input(state, virtual_keycode, modifiers),
|
||||
} => self.handle_keyboard_input(input),
|
||||
Event::WindowEvent {
|
||||
event: winit::WindowEvent::MouseInput { state, button, .. },
|
||||
..
|
||||
|
@ -584,16 +564,6 @@ impl Window {
|
|||
}
|
||||
}
|
||||
|
||||
fn toggle_modifier(&self, modifier: KeyModifiers, pressed: bool) {
|
||||
let mut modifiers = self.key_modifiers.get();
|
||||
if pressed {
|
||||
modifiers.insert(modifier);
|
||||
} else {
|
||||
modifiers.remove(modifier);
|
||||
}
|
||||
self.key_modifiers.set(modifiers);
|
||||
}
|
||||
|
||||
/// Helper function to handle a click
|
||||
fn handle_mouse(
|
||||
&self,
|
||||
|
|
|
@ -8,6 +8,7 @@ extern crate euclid;
|
|||
extern crate gdi32;
|
||||
extern crate gleam;
|
||||
extern crate glutin;
|
||||
extern crate keyboard_types;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue