servo: Merge #11950 - Support non-QWERTY keyboards (from jdm:keylayout2); r=emilio
authorJosh Matthews <josh@joshmatthews.net>
Wed, 06 Jul 2016 02:51:50 -0700
changeset 385862 3183b3611761536bb0d0c4daefa0d79aa7bd55e6
parent 385861 40cd84ff76b228f45bbe92c27552d34a697b3d54
child 385863 e54db68c63dc7da24b9ff45b70bbaf29c30ab584
push id7198
push userjlorenzo@mozilla.com
push dateTue, 18 Apr 2017 12:07:49 +0000
treeherdermozilla-beta@d57aa49c3948 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersemilio
servo: Merge #11950 - Support non-QWERTY keyboards (from jdm:keylayout2); r=emilio Using the ReceivedCharacter event from glutin, we can obtain the actual key characters that the user is pressing and releasing. This gets passed to the script thread along with the physical key data, since KeyboardEvent needs both pieces of information, where they get merged into a single logical key that gets processed by clients like TextInput without any special changes. Tested by switching my macbook keyboard to dvorak and looking at the output of keypress/keyup/keydown event listeners, as well as playing with tests/html/textarea.html. Non-content keybindings like reload work as expected, too - the remapped keybinding triggers the reload action. --- - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #4144 - [X] These changes do not require tests because I can't think of a way to test remapped keyboard input Fixes #11991. Source-Repo: https://github.com/servo/servo Source-Revision: 68fb9ebc413f9cfc1ad4ca578d904c164836db74
servo/components/compositing/compositor.rs
servo/components/compositing/compositor_thread.rs
servo/components/compositing/windowing.rs
servo/components/constellation/constellation.rs
servo/components/script/dom/bindings/str.rs
servo/components/script/dom/bindings/trace.rs
servo/components/script/dom/document.rs
servo/components/script/dom/keyboardevent.rs
servo/components/script/script_thread.rs
servo/components/script/textinput.rs
servo/components/script_traits/lib.rs
servo/components/script_traits/script_msg.rs
servo/components/servo/Cargo.lock
servo/ports/cef/Cargo.lock
servo/ports/cef/browser_host.rs
servo/ports/cef/window.rs
servo/ports/geckolib/Cargo.lock
servo/ports/glutin/window.rs
servo/tests/unit/script/textinput.rs
--- a/servo/components/compositing/compositor.rs
+++ b/servo/components/compositing/compositor.rs
@@ -696,19 +696,19 @@ impl<Window: WindowMethods> IOCompositor
                     }
                 }
             }
 
             (Msg::Recomposite(reason), ShutdownState::NotShuttingDown) => {
                 self.composition_request = CompositionRequest::CompositeNow(reason)
             }
 
