diff --git a/core/src/backend/input.rs b/core/src/backend/input.rs index aba3205a8687..caf76e2a2b19 100644 --- a/core/src/backend/input.rs +++ b/core/src/backend/input.rs @@ -17,6 +17,9 @@ pub trait InputBackend: Downcast { /// Changes the mouse cursor image. fn set_mouse_cursor(&mut self, cursor: MouseCursor); + /// Get the clipboard content + fn clipboard_content(&mut self) -> String; + /// Set the clipboard to the given content fn set_clipboard_content(&mut self, content: String); } @@ -54,6 +57,10 @@ impl InputBackend for NullInputBackend { fn set_mouse_cursor(&mut self, _cursor: MouseCursor) {} + fn clipboard_content(&mut self) -> String { + "".to_string() + } + fn set_clipboard_content(&mut self, _content: String) {} } diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index aeb4a32ddc44..b69bd2266213 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -5,7 +5,7 @@ use crate::backend::input::MouseCursor; use crate::context::{RenderContext, UpdateContext}; use crate::display_object::{DisplayObjectBase, TDisplayObject}; use crate::drawing::Drawing; -use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode}; +use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, TextControlCode}; use crate::font::{round_down_to_pixel, Glyph}; use crate::html::{BoxBounds, FormatSpans, LayoutBox, LayoutContent, TextFormat}; use crate::prelude::*; @@ -1056,15 +1056,71 @@ impl<'gc> EditText<'gc> { None } - pub fn text_input(self, character: char, context: &mut UpdateContext<'_, 'gc, '_>) { - if !self.0.read().is_editable { + pub fn text_control_input( + self, + control_code: TextControlCode, + context: &mut UpdateContext<'_, 'gc, '_>, + ) { + if !self.0.read().is_editable && control_code.is_edit_input() { return; } if let Some(selection) = self.selection() { let mut changed = false; - match character as u8 { - 8 | 127 if !selection.is_caret() => { + let is_selectable = self.0.read().is_selectable; + match control_code { + TextControlCode::SelectAll => { + if is_selectable { + self.set_selection( + Some(TextSelection::for_range(0, self.text().len())), + context.gc_context, + ); + } + } + TextControlCode::Copy => { + if !selection.is_caret() { + let text = &self.text()[selection.start()..selection.end()]; + context.input.set_clipboard_content(text.to_string()); + } + } + TextControlCode::Paste => { + let text = &context.input.clipboard_content(); + self.replace_text(selection.start(), selection.end(), text, context); + let new_start = selection.start() + text.len(); + if is_selectable { + self.set_selection( + Some(TextSelection::for_position(new_start)), + context.gc_context, + ); + } else { + self.set_selection( + Some(TextSelection::for_position(self.text().len())), + context.gc_context, + ); + } + changed = true; + } + TextControlCode::Cut => { + if !selection.is_caret() { + let text = &self.text()[selection.start()..selection.end()]; + context.input.set_clipboard_content(text.to_string()); + + self.replace_text(selection.start(), selection.end(), "", context); + if is_selectable { + self.set_selection( + Some(TextSelection::for_position(selection.start())), + context.gc_context, + ); + } else { + self.set_selection( + Some(TextSelection::for_position(self.text().len())), + context.gc_context, + ); + } + changed = true; + } + } + TextControlCode::Backspace | TextControlCode::Delete if !selection.is_caret() => { // Backspace or delete with multiple characters selected self.replace_text(selection.start(), selection.end(), "", context); self.set_selection( @@ -1073,7 +1129,7 @@ impl<'gc> EditText<'gc> { ); changed = true; } - 8 => { + TextControlCode::Backspace => { // Backspace with caret if selection.start() > 0 { // Delete previous character @@ -1087,7 +1143,7 @@ impl<'gc> EditText<'gc> { changed = true; } } - 127 => { + TextControlCode::Delete => { // Delete with caret if selection.end() < self.text_length() { // Delete next character @@ -1098,6 +1154,31 @@ impl<'gc> EditText<'gc> { changed = true; } } + _ => {} + } + if changed { + let globals = context.avm1.global_object_cell(); + let swf_version = context.swf.header().version; + let mut activation = Activation::from_nothing( + context.reborrow(), + ActivationIdentifier::root("[Propagate Text Binding]"), + swf_version, + globals, + self.into(), + ); + self.propagate_text_binding(&mut activation); + } + } + } + + pub fn text_input(self, character: char, context: &mut UpdateContext<'_, 'gc, '_>) { + if !self.0.read().is_editable { + return; + } + + if let Some(selection) = self.selection() { + let mut changed = false; + match character as u8 { code if !(code as char).is_control() => { self.replace_text( selection.start(), diff --git a/core/src/events.rs b/core/src/events.rs index 88303e22dc0d..c718f306d32d 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -11,6 +11,7 @@ pub enum PlayerEvent { MouseLeft, MouseWheel { delta: MouseWheelDelta }, TextInput { codepoint: char }, + TextControl { code: TextControlCode }, } /// The distance scrolled by the mouse wheel. @@ -137,6 +138,29 @@ impl ClipEvent { } } +/// Control inputs to a text field +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TextControlCode { + // TODO: Extend this + SelectAll, + Copy, + Paste, + Cut, + Backspace, + Enter, + Delete, +} + +impl TextControlCode { + /// Indicates whether this is an event that edits the text content + pub fn is_edit_input(self) -> bool { + matches!( + self, + Self::Paste | Self::Cut | Self::Backspace | Self::Enter | Self::Delete + ) + } +} + /// Flash virtual keycode. #[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] #[repr(u8)] diff --git a/core/src/player.rs b/core/src/player.rs index 85ba4bbd46d2..d89819aa18aa 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -689,6 +689,14 @@ impl Player { }); } + if let PlayerEvent::TextControl { code } = event { + self.mutate_with_update_context(|context| { + if let Some(text) = context.focus_tracker.get().and_then(|o| o.as_edit_text()) { + text.text_control_input(code, context); + } + }); + } + // Propagate clip events. self.mutate_with_update_context(|context| { let (clip_event, listener) = match event { diff --git a/desktop/src/input.rs b/desktop/src/input.rs index 6c84cfee86b5..8dfad79184f6 100644 --- a/desktop/src/input.rs +++ b/desktop/src/input.rs @@ -1,6 +1,6 @@ use clipboard::{ClipboardContext, ClipboardProvider}; use ruffle_core::backend::input::{InputBackend, MouseCursor}; -use ruffle_core::events::{KeyCode, PlayerEvent}; +use ruffle_core::events::{KeyCode, PlayerEvent, TextControlCode}; use std::collections::HashSet; use std::rc::Rc; use winit::event::{ElementState, ModifiersState, VirtualKeyCode, WindowEvent}; @@ -36,13 +36,19 @@ impl WinitInputBackend { ElementState::Pressed => { if let Some(key) = input.virtual_keycode { self.keys_down.insert(key); - self.last_char = - winit_key_to_char(key, input.modifiers.contains(ModifiersState::SHIFT)); - if let Some(key_code) = winit_to_ruffle_key_code(key) { - self.last_key = key_code; - return Some(PlayerEvent::KeyDown { key_code }); + if let Some(code) = winit_to_ruffle_text_control(key, input.modifiers) { + return Some(PlayerEvent::TextControl { code }); } else { - self.last_key = KeyCode::Unknown; + self.last_char = winit_key_to_char( + key, + input.modifiers.contains(ModifiersState::SHIFT), + ); + if let Some(key_code) = winit_to_ruffle_key_code(key) { + self.last_key = key_code; + return Some(PlayerEvent::KeyDown { key_code }); + } else { + self.last_key = KeyCode::Unknown; + } } } } @@ -212,6 +218,12 @@ impl InputBackend for WinitInputBackend { self.window.set_cursor_icon(icon); } + fn clipboard_content(&mut self) -> String { + self.clipboard + .get_contents() + .unwrap_or_else(|_| "".to_string()) + } + fn set_clipboard_content(&mut self, content: String) { self.clipboard.set_contents(content).unwrap(); } @@ -454,3 +466,28 @@ fn winit_key_to_char(key_code: VirtualKeyCode, is_shift_down: bool) -> Option Option { + let ctrl_cmd = modifiers.contains(ModifiersState::CTRL) + || (modifiers.contains(ModifiersState::LOGO) && cfg!(target_os = "macos")); + if ctrl_cmd { + match key { + VirtualKeyCode::A => Some(TextControlCode::SelectAll), + VirtualKeyCode::C => Some(TextControlCode::Copy), + VirtualKeyCode::V => Some(TextControlCode::Paste), + VirtualKeyCode::X => Some(TextControlCode::Cut), + _ => None, + } + } else { + match key { + VirtualKeyCode::Back => Some(TextControlCode::Backspace), + VirtualKeyCode::Delete => Some(TextControlCode::Delete), + _ => None, + } + } +} diff --git a/web/src/input.rs b/web/src/input.rs index c397eac054a3..f23ef6e57cf5 100644 --- a/web/src/input.rs +++ b/web/src/input.rs @@ -1,5 +1,5 @@ use ruffle_core::backend::input::{InputBackend, MouseCursor}; -use ruffle_core::events::KeyCode; +use ruffle_core::events::{KeyCode, TextControlCode}; use ruffle_web_common::JsResult; use std::collections::HashSet; use web_sys::{HtmlCanvasElement, KeyboardEvent}; @@ -13,6 +13,7 @@ pub struct WebInputBackend { cursor: MouseCursor, last_key: KeyCode, last_char: Option, + last_text_control: Option, } impl WebInputBackend { @@ -24,23 +25,33 @@ impl WebInputBackend { cursor: MouseCursor::Arrow, last_key: KeyCode::Unknown, last_char: None, + last_text_control: None, } } /// Register a key press for a given code string. pub fn keydown(&mut self, event: &KeyboardEvent) { let code = event.code(); - self.last_key = web_to_ruffle_key_code(&code).unwrap_or(KeyCode::Unknown); - self.keys_down.insert(code); - self.last_char = web_key_to_codepoint(&event.key()); + self.keys_down.insert(code.clone()); + let is_ctrl_cmd = event.ctrl_key(); // TODO: Use meta key if on MacOS + self.last_text_control = web_to_text_control(&event.key(), is_ctrl_cmd); + if self.last_text_control.is_none() { + self.last_key = web_to_ruffle_key_code(&code).unwrap_or(KeyCode::Unknown); + self.last_char = web_key_to_codepoint(&event.key()); + } } /// Register a key release for a given code string. pub fn keyup(&mut self, event: &KeyboardEvent) { let code = event.code(); - self.last_key = web_to_ruffle_key_code(&code).unwrap_or(KeyCode::Unknown); self.keys_down.remove(&code); + self.last_key = web_to_ruffle_key_code(&code).unwrap_or(KeyCode::Unknown); self.last_char = web_key_to_codepoint(&event.key()); + self.last_text_control = None; + } + + pub fn last_text_control(&self) -> Option { + self.last_text_control } fn update_mouse_cursor(&self) { @@ -195,6 +206,11 @@ impl InputBackend for WebInputBackend { self.update_mouse_cursor(); } + fn clipboard_content(&mut self) -> String { + log::warn!("get clipboard not implemented"); + "".to_string() + } + fn set_clipboard_content(&mut self, _content: String) { log::warn!("set clipboard not implemented"); } @@ -324,3 +340,29 @@ pub fn web_key_to_codepoint(key: &str) -> Option { } } } + +pub fn web_to_text_control(key: &str, ctrl_key: bool) -> Option { + let mut chars = key.chars(); + let (c1, c2) = (chars.next(), chars.next()); + if c2.is_none() { + // Single character. + if ctrl_key { + match c1 { + // TODO: Extend this + Some('a') => Some(TextControlCode::SelectAll), + Some('c') => Some(TextControlCode::Copy), + Some('v') => Some(TextControlCode::Paste), + Some('x') => Some(TextControlCode::Cut), + _ => None, + } + } else { + None + } + } else { + match key { + "Delete" => Some(TextControlCode::Delete), + "Backspace" => Some(TextControlCode::Backspace), + _ => None, + } + } +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 08dd5bd84d22..925979dd85b8 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -730,13 +730,17 @@ impl Ruffle { let key_code = input.last_key_code(); let key_char = input.last_key_char(); - - if key_code != KeyCode::Unknown { - core.handle_event(PlayerEvent::KeyDown { key_code }); - } - - if let Some(codepoint) = key_char { - core.handle_event(PlayerEvent::TextInput { codepoint }); + let text_control = input.last_text_control(); + + if let Some(code) = text_control { + core.handle_event(PlayerEvent::TextControl { code }); + } else { + if key_code != KeyCode::Unknown { + core.handle_event(PlayerEvent::KeyDown { key_code }); + } + if let Some(codepoint) = key_char { + core.handle_event(PlayerEvent::TextInput { codepoint }); + } } js_event.prevent_default();