Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Implement handling of text control input #11059

Merged
merged 6 commits into from
May 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
core: Implement handling of text control input
Co-authored-by: jmckiern <jmckiern@tcd.ie>
  • Loading branch information
2 people authored and Dinnerbone committed May 21, 2023
commit 3f5b2b54338f234dee65f14ca5f936a2996ea7ee
12 changes: 11 additions & 1 deletion core/src/backend/ui.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::events::{KeyCode, PlayerEvent};
use crate::events::{KeyCode, PlayerEvent, TextControlCode};
use fluent_templates::loader::langid;
pub use fluent_templates::LanguageIdentifier;
use std::borrow::Cow;
Expand Down Expand Up @@ -66,6 +66,7 @@ pub struct InputManager {
keys_down: HashSet<KeyCode>,
last_key: KeyCode,
last_char: Option<char>,
last_text_control: Option<TextControlCode>,
}

impl InputManager {
Expand All @@ -74,6 +75,7 @@ impl InputManager {
keys_down: HashSet::new(),
last_key: KeyCode::Unknown,
last_char: None,
last_text_control: None,
}
}

Expand All @@ -100,6 +102,10 @@ impl InputManager {
PlayerEvent::KeyUp { key_code, key_char } => {
self.last_char = key_char;
self.remove_key(key_code);
self.last_text_control = None;
}
PlayerEvent::TextControl { code } => {
self.last_text_control = Some(code);
}
PlayerEvent::MouseDown { button, .. } => self.add_key(button.into()),
PlayerEvent::MouseUp { button, .. } => self.remove_key(button.into()),
Expand All @@ -119,6 +125,10 @@ impl InputManager {
self.last_char
}

pub fn last_text_control(&self) -> Option<TextControlCode> {
self.last_text_control
}

pub fn is_mouse_down(&self) -> bool {
self.is_key_down(KeyCode::MouseLeft)
}
Expand Down
80 changes: 79 additions & 1 deletion core/src/display_object/edit_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::display_object::interactive::{
};
use crate::display_object::{DisplayObjectBase, DisplayObjectPtr, 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, TextRenderSettings};
use crate::html::{BoxBounds, FormatSpans, LayoutBox, LayoutContent, LayoutMetrics, TextFormat};
use crate::prelude::*;
Expand Down Expand Up @@ -1174,6 +1174,84 @@ impl<'gc> EditText<'gc> {
None
}

pub fn text_control_input(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Nit: Would a (maybe one-line) comment to document this function be helpful?).

self,
control_code: TextControlCode,
context: &mut UpdateContext<'_, 'gc>,
) {
if !self.is_editable() && control_code.is_edit_input() {
return;
}

if let Some(selection) = self.selection() {
let mut changed = false;
let is_selectable = self.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.ui.set_clipboard_content(text.to_string());
}
}
TextControlCode::Paste => {
let text = &context.ui.clipboard_content();
self.replace_text(selection.start(), selection.end(), &WString::from_utf8(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.ui.set_clipboard_content(text.to_string());

self.replace_text(selection.start(), selection.end(), WStr::empty(), 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;
}
}
_ => {}
}
if changed {
let mut activation = Avm1Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[Propagate Text Binding]"),
self.into(),
);
self.propagate_text_binding(&mut activation);
self.on_changed(&mut activation);
}
}
}

pub fn text_input(self, character: char, context: &mut UpdateContext<'_, 'gc>) {
if self.0.read().flags.contains(EditTextFlag::READ_ONLY) {
return;
Expand Down
26 changes: 26 additions & 0 deletions core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub enum PlayerEvent {
TextInput {
codepoint: char,
},
TextControl {
code: TextControlCode
},
}

/// The distance scrolled by the mouse wheel.
Expand Down Expand Up @@ -330,6 +333,29 @@ impl<'gc> ClipEvent<'gc> {
}
}

/// 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, Hash, FromPrimitive)]
pub enum KeyCode {
Expand Down
5 changes: 5 additions & 0 deletions core/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,11 @@ impl Player {
text.text_input(codepoint, context);
}
}
if let PlayerEvent::TextControl { code } = event {
if let Some(text) = context.focus_tracker.get().and_then(|o| o.as_edit_text()) {
text.text_control_input(code, context);
}
}
}