-            (Msg::KeyEvent(key, state, modified), ShutdownState::NotShuttingDown) => {
+            (Msg::KeyEvent(ch, key, state, modified), ShutdownState::NotShuttingDown) => {
                 if state == KeyState::Pressed {
-                    self.window.handle_key(key, modified);
+                    self.window.handle_key(ch, key, modified);
                 }
             }
 
             (Msg::TouchEventProcessed(result), ShutdownState::NotShuttingDown) => {
                 self.touch_handler.on_event_processed(result);
             }
 
             (Msg::SetCursor(cursor), ShutdownState::NotShuttingDown) => {
@@ -1343,18 +1343,18 @@ impl<Window: WindowMethods> IOCompositor
             WindowEvent::Navigation(direction) => {
                 self.on_navigation_window_event(direction);
             }
 
             WindowEvent::TouchpadPressure(cursor, pressure, stage) => {
                 self.on_touchpad_pressure_event(cursor, pressure, stage);
             }
 
-            WindowEvent::KeyEvent(key, state, modifiers) => {
-                self.on_key_event(key, state, modifiers);
+            WindowEvent::KeyEvent(ch, key, state, modifiers) => {
+                self.on_key_event(ch, key, state, modifiers);
             }
 
             WindowEvent::Quit => {
                 if self.shutdown_state == ShutdownState::NotShuttingDown {
                     debug!("Shutting down the constellation for WindowEvent::Quit");
                     self.start_shutting_down();
                 }
             }
@@ -1875,18 +1875,18 @@ impl<Window: WindowMethods> IOCompositor
         if let Some(true) = PREFS.get("dom.forcetouch.enabled").as_boolean() {
             match self.find_topmost_layer_at_point(cursor / self.scene.scale) {
                 Some(result) => result.layer.send_touchpad_pressure_event(self, result.point, pressure, phase),
                 None => {},
             }
         }
     }
 
-    fn on_key_event(&self, key: Key, state: KeyState, modifiers: KeyModifiers) {
-        let msg = ConstellationMsg::KeyEvent(key, state, modifiers);
+    fn on_key_event(&self, ch: Option<char>, key: Key, state: KeyState, modifiers: KeyModifiers) {
+        let msg = ConstellationMsg::KeyEvent(ch, key, state, modifiers);
         if let Err(e) = self.constellation_chan.send(msg) {
             warn!("Sending key event to constellation failed ({}).", e);
         }
     }
 
     fn fill_paint_request_with_cached_layer_buffers(&mut self, paint_request: &mut PaintRequest) {
         for buffer_request in &mut paint_request.buffer_requests {
             if self.surface_map.mem() == 0 {
--- a/servo/components/compositing/compositor_thread.rs
+++ b/servo/components/compositing/compositor_thread.rs
@@ -146,17 +146,17 @@ pub enum Msg {
     LoadStart(bool, bool),
     /// The load of a page has completed: (can go back, can go forward, is root frame).
     LoadComplete(bool, bool, bool),
     /// We hit the delayed composition timeout. (See `delayed_composition.rs`.)
     DelayedCompositionTimeout(u64),
     /// Composite.
     Recomposite(CompositingReason),
     /// Sends an unconsumed key event back to the compositor.
-    KeyEvent(Key, KeyState, KeyModifiers),
+    KeyEvent(Option<char>, Key, KeyState, KeyModifiers),
     /// Script has handled a touch event, and either prevented or allowed default actions.
     TouchEventProcessed(EventResult),
     /// Changes the cursor.
     SetCursor(Cursor),
     /// Composite to a PNG file and return the Image over a passed channel.
     CreatePng(IpcSender<Option<Image>>),
     /// Informs the compositor that the paint thread for the given pipeline has exited.
     PaintThreadExited(PipelineId),
--- a/servo/components/compositing/windowing.rs
+++ b/servo/components/compositing/windowing.rs
@@ -71,17 +71,17 @@ pub enum WindowEvent {
     PinchZoom(f32),
     /// Sent when the user resets zoom to default.
     ResetZoom,
     /// Sent when the user uses chrome navigation (i.e. backspace or shift-backspace).
     Navigation(WindowNavigateMsg),
     /// Sent when the user quits the application
     Quit,
     /// Sent when a key input state changes
-    KeyEvent(Key, KeyState, KeyModifiers),
+    KeyEvent(Option<char>, Key, KeyState, KeyModifiers),
     /// Sent when Ctr+R/Apple+R is called to reload the current page.
     Reload,
 }
 
 impl Debug for WindowEvent {
     fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
         match *self {
             WindowEvent::Idle => write!(f, "Idle"),
@@ -154,16 +154,16 @@ pub trait WindowMethods {
     /// some type of platform-specific graphics context current. Returns true if the composite may
     /// proceed and false if it should not.
     fn prepare_for_composite(&self, width: usize, height: usize) -> bool;
 
     /// Sets the cursor to be used in the window.
     fn set_cursor(&self, cursor: Cursor);
 
     /// Process a key event.
-    fn handle_key(&self, key: Key, mods: KeyModifiers);
+    fn handle_key(&self, ch: Option<char>, key: Key, mods: KeyModifiers);
 
     /// Does this window support a clipboard
     fn supports_clipboard(&self) -> bool;
 
     /// Add a favicon
     fn set_favicon(&self, url: Url);
 }
--- a/servo/components/constellation/constellation.rs
+++ b/servo/components/constellation/constellation.rs
@@ -569,19 +569,19 @@ impl<Message, LTF, STF> Constellation<Me
             FromCompositorMsg::GetPipeline(frame_id, resp_chan) => {
                 debug!("constellation got get root pipeline message");
                 self.handle_get_pipeline(frame_id, resp_chan);
             }
             FromCompositorMsg::GetPipelineTitle(pipeline_id) => {
                 debug!("constellation got get-pipeline-title message");
                 self.handle_get_pipeline_title_msg(pipeline_id);
             }
-            FromCompositorMsg::KeyEvent(key, state, modifiers) => {
+            FromCompositorMsg::KeyEvent(ch, key, state, modifiers) => {
                 debug!("constellation got key event message");
-                self.handle_key_msg(key, state, modifiers);
+                self.handle_key_msg(ch, key, state, modifiers);
             }
             // Load a new page from a typed url
             // If there is already a pending page (self.pending_frames), it will not be overridden;
             // However, if the id is not encompassed by another change, it will be.
             FromCompositorMsg::LoadUrl(source_id, load_data) => {
                 debug!("constellation got URL load message from compositor");
                 self.handle_load_url_msg(source_id, load_data);
             }
@@ -798,18 +798,18 @@ impl<Message, LTF, STF> Constellation<Me
             FromScriptMsg::Exit => {
                 self.compositor_proxy.send(ToCompositorMsg::Exit);
             }
 
             FromScriptMsg::SetTitle(pipeline_id, title) => {
                 self.compositor_proxy.send(ToCompositorMsg::ChangePageTitle(pipeline_id, title))
             }
 
-            FromScriptMsg::SendKeyEvent(key, key_state, key_modifiers) => {
-                self.compositor_proxy.send(ToCompositorMsg::KeyEvent(key, key_state, key_modifiers))
+            FromScriptMsg::SendKeyEvent(ch, key, key_state, key_modifiers) => {
+                self.compositor_proxy.send(ToCompositorMsg::KeyEvent(ch, key, key_state, key_modifiers))
             }
 
             FromScriptMsg::TouchEventProcessed(result) => {
                 self.compositor_proxy.send(ToCompositorMsg::TouchEventProcessed(result))
             }
 
             FromScriptMsg::GetScrollOffset(pid, lid, send) => {
                 self.compositor_proxy.send(ToCompositorMsg::GetScrollOffset(pid, lid, send));
@@ -1391,39 +1391,39 @@ impl<Message, LTF, STF> Constellation<Me
             }
 
             // If this is an iframe, send a mozbrowser location change event.
             // This is the result of a back/forward navigation.
             self.trigger_mozbrowserlocationchange(next_pipeline_id);
         }
     }
 
-    fn handle_key_msg(&mut self, key: Key, state: KeyState, mods: KeyModifiers) {
+    fn handle_key_msg(&mut self, ch: Option<char>, key: Key, state: KeyState, mods: KeyModifiers) {
         // Send to the explicitly focused pipeline (if it exists), or the root
         // frame's current pipeline. If neither exist, fall back to sending to
         // the compositor below.
         let root_pipeline_id = self.root_frame_id
             .and_then(|root_frame_id| self.frames.get(&root_frame_id))
             .map(|root_frame| root_frame.current);
         let pipeline_id = self.focus_pipeline_id.or(root_pipeline_id);
 
         match pipeline_id {
             Some(pipeline_id) => {
-                let event = CompositorEvent::KeyEvent(key, state, mods);
+                let event = CompositorEvent::KeyEvent(ch, key, state, mods);
                 let msg = ConstellationControlMsg::SendEvent(pipeline_id, event);
                 let result = match self.pipelines.get(&pipeline_id) {
                     Some(pipeline) => pipeline.script_chan.send(msg),
                     None => return debug!("Pipeline {:?} got key event after closure.", pipeline_id),
                 };
                 if let Err(e) = result {
                     self.handle_send_error(pipeline_id, e);
                 }
             },
             None => {
-                let event = ToCompositorMsg::KeyEvent(key, state, mods);
+                let event = ToCompositorMsg::KeyEvent(ch, key, state, mods);
                 self.compositor_proxy.clone_compositor_proxy().send(event);
             }
         }
     }
 
     fn handle_reload_msg(&mut self) {
         // Send Reload constellation msg to root script channel.
         let root_pipeline_id = self.root_frame_id
@@ -1624,17 +1624,17 @@ impl<Message, LTF, STF> Constellation<Me
                 }
             },
             WebDriverCommandMsg::SendKeys(pipeline_id, cmd) => {
                 let script_channel = match self.pipelines.get(&pipeline_id) {
                     Some(pipeline) => pipeline.script_chan.clone(),
                     None => return warn!("Pipeline {:?} SendKeys after closure.", pipeline_id),
                 };
                 for (key, mods, state) in cmd {
-                    let event = CompositorEvent::KeyEvent(key, state, mods);
+                    let event = CompositorEvent::KeyEvent(None, key, state, mods);
                     let control_msg = ConstellationControlMsg::SendEvent(pipeline_id, event);
                     if let Err(e) = script_channel.send(control_msg) {
                         return self.handle_send_error(pipeline_id, e);
                     }
                 }
             },
             WebDriverCommandMsg::TakeScreenshot(pipeline_id, reply) => {
                 let current_pipeline_id = self.root_frame_id
--- a/servo/components/script/dom/bindings/str.rs
+++ b/servo/components/script/dom/bindings/str.rs
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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/. */
 
 //! The `ByteString` struct.
 
 use std::ascii::AsciiExt;
-use std::borrow::ToOwned;
+use std::borrow::{ToOwned, Cow};
 use std::fmt;
 use std::hash::{Hash, Hasher};
 use std::ops;
 use std::ops::{Deref, DerefMut};
 use std::str;
 use std::str::{Bytes, FromStr};
 use string_cache::Atom;
 
@@ -199,16 +199,25 @@ impl From<String> for DOMString {
 }
 
 impl<'a> From<&'a str> for DOMString {
     fn from(contents: &str) -> DOMString {
         DOMString::from(String::from(contents))
     }
 }
 
+impl<'a> From<Cow<'a, str>> for DOMString {
+    fn from(contents: Cow<'a, str>) -> DOMString {
+        match contents {
+            Cow::Owned(s) => DOMString::from(s),
+            Cow::Borrowed(s) => DOMString::from(s),
+        }
+    }
+}
+
 impl From<DOMString> for Atom {
     fn from(contents: DOMString) -> Atom {
         Atom::from(contents.0)
     }
 }
 
 impl From<DOMString> for String {
     fn from(contents: DOMString) -> String {
--- a/servo/components/script/dom/bindings/trace.rs
+++ b/servo/components/script/dom/bindings/trace.rs
@@ -271,17 +271,17 @@ impl<A: JSTraceable, B: JSTraceable, C: 
     fn trace(&self, trc: *mut JSTracer) {
         let (ref a, ref b, ref c) = *self;
         a.trace(trc);
         b.trace(trc);
         c.trace(trc);
     }
 }
 
-no_jsmanaged_fields!(bool, f32, f64, String, Url, AtomicBool, AtomicUsize, UrlOrigin, Uuid);
+no_jsmanaged_fields!(bool, f32, f64, String, Url, AtomicBool, AtomicUsize, UrlOrigin, Uuid, char);
 no_jsmanaged_fields!(usize, u8, u16, u32, u64);
 no_jsmanaged_fields!(isize, i8, i16, i32, i64);
 no_jsmanaged_fields!(Sender<T>);
 no_jsmanaged_fields!(Receiver<T>);
 no_jsmanaged_fields!(Point2D<T>);
 no_jsmanaged_fields!(Rect<T>);
 no_jsmanaged_fields!(Size2D<T>);
 no_jsmanaged_fields!(Arc<T>);
--- a/servo/components/script/dom/document.rs
+++ b/servo/components/script/dom/document.rs
@@ -1039,16 +1039,17 @@ impl Document {
         window.reflow(ReflowGoal::ForDisplay,
                       ReflowQueryType::NoQuery,
                       ReflowReason::MouseEvent);
         result
     }
 
     /// The entry point for all key processing for web content
     pub fn dispatch_key_event(&self,
+                              ch: Option<char>,
                               key: Key,
                               state: KeyState,
                               modifiers: KeyModifiers,
                               constellation: &IpcSender<ConstellationMsg>) {
         let focused = self.get_focused_element();
         let body = self.GetBody();
 
         let target = match (&focused, &body) {
@@ -1065,26 +1066,27 @@ impl Document {
         let is_composing = false;
         let is_repeating = state == KeyState::Repeated;
         let ev_type = DOMString::from(match state {
                                           KeyState::Pressed | KeyState::Repeated => "keydown",
                                           KeyState::Released => "keyup",
                                       }
                                       .to_owned());
 
-        let props = KeyboardEvent::key_properties(key, modifiers);
+        let props = KeyboardEvent::key_properties(ch, key, modifiers);
 
         let keyevent = KeyboardEvent::new(&self.window,
                                           ev_type,
                                           true,
                                           true,
                                           Some(&self.window),
                                           0,
+                                          ch,
                                           Some(key),
-                                          DOMString::from(props.key_string),
+                                          DOMString::from(props.key_string.clone()),
                                           DOMString::from(props.code),
                                           props.location,
                                           is_repeating,
                                           is_composing,
                                           ctrl,
                                           alt,
                                           shift,
                                           meta,
@@ -1098,16 +1100,17 @@ impl Document {
         if state != KeyState::Released && props.is_printable() && !prevented {
             // https://w3c.github.io/uievents/#keypress-event-order
             let event = KeyboardEvent::new(&self.window,
                                            DOMString::from("keypress"),
                                            true,
                                            true,
                                            Some(&self.window),
                                            0,
+                                           ch,
                                            Some(key),
                                            DOMString::from(props.key_string),
                                            DOMString::from(props.code),
                                            props.location,
                                            is_repeating,
                                            is_composing,
                                            ctrl,
                                            alt,
@@ -1117,17 +1120,17 @@ impl Document {
                                            0);
             let ev = event.upcast::<Event>();
             ev.fire(target);
             prevented = ev.DefaultPrevented();
             // TODO: if keypress event is canceled, prevent firing input events
         }
 
         if !prevented {
-            constellation.send(ConstellationMsg::SendKeyEvent(key, state, modifiers)).unwrap();
+            constellation.send(ConstellationMsg::SendKeyEvent(ch, key, state, modifiers)).unwrap();
         }
 
         // This behavior is unspecced
         // We are supposed to dispatch synthetic click activation for Space and/or Return,
         // however *when* we do it is up to us
         // I'm dispatching it after the key event so the script has a chance to cancel it
         // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337
         match key {
--- a/servo/components/script/dom/keyboardevent.rs
+++ b/servo/components/script/dom/keyboardevent.rs
@@ -12,16 +12,17 @@ use dom::bindings::inheritance::Castable
 use dom::bindings::js::{Root, RootedReference};
 use dom::bindings::reflector::reflect_dom_object;
 use dom::bindings::str::DOMString;
 use dom::event::Event;
 use dom::uievent::UIEvent;
 use dom::window::Window;
 use msg::constellation_msg;
 use msg::constellation_msg::{Key, KeyModifiers};
+use std::borrow::Cow;
 use std::cell::Cell;
 
 no_jsmanaged_fields!(Key);
 
 #[dom_struct]
 pub struct KeyboardEvent {
     uievent: UIEvent,
     key: Cell<Option<Key>>,
@@ -31,16 +32,17 @@ pub struct KeyboardEvent {
     ctrl: Cell<bool>,
     alt: Cell<bool>,
     shift: Cell<bool>,
     meta: Cell<bool>,
     repeat: Cell<bool>,
     is_composing: Cell<bool>,
     char_code: Cell<Option<u32>>,
     key_code: Cell<u32>,
+    printable: Cell<Option<char>>,
 }
 
 impl KeyboardEvent {
     fn new_inherited() -> KeyboardEvent {
         KeyboardEvent {
             uievent: UIEvent::new_inherited(),
             key: Cell::new(None),
             key_string: DOMRefCell::new(DOMString::new()),
@@ -49,31 +51,33 @@ impl KeyboardEvent {
             ctrl: Cell::new(false),
             alt: Cell::new(false),
             shift: Cell::new(false),
             meta: Cell::new(false),
             repeat: Cell::new(false),
             is_composing: Cell::new(false),
             char_code: Cell::new(None),
             key_code: Cell::new(0),
+            printable: Cell::new(None),
         }
     }
 
     pub fn new_uninitialized(window: &Window) -> Root<KeyboardEvent> {
         reflect_dom_object(box KeyboardEvent::new_inherited(),
                            GlobalRef::Window(window),
                            KeyboardEventBinding::Wrap)
     }
 
     pub fn new(window: &Window,
                type_: DOMString,
                canBubble: bool,
                cancelable: bool,
                view: Option<&Window>,
                _detail: i32,
+               ch: Option<char>,
                key: Option<Key>,
                key_string: DOMString,
                code: DOMString,
                location: u32,
                repeat: bool,
                isComposing: bool,
                ctrlKey: bool,
                altKey: bool,
@@ -86,50 +90,57 @@ impl KeyboardEvent {
                              DOMString::new(), repeat, DOMString::new());
         ev.key.set(key);
         *ev.code.borrow_mut() = code;
         ev.ctrl.set(ctrlKey);
         ev.alt.set(altKey);
         ev.shift.set(shiftKey);
         ev.meta.set(metaKey);
         ev.char_code.set(char_code);
+        ev.printable.set(ch);
         ev.key_code.set(key_code);
         ev.is_composing.set(isComposing);
         ev
     }
 
     pub fn Constructor(global: GlobalRef,
                        type_: DOMString,
                        init: &KeyboardEventBinding::KeyboardEventInit) -> Fallible<Root<KeyboardEvent>> {
         let event = KeyboardEvent::new(global.as_window(), type_,
                                        init.parent.parent.parent.bubbles,
                                        init.parent.parent.parent.cancelable,
                                        init.parent.parent.view.r(),
-                                       init.parent.parent.detail, key_from_string(&init.key, init.location),
+                                       init.parent.parent.detail,
+                                       None,
+                                       key_from_string(&init.key, init.location),
                                        init.key.clone(), init.code.clone(), init.location,
                                        init.repeat, init.isComposing, init.parent.ctrlKey,
                                        init.parent.altKey, init.parent.shiftKey, init.parent.metaKey,
                                        None, 0);
         Ok(event)
     }
 
-    pub fn key_properties(key: Key, mods: KeyModifiers)
+    pub fn key_properties(ch: Option<char>, key: Key, mods: KeyModifiers)
         -> KeyEventProperties {
             KeyEventProperties {
-                key_string: key_value(key, mods),
+                key_string: key_value(ch, key, mods),
                 code: code_value(key),
                 location: key_location(key),
-                char_code: key_charcode(key, mods),
+                char_code: ch.map(|ch| ch as u32),
                 key_code: key_keycode(key),
             }
     }
 }
 
 
 impl KeyboardEvent {
+    pub fn printable(&self) -> Option<char> {
+        self.printable.get()
+    }
+
     pub fn get_key(&self) -> Option<Key> {
         self.key.get().clone()
     }
 
     pub fn get_key_modifiers(&self) -> KeyModifiers {
         let mut result = KeyModifiers::empty();
         if self.shift.get() {
             result = result | constellation_msg::SHIFT;
@@ -142,21 +153,24 @@ impl KeyboardEvent {
         }
         if self.meta.get() {
             result = result | constellation_msg::SUPER;
         }
         result
     }
 }
 
+// https://w3c.github.io/uievents-key/#key-value-tables
+pub fn key_value(ch: Option<char>, key: Key, mods: KeyModifiers) -> Cow<'static, str> {
+    if let Some(ch) = ch {
+        return Cow::from(format!("{}", ch));
+    }
 
-// https://w3c.github.io/uievents-key/#key-value-tables
-pub fn key_value(key: Key, mods: KeyModifiers) -> &'static str {
     let shift = mods.contains(constellation_msg::SHIFT);
-    match key {
+    Cow::from(match key {
         Key::Space => " ",
         Key::Apostrophe if shift => "\"",
         Key::Apostrophe => "'",
         Key::Comma if shift => "<",
         Key::Comma => ",",
         Key::Minus if shift => "_",
         Key::Minus => "-",
         Key::Period if shift => ">",
@@ -316,17 +330,17 @@ pub fn key_value(key: Key, mods: KeyModi
         Key::LeftSuper => "Super",
         Key::RightShift => "Shift",
         Key::RightControl => "Control",
         Key::RightAlt => "Alt",
         Key::RightSuper => "Super",
         Key::Menu => "ContextMenu",
         Key::NavigateForward => "BrowserForward",
         Key::NavigateBackward => "BrowserBack",
-    }
+    })
 }
 
 fn key_from_string(key_string: &str, location: u32) -> Option<Key> {
     match key_string {
         " " => Some(Key::Space),
         "\"" => Some(Key::Apostrophe),
         "'" => Some(Key::Apostrophe),
         "<" => Some(Key::Comma),
@@ -642,26 +656,16 @@ fn key_location(key: Key) -> u32 {
         Key::RightShift | Key::RightAlt |
         Key::RightControl | Key::RightSuper =>
             KeyboardEventConstants::DOM_KEY_LOCATION_RIGHT,
 
         _ => KeyboardEventConstants::DOM_KEY_LOCATION_STANDARD,
     }
 }
 
-// https://w3c.github.io/uievents/#dom-keyboardevent-charcode
-fn key_charcode(key: Key, mods: KeyModifiers) -> Option<u32> {
-    let key_string = key_value(key, mods);
-    if key_string.len() == 1 {
-        Some(key_string.chars().next().unwrap() as u32)
-    } else {
-        None
-    }
-}
-
 // https://w3c.github.io/uievents/#legacy-key-models
 fn key_keycode(key: Key) -> u32 {
     match key {
         // https://w3c.github.io/uievents/#legacy-key-models
         Key::Backspace => 8,
         Key::Tab => 9,
         Key::Enter => 13,
         Key::LeftShift | Key::RightShift => 16,
@@ -734,17 +738,17 @@ fn key_keycode(key: Key) -> u32 {
 
         //ยง B.2.1.8
         _ => 0
     }
 }
 
 #[derive(HeapSizeOf)]
 pub struct KeyEventProperties {
-    pub key_string: &'static str,
+    pub key_string: Cow<'static, str>,
     pub code: &'static str,
     pub location: u32,
     pub char_code: Option<u32>,
     pub key_code: u32,
 }
 
 impl KeyEventProperties {
     pub fn is_printable(&self) -> bool {
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -1932,22 +1932,22 @@ impl ScriptThread {
             TouchpadPressureEvent(point, pressure, phase) => {
                 let document = match self.root_browsing_context().find(pipeline_id) {
                     Some(browsing_context) => browsing_context.active_document(),
                     None => return warn!("Message sent to closed pipeline {}.", pipeline_id),
                 };
                 document.r().handle_touchpad_pressure_event(self.js_runtime.rt(), point, pressure, phase);
             }
 
-            KeyEvent(key, state, modifiers) => {
+            KeyEvent(ch, key, state, modifiers) => {
                 let document = match self.root_browsing_context().find(pipeline_id) {
                     Some(browsing_context) => browsing_context.active_document(),
                     None => return warn!("Message sent to closed pipeline {}.", pipeline_id),
                 };
-                document.dispatch_key_event(key, state, modifiers, &self.constellation_chan);
+                document.dispatch_key_event(ch, key, state, modifiers, &self.constellation_chan);
             }
         }
     }
 
     fn handle_mouse_event(&self,
                           pipeline_id: PipelineId,
                           mouse_event_type: MouseEventType,
                           button: MouseButton,
--- a/servo/components/script/textinput.rs
+++ b/servo/components/script/textinput.rs
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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/. */
 
 //! Common handling of keyboard input and state management for text input controls
 
 use clipboard_provider::ClipboardProvider;
 use dom::bindings::str::DOMString;
-use dom::keyboardevent::{KeyboardEvent, key_value};
+use dom::keyboardevent::KeyboardEvent;
 use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER};
 use msg::constellation_msg::{Key, KeyModifiers};
 use std::borrow::ToOwned;
 use std::cmp::{max, min};
 use std::default::Default;
 use std::ops::Range;
 use std::usize;
 
@@ -115,34 +115,16 @@ fn is_control_key(mods: KeyModifiers) ->
     mods.contains(SUPER) && !mods.contains(CONTROL | ALT)
 }
 
 #[cfg(not(target_os = "macos"))]
 fn is_control_key(mods: KeyModifiers) -> bool {
     mods.contains(CONTROL) && !mods.contains(SUPER | ALT)
 }
 
-fn is_printable_key(key: Key) -> bool {
-    match key {
-        Key::Space | Key::Apostrophe | Key::Comma | Key::Minus |
-        Key::Period | Key::Slash | Key::GraveAccent | Key::Num0 |
-        Key::Num1 | Key::Num2 | Key::Num3 | Key::Num4 | Key::Num5 |
-        Key::Num6 | Key::Num7 | Key::Num8 | Key::Num9 | Key::Semicolon |
-        Key::Equal | Key::A | Key::B | Key::C | Key::D | Key::E | Key::F |
-        Key::G | Key::H | Key::I | Key::J | Key::K | Key::L | Key::M | Key::N |
-        Key::O | Key::P | Key::Q | Key::R | Key::S | Key::T | Key::U | Key::V |
-        Key::W | Key::X | Key::Y | Key::Z | Key::LeftBracket | Key::Backslash |
-        Key::RightBracket | Key::Kp0 | Key::Kp1 | Key::Kp2 | Key::Kp3 |
-        Key::Kp4 | Key::Kp5 | Key::Kp6 | Key::Kp7 | Key::Kp8 | Key::Kp9 |
-        Key::KpDecimal | Key::KpDivide | Key::KpMultiply | Key::KpSubtract |
-        Key::KpAdd | Key::KpEqual => true,
-        _ => false,
-    }
-}
-
 /// The length in bytes of the first n characters in a UTF-8 string.
 ///
 /// If the string has fewer than n characters, returns the length of the whole string.
 fn len_of_first_n_chars(text: &str, n: usize) -> usize {
     match text.char_indices().take(n).last() {
         Some((index, ch)) => index + ch.len_utf8(),
         None => 0
     }
@@ -481,90 +463,90 @@ impl<T: ClipboardProvider> TextInput<T> 
     /// Remove the current selection.
     pub fn clear_selection(&mut self) {
         self.selection_begin = None;
     }
 
     /// Process a given `KeyboardEvent` and return an action for the caller to execute.
     pub fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
         if let Some(key) = event.get_key() {
-            self.handle_keydown_aux(key, event.get_key_modifiers())
+            self.handle_keydown_aux(event.printable(), key, event.get_key_modifiers())
         } else {
             KeyReaction::Nothing
         }
     }
-    pub fn handle_keydown_aux(&mut self, key: Key, mods: KeyModifiers) -> KeyReaction {
+
+    pub fn handle_keydown_aux(&mut self,
+                              printable: Option<char>,
+                              key: Key,
+                              mods: KeyModifiers) -> KeyReaction {
         let maybe_select = if mods.contains(SHIFT) { Selection::Selected } else { Selection::NotSelected };
-        match key {
-            Key::A if is_control_key(mods) => {
+        match (printable, key) {
+            (Some('a'), _) if is_control_key(mods) => {
                 self.select_all();
                 KeyReaction::RedrawSelection
             },
-            Key::C if is_control_key(mods) => {
+            (Some('c'), _) if is_control_key(mods) => {
                 if let Some(text) = self.get_selection_text() {
                     self.clipboard_provider.set_clipboard_contents(text);
                 }
                 KeyReaction::DispatchInput
             },
-            Key::V if is_control_key(mods) => {
+            (Some('v'), _) if is_control_key(mods) => {
                 let contents = self.clipboard_provider.clipboard_contents();
                 self.insert_string(contents);
                 KeyReaction::DispatchInput
             },
-            _ if is_printable_key(key) => {
-                self.insert_string(key_value(key, mods));
+            (Some(c), _) => {
+                self.insert_char(c);
                 KeyReaction::DispatchInput
             }
-            Key::Space => {
-                self.insert_char(' ');
-                KeyReaction::DispatchInput
-            }
-            Key::Delete => {
+            (None, Key::Delete) => {
                 self.delete_char(Direction::Forward);
                 KeyReaction::DispatchInput
             }
-            Key::Backspace => {
+            (None, Key::Backspace) => {
                 self.delete_char(Direction::Backward);
                 KeyReaction::DispatchInput
             }
-            Key::Left => {
+            (None, Key::Left) => {
                 self.adjust_horizontal_by_one(Direction::Backward, maybe_select);
                 KeyReaction::RedrawSelection
             }
-            Key::Right => {
+            (None, Key::Right) => {
                 self.adjust_horizontal_by_one(Direction::Forward, maybe_select);
                 KeyReaction::RedrawSelection
             }
-            Key::Up => {
+            (None, Key::Up) => {
                 self.adjust_vertical(-1, maybe_select);
                 KeyReaction::RedrawSelection
             }
-            Key::Down => {
+            (None, Key::Down) => {
                 self.adjust_vertical(1, maybe_select);
                 KeyReaction::RedrawSelection
             }
-            Key::Enter | Key::KpEnter => self.handle_return(),
-            Key::Home => {
+            (None, Key::Enter) | (None, Key::KpEnter) => self.handle_return(),
+            (None, Key::Home) => {
                 self.edit_point.index = 0;
                 KeyReaction::RedrawSelection
             }
-            Key::End => {
+            (None, Key::End) => {
                 self.edit_point.index = self.current_line_length();
                 self.assert_ok_selection();
                 KeyReaction::RedrawSelection
             }
-            Key::PageUp => {
+            (None, Key::PageUp) => {
                 self.adjust_vertical(-28, maybe_select);
                 KeyReaction::RedrawSelection
             }
-            Key::PageDown => {
+            (None, Key::PageDown) => {
                 self.adjust_vertical(28, maybe_select);
                 KeyReaction::RedrawSelection
             }
-            Key::Tab => KeyReaction::TriggerDefaultAction,
+            (None, Key::Tab) => KeyReaction::TriggerDefaultAction,
             _ => KeyReaction::Nothing,
         }
     }
 
     /// Whether the content is empty.
     pub fn is_empty(&self) -> bool {
         self.lines.len() <= 1 && self.lines.get(0).map_or(true, |line| line.is_empty())
     }
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -277,17 +277,17 @@ pub enum CompositorEvent {
     MouseButtonEvent(MouseEventType, MouseButton, Point2D<f32>),
     /// The mouse was moved over a point (or was moved out of the recognizable region).
     MouseMoveEvent(Option<Point2D<f32>>),
     /// A touch event was generated with a touch ID and location.
     TouchEvent(TouchEventType, TouchId, Point2D<f32>),
     /// Touchpad pressure event
     TouchpadPressureEvent(Point2D<f32>, f32, TouchpadPressurePhase),
     /// A key was pressed.
-    KeyEvent(Key, KeyState, KeyModifiers),
+    KeyEvent(Option<char>, Key, KeyState, KeyModifiers),
 }
 
 /// Touchpad pressure phase for TouchpadPressureEvent.
 #[derive(Copy, Clone, HeapSizeOf, PartialEq, Deserialize, Serialize)]
 pub enum TouchpadPressurePhase {
     /// Pressure before a regular click.
     BeforeClick,
     /// Pressure after a regular click.
@@ -581,17 +581,17 @@ pub enum ConstellationMsg {
     /// Requests that the constellation inform the compositor of the title of the pipeline
     /// immediately.
     GetPipelineTitle(PipelineId),
     /// Request to load the initial page.
     InitLoadUrl(Url),
     /// Query the constellation to see if the current compositor output is stable
     IsReadyToSaveImage(HashMap<PipelineId, Epoch>),
     /// Inform the constellation of a key event.
-    KeyEvent(Key, KeyState, KeyModifiers),
+    KeyEvent(Option<char>, Key, KeyState, KeyModifiers),
     /// Request to load a page.
     LoadUrl(PipelineId, LoadData),
     /// Request to navigate a frame.
     Navigate(Option<(PipelineId, SubpageId)>, NavigationDirection),
     /// Inform the constellation of a window being resized.
     WindowSize(WindowSizeData, WindowSizeType),
     /// Requests that the constellation instruct layout to begin a new tick of the animation.
     TickAnimation(PipelineId, AnimationTickType),
--- a/servo/components/script_traits/script_msg.rs
+++ b/servo/components/script_traits/script_msg.rs
@@ -98,17 +98,17 @@ pub enum ScriptMsg {
     /// Check if an alert dialog box should be presented
     Alert(PipelineId, String, IpcSender<bool>),
     /// Scroll a page in a window
     ScrollFragmentPoint(PipelineId, LayerId, Point2D<f32>, bool),
     /// Set title of current page
     /// https://html.spec.whatwg.org/multipage/#document.title
     SetTitle(PipelineId, Option<String>),
     /// Send a key event
-    SendKeyEvent(Key, KeyState, KeyModifiers),
+    SendKeyEvent(Option<char>, Key, KeyState, KeyModifiers),
     /// Get Window Informations size and position
     GetClientWindow(IpcSender<(Size2D<u32>, Point2D<i32>)>),
     /// Move the window to a point
     MoveTo(Point2D<i32>),
     /// Resize the window to size
     ResizeTo(Size2D<u32>),
     /// Script has handled a touch event, and either prevented or allowed default actions.
     TouchEventProcessed(EventResult),
--- a/servo/components/servo/Cargo.lock
+++ b/servo/components/servo/Cargo.lock
@@ -141,17 +141,17 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "bincode"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -1056,17 +1056,17 @@ dependencies = [
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "ipc-channel"
 version = "0.2.3"
 source = "git+https://github.com/servo/ipc-channel#48137d69955f5460da586c552de275ecdc3f4efe"
 dependencies = [
- "bincode 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bincode 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_macros 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
--- a/servo/ports/cef/Cargo.lock
+++ b/servo/ports/cef/Cargo.lock
@@ -115,17 +115,17 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "bincode"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -965,17 +965,17 @@ dependencies = [
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "ipc-channel"
 version = "0.2.3"
 source = "git+https://github.com/servo/ipc-channel#48137d69955f5460da586c552de275ecdc3f4efe"
 dependencies = [
- "bincode 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bincode 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_macros 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
--- a/servo/ports/cef/browser_host.rs
+++ b/servo/ports/cef/browser_host.rs
@@ -12,16 +12,17 @@ use wrappers::CefWrap;
 
 use compositing::windowing::{WindowEvent, MouseWindowEvent};
 use euclid::point::Point2D;
 use euclid::size::Size2D;
 use libc::{c_double, c_int};
 use msg::constellation_msg::{self, KeyModifiers, KeyState};
 use script_traits::{MouseButton, TouchEventType};
 use std::cell::{Cell, RefCell};
+use std::char;
 
 pub struct ServoCefBrowserHost {
     /// A reference to the browser.
     pub browser: RefCell<Option<CefBrowser>>,
     /// A reference to the client.
     pub client: CefClient,
     /// flag for return value of prepare_for_composite
     pub composite_ok: Cell<bool>,
@@ -419,17 +420,18 @@ full_cef_class_impl! {
                key_modifiers = key_modifiers | constellation_msg::SHIFT;
             }
             if (*event).modifiers & EVENTFLAG_CONTROL_DOWN as u32 != 0 {
                key_modifiers = key_modifiers | constellation_msg::CONTROL;
             }
             if (*event).modifiers & EVENTFLAG_ALT_DOWN as u32 != 0 {
                key_modifiers = key_modifiers | constellation_msg::ALT;
             }
-            this.downcast().send_window_event(WindowEvent::KeyEvent(key, key_state, key_modifiers))
+            let ch = char::from_u32((*event).character as u32);
+            this.downcast().send_window_event(WindowEvent::KeyEvent(ch, key, key_state, key_modifiers))
         }}
 
         fn send_mouse_click_event(&this,
                                   event: *const cef_mouse_event [&cef_mouse_event],
                                   mouse_button_type: cef_mouse_button_type_t [cef_mouse_button_type_t],
                                   mouse_up: c_int [c_int],
                                   _click_count: c_int [c_int],)
                                   -> () {{
--- a/servo/ports/cef/window.rs
+++ b/servo/ports/cef/window.rs
@@ -474,17 +474,17 @@ impl WindowMethods for Window {
         *frame_url = url.to_string();
         let utf16_chars: Vec<u16> = Utf16Encoder::new((*frame_url).chars()).collect();
         if check_ptr_exist!(browser.get_host().get_client(), get_display_handler) &&
            check_ptr_exist!(browser.get_host().get_client().get_display_handler(), on_address_change) {
             browser.get_host().get_client().get_display_handler().on_address_change((*browser).clone(), frame.clone(), utf16_chars.as_slice());
         }
     }
 
-    fn handle_key(&self, _: Key, _: KeyModifiers) {
+    fn handle_key(&self, _: Option<char>, _: Key, _: KeyModifiers) {
         // TODO(negge)
     }
 
     fn set_cursor(&self, cursor: Cursor) {
         use types::{CefCursorInfo,cef_point_t,cef_size_t};
         let browser = self.cef_browser.borrow();
         match *browser {
             None => {}
--- a/servo/ports/geckolib/Cargo.lock
+++ b/servo/ports/geckolib/Cargo.lock
@@ -67,17 +67,17 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "bincode"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -249,17 +249,17 @@ dependencies = [
  "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "ipc-channel"
 version = "0.2.4"
 source = "git+https://github.com/servo/ipc-channel#8411eeabf3a712006ad1b47637b2d8fe71177f85"
 dependencies = [
- "bincode 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bincode 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
--- a/servo/ports/glutin/window.rs
+++ b/servo/ports/glutin/window.rs
@@ -9,20 +9,20 @@ use compositing::compositor_thread::{sel
 use compositing::windowing::{MouseWindowEvent, WindowNavigateMsg};
 use compositing::windowing::{WindowEvent, WindowMethods};
 use euclid::scale_factor::ScaleFactor;
 use euclid::size::TypedSize2D;
 use euclid::{Size2D, Point2D};
 #[cfg(target_os = "windows")] use gdi32;
 use gleam::gl;
 use glutin;
-use glutin::TouchPhase;
 #[cfg(target_os = "macos")]
 use glutin::os::macos::{ActivationPolicy, WindowBuilderExt};
 use glutin::{Api, ElementState, Event, GlRequest, MouseButton, VirtualKeyCode, MouseScrollDelta};
+use glutin::{ScanCode, TouchPhase};
 use layers::geometry::DevicePixel;
 use layers::platform::surface::NativeDisplay;
 use msg::constellation_msg::{KeyState, NONE, CONTROL, SHIFT, ALT, SUPER};
 use msg::constellation_msg::{self, Key};
 use net_traits::net_error_list::NetError;
 use script_traits::{TouchEventType, TouchpadPressurePhase};
 use std::cell::{Cell, RefCell};
 #[cfg(not(target_os = "android"))]
@@ -94,16 +94,22 @@ pub struct Window {
 
     mouse_down_button: Cell<Option<glutin::MouseButton>>,
     mouse_down_point: Cell<Point2D<i32>>,
     event_queue: RefCell<Vec<WindowEvent>>,
 
     mouse_pos: Cell<Point2D<i32>>,
     key_modifiers: Cell<KeyModifiers>,
     current_url: RefCell<Option<Url>>,
+
+    /// The contents of the last ReceivedCharacter event for use in a subsequent KeyEvent.
+    pending_key_event_char: Cell<Option<char>>,
+    /// The list of keys that have been pressed but not yet released, to allow providing
+    /// the equivalent ReceivedCharacter data as was received for the press event.
+    pressed_key_map: RefCell<Vec<(ScanCode, char)>>,
 }
 
 #[cfg(not(target_os = "windows"))]
 fn window_creation_scale_factor() -> ScaleFactor<ScreenPx, DevicePixel, f32> {
     ScaleFactor::new(1.0)
 }
 
 #[cfg(target_os = "windows")]
@@ -170,16 +176,19 @@ impl Window {
             window: glutin_window,
             event_queue: RefCell::new(vec!()),
             mouse_down_button: Cell::new(None),
             mouse_down_point: Cell::new(Point2D::new(0, 0)),
 
             mouse_pos: Cell::new(Point2D::new(0, 0)),
             key_modifiers: Cell::new(KeyModifiers::empty()),
             current_url: RefCell::new(None),
+
+            pending_key_event_char: Cell::new(None),
+            pressed_key_map: RefCell::new(vec![]),
         };
 
         gl::clear_color(0.6, 0.6, 0.6, 1.0);
         gl::clear(gl::COLOR_BUFFER_BIT);
         gl::finish();
         window.present();
 
         Rc::new(window)
@@ -227,36 +236,68 @@ impl Window {
     }
 
     #[cfg(target_os = "android")]
     fn load_gl_functions(_: &glutin::Window) {
     }
 
     fn handle_window_event(&self, event: glutin::Event) -> bool {
         match event {
-            Event::KeyboardInput(element_state, _scan_code, Some(virtual_key_code)) => {
+            Event::ReceivedCharacter(ch) => {
+                assert!(self.pending_key_event_char.get().is_none());
+                if !ch.is_control() {
+                    self.pending_key_event_char.set(Some(ch));
+                }
+            }
+            Event::KeyboardInput(element_state, scan_code, Some(virtual_key_code)) => {
                 match virtual_key_code {
                     VirtualKeyCode::LControl => self.toggle_modifier(LEFT_CONTROL),
                     VirtualKeyCode::RControl => self.toggle_modifier(RIGHT_CONTROL),
                     VirtualKeyCode::LShift => self.toggle_modifier(LEFT_SHIFT),
                     VirtualKeyCode::RShift => self.toggle_modifier(RIGHT_SHIFT),
                     VirtualKeyCode::LAlt => self.toggle_modifier(LEFT_ALT),
                     VirtualKeyCode::RAlt => self.toggle_modifier(RIGHT_ALT),
                     VirtualKeyCode::LWin => self.toggle_modifier(LEFT_SUPER),
                     VirtualKeyCode::RWin => self.toggle_modifier(RIGHT_SUPER),
                     _ => {}
                 }
 
+                let ch = match element_state {
+                    ElementState::Pressed => {
+                        // Retrieve any previosly stored ReceivedCharacter value.
+                        // Store the association between the scan code and the actual
+                        // character value, if there is one.
+                        let ch = self.pending_key_event_char
+                                     .get()
+                                     .and_then(|ch| filter_nonprintable(ch, virtual_key_code));
+                        self.pending_key_event_char.set(None);
+                        if let Some(ch) = ch {
+                            self.pressed_key_map.borrow_mut().push((scan_code, ch));
+                        }
+                        ch
+                    }
+
+                    ElementState::Released => {
+                        // Retrieve the associated character value for this release key,
+                        // if one was previously stored.
+                        let idx = self.pressed_key_map
+                                      .borrow()
+                                      .iter()
+                                      .position(|&(code, _)| code == scan_code);
+                        idx.map(|idx| self.pressed_key_map.borrow_mut().swap_remove(idx).1)
+                    }
+                };
+
                 if let Ok(key) = Window::glutin_key_to_script_key(virtual_key_code) {
                     let state = match element_state {
                         ElementState::Pressed => KeyState::Pressed,
                         ElementState::Released => KeyState::Released,
                     };
                     let modifiers = Window::glutin_mods_to_script_mods(self.key_modifiers.get());
-                    self.event_queue.borrow_mut().push(WindowEvent::KeyEvent(key, state, modifiers));
+                    self.event_queue.borrow_mut().push(WindowEvent::KeyEvent(ch, key, state, modifiers));
                 }
             }
             Event::KeyboardInput(_, _, None) => {
                 debug!("Keyboard input without virtual key.");
             }
             Event::Resized(width, height) => {
                 self.event_queue.borrow_mut().push(WindowEvent::Resize(Size2D::typed(width, height)));
             }
@@ -798,87 +839,86 @@ impl WindowMethods for Window {
     }
 
     #[cfg(not(target_os = "linux"))]
     fn native_display(&self) -> NativeDisplay {
         NativeDisplay::new()
     }
 
     /// Helper function to handle keyboard events.
-    fn handle_key(&self, key: Key, mods: constellation_msg::KeyModifiers) {
-        match (mods, key) {
-            (_, Key::Equal) => {
+    fn handle_key(&self, ch: Option<char>, key: Key, mods: constellation_msg::KeyModifiers) {
+        match (mods, ch, key) {
+            (_, Some('+'), _) => {
                 if mods & !SHIFT == CMD_OR_CONTROL {
                     self.event_queue.borrow_mut().push(WindowEvent::Zoom(1.1));
                 } else if mods & !SHIFT == CMD_OR_CONTROL | ALT {
                     self.event_queue.borrow_mut().push(WindowEvent::PinchZoom(1.1));
                 }
             }
-            (CMD_OR_CONTROL, Key::Minus) => {
+            (CMD_OR_CONTROL, Some('-'), _) => {
                 self.event_queue.borrow_mut().push(WindowEvent::Zoom(1.0 / 1.1));
             }
-            (_, Key::Minus) if mods == CMD_OR_CONTROL | ALT => {
+            (_, Some('-'), _) if mods == CMD_OR_CONTROL | ALT => {
                 self.event_queue.borrow_mut().push(WindowEvent::PinchZoom(1.0 / 1.1));
             }
-            (CMD_OR_CONTROL, Key::Num0) |
-            (CMD_OR_CONTROL, Key::Kp0) => {
+            (CMD_OR_CONTROL, Some('0'), _) => {
                 self.event_queue.borrow_mut().push(WindowEvent::ResetZoom);
             }
 
-            (NONE, Key::NavigateForward) => {
+            (NONE, None, Key::NavigateForward) => {
                 self.event_queue.borrow_mut().push(WindowEvent::Navigation(WindowNavigateMsg::Forward));
             }
-            (NONE, Key::NavigateBackward) => {
+            (NONE, None, Key::NavigateBackward) => {
                 self.event_queue.borrow_mut().push(WindowEvent::Navigation(WindowNavigateMsg::Back));
             }
 
-            (NONE, Key::Escape) => {
+            (NONE, None, Key::Escape) => {
                 if let Some(true) = PREFS.get("shell.builtin-key-shortcuts.enabled").as_boolean() {
                     self.event_queue.borrow_mut().push(WindowEvent::Quit);
                 }
             }
 
-            (CMD_OR_ALT, Key::Right) => {
+            (CMD_OR_ALT, None, Key::Right) => {
                 self.event_queue.borrow_mut().push(WindowEvent::Navigation(WindowNavigateMsg::Forward));
             }
-            (CMD_OR_ALT, Key::Left) => {
+            (CMD_OR_ALT, None, Key::Left) => {
                 self.event_queue.borrow_mut().push(WindowEvent::Navigation(WindowNavigateMsg::Back));
             }
 
-            (NONE, Key::PageDown) |
-            (NONE, Key::Space) => {
+            (NONE, None, Key::PageDown) |
+            (NONE, Some(' '), _) => {
                 self.scroll_window(0.0,
                                    -self.framebuffer_size()
                                         .as_f32()
                                         .to_untyped()
                                         .height + 2.0 * LINE_HEIGHT,
                                    TouchEventType::Move);
             }
-            (NONE, Key::PageUp) |
-            (SHIFT, Key::Space) => {
+            (NONE, None, Key::PageUp) |
+            (SHIFT, Some(' '), _) => {
                 self.scroll_window(0.0,
                                    self.framebuffer_size()
                                        .as_f32()
                                        .to_untyped()
                                        .height - 2.0 * LINE_HEIGHT,
                                    TouchEventType::Move);
             }
-            (NONE, Key::Up) => {
+            (NONE, None, Key::Up) => {
                 self.scroll_window(0.0, 3.0 * LINE_HEIGHT, TouchEventType::Move);
             }
-            (NONE, Key::Down) => {
+            (NONE, None, Key::Down) => {
                 self.scroll_window(0.0, -3.0 * LINE_HEIGHT, TouchEventType::Move);
             }
-            (NONE, Key::Left) => {
+            (NONE, None, Key::Left) => {
                 self.scroll_window(LINE_HEIGHT, 0.0, TouchEventType::Move);
             }
-            (NONE, Key::Right) => {
+            (NONE, None, Key::Right) => {
                 self.scroll_window(-LINE_HEIGHT, 0.0, TouchEventType::Move);
             }
-            (CMD_OR_CONTROL, Key::R) => {
+            (CMD_OR_CONTROL, Some('r'), _) => {
                 if let Some(true) = PREFS.get("shell.builtin-key-shortcuts.enabled").as_boolean() {
                     self.event_queue.borrow_mut().push(WindowEvent::Reload);
                 }
             }
 
             _ => {
                 self.platform_handle_key(key, mods);
             }
@@ -927,16 +967,87 @@ fn glutin_pressure_stage_to_touchpad_pre
         TouchpadPressurePhase::BeforeClick
     } else if stage < 2 {
         TouchpadPressurePhase::AfterFirstClick
     } else {
         TouchpadPressurePhase::AfterSecondClick
     }
 }
 
+fn filter_nonprintable(ch: char, key_code: VirtualKeyCode) -> Option<char> {
+    use glutin::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 |
+        LMenu |
+        LShift |
+        LWin |
+        Mail |
+        MediaSelect |
+        MediaStop |
+        Mute |
+        MyComputer |
+        NavigateForward |
+        NavigateBackward |
+        NextTrack |
+        NoConvert |
+        PlayPause |
+        Power |
+        PrevTrack |
+        RAlt |
+        RControl |
+        RMenu |
+        RShift |
+        RWin |
+        Sleep |
+        Stop |
+        VolumeDown |
+        VolumeUp |
+        Wake |
+        WebBack |
+        WebFavorites |
+        WebForward |
+        WebHome |
+        WebRefresh |
+        WebSearch |
+        WebStop => None,
+        _ => Some(ch),
+    }
+}
+
 // These functions aren't actually called. They are here as a link
 // hack because Skia references them.
 
 #[allow(non_snake_case)]
 #[no_mangle]
 pub extern "C" fn glBindVertexArrayOES(_array: usize)
 {
     unimplemented!()
--- a/servo/tests/unit/script/textinput.rs
+++ b/servo/tests/unit/script/textinput.rs
@@ -396,17 +396,17 @@ fn test_clipboard_paste() {
 
     let mut textinput = TextInput::new(Lines::Single,
                                        DOMString::from("defg"),
                                        DummyClipboardContext::new("abc"),
                                        None,
                                        SelectionDirection::None);
     assert_eq!(textinput.get_content(), "defg");
     assert_eq!(textinput.edit_point.index, 0);
-    textinput.handle_keydown_aux(Key::V, MODIFIERS);
+    textinput.handle_keydown_aux(Some('v'), Key::V, MODIFIERS);
     assert_eq!(textinput.get_content(), "abcdefg");
 }
 
 #[test]
 fn test_textinput_cursor_position_correct_after_clearing_selection() {
     let mut textinput = text_input(Lines::Single, "abcdef");
 
     // Single line - Forward