core: Implement restrict for EditText

EditTextRestrict models the `restrict` property, which is used
to specify which characters the user is allowed to type.
This commit is contained in:
Kamil Jarosz 2024-01-11 13:41:33 +01:00 committed by Nathan Adams
parent 5ece6981ce
commit 43d4c26b77
1 changed files with 254 additions and 42 deletions

View File

@ -34,6 +34,7 @@ use gc_arena::{Collect, Gc, GcCell, Mutation};
use ruffle_render::commands::CommandHandler;
use ruffle_render::shape_utils::DrawCommand;
use ruffle_render::transform::Transform;
use std::collections::VecDeque;
use std::{cell::Ref, cell::RefMut, sync::Arc};
use swf::{Color, ColorTransform, Twips};
@ -168,6 +169,10 @@ pub struct EditTextData<'gc> {
/// Whether this EditText represents an AVM2 TextLine.
is_tlf: bool,
/// Restrict what characters the user may input.
#[collect(require_static)]
restrict: EditTextRestrict,
}
impl<'gc> EditTextData<'gc> {
@ -334,6 +339,7 @@ impl<'gc> EditText<'gc> {
scroll: 1,
max_chars: swf_tag.max_length().unwrap_or_default() as i32,
is_tlf: false,
restrict: EditTextRestrict::allow_all(),
},
));
@ -502,6 +508,14 @@ impl<'gc> EditText<'gc> {
self.relayout(context);
}
pub fn restrict(self) -> Option<WString> {
return self.0.read().restrict.value().map(Into::into);
}
pub fn set_restrict(self, text: Option<&WStr>, context: &mut UpdateContext<'_, 'gc>) {
self.0.write(context.gc_context).restrict = EditTextRestrict::from(text);
}
pub fn set_multiline(self, is_multiline: bool, context: &mut UpdateContext<'_, 'gc>) {
self.0
.write(context.gc_context)
@ -1487,57 +1501,58 @@ impl<'gc> EditText<'gc> {
}
pub fn text_input(self, character: char, context: &mut UpdateContext<'_, 'gc>) {
if self.0.read().flags.contains(EditTextFlag::READ_ONLY) {
if self.0.read().flags.contains(EditTextFlag::READ_ONLY)
|| character.is_control()
|| self.available_chars() == 0
{
return;
}
if let Some(selection) = self.selection() {
let mut changed = false;
let mut cancelled = false;
if !character.is_control() && self.available_chars() > 0 {
if let Avm2Value::Object(target) = self.object2() {
let character_string =
AvmString::new_utf8(context.gc_context, character.to_string());
let Some(selection) = self.selection() else {
return;
};
let mut activation = Avm2Activation::from_nothing(context.reborrow());
let text_evt = Avm2EventObject::text_event(
&mut activation,
"textInput",
character_string,
true,
true,
);
Avm2::dispatch_event(&mut activation.context, text_evt, target);
let Some(character) = self.0.read().restrict.to_allowed(character) else {
return;
};
cancelled = text_evt.as_event().unwrap().is_cancelled();
}
if let Avm2Value::Object(target) = self.object2() {
let character_string = AvmString::new_utf8(context.gc_context, character.to_string());
if !cancelled {
self.replace_text(
selection.start(),
selection.end(),
&WString::from_char(character),
context,
);
let new_pos = selection.start() + character.len_utf8();
self.set_selection(
Some(TextSelection::for_position(new_pos)),
context.gc_context,
);
changed = true;
}
}
let mut activation = Avm2Activation::from_nothing(context.reborrow());
let text_evt = Avm2EventObject::text_event(
&mut activation,
"textInput",
character_string,
true,
true,
);
Avm2::dispatch_event(&mut activation.context, text_evt, target);
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);
if text_evt.as_event().unwrap().is_cancelled() {
return;
}
}
self.replace_text(
selection.start(),
selection.end(),
&WString::from_char(character),
context,
);
let new_pos = selection.start() + character.len_utf8();
self.set_selection(
Some(TextSelection::for_position(new_pos)),
context.gc_context,
);
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);
}
fn initialize_as_broadcaster(&self, activation: &mut Avm1Activation<'_, 'gc>) {
@ -2321,3 +2336,200 @@ impl TextSelection {
self.to == self.from
}
}
#[derive(Clone, Debug)]
struct EditTextRestrict {
/// Original string value.
value: Option<WString>,
/// List of intervals (inclusive, inclusive) with allowed characters.
allowed: Vec<(char, char)>,
/// List of intervals (inclusive, inclusive) with disallowed characters.
disallowed: Vec<(char, char)>,
}
enum EditTextRestrictToken {
Char(char),
Range,
Caret,
}
impl EditTextRestrict {
const INTERVAL_ALL: (char, char) = ('\0', char::MAX);
pub fn allow_all() -> Self {
Self {
value: None,
allowed: vec![Self::INTERVAL_ALL],
disallowed: vec![],
}
}
pub fn allow_none() -> Self {
Self {
value: Some(WString::new()),
allowed: vec![],
disallowed: vec![],
}
}
pub fn from(value: Option<&WStr>) -> Self {
match value {
None => Self::allow_all(),
Some(string) => Self::from_string(string),
}
}
pub fn from_string(string: &WStr) -> Self {
if string.is_empty() {
return Self::allow_none();
}
let mut tokens = Self::tokenize_restrict(string);
let mut allowed: Vec<(char, char)> = vec![];
let mut disallowed: Vec<(char, char)> = vec![];
Self::parse_restrict(&mut tokens, &mut allowed, &mut disallowed);
Self {
value: Some(string.into()),
allowed,
disallowed,
}
}
fn tokenize_restrict(string: &WStr) -> VecDeque<EditTextRestrictToken> {
let mut characters: VecDeque<char> = string
.chars()
.map(|c| c.unwrap_or(char::REPLACEMENT_CHARACTER))
.collect::<VecDeque<char>>();
let mut tokens: VecDeque<EditTextRestrictToken> = VecDeque::with_capacity(characters.len());
while !characters.is_empty() {
match characters.pop_front().unwrap() {
// Handle escapes: \\, \-, \^.
// In fact, other escapes also work, so that \a is equivalent to a, not to \\a.
'\\' => {
if let Some(escaped) = characters.pop_front() {
tokens.push_back(EditTextRestrictToken::Char(escaped));
} else {
// Ignore truncated escapes (when the string ends with \).
}
}
'^' => {
tokens.push_back(EditTextRestrictToken::Caret);
}
'-' => {
tokens.push_back(EditTextRestrictToken::Range);
}
c => {
tokens.push_back(EditTextRestrictToken::Char(c));
}
}
}
tokens
}
fn parse_restrict(
tokens: &mut VecDeque<EditTextRestrictToken>,
allowed: &mut Vec<(char, char)>,
disallowed: &mut Vec<(char, char)>,
) {
let mut current_intervals: Vec<(char, char)> = vec![];
let mut last_char: Option<char> = None;
let mut now_allowing = true;
while !tokens.is_empty() {
last_char = match tokens.pop_front().unwrap() {
EditTextRestrictToken::Char(c) => {
current_intervals.push((c, c));
Some(c)
}
EditTextRestrictToken::Caret => {
if now_allowing {
if current_intervals.is_empty() && allowed.is_empty() {
// If restrict starts with ^, we are assuming that
// all characters are allowed and disallowing from that.
allowed.append(&mut vec![Self::INTERVAL_ALL]);
} else {
allowed.append(&mut current_intervals);
}
} else {
disallowed.append(&mut current_intervals);
}
// Caret according to the documentation indicates
// that we are now disallowing characters.
// In reality it just switches allowing/disallowing.
now_allowing = !now_allowing;
None
}
EditTextRestrictToken::Range => {
let range_start = if let Some(last_char) = last_char {
current_intervals.pop();
last_char
} else {
// When the range is truncated from the left side (-z),
// it is equivalent to \0-z.
'\0'
};
let range_end;
if let Some(EditTextRestrictToken::Char(c)) = tokens.front() {
range_end = *c;
tokens.pop_front();
} else {
// When the range is truncated from the right side (a-),
// it is equivalent to the first character (a).
range_end = range_start;
}
// If the range a-z is inverted (z-a), it is equivalent to
// the first character only (z).
current_intervals.push((range_start, range_end.max(range_start)));
None
}
}
}
if now_allowing {
allowed.append(&mut current_intervals);
} else {
disallowed.append(&mut current_intervals);
}
}
pub fn value(&self) -> Option<&WStr> {
self.value.as_deref()
}
pub fn is_allowed(&self, character: char) -> bool {
self.intervals_contain(character, &self.allowed)
&& !self.intervals_contain(character, &self.disallowed)
}
fn intervals_contain(&self, character: char, intervals: &Vec<(char, char)>) -> bool {
for interval in intervals {
if self.interval_contains(character, interval) {
return true;
}
}
false
}
#[inline]
fn interval_contains(&self, character: char, interval: &(char, char)) -> bool {
character >= interval.0 && character <= interval.1
}
pub fn to_allowed(&self, character: char) -> Option<char> {
if self.is_allowed(character) {
Some(character)
} else if self.is_allowed(character.to_ascii_uppercase()) {
Some(character.to_ascii_uppercase())
} else if self.is_allowed(character.to_ascii_lowercase()) {
Some(character.to_ascii_lowercase())
} else {
None
}
}
}