diff --git a/docs/guide/src/advanced-guides/custom-renderer.md b/docs/guide/src/advanced-guides/custom-renderer.md index d2aa40bc9..a8bec614c 100644 --- a/docs/guide/src/advanced-guides/custom-renderer.md +++ b/docs/guide/src/advanced-guides/custom-renderer.md @@ -204,7 +204,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { fn ctrl_key(&self) -> bool { self.0.ctrl_key() } fn key(&self) -> String { self.0.key() } fn key_code(&self) -> usize { self.0.key_code() } - fn locale(&self) -> String { self.0.locale() } fn location(&self) -> usize { self.0.location() } fn meta_key(&self) -> bool { self.0.meta_key() } fn repeat(&self) -> bool { self.0.repeat() } diff --git a/docs/reference/src/guide/custom_renderer.md b/docs/reference/src/guide/custom_renderer.md index 9a73dd7f7..35187e2e3 100644 --- a/docs/reference/src/guide/custom_renderer.md +++ b/docs/reference/src/guide/custom_renderer.md @@ -206,7 +206,6 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { ctrl_key: event.ctrl_key(), meta_key: event.meta_key(), shift_key: event.shift_key(), - locale: "".to_string(), location: event.location(), repeat: event.repeat(), which: event.which(), diff --git a/examples/all_events.rs b/examples/all_events.rs new file mode 100644 index 000000000..5430bdbf5 --- /dev/null +++ b/examples/all_events.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; +use dioxus_html::on::{FocusData, KeyboardData, MouseData, WheelData}; +use std::sync::Arc; + +fn main() { + dioxus::desktop::launch(app); +} + +#[derive(Debug)] +enum Event { + MouseMove(Arc), + MouseClick(Arc), + MouseDoubleClick(Arc), + MouseDown(Arc), + MouseUp(Arc), + + Wheel(Arc), + + KeyDown(Arc), + KeyUp(Arc), + KeyPress(Arc), + + FocusIn(Arc), + FocusOut(Arc), +} + +const MAX_EVENTS: usize = 8; + +fn app(cx: Scope) -> Element { + let container_style = r#" + display: flex; + flex-direction: column; + align-items: center; + "#; + let rect_style = r#" + background: deepskyblue; + height: 50vh; + width: 50vw; + color: white; + padding: 20px; + margin: 20px; + text-aligh: center; + "#; + + let events = use_ref(&cx, || Vec::new()); + + let events_lock = events.read(); + let first_index = events_lock.len().saturating_sub(MAX_EVENTS); + let events_rendered = events_lock[first_index..] + .iter() + .map(|event| cx.render(rsx!(div {"{event:?}"}))); + + let log_event = move |event: Event| { + events.write().push(event); + }; + + cx.render(rsx! ( + div { + style: "{container_style}", + div { + style: "{rect_style}", + // focusing is necessary to catch keyboard events + tabindex: "0", + + onmousemove: move |event| log_event(Event::MouseMove(event.data)), + onclick: move |event| log_event(Event::MouseClick(event.data)), + ondblclick: move |event| log_event(Event::MouseDoubleClick(event.data)), + onmousedown: move |event| log_event(Event::MouseDown(event.data)), + onmouseup: move |event| log_event(Event::MouseUp(event.data)), + + onwheel: move |event| log_event(Event::Wheel(event.data)), + + onkeydown: move |event| log_event(Event::KeyDown(event.data)), + onkeyup: move |event| log_event(Event::KeyUp(event.data)), + onkeypress: move |event| log_event(Event::KeyPress(event.data)), + + onfocusin: move |event| log_event(Event::FocusIn(event.data)), + onfocusout: move |event| log_event(Event::FocusOut(event.data)), + + "Hover, click, type or scroll to see the info down below" + } + div { events_rendered }, + }, + )) +} diff --git a/examples/calculator.rs b/examples/calculator.rs index f63b8a754..75da8f1d3 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -5,6 +5,7 @@ This calculator version uses React-style state management. All state is held as use dioxus::events::*; use dioxus::prelude::*; +use dioxus_html::input_data::keyboard_types::Key; fn main() { use dioxus::desktop::tao::dpi::LogicalSize; @@ -29,33 +30,38 @@ fn app(cx: Scope) -> Element { let input_operator = move |key: &str| val.make_mut().push_str(key); + let handle_key_down_event = move |evt: KeyboardEvent| match evt.key() { + Key::Backspace => { + if !val.len() != 0 { + val.make_mut().pop(); + } + } + Key::Character(character) => match character.as_str() { + "+" => input_operator("+"), + "-" => input_operator("-"), + "/" => input_operator("/"), + "*" => input_operator("*"), + "0" => input_digit(0), + "1" => input_digit(1), + "2" => input_digit(2), + "3" => input_digit(3), + "4" => input_digit(4), + "5" => input_digit(5), + "6" => input_digit(6), + "7" => input_digit(7), + "8" => input_digit(8), + "9" => input_digit(9), + _ => {} + }, + _ => {} + }; + cx.render(rsx!( style { [include_str!("./assets/calculator.css")] } div { id: "wrapper", div { class: "app", div { class: "calculator", - onkeydown: move |evt| match evt.key_code { - KeyCode::Add => input_operator("+"), - KeyCode::Subtract => input_operator("-"), - KeyCode::Divide => input_operator("/"), - KeyCode::Multiply => input_operator("*"), - KeyCode::Num0 => input_digit(0), - KeyCode::Num1 => input_digit(1), - KeyCode::Num2 => input_digit(2), - KeyCode::Num3 => input_digit(3), - KeyCode::Num4 => input_digit(4), - KeyCode::Num5 => input_digit(5), - KeyCode::Num6 => input_digit(6), - KeyCode::Num7 => input_digit(7), - KeyCode::Num8 => input_digit(8), - KeyCode::Num9 => input_digit(9), - KeyCode::Backspace => { - if !val.len() != 0 { - val.make_mut().pop(); - } - } - _ => {} - }, + onkeydown: handle_key_down_event, div { class: "calculator-display", [val.to_string()] } div { class: "calculator-keypad", div { class: "input-keys", diff --git a/examples/events.rs b/examples/events.rs deleted file mode 100644 index 0a82e9bd7..000000000 --- a/examples/events.rs +++ /dev/null @@ -1,26 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - dioxus::desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! { - div { - button { - ondblclick: move |_| { - // - println!("double clicked!"); - }, - "Click me!" - } - input { - onfocusin: move |_| { - // - println!("blurred!"); - }, - "onblur": "console.log('blurred!')" - } - } - }) -} diff --git a/examples/mouse_event.rs b/examples/mouse_event.rs deleted file mode 100644 index 165d9445c..000000000 --- a/examples/mouse_event.rs +++ /dev/null @@ -1,56 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core::UiEvent; -use dioxus_html::on::MouseData; - -fn main() { - dioxus::desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - let page_coordinates = use_state(&cx, || "".to_string()); - let screen_coordinates = use_state(&cx, || "".to_string()); - let element_coordinates = use_state(&cx, || "".to_string()); - let buttons = use_state(&cx, || "".to_string()); - let modifiers = use_state(&cx, || "".to_string()); - - let container_style = r#" - display: flex; - flex-direction: column; - align-items: center; - "#; - let rect_style = r#" - background: deepskyblue; - height: 50vh; - width: 50vw; - "#; - - let update_mouse_position = move |event: UiEvent| { - let mouse_data = event.data; - - page_coordinates.set(format!("{:?}", mouse_data.page_coordinates())); - screen_coordinates.set(format!("{:?}", mouse_data.screen_coordinates())); - element_coordinates.set(format!("{:?}", mouse_data.element_coordinates())); - - // Note: client coordinates are also available, but they would be the same as the page coordinates in this example, because there is no scrolling. - - buttons.set(format!("{:?}", mouse_data.held_buttons())); - modifiers.set(format!("{:?}", mouse_data.modifiers())); - }; - - cx.render(rsx! ( - div { - style: "{container_style}", - "Hover over to display coordinates:", - div { - style: "{rect_style}", - onmousemove: update_mouse_position, - prevent_default: "mousedown", - } - div {"Page coordinates: {page_coordinates}"}, - div {"Screen coordinates: {screen_coordinates}"}, - div {"Element coordinates: {element_coordinates}"}, - div {"Buttons: {buttons}"}, - div {"Modifiers: {modifiers}"}, - } - )) -} diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index a7b821750..c88bc78fb 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -20,6 +20,7 @@ use dioxus::desktop::wry::application::dpi::LogicalSize; use dioxus::events::*; use dioxus::prelude::*; +use dioxus_html::input_data::keyboard_types::Key; fn main() { dioxus::desktop::launch_cfg(app, |cfg| { @@ -212,22 +213,26 @@ impl Calculator { self.waiting_for_operand = true; } fn handle_keydown(&mut self, evt: KeyboardEvent) { - match evt.key_code { - KeyCode::Backspace => self.backspace(), - KeyCode::Num0 => self.input_digit(0), - KeyCode::Num1 => self.input_digit(1), - KeyCode::Num2 => self.input_digit(2), - KeyCode::Num3 => self.input_digit(3), - KeyCode::Num4 => self.input_digit(4), - KeyCode::Num5 => self.input_digit(5), - KeyCode::Num6 => self.input_digit(6), - KeyCode::Num7 => self.input_digit(7), - KeyCode::Num8 => self.input_digit(8), - KeyCode::Num9 => self.input_digit(9), - KeyCode::Add => self.operator = Some(Operator::Add), - KeyCode::Subtract => self.operator = Some(Operator::Sub), - KeyCode::Divide => self.operator = Some(Operator::Div), - KeyCode::Multiply => self.operator = Some(Operator::Mul), + match evt.key() { + Key::Backspace => self.backspace(), + Key::Character(c) => match c.as_str() { + "0" => self.input_digit(0), + "1" => self.input_digit(1), + "2" => self.input_digit(2), + "3" => self.input_digit(3), + "4" => self.input_digit(4), + "5" => self.input_digit(5), + "6" => self.input_digit(6), + "7" => self.input_digit(7), + "8" => self.input_digit(8), + "9" => self.input_digit(9), + "+" => self.operator = Some(Operator::Add), + "-" => self.operator = Some(Operator::Sub), + "/" => self.operator = Some(Operator::Div), + "*" => self.operator = Some(Operator::Mul), + _ => {} + }, + _ => {} } } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 99cf67a69..cf9a49410 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; fn main() { dioxus::desktop::launch(app); @@ -56,7 +57,7 @@ pub fn app(cx: Scope<()>) -> Element { autofocus: "true", oninput: move |evt| draft.set(evt.value.clone()), onkeydown: move |evt| { - if evt.key == "Enter" && !draft.is_empty() { + if evt.key() == Key::Enter && !draft.is_empty() { todos.make_mut().insert( **todo_id, TodoItem { @@ -148,8 +149,8 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { autofocus: "true", onfocusout: move |_| is_editing.set(false), onkeydown: move |evt| { - match evt.key.as_str() { - "Enter" | "Escape" | "Tab" => is_editing.set(false), + match evt.key() { + Key::Enter | Key::Escape | Key::Tab => is_editing.set(false), _ => {} } }, diff --git a/examples/tui_all_events.rs b/examples/tui_all_events.rs new file mode 100644 index 000000000..732b62532 --- /dev/null +++ b/examples/tui_all_events.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; +use dioxus_html::on::{FocusData, KeyboardData, MouseData, WheelData}; +use std::sync::Arc; + +fn main() { + dioxus::tui::launch(app); +} + +#[derive(Debug)] +enum Event { + MouseMove(Arc), + MouseClick(Arc), + MouseDoubleClick(Arc), + MouseDown(Arc), + MouseUp(Arc), + + Wheel(Arc), + + KeyDown(Arc), + KeyUp(Arc), + KeyPress(Arc), + + FocusIn(Arc), + FocusOut(Arc), +} + +const MAX_EVENTS: usize = 8; + +fn app(cx: Scope) -> Element { + let events = use_ref(&cx, || Vec::new()); + + let events_lock = events.read(); + let first_index = events_lock.len().saturating_sub(MAX_EVENTS); + let events_rendered = events_lock[first_index..].iter().map(|event| { + // TUI panics if text overflows (https://github.com/DioxusLabs/dioxus/issues/371) + // temporary hack: just trim the strings (and make sure viewport is big enough) + // todo: remove + let mut trimmed = format!("{event:?}"); + trimmed.truncate(200); + cx.render(rsx!(p { "{trimmed}" })) + }); + + let log_event = move |event: Event| { + events.write().push(event); + }; + + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + div { + width: "80%", + height: "50%", + border_width: "1px", + justify_content: "center", + align_items: "center", + background_color: "hsl(248, 53%, 58%)", + + onmousemove: move |event| log_event(Event::MouseMove(event.data)), + onclick: move |event| log_event(Event::MouseClick(event.data)), + ondblclick: move |event| log_event(Event::MouseDoubleClick(event.data)), + onmousedown: move |event| log_event(Event::MouseDown(event.data)), + onmouseup: move |event| log_event(Event::MouseUp(event.data)), + + onwheel: move |event| log_event(Event::Wheel(event.data)), + + onkeydown: move |event| log_event(Event::KeyDown(event.data)), + onkeyup: move |event| log_event(Event::KeyUp(event.data)), + onkeypress: move |event| log_event(Event::KeyPress(event.data)), + + onfocusin: move |event| log_event(Event::FocusIn(event.data)), + onfocusout: move |event| log_event(Event::FocusOut(event.data)), + + "Hover, click, type or scroll to see the info down below" + }, + div { + width: "80%", + height: "50%", + flex_direction: "column", + events_rendered, + }, + }, + }) +} diff --git a/examples/tui_border.rs b/examples/tui_border.rs index 041df4a8b..3e1c01849 100644 --- a/examples/tui_border.rs +++ b/examples/tui_border.rs @@ -14,7 +14,7 @@ fn app(cx: Scope) -> Element { justify_content: "center", align_items: "center", background_color: "hsl(248, 53%, 58%)", - onwheel: move |w| radius.modify(|r| (r + w.delta_y as i8).abs()), + onwheel: move |w| radius.modify(|r| (r + w.delta().strip_units().y as i8).abs()), border_style: "solid none solid double", border_width: "thick", diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs index 31d2e1ce0..f0c4b3948 100644 --- a/examples/tui_buttons.rs +++ b/examples/tui_buttons.rs @@ -1,4 +1,5 @@ -use dioxus::{events::KeyCode, prelude::*}; +use dioxus::prelude::*; +use dioxus_html::input_data::keyboard_types::Code; fn main() { dioxus::tui::launch(app); @@ -27,7 +28,7 @@ fn Button(cx: Scope) -> Element { background_color: "{color}", tabindex: "{cx.props.layer}", onkeydown: |e| { - if let KeyCode::Space = e.data.key_code{ + if let Code::Space = e.data.code() { toggle.modify(|f| !f); } }, diff --git a/examples/tui_hover.rs b/examples/tui_hover.rs index 136213c25..5c3b2d832 100644 --- a/examples/tui_hover.rs +++ b/examples/tui_hover.rs @@ -65,7 +65,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q1_color.set([get_brightness(m.data), 0, 0]), onmousedown: move |m| q1_color.set([get_brightness(m.data), 0, 0]), onmouseup: move |m| q1_color.set([get_brightness(m.data), 0, 0]), - onwheel: move |w| q1_color.set([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]), + onwheel: move |w| q1_color.set([q1_color[0] + (10.0*w.delta().strip_units().y) as i32, 0, 0]), onmouseleave: move |_| q1_color.set([200; 3]), onmousemove: update_data, "click me" @@ -79,7 +79,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q2_color.set([get_brightness(m.data); 3]), onmousedown: move |m| q2_color.set([get_brightness(m.data); 3]), onmouseup: move |m| q2_color.set([get_brightness(m.data); 3]), - onwheel: move |w| q2_color.set([q2_color[0] + (10.0*w.delta_y) as i32;3]), + onwheel: move |w| q2_color.set([q2_color[0] + (10.0*w.delta().strip_units().y) as i32;3]), onmouseleave: move |_| q2_color.set([200; 3]), onmousemove: update_data, "click me" @@ -99,7 +99,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q3_color.set([0, get_brightness(m.data), 0]), onmousedown: move |m| q3_color.set([0, get_brightness(m.data), 0]), onmouseup: move |m| q3_color.set([0, get_brightness(m.data), 0]), - onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]), + onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0*w.delta().strip_units().y) as i32, 0]), onmouseleave: move |_| q3_color.set([200; 3]), onmousemove: update_data, "click me" @@ -113,7 +113,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.data)]), onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.data)]), onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.data)]), - onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]), + onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0*w.delta().strip_units().y) as i32]), onmouseleave: move |_| q4_color.set([200; 3]), onmousemove: update_data, "click me" diff --git a/examples/tui_keys.rs b/examples/tui_keys.rs index 91c42e30e..f947f97d0 100644 --- a/examples/tui_keys.rs +++ b/examples/tui_keys.rs @@ -1,9 +1,9 @@ use dioxus::events::WheelEvent; use dioxus::prelude::*; use dioxus_html::geometry::ScreenPoint; +use dioxus_html::input_data::keyboard_types::Code; use dioxus_html::input_data::MouseButtonSet; use dioxus_html::on::{KeyboardEvent, MouseEvent}; -use dioxus_html::KeyCode; fn main() { dioxus::tui::launch(app); @@ -16,6 +16,21 @@ fn app(cx: Scope) -> Element { let buttons = use_state(&cx, MouseButtonSet::empty); let mouse_clicked = use_state(&cx, || false); + let key_down_handler = move |evt: KeyboardEvent| { + match evt.data.code() { + Code::ArrowLeft => count.set(count + 1), + Code::ArrowRight => count.set(count - 1), + Code::ArrowUp => count.set(count + 10), + Code::ArrowDown => count.set(count - 10), + _ => {} + } + key.set(format!( + "{:?} repeating: {:?}", + evt.key(), + evt.is_auto_repeating() + )); + }; + cx.render(rsx! { div { width: "100%", @@ -24,18 +39,9 @@ fn app(cx: Scope) -> Element { justify_content: "center", align_items: "center", flex_direction: "column", - onkeydown: move |evt: KeyboardEvent| { - match evt.data.key_code { - KeyCode::LeftArrow => count.set(count + 1), - KeyCode::RightArrow => count.set(count - 1), - KeyCode::UpArrow => count.set(count + 10), - KeyCode::DownArrow => count.set(count - 10), - _ => {}, - } - key.set(format!("{:?} repeating: {:?}", evt.key, evt.repeat)); - }, + onkeydown: key_down_handler, onwheel: move |evt: WheelEvent| { - count.set(count + evt.data.delta_y as i64); + count.set(count + evt.data.delta().strip_units().y as i64); }, ondrag: move |evt: MouseEvent| { mouse.set(evt.data.screen_coordinates()); diff --git a/examples/tui_text.rs b/examples/tui_text.rs index b8f04d99a..a2992a8a0 100644 --- a/examples/tui_text.rs +++ b/examples/tui_text.rs @@ -12,7 +12,7 @@ fn app(cx: Scope) -> Element { width: "100%", height: "100%", flex_direction: "column", - onwheel: move |evt| alpha.set((**alpha + evt.data.delta_y as i64).min(100).max(0)), + onwheel: move |evt| alpha.set((**alpha + evt.data.delta().strip_units().y as i64).min(100).max(0)), p { background_color: "black", diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index ac5cd340f..8464189ea 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -40,5 +40,5 @@ features = [ [features] default = [] -serialize = ["serde", "serde_repr"] +serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"] wasm-bind = ["web-sys", "wasm-bindgen"] diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 67522e87a..d2fa8d3ba 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -5,12 +5,20 @@ use dioxus_core::*; pub mod on { //! Input events and associated data - use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; - use crate::input_data::{ - decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet, + use crate::geometry::{ + ClientPoint, Coordinates, ElementPoint, LinesVector, PagePoint, PagesVector, PixelsVector, + ScreenPoint, WheelDelta, }; - use keyboard_types::Modifiers; + use crate::input_data::{ + decode_key_location, decode_mouse_button_set, encode_key_location, encode_mouse_button_set, + MouseButton, MouseButtonSet, + }; + use euclid::UnknownUnit; + use keyboard_types::{Code, Key, Location, Modifiers}; use std::collections::HashMap; + use std::convert::TryInto; + use std::fmt::{Debug, Formatter}; + use std::str::FromStr; use super::*; macro_rules! event_directory { @@ -419,71 +427,142 @@ pub mod on { pub type KeyboardEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] + #[derive(Clone)] pub struct KeyboardData { + #[deprecated( + since = "0.3.0", + note = "This may not work in all environments. Use key() instead." + )] pub char_code: u32, /// Identify which "key" was entered. - /// - /// This is the best method to use for all languages. They key gets mapped to a String sequence which you can match on. - /// The key isn't an enum because there are just so many context-dependent keys. - /// - /// A full list on which keys to use is available at: - /// - /// - /// # Example - /// - /// ```rust, ignore - /// match event.key().as_str() { - /// "Esc" | "Escape" => {} - /// "ArrowDown" => {} - /// "ArrowLeft" => {} - /// _ => {} - /// } - /// ``` - /// + #[deprecated(since = "0.3.0", note = "use key() instead")] pub key: String, /// Get the key code as an enum Variant. - /// - /// This is intended for things like arrow keys, escape keys, function keys, and other non-international keys. - /// To match on unicode sequences, use the [`KeyboardData::key`] method - this will return a string identifier instead of a limited enum. - /// - /// - /// ## Example - /// - /// ```rust, ignore - /// use dioxus::KeyCode; - /// match event.key_code() { - /// KeyCode::Escape => {} - /// KeyCode::LeftArrow => {} - /// KeyCode::RightArrow => {} - /// _ => {} - /// } - /// ``` - /// + #[deprecated( + since = "0.3.0", + note = "This may not work in all environments. Use code() instead." + )] pub key_code: KeyCode, + /// the physical key on the keyboard + code: Code, + /// Indicate if the `alt` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] pub alt_key: bool, /// Indicate if the `ctrl` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] pub ctrl_key: bool, /// Indicate if the `meta` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] pub meta_key: bool, /// Indicate if the `shift` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] pub shift_key: bool, - pub locale: String, - + #[deprecated(since = "0.3.0", note = "use location() instead")] pub location: usize, + #[deprecated(since = "0.3.0", note = "use is_auto_repeating() instead")] pub repeat: bool, + #[deprecated(since = "0.3.0", note = "use code() or key() instead")] pub which: usize, - // get_modifier_state: bool, + } + + impl KeyboardData { + pub fn new( + key: Key, + code: Code, + location: Location, + is_auto_repeating: bool, + modifiers: Modifiers, + ) -> Self { + #[allow(deprecated)] + KeyboardData { + char_code: key.legacy_charcode(), + key: key.to_string(), + key_code: KeyCode::from_raw_code( + key.legacy_keycode() + .try_into() + .expect("could not convert keycode to u8"), + ), + code, + alt_key: modifiers.contains(Modifiers::ALT), + ctrl_key: modifiers.contains(Modifiers::CONTROL), + meta_key: modifiers.contains(Modifiers::META), + shift_key: modifiers.contains(Modifiers::SHIFT), + location: encode_key_location(location), + repeat: is_auto_repeating, + which: key + .legacy_charcode() + .try_into() + .expect("could not convert charcode to usize"), + } + } + + /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. + pub fn key(&self) -> Key { + #[allow(deprecated)] + FromStr::from_str(&self.key).expect("could not parse") + } + + /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. + pub fn code(&self) -> Code { + self.code + } + + /// The set of modifier keys which were pressed when the event occurred + pub fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + #[allow(deprecated)] + { + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + } + + modifiers + } + + /// The location of the key on the keyboard or other input device. + pub fn location(&self) -> Location { + #[allow(deprecated)] + decode_key_location(self.location) + } + + /// `true` iff the key is being held down such that it is automatically repeating. + pub fn is_auto_repeating(&self) -> bool { + #[allow(deprecated)] + self.repeat + } + } + + impl Debug for KeyboardData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyboardData") + .field("key", &self.key()) + .field("code", &self.code()) + .field("modifiers", &self.modifiers()) + .field("location", &self.location()) + .field("is_auto_repeating", &self.is_auto_repeating()) + .finish() + } } pub type FocusEvent = UiEvent; @@ -502,7 +581,7 @@ pub mod on { pub type MouseEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] + #[derive(Clone)] /// Data associated with a mouse event /// /// Do not use the deprecated fields; they may change or become private in the future. @@ -687,6 +766,17 @@ pub mod on { } } + impl Debug for MouseData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MouseData") + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } + } + pub type PointerEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] @@ -738,14 +828,75 @@ pub mod on { pub type WheelEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] + #[derive(Clone)] pub struct WheelData { + #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_mode: u32, + #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_x: f64, + #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_y: f64, + #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_z: f64, } + impl WheelData { + /// Construct a new WheelData with the specified wheel movement delta + pub fn new(delta: WheelDelta) -> Self { + let (delta_mode, vector) = match delta { + WheelDelta::Pixels(v) => (0, v.cast_unit::()), + WheelDelta::Lines(v) => (1, v.cast_unit::()), + WheelDelta::Pages(v) => (2, v.cast_unit::()), + }; + + #[allow(deprecated)] + WheelData { + delta_mode, + delta_x: vector.x, + delta_y: vector.y, + delta_z: vector.z, + } + } + + /// Construct from the attributes of the web wheel event + pub fn from_web_attributes( + delta_mode: u32, + delta_x: f64, + delta_y: f64, + delta_z: f64, + ) -> Self { + #[allow(deprecated)] + Self { + delta_mode, + delta_x, + delta_y, + delta_z, + } + } + + /// The amount of wheel movement + #[allow(deprecated)] + pub fn delta(&self) -> WheelDelta { + let x = self.delta_x; + let y = self.delta_y; + let z = self.delta_z; + match self.delta_mode { + 0 => WheelDelta::Pixels(PixelsVector::new(x, y, z)), + 1 => WheelDelta::Lines(LinesVector::new(x, y, z)), + 2 => WheelDelta::Pages(PagesVector::new(x, y, z)), + _ => panic!("Invalid delta mode, {:?}", self.delta_mode), + } + } + } + + impl Debug for WheelData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WheelData") + .field("delta", &self.delta()) + .finish() + } + } + pub type MediaEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] diff --git a/packages/html/src/geometry.rs b/packages/html/src/geometry.rs index 5a7c8112e..990ca5b5e 100644 --- a/packages/html/src/geometry.rs +++ b/packages/html/src/geometry.rs @@ -25,7 +25,78 @@ pub struct PageSpace; /// A point in PageSpace pub type PagePoint = Point2D; +/// A pixel unit: one unit corresponds to 1 pixel +pub struct Pixels; +/// A vector expressed in Pixels +pub type PixelsVector = Vector3D; + +/// A unit in terms of Lines +/// +/// One unit is relative to the size of one line +pub struct Lines; +/// A vector expressed in Lines +pub type LinesVector = Vector3D; + +/// A unit in terms of Screens: +/// +/// One unit is relative to the size of a page +pub struct Pages; +/// A vector expressed in Pages +pub type PagesVector = Vector3D; + +/// A vector representing the amount the mouse wheel was moved +/// +/// This may be expressed in Pixels, Lines or Pages +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub enum WheelDelta { + /// Movement in Pixels + Pixels(PixelsVector), + /// Movement in Lines + Lines(LinesVector), + /// Movement in Pages + Pages(PagesVector), +} + +impl WheelDelta { + /// Convenience function for constructing a WheelDelta with pixel units + pub fn pixels(x: f64, y: f64, z: f64) -> Self { + WheelDelta::Pixels(PixelsVector::new(x, y, z)) + } + + /// Convenience function for constructing a WheelDelta with line units + pub fn lines(x: f64, y: f64, z: f64) -> Self { + WheelDelta::Lines(LinesVector::new(x, y, z)) + } + + /// Convenience function for constructing a WheelDelta with page units + pub fn pages(x: f64, y: f64, z: f64) -> Self { + WheelDelta::Pages(PagesVector::new(x, y, z)) + } + + /// Returns true iff there is no wheel movement + /// + /// i.e. the x, y and z delta is zero (disregards units) + pub fn is_zero(&self) -> bool { + self.strip_units() == Vector3D::new(0., 0., 0.) + } + + /// A Vector3D proportional to the amount scrolled + /// + /// Note that this disregards the 3 possible units: this could be expressed in terms of pixels, lines, or pages. + /// + /// In most cases, to properly handle scrolling, you should handle all 3 possible enum variants instead of stripping units. Otherwise, if you assume that the units will always be pixels, the user may experience some unexpectedly slow scrolling if their mouse/OS sends values expressed in lines or pages. + pub fn strip_units(&self) -> Vector3D { + match self { + WheelDelta::Pixels(v) => v.cast_unit(), + WheelDelta::Lines(v) => v.cast_unit(), + WheelDelta::Pages(v) => v.cast_unit(), + } + } +} + /// Coordinates of a point in the app's interface +#[derive(Debug)] pub struct Coordinates { screen: ScreenPoint, client: ClientPoint, diff --git a/packages/html/src/input_data.rs b/packages/html/src/input_data.rs index b5bc73a44..6b994d5f0 100644 --- a/packages/html/src/input_data.rs +++ b/packages/html/src/input_data.rs @@ -3,6 +3,7 @@ use enumset::{EnumSet, EnumSetType}; /// A re-export of keyboard_types pub use keyboard_types; +use keyboard_types::Location; /// A mouse button type (such as Primary/Secondary) // note: EnumSetType also derives Copy and Clone for some reason @@ -118,3 +119,25 @@ pub fn encode_mouse_button_set(set: MouseButtonSet) -> u16 { code } + +pub fn decode_key_location(code: usize) -> Location { + match code { + 0 => Location::Standard, + 1 => Location::Left, + 2 => Location::Right, + 3 => Location::Numpad, + // keyboard_types doesn't yet support mobile/joystick locations + 4 | 5 => Location::Standard, + // unknown location; Standard seems better than panicking + _ => Location::Standard, + } +} + +pub fn encode_key_location(location: Location) -> usize { + match location { + Location::Standard => 0, + Location::Left => 1, + Location::Right => 2, + Location::Numpad => 3, + } +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 670b211c0..197eed737 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -1,11 +1,12 @@ use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; -use crate::input_data::{decode_mouse_button_set, MouseButton}; +use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; use crate::on::{ AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData, TransitionData, WheelData, }; -use crate::KeyCode; -use keyboard_types::Modifiers; +use keyboard_types::{Code, Key, Modifiers}; +use std::convert::TryInto; +use std::str::FromStr; use wasm_bindgen::JsCast; use web_sys::{ AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, @@ -56,19 +57,32 @@ impl From<&CompositionEvent> for CompositionData { impl From<&KeyboardEvent> for KeyboardData { fn from(e: &KeyboardEvent) -> Self { - Self { - alt_key: e.alt_key(), - char_code: e.char_code(), - key: e.key(), - key_code: KeyCode::from_raw_code(e.key_code() as u8), - ctrl_key: e.ctrl_key(), - locale: "not implemented".to_string(), - location: e.location() as usize, - meta_key: e.meta_key(), - repeat: e.repeat(), - shift_key: e.shift_key(), - which: e.which() as usize, + let mut modifiers = Modifiers::empty(); + + if e.alt_key() { + modifiers.insert(Modifiers::ALT); } + if e.ctrl_key() { + modifiers.insert(Modifiers::CONTROL); + } + if e.meta_key() { + modifiers.insert(Modifiers::META); + } + if e.shift_key() { + modifiers.insert(Modifiers::SHIFT); + } + + Self::new( + Key::from_str(&e.key()).expect("could not parse key"), + Code::from_str(&e.code()).expect("could not parse code"), + decode_key_location( + e.location() + .try_into() + .expect("could not convert location to u32"), + ), + e.repeat(), + modifiers, + ) } } @@ -146,12 +160,7 @@ impl From<&PointerEvent> for PointerData { impl From<&WheelEvent> for WheelData { fn from(e: &WheelEvent) -> Self { - Self { - delta_x: e.delta_x(), - delta_y: e.delta_y(), - delta_z: e.delta_z(), - delta_mode: e.delta_mode(), - } + WheelData::from_web_attributes(e.delta_mode(), e.delta_x(), e.delta_y(), e.delta_z()) } } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 759e83c9f..ba38f06e1 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -388,6 +388,7 @@ export function serialize_event(event) { location, repeat, which, + code, } = event; return { char_code: charCode, @@ -400,7 +401,7 @@ export function serialize_event(event) { location: location, repeat: repeat, which: which, - locale: "locale", + code, }; } case "focus": diff --git a/packages/liveview/src/interpreter.js b/packages/liveview/src/interpreter.js index 383f33a5c..f1eec3fde 100644 --- a/packages/liveview/src/interpreter.js +++ b/packages/liveview/src/interpreter.js @@ -358,7 +358,6 @@ function serialize_event(event) { location: location, repeat: repeat, which: which, - locale: "locale", }; } case "focus": diff --git a/packages/router/tests/web_router.rs b/packages/router/tests/web_router.rs index 94f57e445..8987ef3b5 100644 --- a/packages/router/tests/web_router.rs +++ b/packages/router/tests/web_router.rs @@ -1,9 +1,9 @@ #![cfg(target_arch = "wasm32")] +#![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_router::*; use gloo_utils::document; -use serde::{Deserialize, Serialize}; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -47,7 +47,7 @@ fn simple_test() { } fn BlogPost(cx: Scope) -> Element { - let id = use_route(&cx).parse_segment::("id")?; + let _id = use_route(&cx).parse_segment::("id")?; cx.render(rsx! { div { @@ -58,5 +58,5 @@ fn simple_test() { main(); - let element = gloo_utils::document(); + let _ = document(); } diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 6d633eab8..59aa7a7e3 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -5,11 +5,13 @@ use dioxus_core::*; use fxhash::{FxHashMap, FxHashSet}; use dioxus_html::geometry::euclid::{Point2D, Rect, Size2D}; -use dioxus_html::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; -use dioxus_html::input_data::keyboard_types::Modifiers; +use dioxus_html::geometry::{ + ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint, WheelDelta, +}; +use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{event_bubbles, on::*, KeyCode}; +use dioxus_html::{event_bubbles, on::*}; use std::{ any::Any, cell::{RefCell, RefMut}, @@ -140,14 +142,20 @@ impl InnerInputState { EventData::Wheel(ref w) => self.wheel = Some(w.clone()), EventData::Screen(ref s) => self.screen = Some(*s), EventData::Keyboard(ref mut k) => { - let repeat = self + let is_repeating = self .last_key_pressed .as_ref() - .filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME) + // heuristic for guessing which presses are auto-repeating. not necessarily accurate + .filter(|(last_data, last_instant)| { + last_data.key() == k.key() && last_instant.elapsed() < MAX_REPEAT_TIME + }) .is_some(); - k.repeat = repeat; - let new = k.clone(); - self.last_key_pressed = Some((new, Instant::now())); + + if is_repeating { + *k = KeyboardData::new(k.key(), k.code(), k.location(), true, k.modifiers()); + } + + self.last_key_pressed = Some((k.clone(), Instant::now())); } } } @@ -166,8 +174,10 @@ impl InnerInputState { let old_focus = self.focus_state.last_focused_id; evts.retain(|e| match &e.1 { - EventData::Keyboard(k) => match k.key_code { - KeyCode::Tab => !self.focus_state.progress(dom, !k.shift_key), + EventData::Keyboard(k) => match k.code() { + Code::Tab => !self + .focus_state + .progress(dom, !k.modifiers().contains(Modifiers::SHIFT)), _ => true, }, _ => true, @@ -293,7 +303,10 @@ impl InnerInputState { // a mouse button is released if a button was down and is now not down let was_released = !(previous_buttons - mouse_data.held_buttons()).is_empty(); - let wheel_delta = self.wheel.as_ref().map_or(0.0, |w| w.delta_y); + let was_scrolled = self + .wheel + .as_ref() + .map_or(false, |data| !data.delta().is_zero()); let wheel_data = &self.wheel; { @@ -457,7 +470,7 @@ impl InnerInputState { { // wheel if let Some(w) = wheel_data { - if wheel_delta != 0.0 { + if was_scrolled { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("wheel") { let node_layout = get_abs_layout(node, dom, layout); @@ -723,13 +736,8 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { }; let get_wheel_data = |up| { - // from https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent - EventData::Wheel(WheelData { - delta_mode: 0x01, - delta_x: 0.0, - delta_y: if up { -1.0 } else { 1.0 }, - delta_z: 0.0, - }) + let y = if up { -1.0 } else { 1.0 }; + EventData::Wheel(WheelData::new(WheelDelta::lines(0., y, 0.))) }; match m.kind { @@ -748,147 +756,222 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { } fn translate_key_event(event: crossterm::event::KeyEvent) -> Option { - let (code, key_str); - let mut shift_key = event.modifiers.contains(KeyModifiers::SHIFT); - if let TermKeyCode::Char(c) = event.code { - code = match c { - 'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() { - 'A' => KeyCode::A, - 'B' => KeyCode::B, - 'C' => KeyCode::C, - 'D' => KeyCode::D, - 'E' => KeyCode::E, - 'F' => KeyCode::F, - 'G' => KeyCode::G, - 'H' => KeyCode::H, - 'I' => KeyCode::I, - 'J' => KeyCode::J, - 'K' => KeyCode::K, - 'L' => KeyCode::L, - 'M' => KeyCode::M, - 'N' => KeyCode::N, - 'O' => KeyCode::O, - 'P' => KeyCode::P, - 'Q' => KeyCode::Q, - 'R' => KeyCode::R, - 'S' => KeyCode::S, - 'T' => KeyCode::T, - 'U' => KeyCode::U, - 'V' => KeyCode::V, - 'W' => KeyCode::W, - 'X' => KeyCode::X, - 'Y' => KeyCode::Y, - 'Z' => KeyCode::Z, - _ => return None, - }, - ' ' => KeyCode::Space, - '[' => KeyCode::OpenBracket, - '{' => KeyCode::OpenBracket, - ']' => KeyCode::CloseBraket, - '}' => KeyCode::CloseBraket, - ';' => KeyCode::Semicolon, - ':' => KeyCode::Semicolon, - ',' => KeyCode::Comma, - '<' => KeyCode::Comma, - '.' => KeyCode::Period, - '>' => KeyCode::Period, - '1' => KeyCode::Num1, - '2' => KeyCode::Num2, - '3' => KeyCode::Num3, - '4' => KeyCode::Num4, - '5' => KeyCode::Num5, - '6' => KeyCode::Num6, - '7' => KeyCode::Num7, - '8' => KeyCode::Num8, - '9' => KeyCode::Num9, - '0' => KeyCode::Num0, - '!' => KeyCode::Num1, - '@' => KeyCode::Num2, - '#' => KeyCode::Num3, - '$' => KeyCode::Num4, - '%' => KeyCode::Num5, - '^' => KeyCode::Num6, - '&' => KeyCode::Num7, - '*' => KeyCode::Num8, - '(' => KeyCode::Num9, - ')' => KeyCode::Num0, - // numpad charicter are ambiguous to tui - // '*' => KeyCode::Multiply, - // '/' => KeyCode::Divide, - // '-' => KeyCode::Subtract, - // '+' => KeyCode::Add, - '+' => KeyCode::EqualSign, - '-' => KeyCode::Dash, - '_' => KeyCode::Dash, - '\'' => KeyCode::SingleQuote, - '"' => KeyCode::SingleQuote, - '\\' => KeyCode::BackSlash, - '|' => KeyCode::BackSlash, - '/' => KeyCode::ForwardSlash, - '?' => KeyCode::ForwardSlash, - '=' => KeyCode::EqualSign, - '`' => KeyCode::GraveAccent, - '~' => KeyCode::GraveAccent, - _ => return None, - }; - key_str = c.to_string(); - } else { - code = match event.code { - TermKeyCode::Esc => KeyCode::Escape, - TermKeyCode::Backspace => KeyCode::Backspace, - TermKeyCode::Enter => KeyCode::Enter, - TermKeyCode::Left => KeyCode::LeftArrow, - TermKeyCode::Right => KeyCode::RightArrow, - TermKeyCode::Up => KeyCode::UpArrow, - TermKeyCode::Down => KeyCode::DownArrow, - TermKeyCode::Home => KeyCode::Home, - TermKeyCode::End => KeyCode::End, - TermKeyCode::PageUp => KeyCode::PageUp, - TermKeyCode::PageDown => KeyCode::PageDown, - TermKeyCode::Tab => KeyCode::Tab, - TermKeyCode::Delete => KeyCode::Delete, - TermKeyCode::Insert => KeyCode::Insert, - TermKeyCode::F(fn_num) => match fn_num { - 1 => KeyCode::F1, - 2 => KeyCode::F2, - 3 => KeyCode::F3, - 4 => KeyCode::F4, - 5 => KeyCode::F5, - 6 => KeyCode::F6, - 7 => KeyCode::F7, - 8 => KeyCode::F8, - 9 => KeyCode::F9, - 10 => KeyCode::F10, - 11 => KeyCode::F11, - 12 => KeyCode::F12, - _ => return None, - }, - // backtab is Shift + Tab - TermKeyCode::BackTab => { - shift_key = true; - KeyCode::Tab - } - TermKeyCode::Null => return None, - _ => return None, - }; - key_str = if let KeyCode::BackSlash = code { - "\\".to_string() - } else { - format!("{code:?}") - } - }; - // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent - Some(EventData::Keyboard(KeyboardData { - char_code: code.raw_code(), - key: key_str, - key_code: code, - alt_key: event.modifiers.contains(KeyModifiers::ALT), - ctrl_key: event.modifiers.contains(KeyModifiers::CONTROL), - meta_key: false, - shift_key, - locale: Default::default(), - location: 0x00, - repeat: Default::default(), - which: Default::default(), - })) + let key = key_from_crossterm_key_code(event.code); + // crossterm does not provide code. we make a guess as to which key might have been pressed + // this is probably garbage if the user has a custom keyboard layout + let code = guess_code_from_crossterm_key_code(event.code)?; + let modifiers = modifiers_from_crossterm_modifiers(event.modifiers); + + Some(EventData::Keyboard(KeyboardData::new( + key, + code, + Location::Standard, + false, + modifiers, + ))) +} + +/// The crossterm key_code nicely represents the meaning of the key and we can mostly convert it without any issues +/// +/// Exceptions: +/// BackTab is converted to Key::Tab, and Null is converted to Key::Unidentified +fn key_from_crossterm_key_code(key_code: TermKeyCode) -> Key { + match key_code { + TermKeyCode::Backspace => Key::Backspace, + TermKeyCode::Enter => Key::Enter, + TermKeyCode::Left => Key::ArrowLeft, + TermKeyCode::Right => Key::ArrowRight, + TermKeyCode::Up => Key::ArrowUp, + TermKeyCode::Down => Key::ArrowDown, + TermKeyCode::Home => Key::Home, + TermKeyCode::End => Key::End, + TermKeyCode::PageUp => Key::PageUp, + TermKeyCode::PageDown => Key::PageDown, + TermKeyCode::Tab => Key::Tab, + // ? no corresponding Key + TermKeyCode::BackTab => Key::Tab, + TermKeyCode::Delete => Key::Delete, + TermKeyCode::Insert => Key::Insert, + TermKeyCode::F(1) => Key::F1, + TermKeyCode::F(2) => Key::F2, + TermKeyCode::F(3) => Key::F3, + TermKeyCode::F(4) => Key::F4, + TermKeyCode::F(5) => Key::F5, + TermKeyCode::F(6) => Key::F6, + TermKeyCode::F(7) => Key::F7, + TermKeyCode::F(8) => Key::F8, + TermKeyCode::F(9) => Key::F9, + TermKeyCode::F(10) => Key::F10, + TermKeyCode::F(11) => Key::F11, + TermKeyCode::F(12) => Key::F12, + TermKeyCode::F(13) => Key::F13, + TermKeyCode::F(14) => Key::F14, + TermKeyCode::F(15) => Key::F15, + TermKeyCode::F(16) => Key::F16, + TermKeyCode::F(17) => Key::F17, + TermKeyCode::F(18) => Key::F18, + TermKeyCode::F(19) => Key::F19, + TermKeyCode::F(20) => Key::F20, + TermKeyCode::F(21) => Key::F21, + TermKeyCode::F(22) => Key::F22, + TermKeyCode::F(23) => Key::F23, + TermKeyCode::F(24) => Key::F24, + TermKeyCode::F(other) => { + panic!("Unexpected function key: {other:?}") + } + TermKeyCode::Char(c) => Key::Character(c.to_string()), + TermKeyCode::Null => Key::Unidentified, + TermKeyCode::Esc => Key::Escape, + } +} + +// Crossterm does not provide a way to get the `code` (physical key on keyboard) +// So we make a guess based on their `key_code`, but this is probably going to break on anything other than a very standard european keyboard +// It may look fine, but it's a horrible hack. But there's nothing better we can do. +fn guess_code_from_crossterm_key_code(key_code: TermKeyCode) -> Option { + let code = match key_code { + TermKeyCode::Backspace => Code::Backspace, + TermKeyCode::Enter => Code::Enter, + TermKeyCode::Left => Code::ArrowLeft, + TermKeyCode::Right => Code::ArrowRight, + TermKeyCode::Up => Code::ArrowUp, + TermKeyCode::Down => Code::ArrowDown, + TermKeyCode::Home => Code::Home, + TermKeyCode::End => Code::End, + TermKeyCode::PageUp => Code::PageUp, + TermKeyCode::PageDown => Code::PageDown, + TermKeyCode::Tab => Code::Tab, + // ? Apparently you get BackTab by pressing Tab + TermKeyCode::BackTab => Code::Tab, + TermKeyCode::Delete => Code::Delete, + TermKeyCode::Insert => Code::Insert, + TermKeyCode::F(1) => Code::F1, + TermKeyCode::F(2) => Code::F2, + TermKeyCode::F(3) => Code::F3, + TermKeyCode::F(4) => Code::F4, + TermKeyCode::F(5) => Code::F5, + TermKeyCode::F(6) => Code::F6, + TermKeyCode::F(7) => Code::F7, + TermKeyCode::F(8) => Code::F8, + TermKeyCode::F(9) => Code::F9, + TermKeyCode::F(10) => Code::F10, + TermKeyCode::F(11) => Code::F11, + TermKeyCode::F(12) => Code::F12, + TermKeyCode::F(13) => Code::F13, + TermKeyCode::F(14) => Code::F14, + TermKeyCode::F(15) => Code::F15, + TermKeyCode::F(16) => Code::F16, + TermKeyCode::F(17) => Code::F17, + TermKeyCode::F(18) => Code::F18, + TermKeyCode::F(19) => Code::F19, + TermKeyCode::F(20) => Code::F20, + TermKeyCode::F(21) => Code::F21, + TermKeyCode::F(22) => Code::F22, + TermKeyCode::F(23) => Code::F23, + TermKeyCode::F(24) => Code::F24, + TermKeyCode::F(other) => { + panic!("Unexpected function key: {other:?}") + } + // this is a horrible way for crossterm to represent keys but we have to deal with it + TermKeyCode::Char(c) => match c { + 'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() { + 'A' => Code::KeyA, + 'B' => Code::KeyB, + 'C' => Code::KeyC, + 'D' => Code::KeyD, + 'E' => Code::KeyE, + 'F' => Code::KeyF, + 'G' => Code::KeyG, + 'H' => Code::KeyH, + 'I' => Code::KeyI, + 'J' => Code::KeyJ, + 'K' => Code::KeyK, + 'L' => Code::KeyL, + 'M' => Code::KeyM, + 'N' => Code::KeyN, + 'O' => Code::KeyO, + 'P' => Code::KeyP, + 'Q' => Code::KeyQ, + 'R' => Code::KeyR, + 'S' => Code::KeyS, + 'T' => Code::KeyT, + 'U' => Code::KeyU, + 'V' => Code::KeyV, + 'W' => Code::KeyW, + 'X' => Code::KeyX, + 'Y' => Code::KeyY, + 'Z' => Code::KeyZ, + _ => unreachable!("Exhaustively checked all characters in range A..Z"), + }, + ' ' => Code::Space, + '[' | '{' => Code::BracketLeft, + ']' | '}' => Code::BracketRight, + ';' => Code::Semicolon, + ':' => Code::Semicolon, + ',' => Code::Comma, + '<' => Code::Comma, + '.' => Code::Period, + '>' => Code::Period, + '1' => Code::Digit1, + '2' => Code::Digit2, + '3' => Code::Digit3, + '4' => Code::Digit4, + '5' => Code::Digit5, + '6' => Code::Digit6, + '7' => Code::Digit7, + '8' => Code::Digit8, + '9' => Code::Digit9, + '0' => Code::Digit0, + '!' => Code::Digit1, + '@' => Code::Digit2, + '#' => Code::Digit3, + '$' => Code::Digit4, + '%' => Code::Digit5, + '^' => Code::Digit6, + '&' => Code::Digit7, + '*' => Code::Digit8, + '(' => Code::Digit9, + ')' => Code::Digit0, + // numpad characters are ambiguous; we don't know which key was really pressed + // it could be also: + // '*' => Code::Multiply, + // '/' => Code::Divide, + // '-' => Code::Subtract, + // '+' => Code::Add, + '+' => Code::Equal, + '-' | '_' => Code::Minus, + '\'' => Code::Quote, + '"' => Code::Quote, + '\\' => Code::Backslash, + '|' => Code::Backslash, + '/' => Code::Slash, + '?' => Code::Slash, + '=' => Code::Equal, + '`' => Code::Backquote, + '~' => Code::Backquote, + _ => return None, + }, + TermKeyCode::Null => return None, + TermKeyCode::Esc => Code::Escape, + }; + + Some(code) +} + +fn modifiers_from_crossterm_modifiers(src: KeyModifiers) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if src.contains(KeyModifiers::SHIFT) { + modifiers.insert(Modifiers::SHIFT); + } + + if src.contains(KeyModifiers::ALT) { + modifiers.insert(Modifiers::ALT); + } + + if src.contains(KeyModifiers::CONTROL) { + modifiers.insert(Modifiers::CONTROL); + } + + modifiers } diff --git a/packages/tui/tests/events.rs b/packages/tui/tests/events.rs index 217959531..dfaedab40 100644 --- a/packages/tui/tests/events.rs +++ b/packages/tui/tests/events.rs @@ -1,5 +1,6 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent}; use dioxus::prelude::*; +use dioxus_html::input_data::keyboard_types::Code; use dioxus_tui::TuiContext; use std::future::Future; use std::pin::Pin; @@ -56,7 +57,7 @@ fn key_down() { width: "100%", height: "100%", onkeydown: move |evt| { - assert_eq!(evt.data.key_code, dioxus_html::KeyCode::A); + assert_eq!(evt.data.code(), Code::KeyA); tui_ctx.quit(); }, } @@ -286,7 +287,7 @@ fn wheel() { width: "100%", height: "100%", onwheel: move |evt| { - assert!(evt.data.delta_y > 0.0); + assert!(evt.data.delta().strip_units().y > 0.0); tui_ctx.quit(); }, } diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs index 8654ced90..d6035d5ab 100644 --- a/packages/web/src/olddom.rs +++ b/packages/web/src/olddom.rs @@ -521,7 +521,6 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc