avm2: Implement StyleSheet.transform()

This commit is contained in:
Nathan Adams 2024-05-13 01:25:36 +02:00
parent 7e1bc84bdf
commit 24b01b8f89
11 changed files with 793 additions and 15 deletions

View File

@ -1,6 +1,4 @@
package flash.text {
import __ruffle__.stub_method;
public dynamic class StyleSheet {
// Shallow copies of the original style objects. Not used by Ruffle itself, just for getStyle()
private var _styles: Object = {};
@ -40,8 +38,82 @@ package flash.text {
}
public function transform(formatObject:Object):TextFormat {
stub_method("flash.text.StyleSheet", "transform");
return null;
if (!formatObject) {
return null;
}
var result = new TextFormat();
if (formatObject.color) {
result.color = innerParseColor(formatObject.color);
}
if (formatObject.display) {
result.display = formatObject.display;
}
if (formatObject.fontFamily) {
result.font = innerParseFontFamily(formatObject.fontFamily);
}
if (formatObject.fontSize) {
var size = parseInt(formatObject.fontSize);
if (size > 0) {
result.size = size;
}
}
if (formatObject.fontStyle == "italic") {
result.italic = true;
} else if (formatObject.fontStyle == "normal") {
result.italic = false;
}
if (formatObject.fontWeight == "bold") {
result.bold = true;
} else if (formatObject.fontWeight == "normal") {
result.bold = false;
}
if (formatObject.kerning == "true") {
result.kerning = true;
} else if (formatObject.kerning == "false") {
result.kerning = false;
} else {
// Seems to always set, not just if defined
result.kerning = parseInt(formatObject.kerning);
}
if (formatObject.leading) {
result.leading = parseInt(formatObject.leading);
}
if (formatObject.letterSpacing) {
result.letterSpacing = parseFloat(formatObject.letterSpacing);
}
if (formatObject.marginLeft) {
result.leftMargin = parseFloat(formatObject.marginLeft);
}
if (formatObject.marginRight) {
result.rightMargin = parseFloat(formatObject.marginRight);
}
if (formatObject.textAlign) {
result.align = formatObject.textAlign;
}
if (formatObject.textDecoration == "underline") {
result.underline = true;
} else if (formatObject.textDecoration == "none") {
result.underline = false;
}
if (formatObject.textIndent) {
result.indent = parseInt(formatObject.textIndent);
}
return result;
}
private function _createShallowCopy(original: *): Object {
@ -54,5 +126,7 @@ package flash.text {
// Avoid doing potentially expensive string parsing in AS :D
private native function innerParseCss(css: String): Object;
private native function innerParseColor(color: String): Number;
private native function innerParseFontFamily(fontFamily: String): String;
}
}

View File

@ -2,6 +2,7 @@ use crate::avm2::parameters::ParametersExt;
use crate::avm2::{Activation, Error, Object, TObject, Value};
use crate::html::CssStream;
use crate::string::AvmString;
use ruffle_wstr::{WStr, WString};
pub fn inner_parse_css<'gc>(
activation: &mut Activation<'_, 'gc>,
@ -39,3 +40,69 @@ pub fn inner_parse_css<'gc>(
Ok(Value::Object(result))
}
pub fn inner_parse_color<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let input = args.get_string(activation, 0)?;
if let Some(stripped) = input.strip_prefix(WStr::from_units(b"#")) {
if stripped.len() <= 6 {
if let Ok(number) = u32::from_str_radix(&stripped.to_string(), 16) {
return Ok(number.into());
}
}
}
Ok(0.into())
}
pub fn inner_parse_font_family<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let input = args.get_string(activation, 0)?;
let mut result = WString::new();
let mut pos = 0;
while pos < input.len() {
// Skip whitespace
while input.get(pos) == Some(' ' as u16) {
pos += 1;
}
// Find the whole value
let start = pos;
while input.get(pos) != Some(',' as u16) && pos < input.len() {
pos += 1;
}
let mut value = &input[start..pos];
if pos < input.len() {
pos += 1; // move past the comma
}
// Transform some names
if value == WStr::from_units(b"mono") {
value = WStr::from_units(b"_typewriter");
} else if value == WStr::from_units(b"sans-serif") {
value = WStr::from_units(b"_sans");
} else if value == WStr::from_units(b"serif") {
value = WStr::from_units(b"_serif");
}
// Add it to the result (without any extra space)
if !value.is_empty() {
if !result.is_empty() {
result.push_char(',');
}
result.push_str(value);
}
}
Ok(Value::String(AvmString::new(activation.gc(), result)))
}

