Store all text internally as an HTML tree, alongside the existing strings.

We're reusing the XML machinery to handle HTML - this is probably not 100% correct, but writing a new HTML parser to cover just `EditText` will be rather complicated.
This commit is contained in:
David Wendt 2020-05-16 15:22:20 -04:00
parent cc854db9b3
commit a3f4509d63
2 changed files with 56 additions and 31 deletions

View File

@ -44,7 +44,7 @@ pub fn set_text<'gc>(
text_field.set_text( text_field.set_text(
value.coerce_to_string(avm, context)?.to_string(), value.coerce_to_string(avm, context)?.to_string(),
context.gc_context, context.gc_context,
) )?;
} }
} }
} }

View File

@ -9,9 +9,13 @@ use crate::library::Library;
use crate::prelude::*; use crate::prelude::*;
use crate::tag_utils::SwfMovie; use crate::tag_utils::SwfMovie;
use crate::transform::Transform; use crate::transform::Transform;
use crate::xml::{XMLDocument, XMLNode};
use gc_arena::{Collect, Gc, GcCell, MutationContext}; use gc_arena::{Collect, Gc, GcCell, MutationContext};
use std::sync::Arc; use std::sync::Arc;
/// Boxed error type.
pub type Error = Box<dyn std::error::Error>;
/// A dynamic text field. /// A dynamic text field.
/// The text in this text field can be changed dynamically. /// The text in this text field can be changed dynamically.
/// It may be selectable or editable by the user, depending on the text field properties. /// It may be selectable or editable by the user, depending on the text field properties.
@ -37,6 +41,9 @@ pub struct EditTextData<'gc> {
/// The current text displayed by this text field. /// The current text displayed by this text field.
text: String, text: String,
/// The current HTML document displayed by this `EditText`.
document: XMLDocument<'gc>,
/// The text formatting for newly inserted text spans. /// The text formatting for newly inserted text spans.
new_format: TextFormat, new_format: TextFormat,
@ -62,35 +69,28 @@ impl<'gc> EditText<'gc> {
) -> Self { ) -> Self {
let is_multiline = swf_tag.is_multiline; let is_multiline = swf_tag.is_multiline;
let is_word_wrap = swf_tag.is_word_wrap; let is_word_wrap = swf_tag.is_word_wrap;
let document = XMLDocument::new(context.gc_context);
let text = swf_tag.initial_text.clone().unwrap_or_default();
let text = if swf_tag.is_html { if swf_tag.is_html {
let mut result = String::new(); document
let tag_text = swf_tag.initial_text.clone().unwrap_or_default(); .as_node()
let mut chars = tag_text.chars().peekable(); .replace_with_str(context.gc_context, &text)
.unwrap();
while let Some(c) = chars.next() {
// TODO: SWF text fields can contain a limited subset of HTML (and often do in SWF versions >6).
// This is a quicky-and-dirty way to skip the HTML tags. This is obviously not correct
// and we will need to properly parse and handle the HTML at some point.
// See SWF19 pp. 173-174 for supported HTML tags.
if c == '<' {
// Skip characters until we see a close bracket.
chars.by_ref().find(|&x| x == '>');
} else {
result.push(c);
}
}
result
} else { } else {
swf_tag.initial_text.clone().unwrap_or_default() let tnode = XMLNode::new_text(context.gc_context, &text, document);
}; document
.as_node()
.append_child(context.gc_context, tnode)
.unwrap();
}
EditText(GcCell::allocate( EditText(GcCell::allocate(
context.gc_context, context.gc_context,
EditTextData { EditTextData {
base: Default::default(), base: Default::default(),
text, text,
document,
new_format: TextFormat::default(), new_format: TextFormat::default(),
static_data: gc_arena::Gc::allocate( static_data: gc_arena::Gc::allocate(
context.gc_context, context.gc_context,
@ -158,14 +158,37 @@ impl<'gc> EditText<'gc> {
Self::from_swf_tag(context, swf_movie, swf_tag) Self::from_swf_tag(context, swf_movie, swf_tag)
} }
// TODO: This needs to strip away HTML
pub fn text(self) -> String { pub fn text(self) -> String {
self.0.read().text.to_owned() self.0.read().text.to_owned()
} }
pub fn set_text(self, text: String, gc_context: MutationContext<'gc, '_>) { pub fn set_text(self, text: String, gc_context: MutationContext<'gc, '_>) -> Result<(), Error> {
self.0.write(gc_context).cached_break_points = None; let mut edit_text = self.0.write(gc_context);
self.0.write(gc_context).text = text; let mut child_ptr = edit_text
.document
.as_node()
.children()
.and_then(|mut ch| ch.next());
while let Some(child) = child_ptr {
edit_text
.document
.as_node()
.remove_child(gc_context, child)?;
child_ptr = child.next_sibling().unwrap();
}
let text_node = XMLNode::new_text(gc_context, &text, edit_text.document);
edit_text
.document
.as_node()
.append_child(gc_context, text_node)?;
edit_text.cached_break_points = None;
edit_text.text = text;
Ok(())
} }
pub fn new_text_format(self) -> TextFormat { pub fn new_text_format(self) -> TextFormat {
@ -303,7 +326,7 @@ impl<'gc> EditText<'gc> {
.height .height
.unwrap_or_else(|| Twips::from_pixels(font.scale().into())); .unwrap_or_else(|| Twips::from_pixels(font.scale().into()));
for natural_line in edit_text.text.split('\n') { for natural_line in self.text().split('\n') {
if break_base != 0 { if break_base != 0 {
breakpoints.push(break_base); breakpoints.push(break_base);
} }
@ -355,6 +378,7 @@ impl<'gc> EditText<'gc> {
/// The returned tuple should be interpreted as width, then height. /// The returned tuple should be interpreted as width, then height.
pub fn measure_text(self, context: &mut UpdateContext<'_, 'gc, '_>) -> (Twips, Twips) { pub fn measure_text(self, context: &mut UpdateContext<'_, 'gc, '_>) -> (Twips, Twips) {
let breakpoints = self.line_breaks_cached(context.gc_context, context.library); let breakpoints = self.line_breaks_cached(context.gc_context, context.library);
let text = self.text();
let edit_text = self.0.read(); let edit_text = self.0.read();
let static_data = &edit_text.static_data; let static_data = &edit_text.static_data;
@ -365,11 +389,11 @@ impl<'gc> EditText<'gc> {
let mut start = 0; let mut start = 0;
let mut chunks = vec![]; let mut chunks = vec![];
for breakpoint in breakpoints { for breakpoint in breakpoints {
chunks.push(&edit_text.text[start..breakpoint]); chunks.push(&text[start..breakpoint]);
start = breakpoint; start = breakpoint;
} }
chunks.push(&edit_text.text[start..]); chunks.push(&text[start..]);
let height = static_data let height = static_data
.text .text
@ -470,6 +494,7 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> {
context.transform_stack.push(&*self.transform()); context.transform_stack.push(&*self.transform());
let mut text_transform = self.text_transform(); let mut text_transform = self.text_transform();
let text = self.text();
let edit_text = self.0.read(); let edit_text = self.0.read();
let static_data = &edit_text.static_data; let static_data = &edit_text.static_data;
@ -491,11 +516,11 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> {
let mut start = 0; let mut start = 0;
let mut chunks = vec![]; let mut chunks = vec![];
for breakpoint in breakpoints { for breakpoint in breakpoints {
chunks.push(&edit_text.text[start..breakpoint]); chunks.push(&text[start..breakpoint]);
start = breakpoint; start = breakpoint;
} }
chunks.push(&edit_text.text[start..]); chunks.push(&text[start..]);
for chunk in chunks { for chunk in chunks {
font.evaluate( font.evaluate(