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 all commits
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
6 changes: 6 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
[target.'cfg(all())']
# NOTE that the web build overrides this setting in package.json via the RUSTFLAGS environment variable
rustflags = [
# We need to specify this flag for all targets because Clippy checks all of our code against all targets
# and our web code does not compile without this flag
"--cfg=web_sys_unstable_apis",

# CLIPPY LINT SETTINGS
# This is a workaround to configure lints for the entire workspace, pending the ability to configure this via TOML.
# See: https://github.com/rust-lang/cargo/issues/5034
# https://github.com/EmbarkStudios/rust-ecosystem/issues/22#issuecomment-947011395
# TODO: Move these to the root Cargo.toml once support is merged and stable
# See: https://github.com/rust-lang/cargo/pull/12148

# Clippy nightly often adds new/buggy lints that we want to ignore.
# Don't warn about these new lints on stable.
Expand Down
19 changes: 18 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 All @@ -15,6 +15,9 @@ pub trait UiBackend {
/// Changes the mouse cursor image.
fn set_mouse_cursor(&mut self, cursor: MouseCursor);

/// Get the clipboard content
fn clipboard_content(&mut self) -> String;
relrelb marked this conversation as resolved.
Show resolved Hide resolved

/// Sets the clipboard to the given content.
fn set_clipboard_content(&mut self, content: String);

Expand Down Expand Up @@ -63,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 @@ -71,6 +75,7 @@ impl InputManager {
keys_down: HashSet::new(),
last_key: KeyCode::Unknown,
last_char: None,
last_text_control: None,
}
}

Expand All @@ -97,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 @@ -116,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 Expand Up @@ -145,6 +158,10 @@ impl UiBackend for NullUiBackend {

fn set_mouse_cursor(&mut self, _cursor: MouseCursor) {}

fn clipboard_content(&mut self) -> String {
"".into()
}

fn set_clipboard_content(&mut self, _content: String) {}

fn set_fullscreen(&mut self, _is_full: bool) -> Result<(), FullscreenError> {
Expand Down
237 changes: 161 additions & 76 deletions 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::{ClipEvent, ClipEventResult, 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,15 +1174,143 @@ impl<'gc> EditText<'gc> {
None
}

pub fn text_input(self, character: char, context: &mut UpdateContext<'_, 'gc>) {
if self.0.read().flags.contains(EditTextFlag::READ_ONLY) {
/// The number of characters that currently can be inserted, considering `TextField.maxChars`
/// constraint, current text length, and current text selection length.
fn available_chars(self) -> usize {
n0samu marked this conversation as resolved.
Show resolved Hide resolved
let read = self.0.read();
let max_chars = read.max_chars;
if max_chars == 0 {
usize::MAX
} else {
let text_len = read.text_spans.text().len() as i32;
let selection_len = if let Some(selection) = self.selection() {
(selection.end() - selection.start()) as i32
} else {
0
};
0.max(max_chars.max(0) - (text_len - selection_len)) as usize
}
}

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;
match character as u8 {
8 | 127 if !selection.is_caret() => {
let is_selectable = self.is_selectable();
match control_code {
TextControlCode::MoveLeft => {
let new_pos = if selection.is_caret() && selection.to > 0 {
n0samu marked this conversation as resolved.
Show resolved Hide resolved
string_utils::prev_char_boundary(&self.text(), selection.to)
} else {
selection.start()
};
self.set_selection(
Some(TextSelection::for_position(new_pos)),
context.gc_context,
);
}
TextControlCode::MoveRight => {
let new_pos = if selection.is_caret() && selection.to < self.text().len() {
string_utils::next_char_boundary(&self.text(), selection.to)
} else {
selection.end()
};
self.set_selection(
Some(TextSelection::for_position(new_pos)),
context.gc_context,
);
}
TextControlCode::SelectLeft => {
if is_selectable && selection.to > 0 {
let new_pos = string_utils::prev_char_boundary(&self.text(), selection.to);
self.set_selection(
Some(TextSelection::for_range(selection.from, new_pos)),
context.gc_context,
);
}
}
TextControlCode::SelectRight => {
if is_selectable && selection.to < self.text().len() {
let new_pos = string_utils::next_char_boundary(&self.text(), selection.to);
self.set_selection(
Some(TextSelection::for_range(selection.from, new_pos)),
context.gc_context,
)
}
}
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();
// TODO: To match Flash Player, we should truncate pasted text that is longer than max_chars
// instead of canceling the paste action entirely
if text.len() <= self.available_chars() {
self.replace_text(
selection.start(),
selection.end(),
&WString::from_utf8(text),
context,
);
let new_pos = selection.start() + text.len();
if is_selectable {
self.set_selection(
Some(TextSelection::for_position(new_pos)),
context.gc_context,
);
} else {
self.set_selection(
Some(TextSelection::for_position(self.text().len())),
Copy link
Contributor

Choose a reason for hiding this comment

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

(Nit: I am not certain about the value being self.text().len(), but I also do not know what else it should be, and it might be a decent or correct value).

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;
}
}
TextControlCode::Backspace | TextControlCode::Delete if !selection.is_caret() => {
// Backspace or delete with multiple characters selected
self.replace_text(selection.start(), selection.end(), WStr::empty(), context);
self.set_selection(
Expand All @@ -1191,7 +1319,7 @@ impl<'gc> EditText<'gc> {
);
changed = true;
}
8 => {
TextControlCode::Backspace => {
// Backspace with caret
if selection.start() > 0 {
// Delete previous character
Expand All @@ -1205,7 +1333,7 @@ impl<'gc> EditText<'gc> {
changed = true;
}
}
127 => {
TextControlCode::Delete => {
// Delete with caret
if selection.end() < self.text_length() {
// Delete next character
Expand All @@ -1216,27 +1344,39 @@ impl<'gc> EditText<'gc> {
changed = true;
}
}
_ => {}
}
if changed {
let mut activation = Avm1Activation::from_nothing(
Copy link
Contributor

Choose a reason for hiding this comment

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

(Nit: I do not know or understand this part of the codebase, but, would this also work for AVM2?).

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm just doing exactly the same thing that's already done in the existing text_input function:

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);
}

I don't know too much about this but I think if it's fine there it's fine here. And well, implementing whatever AVM2 feature/event may rely on this could be done in a separate PR, if that's indeed something that's missing (I have no idea)

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>) {
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?).

if self.0.read().flags.contains(EditTextFlag::READ_ONLY) {
return;
}

if let Some(selection) = self.selection() {
let mut changed = false;
match character as u8 {
code if !(code as char).is_control() => {
let can_insert = {
let read = self.0.read();
let max_chars = read.max_chars;
if max_chars == 0 {
true
} else {
let text_len = read.text_spans.text().len();
text_len < max_chars.max(0) as usize
}
};
if can_insert {
if self.available_chars() > 0 {
self.replace_text(
selection.start(),
selection.end(),
&WString::from_char(character),
context,
);
let new_start = selection.start() + character.len_utf8();
let new_pos = selection.start() + character.len_utf8();
self.set_selection(
Some(TextSelection::for_position(new_start)),
Some(TextSelection::for_position(new_pos)),
context.gc_context,
);
changed = true;
Expand All @@ -1257,61 +1397,6 @@ impl<'gc> EditText<'gc> {
}
}

/// Listens for keyboard text control commands.
///
/// TODO: Add explicit text control events (#4452).
pub fn handle_text_control_event(
self,
context: &mut UpdateContext<'_, 'gc>,
event: ClipEvent,
) -> ClipEventResult {
if let ClipEvent::KeyPress { key_code } = event {
let mut edit_text = self.0.write(context.gc_context);
let selection = edit_text.selection;
if let Some(mut selection) = selection {
let text = edit_text.text_spans.text();
let length = text.len();
match key_code {
ButtonKeyCode::Left => {
if (context.input.is_key_down(KeyCode::Shift) || selection.is_caret())
&& selection.to > 0
{
selection.to = string_utils::prev_char_boundary(text, selection.to);
if !context.input.is_key_down(KeyCode::Shift) {
selection.from = selection.to;
}
} else if !context.input.is_key_down(KeyCode::Shift) {
selection.to = selection.start();
selection.from = selection.to;
}
selection.clamp(length);
edit_text.selection = Some(selection);
return ClipEventResult::Handled;
}
ButtonKeyCode::Right => {
if (context.input.is_key_down(KeyCode::Shift) || selection.is_caret())
&& selection.to < length
{
selection.to = string_utils::next_char_boundary(text, selection.to);
if !context.input.is_key_down(KeyCode::Shift) {
selection.from = selection.to;
}
} else if !context.input.is_key_down(KeyCode::Shift) {
selection.to = selection.end();
selection.from = selection.to;
}
selection.clamp(length);
edit_text.selection = Some(selection);
return ClipEventResult::Handled;
}
_ => (),
}
}
}

ClipEventResult::NotHandled
}

fn initialize_as_broadcaster(&self, activation: &mut Avm1Activation<'_, 'gc>) {
if let Avm1Value::Object(object) = self.object() {
activation.context.avm1.broadcaster_functions().initialize(
Expand Down Expand Up @@ -1955,7 +2040,7 @@ impl TextSelection {
self.from.min(self.to)
}

/// The "end" part of the range is the smallest (closest to 0) part of this selection range.
/// The "end" part of the range is the largest (farthest from 0) part of this selection range.
pub fn end(&self) -> usize {
self.from.max(self.to)
}
Expand Down
Loading