-
-
Notifications
You must be signed in to change notification settings - Fork 883
core: Implement handling of text control input #11059
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
Changes from all commits
7796588
3f5b2b5
fa41d3a
ec0d9b3
a36aad0
49cf622
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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::*; | ||||||||||||||||||||
|
@@ -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( | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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())), | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Nit: I am not certain about the value being |
||||||||||||||||||||
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( | ||||||||||||||||||||
|
@@ -1191,7 +1319,7 @@ impl<'gc> EditText<'gc> { | |||||||||||||||||||
); | ||||||||||||||||||||
changed = true; | ||||||||||||||||||||
} | ||||||||||||||||||||
8 => { | ||||||||||||||||||||
TextControlCode::Backspace => { | ||||||||||||||||||||
// Backspace with caret | ||||||||||||||||||||
if selection.start() > 0 { | ||||||||||||||||||||
// Delete previous character | ||||||||||||||||||||
|
@@ -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 | ||||||||||||||||||||
|
@@ -1216,27 +1344,39 @@ impl<'gc> EditText<'gc> { | |||||||||||||||||||
changed = true; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
_ => {} | ||||||||||||||||||||
} | ||||||||||||||||||||
if changed { | ||||||||||||||||||||
let mut activation = Avm1Activation::from_nothing( | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ruffle/core/src/display_object/edit_text.rs Lines 1248 to 1256 in 1065662
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>) { | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||||||||||||||||
|
@@ -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( | ||||||||||||||||||||
|
@@ -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) | ||||||||||||||||||||
} | ||||||||||||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.