View File

@ -4,8 +4,8 @@ use crate::avm2::object::{ArrayObject, Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::ecma_conversions::round_to_even;
use crate::html::TextDisplay;
use crate::string::{AvmString, WStr};
use crate::{avm2_stub_getter, avm2_stub_setter};
pub use crate::avm2::object::textformat_allocator as text_format_allocator;
@ -185,20 +185,51 @@ pub fn set_color<'gc>(
}
pub fn get_display<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
_activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm2_stub_getter!(activation, "flash.text.TextFormat", "display");
Ok("block".into())
if let Some(text_format) = this.as_text_format() {
return Ok(text_format
.display
.as_ref()
.map_or(Value::Null, |display| match display {
TextDisplay::Block => "block".into(),
TextDisplay::Inline => "inline".into(),
TextDisplay::None => "none".into(),
}));
}
Ok(Value::Undefined)
}
pub fn set_display<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
_args: &[Value<'gc>],
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm2_stub_setter!(activation, "flash.text.TextFormat", "display");
if let Some(mut text_format) = this.as_text_format_mut() {
let value = args.get(0).unwrap_or(&Value::Undefined);
let value = match value {
Value::Undefined | Value::Null => {
text_format.display = None;
return Ok(Value::Undefined);
}
value => value.coerce_to_string(activation)?,
};
text_format.display = if value == WStr::from_units(b"block") {
Some(TextDisplay::Block)
} else if value == WStr::from_units(b"inline") {
Some(TextDisplay::Inline)
} else if value == WStr::from_units(b"none") {
Some(TextDisplay::None)
} else {
// No error message for this, silently set it to None/null
None
};
}
Ok(Value::Undefined)
}

View File

@ -5,7 +5,7 @@ use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::html::TextFormat;
use crate::html::{TextDisplay, TextFormat};
use core::fmt;
use gc_arena::barrier::unlock;
use gc_arena::lock::RefLock;
@ -21,7 +21,10 @@ pub fn textformat_allocator<'gc>(
activation.gc(),
TextFormatObjectData {
base: RefLock::new(ScriptObjectData::new(class)),
text_format: Default::default(),
text_format: RefCell::new(TextFormat {
display: Some(TextDisplay::Block),
..Default::default()
}),
},
))
.into())

View File

@ -9,7 +9,7 @@ pub use dimensions::BoxBounds;
pub use dimensions::Position;
pub use layout::{LayoutBox, LayoutContent, LayoutMetrics};
pub use stylesheet::CssStream;
pub use text_format::{FormatSpans, TextFormat, TextSpan};
pub use text_format::{FormatSpans, TextDisplay, TextFormat, TextSpan};
mod stylesheet;
#[cfg(test)]

View File

@ -101,6 +101,14 @@ fn process_html_entity(src: &WStr) -> Option<WString> {
Some(result_str)
}
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub enum TextDisplay {
#[default]
Block,
Inline,
None,
}
/// A set of text formatting options to be applied to some part, or the whole
/// of, a given text field.
///
@ -131,6 +139,7 @@ pub struct TextFormat {
pub bullet: Option<bool>,
pub url: Option<WString>,
pub target: Option<WString>,
pub display: Option<TextDisplay>,
}
impl TextFormat {
@ -191,6 +200,7 @@ impl TextFormat {
// TODO: These are probably empty strings by default
url: Some(WString::new()),
target: Some(WString::new()),
display: None,
}
}
@ -284,6 +294,11 @@ impl TextFormat {
} else {
None
},
display: if self.display == rhs.display {
self.display
} else {
None
},
}
}
@ -311,6 +326,7 @@ impl TextFormat {
bullet: self.bullet.or(rhs.bullet),
url: self.url.or(rhs.url),
target: self.target.or(rhs.target),
display: self.display.or(rhs.display),
}
}
}
@ -542,6 +558,7 @@ impl TextSpan {
bullet: Some(self.bullet),
url: Some(self.url.clone()),
target: Some(self.target.clone()),
display: None, // TODO
}
}
}

