Skip to content

Commit

Permalink
core: Add style sheet support for TextField
Browse files Browse the repository at this point in the history
This patch adds TextField.styleSheet support.  Now when parsing HTML,
the attached style sheet is used to provide styles for HTML elements.

This patch also adds support for:
* `class` attributes in HTML,
* `<span>` HTML elements,
* custom HTML elements.

CSS support is spanned across all existing HTML elements and custom ones.

Additionally, some observable behaviors related to .styleSheet are
implemented, e.g. performing relayout after setting .styleSheet,
or .text behaving as .htmlText with style sheet set.

Currently, the implementation is coupled with AVM2 objects, that
should change when AVM1 support is added.
  • Loading branch information
kjarosh authored and torokati44 committed Jan 11, 2025
1 parent f861125 commit 2ccc755
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 25 deletions.
9 changes: 2 additions & 7 deletions core/src/avm2/globals/flash/text/TextField.as
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions core/src/avm2/globals/flash/text/text_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value<'gc>, 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<Value<'gc>, 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)
}
85 changes: 71 additions & 14 deletions core/src/display_object/edit_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<ClickEventData>,

/// Style sheet used when parsing HTML.
///
/// TODO Add support for AVM1.
style_sheet: Option<StyleSheetObject<'gc>>,

/// 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<WString>,
}

impl EditTextData<'_> {
Expand All @@ -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> {
Expand Down Expand Up @@ -237,6 +267,7 @@ impl<'gc> EditText<'gc> {
FormatSpans::from_html(
&text,
default_format,
None,
swf_tag.is_multiline(),
false,
swf_movie.version(),
Expand Down Expand Up @@ -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,
},
));

Expand Down Expand Up @@ -410,16 +443,27 @@ 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);
}

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()
Expand All @@ -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);
Expand Down Expand Up @@ -668,6 +702,29 @@ impl<'gc> EditText<'gc> {
.set(EditTextFlag::HTML, is_html);
}

pub fn style_sheet(self) -> Option<StyleSheetObject<'gc>> {
self.0.read().style_sheet
}

pub fn set_style_sheet(
self,
context: &mut UpdateContext<'gc>,
style_sheet: Option<StyleSheetObject<'gc>>,
) {
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
}
Expand Down
59 changes: 55 additions & 4 deletions core/src/html/text_format.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -624,6 +625,7 @@ impl FormatSpans {
pub fn from_html(
html: &WStr,
default_format: TextFormat,
style_sheet: Option<StyleSheetObject<'_>>,
is_multiline: bool,
condense_white: bool,
swf_version: u8,
Expand Down Expand Up @@ -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<StyleSheetObject<'_>>,
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)) => {
Expand Down Expand Up @@ -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") {
Expand All @@ -732,14 +764,22 @@ impl FormatSpans {
}
}
}
b"a" => {
tag @ b"a" => {
if let Some(href) = attribute(b"href") {
format.url = Some(href);
}

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") {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 2ccc755

Please sign in to comment.