// Propagate clip events.
Expand Down
30 changes: 28 additions & 2 deletions desktop/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ use rfd::FileDialog;
use ruffle_core::backend::audio::AudioBackend;
use ruffle_core::backend::navigator::OpenURLMode;
use ruffle_core::{
config::Letterbox, events::KeyCode, tag_utils::SwfMovie, LoadBehavior, Player, PlayerBuilder,
config::Letterbox, tag_utils::SwfMovie, LoadBehavior, Player, PlayerBuilder,
PlayerEvent, StageDisplayState, StageScaleMode, StaticCallstack, ViewportDimensions,
};
use ruffle_core::events::{KeyCode, TextControlCode};
use ruffle_render::backend::RenderBackend;
use ruffle_render::quality::StageQuality;
use ruffle_render_wgpu::backend::WgpuRenderBackend;
Expand Down Expand Up @@ -588,7 +589,11 @@ impl App {
let key_char = winit_key_to_char(key, modifiers.shift());
let event = match input.state {
ElementState::Pressed => {
PlayerEvent::KeyDown { key_code, key_char }
if let Some(control_code) = winit_to_ruffle_text_control(key, modifiers) {
PlayerEvent::TextControl { code: control_code }
} else {
PlayerEvent::KeyDown { key_code, key_char }
}
}
ElementState::Released => {
PlayerEvent::KeyUp { key_code, key_char }
Expand Down Expand Up @@ -906,6 +911,27 @@ fn winit_key_to_char(key_code: VirtualKeyCode, is_shift_down: bool) -> Option<ch
})
}

/// Converts a `VirtualKeyCode` and `ModifiersState` to a Ruffle `TextControlCode`.
/// Returns `None` if there is no match.
fn winit_to_ruffle_text_control(
key: VirtualKeyCode,
modifiers: ModifiersState,
) -> Option<TextControlCode> {
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 {
None
}
}

fn run_timedemo(opt: Opt) -> Result<(), Error> {
let path = opt
.input_path
Expand Down
30 changes: 28 additions & 2 deletions web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use ruffle_core::backend::navigator::OpenURLMode;
use ruffle_core::compatibility_rules::CompatibilityRules;
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
use ruffle_core::context::UpdateContext;
use ruffle_core::events::{KeyCode, MouseButton, MouseWheelDelta};
use ruffle_core::events::{KeyCode, MouseButton, MouseWheelDelta, TextControlCode};
use ruffle_core::external::{
ExternalInterfaceMethod, ExternalInterfaceProvider, Value as ExternalValue, Value,
};
Expand Down Expand Up @@ -840,9 +840,12 @@ impl Ruffle {
let _ = instance.with_core_mut(|core| {
let key_code = web_to_ruffle_key_code(&js_event.code());
let key_char = web_key_to_codepoint(&js_event.key());
let is_ctrl_cmd = js_event.ctrl_key() || js_event.meta_key();
core.handle_event(PlayerEvent::KeyDown { key_code, key_char });

if let Some(codepoint) = key_char {
if let Some(control_code) = web_to_text_control(&js_event.key(), is_ctrl_cmd) {
core.handle_event(PlayerEvent::TextControl { code: control_code })
}else if let Some(codepoint) = key_char {
core.handle_event(PlayerEvent::TextInput { codepoint });
}
});
Expand Down Expand Up @@ -1740,3 +1743,26 @@ fn web_key_to_codepoint(key: &str) -> Option<char> {
}
}
}

pub fn web_to_text_control(key: &str, ctrl_key: bool) -> Option<TextControlCode> {
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 {
// TODO: Check for special characters.
None
}
}