View File

@ -0,0 +1,277 @@
package {
import flash.display.MovieClip;
import flash.text.StyleSheet;
import flash.text.TextFormat;
public class Test extends MovieClip {
// this is only relevant properties, some can't be set through css
var textFormatProperties = [
"align",
"bold",
"color",
"font",
"indent",
"italic",
"kerning",
"leading",
"leftMargin",
"letterSpacing",
"rightMargin",
"size",
"underline",
"display" // undocumented!
];
var interestingNumbers = [
"",
"50",
"50.5 px",
"50 pt",
"50xx",
" 50",
"x50",
"0",
"-50",
"09"
];
public function Test() {
var styleSheet: StyleSheet = new StyleSheet();
test("Empty", styleSheet, {});
test("Null", styleSheet, null);
test("Undefined", styleSheet, undefined);
test("Number", styleSheet, 5);
testSingleProperty("Color", styleSheet, "color", "color", [
"red",
"",
"123",
"#",
"#1",
"#12",
"#123",
"#1234",
"#12345",
"#123456",
"#1234567",
"#red"
]);
testSingleProperty("Display", styleSheet, "display", "display", [
"inline",
"block",
"none",
"invalid",
""
]);
testSingleProperty("Font Family", styleSheet, "fontFamily", "font", [
"",
"mono, sans-serif, serif",
"a b c, d e f , , g h",
"Times New Roman"
]);
testSingleProperty("Font Size", styleSheet, "fontSize", "size", interestingNumbers);
testSingleProperty("Font Style", styleSheet, "fontStyle", "italic", [
"",
"bold",
"italic",
"normal"
]);
testSingleProperty("Font Weight", styleSheet, "fontWeight", "bold", [
"",
"bold",
"italic",
"normal"
]);
testSingleProperty("Kerning", styleSheet, "kerning", "kerning", [
"",
"true",
"false",
"lots",
"50",
"0"
]);
testSingleProperty("Leading", styleSheet, "leading", "leading", interestingNumbers);
testSingleProperty("Letter Spacing", styleSheet, "letterSpacing", "letterSpacing", interestingNumbers);
testSingleProperty("Margin Left", styleSheet, "marginLeft", "leftMargin", interestingNumbers);
testSingleProperty("Margin Right", styleSheet, "marginRight", "rightMargin", interestingNumbers);
testSingleProperty("Text Align", styleSheet, "textAlign", "align", [
"",
"invalid",
"left",
"right",
"justify",
"center"
]);
testSingleProperty("Text Decoration", styleSheet, "textDecoration", "underline", [
"",
"bold",
"none",
"underline"
]);
testSingleProperty("Text Indent", styleSheet, "textIndent", "indent", interestingNumbers);
test("Every property is valid", styleSheet, {
"color": "#FF0000",
"display": "block",
"fontFamily": "sans-serif",
"fontSize": "75px",
"fontStyle": "italic",
"fontWeight": "bold",
"kerning": "true",
"leading": "5",
"letterSpacing": "0",
"marginLeft": "0",
"marginRight": "0",
"textAlign": "justify",
"textDecoration": "underline",
"textIndent": "0"
});
}
function test(name: String, styleSheet: StyleSheet, style: *) {
trace("/// " + name);
dumpStyle("style", style);
trace("");
try {
dumpFormat("format", styleSheet.transform(style));
} catch (e) {
// [NA] The exact error message deviates at time of writing because of int vs Number
trace("! " + e.errorID);
}
trace("");
}
function testSingleProperty(name: String, styleSheet: StyleSheet, property: String, transformProperty: String, values: Array) {
trace("/// " + name);
for each (var value in values) {
trace("// styleSheet.transform({" + escapeString(property) + ": " + escapeString(value) + "})");
try {
var input = {};
input[property] = value;
var result = styleSheet.transform(input);
dumpValue(transformProperty, result[transformProperty]);
} catch (e) {
trace("! " + e);
}
}
trace("");
}
function dumpStyle(name: String, style: *) {
if (style === undefined) {
trace( name + " = undefined");
} else if (style === null) {
trace( name + " = null");
} else {
var first = true;
// Sort is not deterministic
var sortedKeys = [];
for (var key in style) {
sortedKeys.push(key);
}
sortedKeys.sort();
for each (var key in sortedKeys) {
if (first) {
first = false;
trace(name + " = {");
}
dumpValue(" " + escapeString(key), style[key]);
}
if (first) {
trace(name + " = {}");
} else {
trace("}");
}
}
}
function dumpFormat(name: String, format: *) {
if (format === undefined) {
trace( name + " = undefined");
} else if (format === null) {
trace( name + " = null");
} else {
var first = true;
for each (var key in textFormatProperties) {
if (first) {
first = false;
trace(name + " = {");
}
dumpValue(" " + escapeString(key), format[key]);
}
if (first) {
trace(name + " = {}");
} else {
trace("}");
}
}
}
function dumpValue(name: String, value: *) {
if (value === undefined) {
return;
} else if (value === null) {
trace(name + " = null");
} else if (value is Number) {
trace(name + " = number " + value);
} else if (value is String) {
trace(name + " = string " + escapeString(value));
} else if (value is Boolean) {
trace(name + " = boolean " + value);
} else if (value is Object) {
trace(name + " = object " + value);
} else {
trace(name + " = unknown " + value);
}
}
function escapeString(input: String): String {
var output:String = "\"";
for (var i:int = 0; i < input.length; i++) {
var char:String = input.charAt(i);
switch (char) {
case "\\":
output += "\\\\";
break;
case "\"":
output += "\\\"";
break;
case "\n":
output += "\\n";
break;
case "\r":
output += "\\r";
break;
case "\t":
output += "\\t";
break;
default:
output += char;
}
}
return output + "\"";
}
}
}

