diff --git a/core/src/avm2/globals/flash/text/TextField.as b/core/src/avm2/globals/flash/text/TextField.as index b0326ccee604..07e4fdf63ca0 100644 --- a/core/src/avm2/globals/flash/text/TextField.as +++ b/core/src/avm2/globals/flash/text/TextField.as @@ -73,13 +73,8 @@ package flash.text { public native function get selectable():Boolean; public native function set selectable(value:Boolean):void; - public function get styleSheet():StyleSheet { - return this._styleSheet; - } - public function set styleSheet(value:StyleSheet):void { - this._styleSheet = value; - stub_setter("flash.text.TextField", "styleSheet"); - } + public native function get styleSheet():StyleSheet; + public native function set styleSheet(value:StyleSheet):void; public native function get text():String; public native function set text(value:String):void; diff --git a/core/src/avm2/globals/flash/text/text_field.rs b/core/src/avm2/globals/flash/text/text_field.rs index 33e392f3220b..ff3256e7b9f4 100644 --- a/core/src/avm2/globals/flash/text/text_field.rs +++ b/core/src/avm2/globals/flash/text/text_field.rs @@ -1804,3 +1804,46 @@ pub fn get_char_boundaries<'gc>( .into(); Ok(rect) } + +pub fn get_style_sheet<'gc>( + _activation: &mut Activation<'_, 'gc>, + this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.as_object().unwrap(); + + let Some(this) = this + .as_display_object() + .and_then(|this| this.as_edit_text()) + else { + return Ok(Value::Undefined); + }; + + Ok(match this.style_sheet() { + Some(style_sheet) => Value::Object(Object::StyleSheetObject(style_sheet)), + None => Value::Null, + }) +} + +pub fn set_style_sheet<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.as_object().unwrap(); + + let Some(this) = this + .as_display_object() + .and_then(|this| this.as_edit_text()) + else { + return Ok(Value::Undefined); + }; + + let style_sheet = args + .try_get_object(activation, 0) + .and_then(|o| o.as_style_sheet()); + + this.set_style_sheet(activation.context, style_sheet); + + Ok(Value::Undefined) +} diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index f02123e0cac9..0e140941b478 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -7,6 +7,7 @@ use crate::avm1::{ Object as Avm1Object, StageObject as Avm1StageObject, TObject as Avm1TObject, Value as Avm1Value, }; +use crate::avm2::object::StyleSheetObject; use crate::avm2::Avm2; use crate::avm2::{ Activation as Avm2Activation, ClassObject as Avm2ClassObject, EventObject as Avm2EventObject, @@ -184,6 +185,18 @@ pub struct EditTextData<'gc> { /// Information related to the last click event inside this text field. #[collect(require_static)] last_click: Option, + + /// Style sheet used when parsing HTML. + /// + /// TODO Add support for AVM1. + style_sheet: Option>, + + /// Original HTML text before parsing. + /// + /// It is used only when a style sheet is available + /// in order to preserve styles. + #[collect(require_static)] + original_html_text: Option, } impl EditTextData<'_> { @@ -210,6 +223,23 @@ impl EditTextData<'_> { FontType::Embedded } } + + fn parse_html(&mut self, text: &WStr) { + let default_format = self.text_spans.default_format().clone(); + self.text_spans = FormatSpans::from_html( + text, + default_format, + self.style_sheet, + self.flags.contains(EditTextFlag::MULTILINE), + self.flags.contains(EditTextFlag::CONDENSE_WHITE), + self.static_data.swf.version(), + ); + self.original_html_text = if self.style_sheet.is_some() { + Some(text.to_owned()) + } else { + None + }; + } } impl<'gc> EditText<'gc> { @@ -237,6 +267,7 @@ impl<'gc> EditText<'gc> { FormatSpans::from_html( &text, default_format, + None, swf_tag.is_multiline(), false, swf_movie.version(), @@ -337,6 +368,8 @@ impl<'gc> EditText<'gc> { restrict: EditTextRestrict::allow_all(), last_click: None, layout_debug_boxes_flags: LayoutDebugBoxesFlag::empty(), + style_sheet: None, + original_html_text: None, }, )); @@ -410,8 +443,13 @@ impl<'gc> EditText<'gc> { } let mut edit_text = self.0.write(context.gc()); - let default_format = edit_text.text_spans.default_format().clone(); - edit_text.text_spans = FormatSpans::from_text(text.into(), default_format); + if edit_text.style_sheet.is_some() { + // When CSS is set, text will always be treated as HTML. + edit_text.parse_html(text); + } else { + let default_format = edit_text.text_spans.default_format().clone(); + edit_text.text_spans = FormatSpans::from_text(text.into(), default_format); + } drop(edit_text); self.relayout(context); @@ -419,7 +457,13 @@ impl<'gc> EditText<'gc> { pub fn html_text(self) -> WString { if self.is_html() { - self.0.read().text_spans.to_html() + let text = self.0.read(); + + if let Some(ref html) = text.original_html_text { + return html.clone(); + } + + text.text_spans.to_html() } else { // Non-HTML text fields always return plain text. self.text() @@ -438,17 +482,7 @@ impl<'gc> EditText<'gc> { } if self.is_html() { - let mut write = self.0.write(context.gc()); - let default_format = write.text_spans.default_format().clone(); - write.text_spans = FormatSpans::from_html( - text, - default_format, - write.flags.contains(EditTextFlag::MULTILINE), - write.flags.contains(EditTextFlag::CONDENSE_WHITE), - write.static_data.swf.version(), - ); - drop(write); - + self.0.write(context.gc()).parse_html(text); self.relayout(context); } else { self.set_text(text, context); @@ -668,6 +702,29 @@ impl<'gc> EditText<'gc> { .set(EditTextFlag::HTML, is_html); } + pub fn style_sheet(self) -> Option> { + self.0.read().style_sheet + } + + pub fn set_style_sheet( + self, + context: &mut UpdateContext<'gc>, + style_sheet: Option>, + ) { + self.set_is_html(context, true); + + let mut text = self.0.write(context.gc()); + text.style_sheet = style_sheet; + + if text.style_sheet.is_none() { + text.original_html_text = None; + } + + if let Some(html) = text.original_html_text.clone() { + text.parse_html(&html); + } + } + pub fn is_fte(self) -> bool { self.0.read().is_fte } diff --git a/core/src/html/text_format.rs b/core/src/html/text_format.rs index 320fec46b322..18856c3ada52 100644 --- a/core/src/html/text_format.rs +++ b/core/src/html/text_format.rs @@ -1,5 +1,6 @@ //! Classes that store formatting options +use crate::avm2::object::StyleSheetObject; use crate::context::UpdateContext; use crate::html::iterators::TextSpanIter; use crate::string::{Integer, SwfStrExt as _, Units, WStr, WString}; @@ -624,6 +625,7 @@ impl FormatSpans { pub fn from_html( html: &WStr, default_format: TextFormat, + style_sheet: Option>, is_multiline: bool, condense_white: bool, swf_version: u8, @@ -676,6 +678,28 @@ impl FormatSpans { reader_config.check_end_names = false; reader_config.allow_unmatched_ends = true; + fn class_name_to_selector(class: &WStr) -> WString { + let mut selector = WString::from_utf8("."); + selector.push_str(&class.to_ascii_lowercase()); + selector + } + + fn apply_style( + style_sheet: Option>, + format: TextFormat, + selector: &WStr, + ) -> TextFormat { + let Some(style_sheet) = style_sheet else { + return format; + }; + + if let Some(style) = style_sheet.get_style(selector) { + style.clone().mix_with(format) + } else { + format + } + } + loop { match reader.read_event() { Ok(Event::Start(ref e)) => { @@ -717,8 +741,16 @@ impl FormatSpans { // Skip push to `format_stack`. continue; } - b"p" => { + tag @ b"p" => { p_open = true; + + format = apply_style(style_sheet, format, WStr::from_units(tag)); + + if let Some(class) = attribute(b"class") { + let selector = &class_name_to_selector(&class); + format = apply_style(style_sheet, format, selector); + } + if let Some(align) = attribute(b"align") { let align = align.to_ascii_lowercase(); if align == WStr::from_units(b"left") { @@ -732,7 +764,7 @@ impl FormatSpans { } } } - b"a" => { + tag @ b"a" => { if let Some(href) = attribute(b"href") { format.url = Some(href); } @@ -740,6 +772,14 @@ impl FormatSpans { if let Some(target) = attribute(b"target") { format.target = Some(target); } + + // TODO Docs claim that a:link, a:hover, a:active, should work. + format = apply_style(style_sheet, format, WStr::from_units(tag)); + + if let Some(class) = attribute(b"class") { + let selector = &class_name_to_selector(&class); + format = apply_style(style_sheet, format, selector); + } } b"font" => { if let Some(face) = attribute(b"face") { @@ -824,7 +864,9 @@ impl FormatSpans { b"u" => { format.underline = Some(true); } - b"li" => { + tag @ b"li" => { + format = apply_style(style_sheet, format, WStr::from_units(tag)); + let is_last_nl = text.iter().last() == Some(HTML_NEWLINE); if is_multiline && !is_last_nl && text.len() > 0 { // If the last paragraph was not closed and @@ -870,7 +912,16 @@ impl FormatSpans { ); } } - _ => {} + b"span" => { + if let Some(class) = attribute(b"class") { + let selector = &class_name_to_selector(&class); + format = apply_style(style_sheet, format, selector); + } + } + tag => { + // TODO Add 'display' style support, Flash adds a newline on 'display: block' + format = apply_style(style_sheet, format, WStr::from_units(tag)); + } } opened_starts.push(opened_buffer.len()); opened_buffer.extend(tag_name);