View File

@ -0,0 +1,308 @@
/// Empty
style = {}
format = {
"align" = null
"bold" = null
"color" = null
"font" = null
"indent" = null
"italic" = null
"kerning" = boolean false
"leading" = null
"leftMargin" = null
"letterSpacing" = null
"rightMargin" = null
"size" = null
"underline" = null
"display" = string "block"
}
/// Null
style = null
format = null
/// Undefined
style = undefined
format = null
/// Number
style = {}
! 1069
/// Color
// styleSheet.transform({"color": "red"})
color = number 0
// styleSheet.transform({"color": ""})
color = null
// styleSheet.transform({"color": "123"})
color = number 0
// styleSheet.transform({"color": "#"})
color = number 0
// styleSheet.transform({"color": "#1"})
color = number 1
// styleSheet.transform({"color": "#12"})
color = number 18
// styleSheet.transform({"color": "#123"})
color = number 291
// styleSheet.transform({"color": "#1234"})
color = number 4660
// styleSheet.transform({"color": "#12345"})
color = number 74565
// styleSheet.transform({"color": "#123456"})
color = number 1193046
// styleSheet.transform({"color": "#1234567"})
color = number 0
// styleSheet.transform({"color": "#red"})
color = number 0
/// Display
// styleSheet.transform({"display": "inline"})
display = string "inline"
// styleSheet.transform({"display": "block"})
display = string "block"
// styleSheet.transform({"display": "none"})
display = string "none"
// styleSheet.transform({"display": "invalid"})
display = null
// styleSheet.transform({"display": ""})
display = string "block"
/// Font Family
// styleSheet.transform({"fontFamily": ""})
font = null
// styleSheet.transform({"fontFamily": "mono, sans-serif, serif"})
font = string "_typewriter,_sans,_serif"
// styleSheet.transform({"fontFamily": "a b c, d e f , , g h"})
font = string "a b c,d e f ,g h"
// styleSheet.transform({"fontFamily": "Times New Roman"})
font = string "Times New Roman"
/// Font Size
// styleSheet.transform({"fontSize": ""})
size = null
// styleSheet.transform({"fontSize": "50"})
size = number 50
// styleSheet.transform({"fontSize": "50.5 px"})
size = number 50
// styleSheet.transform({"fontSize": "50 pt"})
size = number 50
// styleSheet.transform({"fontSize": "50xx"})
size = number 50
// styleSheet.transform({"fontSize": " 50"})
size = number 50
// styleSheet.transform({"fontSize": "x50"})
size = null
// styleSheet.transform({"fontSize": "0"})
size = null
// styleSheet.transform({"fontSize": "-50"})
size = null
// styleSheet.transform({"fontSize": "09"})
size = number 9
/// Font Style
// styleSheet.transform({"fontStyle": ""})
italic = null
// styleSheet.transform({"fontStyle": "bold"})
italic = null
// styleSheet.transform({"fontStyle": "italic"})
italic = boolean true
// styleSheet.transform({"fontStyle": "normal"})
italic = boolean false
/// Font Weight
// styleSheet.transform({"fontWeight": ""})
bold = null
// styleSheet.transform({"fontWeight": "bold"})
bold = boolean true
// styleSheet.transform({"fontWeight": "italic"})
bold = null
// styleSheet.transform({"fontWeight": "normal"})
bold = boolean false
/// Kerning
// styleSheet.transform({"kerning": ""})
kerning = boolean false
// styleSheet.transform({"kerning": "true"})
kerning = boolean true
// styleSheet.transform({"kerning": "false"})
kerning = boolean false
// styleSheet.transform({"kerning": "lots"})
kerning = boolean false
// styleSheet.transform({"kerning": "50"})
kerning = boolean true
// styleSheet.transform({"kerning": "0"})
kerning = boolean false
/// Leading
// styleSheet.transform({"leading": ""})
leading = null
// styleSheet.transform({"leading": "50"})
leading = number 50
// styleSheet.transform({"leading": "50.5 px"})
leading = number 50
// styleSheet.transform({"leading": "50 pt"})
leading = number 50
// styleSheet.transform({"leading": "50xx"})
leading = number 50
// styleSheet.transform({"leading": " 50"})
leading = number 50
// styleSheet.transform({"leading": "x50"})
leading = number -2147483648
// styleSheet.transform({"leading": "0"})
leading = number 0
// styleSheet.transform({"leading": "-50"})
leading = number -50
// styleSheet.transform({"leading": "09"})
leading = number 9
/// Letter Spacing
// styleSheet.transform({"letterSpacing": ""})
letterSpacing = null
// styleSheet.transform({"letterSpacing": "50"})
letterSpacing = number 50
// styleSheet.transform({"letterSpacing": "50.5 px"})
letterSpacing = number 50.5
// styleSheet.transform({"letterSpacing": "50 pt"})
letterSpacing = number 50
// styleSheet.transform({"letterSpacing": "50xx"})
letterSpacing = number 50
// styleSheet.transform({"letterSpacing": " 50"})
letterSpacing = number 50
// styleSheet.transform({"letterSpacing": "x50"})
letterSpacing = number NaN
// styleSheet.transform({"letterSpacing": "0"})
letterSpacing = number 0
// styleSheet.transform({"letterSpacing": "-50"})
letterSpacing = number -50
// styleSheet.transform({"letterSpacing": "09"})
letterSpacing = number 9
/// Margin Left
// styleSheet.transform({"marginLeft": ""})
leftMargin = null
// styleSheet.transform({"marginLeft": "50"})
leftMargin = number 50
// styleSheet.transform({"marginLeft": "50.5 px"})
leftMargin = number 50
// styleSheet.transform({"marginLeft": "50 pt"})
leftMargin = number 50
// styleSheet.transform({"marginLeft": "50xx"})
leftMargin = number 50
// styleSheet.transform({"marginLeft": " 50"})
leftMargin = number 50
// styleSheet.transform({"marginLeft": "x50"})
leftMargin = number -2147483648
// styleSheet.transform({"marginLeft": "0"})
leftMargin = number 0
// styleSheet.transform({"marginLeft": "-50"})
leftMargin = number -50
// styleSheet.transform({"marginLeft": "09"})
leftMargin = number 9
/// Margin Right
// styleSheet.transform({"marginRight": ""})
rightMargin = null
// styleSheet.transform({"marginRight": "50"})
rightMargin = number 50
// styleSheet.transform({"marginRight": "50.5 px"})
rightMargin = number 50
// styleSheet.transform({"marginRight": "50 pt"})
rightMargin = number 50
// styleSheet.transform({"marginRight": "50xx"})
rightMargin = number 50
// styleSheet.transform({"marginRight": " 50"})
rightMargin = number 50
// styleSheet.transform({"marginRight": "x50"})
rightMargin = number -2147483648
// styleSheet.transform({"marginRight": "0"})
rightMargin = number 0
// styleSheet.transform({"marginRight": "-50"})
rightMargin = number -50
// styleSheet.transform({"marginRight": "09"})
rightMargin = number 9
/// Text Align
// styleSheet.transform({"textAlign": ""})
align = null
// styleSheet.transform({"textAlign": "invalid"})
! ArgumentError: Error #2008: Parameter align must be one of the accepted values.
// styleSheet.transform({"textAlign": "left"})
align = string "left"
// styleSheet.transform({"textAlign": "right"})
align = string "right"
// styleSheet.transform({"textAlign": "justify"})
align = string "justify"
// styleSheet.transform({"textAlign": "center"})
align = string "center"
/// Text Decoration
// styleSheet.transform({"textDecoration": ""})
underline = null
// styleSheet.transform({"textDecoration": "bold"})
underline = null
// styleSheet.transform({"textDecoration": "none"})
underline = boolean false
// styleSheet.transform({"textDecoration": "underline"})
underline = boolean true
/// Text Indent
// styleSheet.transform({"textIndent": ""})
indent = null
// styleSheet.transform({"textIndent": "50"})
indent = number 50
// styleSheet.transform({"textIndent": "50.5 px"})
indent = number 50
// styleSheet.transform({"textIndent": "50 pt"})
indent = number 50
// styleSheet.transform({"textIndent": "50xx"})
indent = number 50
// styleSheet.transform({"textIndent": " 50"})
indent = number 50
// styleSheet.transform({"textIndent": "x50"})
indent = number -2147483648
// styleSheet.transform({"textIndent": "0"})
indent = number 0
// styleSheet.transform({"textIndent": "-50"})
indent = number -50
// styleSheet.transform({"textIndent": "09"})
indent = number 9
/// Every property is valid
style = {
"color" = string "#FF0000"
"display" = string "block"
"fontFamily" = string "sans-serif"
"fontSize" = string "75px"
"fontStyle" = string "italic"
"fontWeight" = string "bold"
"kerning" = string "true"
"leading" = string "5"
"letterSpacing" = string "0"
"marginLeft" = string "0"
"marginRight" = string "0"
"textAlign" = string "justify"
"textDecoration" = string "underline"
"textIndent" = string "0"
}
format = {
"align" = string "justify"
"bold" = boolean true
"color" = number 16711680
"font" = string "_sans"
"indent" = number 0
"italic" = boolean true
"kerning" = boolean true
"leading" = number 5
"leftMargin" = number 0
"letterSpacing" = number 0
"rightMargin" = number 0
"size" = number 75
"underline" = boolean true
"display" = string "block"
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 1