avm2: Basic TLF rendering support

This commit is contained in:
Lord-McSweeney 2023-09-17 15:45:51 -07:00 committed by Nathan Adams
parent 94009e4b1a
commit 597e4e8b9b
14 changed files with 667 additions and 31 deletions

View File

@ -124,6 +124,7 @@ pub struct Avm2<'gc> {
pub flash_utils_internal: Namespace<'gc>,
pub flash_geom_internal: Namespace<'gc>,
pub flash_events_internal: Namespace<'gc>,
pub flash_text_engine_internal: Namespace<'gc>,
#[collect(require_static)]
native_method_table: &'static [Option<(&'static str, NativeMethodImpl)>],
@ -190,6 +191,7 @@ impl<'gc> Avm2<'gc> {
flash_utils_internal: Namespace::internal("flash.utils", context),
flash_geom_internal: Namespace::internal("flash.geom", context),
flash_events_internal: Namespace::internal("flash.events", context),
flash_text_engine_internal: Namespace::internal("flash.text.engine", context),
native_method_table: Default::default(),
native_instance_allocator_table: Default::default(),

View File

@ -167,6 +167,7 @@ pub struct SystemClasses<'gc> {
pub statusevent: ClassObject<'gc>,
pub contextmenuevent: ClassObject<'gc>,
pub font: ClassObject<'gc>,
pub textline: ClassObject<'gc>,
}
impl<'gc> SystemClasses<'gc> {
@ -293,6 +294,7 @@ impl<'gc> SystemClasses<'gc> {
statusevent: object,
contextmenuevent: object,
font: object,
textline: object,
}
}
}
@ -801,6 +803,7 @@ fn load_playerglobal<'gc>(
("flash.text", "TextFormat", textformat),
("flash.text", "TextField", textfield),
("flash.text", "TextLineMetrics", textlinemetrics),
("flash.text.engine", "TextLine", textline),
("flash.filters", "BevelFilter", bevelfilter),
("flash.filters", "BitmapFilter", bitmapfilter),
("flash.filters", "BlurFilter", blurfilter),

View File

@ -1,4 +1,5 @@
//! `flash.text.engine` namespace
pub mod text_block;
pub mod text_justifier;
pub mod text_line;

View File

@ -3,6 +3,8 @@ package flash.text.engine {
public class ContentElement {
public static const GRAPHIC_ELEMENT:uint = 65007;
public var userData;
internal var _text:String = null;
private var _elementFormat:ElementFormat;
@ -10,6 +12,10 @@ package flash.text.engine {
// FIXME: `new ContentElement()` throws an error in Flash; see TextJustifier
this._elementFormat = elementFormat;
}
public function get text():String {
return this._text;
}
public function get elementFormat():ElementFormat {
return this._elementFormat;

View File

@ -1,10 +1,200 @@
package flash.text.engine {
public final class ElementFormat {
private var _alignmentBaseline:String;
private var _alpha:Number;
private var _baselineShift:Number;
private var _breakOpportunity:String;
private var _color:uint;
private var _digitCase:String;
private var _digitWidth:String;
private var _dominantBaseline:String;
private var _fontDescription:FontDescription;
private var _fontSize:Number;
private var _kerning:String;
private var _ligatureLevel:String;
private var _locale:String;
private var _textRotation:String;
private var _trackingLeft:Number;
private var _trackingRight:Number;
private var _typographicCase:String;
public function ElementFormat(fontDescription:FontDescription = null, fontSize:Number = 12, color:uint = 0, alpha:Number = 1,
textRotation:String = "auto", dominantBaseline:String = "roman",
alignmentBaseline:String = "useDominantBaseline", baselineShift:Number = 0, kerning:String = "on",
trackingRight:Number = 0, trackingLeft:Number = 0, locale:String = "en", breakOpportunity:String = "auto",
digitCase:String = "default", digitWidth:String = "default", ligatureLevel:String = "common",
typographicCase:String = "default") {}
typographicCase:String = "default") {
this.fontDescription = (fontDescription != null) ? fontDescription : new FontDescription();
this.alignmentBaseline = alignmentBaseline;
this.alpha = alpha;
this.baselineShift = baselineShift;
this.breakOpportunity = breakOpportunity;
this.color = color;
this.digitCase = digitCase;
this.digitWidth = digitWidth;
this.dominantBaseline = dominantBaseline;
this.fontSize = fontSize;
this.kerning = kerning;
this.ligatureLevel = ligatureLevel;
this.locale = locale;
this.textRotation = textRotation;
this.trackingLeft = trackingLeft;
this.trackingRight = trackingRight;
this.typographicCase = typographicCase;
}
public function get alignmentBaseline():String {
return this._alignmentBaseline;
}
public function set alignmentBaseline(value:String):void {
this._alignmentBaseline = value;
}
public function get alpha():Number {
return this._alpha;
}
public function set alpha(value:Number):void {
this._alpha = value;
}
public function get baselineShift():Number {
return this._baselineShift;
}
public function set baselineShift(value:Number):void {
this._baselineShift = value;
}
public function get breakOpportunity():String {
return this._breakOpportunity;
}
public function set breakOpportunity(value:String):void {
this._breakOpportunity = value;
}
public function get color():uint {
return this._color;
}
public function set color(value:uint):void {
this._color = value;
}
public function get digitCase():String {
return this._digitCase;
}
public function set digitCase(value:String):void {
this._digitCase = value;
}
public function get digitWidth():String {
return this._digitWidth;
}
public function set digitWidth(value:String):void {
this._digitWidth = value;
}
public function get dominantBaseline():String {
return this._dominantBaseline;
}
public function set dominantBaseline(value:String):void {
this._dominantBaseline = value;
}
public function get fontDescription():FontDescription {
return this._fontDescription;
}
public function set fontDescription(value:FontDescription):void {
this._fontDescription = value;
}
public function get fontSize():Number {
return this._fontSize;
}
public function set fontSize(value:Number):void {
this._fontSize = value;
}
public function get kerning():String {
return this._kerning;
}
public function set kerning(value:String):void {
this._kerning = value;
}
public function get ligatureLevel():String {
return this._ligatureLevel;
}
public function set ligatureLevel(value:String):void {
this._ligatureLevel = value;
}
public function get locale():String {
return this._locale;
}
public function set locale(value:String):void {
this._locale = value;
}
public function get textRotation():String {
return this._textRotation;
}
public function set textRotation(value:String):void {
this._textRotation = value;
}
public function get trackingLeft():Number {
return this._trackingLeft;
}
public function set trackingLeft(value:Number):void {
this._trackingLeft = value;
}
public function get trackingRight():Number {
return this._trackingRight;
}
public function set trackingRight(value:Number):void {
this._trackingRight = value;
}
public function get typographicCase():String {
return this._typographicCase;
}
public function set typographicCase(value:String):void {
this._typographicCase = value;
}
}
}

View File

@ -1,6 +1,75 @@
package flash.text.engine {
public final class FontDescription {
private var _fontName:String;
private var _fontWeight:String;
private var _fontPosture:String;
private var _fontLookup:String;
private var _renderingMode:String;
private var _cffHinting:String;
public function FontDescription(fontName:String = "_serif", fontWeight:String = "normal", fontPosture:String = "normal",
fontLookup:String = "device", renderingMode:String = "cff", cffHinting:String = "horizontalStem") {}
fontLookup:String = "device", renderingMode:String = "cff", cffHinting:String = "horizontalStem") {
this.fontName = fontName;
this.fontWeight = fontWeight;
this.fontPosture = fontPosture;
this.fontLookup = fontLookup;
this.renderingMode = renderingMode;
this.cffHinting = cffHinting;
}
public function get fontName():String {
return this._fontName;
}
public function set fontName(value:String):void {
this._fontName = value;
}
public function get fontWeight():String {
return this._fontWeight;
}
public function set fontWeight(value:String):void {
this._fontWeight = value;
}
public function get fontPosture():String {
return this._fontPosture;
}
public function set fontPosture(value:String):void {
this._fontPosture = value;
}
public function get fontLookup():String {
return this._fontLookup;
}
public function set fontLookup(value:String):void {
this._fontLookup = value;
}
public function get renderingMode():String {
return this._renderingMode;
}
public function set renderingMode(value:String):void {
this._renderingMode = value;
}
public function get cffHinting():String {
return this._cffHinting;
}
public function set cffHinting(value:String):void {
this._cffHinting = value;
}
}
}

View File

@ -0,0 +1,39 @@
package flash.text.engine {
import flash.geom.Rectangle;
public final class FontMetrics {
public var emBox:Rectangle;
public var strikethroughOffset:Number;
public var strikethroughThickness:Number;
public var underlineOffset:Number;
public var underlineThickness:Number;
public var subscriptOffset:Number;
public var subscriptScale:Number;
public var superscriptOffset:Number;
public var superscriptScale:Number;
public var lineGap:Number;
public function FontMetrics(emBox:Rectangle, strikethroughOffset:Number, strikethroughThickness:Number, underlineOffset:Number, underlineThickness:Number, subscriptOffset:Number, subscriptScale:Number, superscriptOffset:Number, superscriptScale:Number, lineGap:Number = 0) {
this.emBox = emBox;
this.strikethroughOffset = strikethroughOffset;
this.strikethroughThickness = strikethroughThickness;
this.underlineOffset = underlineOffset;
this.underlineThickness = underlineThickness;
this.subscriptOffset = subscriptOffset;
this.subscriptScale = subscriptScale;
this.superscriptOffset = superscriptOffset;
this.superscriptScale = superscriptScale;
this.lineGap = lineGap;
}
}
}

View File

@ -0,0 +1,11 @@
package flash.text.engine {
import flash.events.EventDispatcher;
public final class GroupElement extends ContentElement {
public function GroupElement(elements:Vector.<ContentElement> = null, elementFormat:ElementFormat = null, eventMirror:EventDispatcher = null, textRotation:String = "rotate0") {
super(elementFormat, eventMirror, textRotation);
this.setElements(elements);
}
}
}

View File

@ -1,18 +1,45 @@
package flash.text.engine {
public final class SpaceJustifier extends TextJustifier {
private var _letterSpacing:Boolean;
private var _minimumSpacing:Number = 0.5;
private var _optimumSpacing:Number = 1.0;
private var _maximumSpacing:Number = 1.5;
public function SpaceJustifier(locale:String = "en", lineJustification:String = "unjustified", letterSpacing:Boolean = false) {
super(locale, lineJustification);
this._letterSpacing = letterSpacing;
}
public function get letterSpacing():Boolean {
return this._letterSpacing;
}
public function set letterSpacing(value:Boolean):void {
this._letterSpacing = value;
}
public function get minimumSpacing():Number {
return this._minimumSpacing;
}
public function set minimumSpacing(value:Number):void {
this._minimumSpacing = value;
}
public function get maximumSpacing():Number {
return this._maximumSpacing;
}
public function set maximumSpacing(value:Number):void {
this._maximumSpacing = value;
}
public function get optimumSpacing():Number {
return this._optimumSpacing;
}
public function set optimumSpacing(value:Number):void {
this._optimumSpacing = value;
}
}
}

View File

@ -1,7 +1,7 @@
package flash.text.engine {
public final class TextBlock {
public var userData;
private var _applyNonLinearFontScaling:Boolean;
private var _baselineFontDescription:FontDescription = null;
private var _baselineFontSize:Number = 12;
@ -11,8 +11,11 @@ package flash.text.engine {
private var _tabStops:Vector.<TabStop>;
private var _textJustifier:TextJustifier;
private var _content:ContentElement;
internal var _textLineCreationResult:String = null;
internal var _firstLine:TextLine = null;
public function TextBlock(content:ContentElement = null,
tabStops:Vector.<TabStop> = null,
textJustifier:TextJustifier = null,
@ -36,9 +39,9 @@ package flash.text.engine {
} else {
// This should creaate a new TextJustifier with locale "en", but we don't actually support creating TextJustifiers yet.
}
this.lineRotation = lineRotation;
if (baselineZero) {
this.baselineZero = baselineZero;
}
@ -48,51 +51,51 @@ package flash.text.engine {
}
this.applyNonLinearFontScaling = applyNonLinearFontScaling;
}
public function get applyNonLinearFontScaling():Boolean {
return this._applyNonLinearFontScaling;
}
public function set applyNonLinearFontScaling(value:Boolean):void {
this._applyNonLinearFontScaling = value;
}
public function get baselineFontDescription():FontDescription {
return this._baselineFontDescription;
}
public function set baselineFontDescription(value:FontDescription):void {
this._baselineFontDescription = value;
}
public function get baselineFontSize():Number {
return this._baselineFontSize;
}
public function set baselineFontSize(value:Number):void {
this._baselineFontSize = value;
}
public function get baselineZero():String {
return this._baselineZero;
}
public function set baselineZero(value:String):void {
this._baselineZero = value;
}
public function get bidiLevel():int {
return this._bidiLevel;
}
public function set bidiLevel(value:int):void {
this._bidiLevel = value;
}
public function get lineRotation():String {
return this._lineRotation;
}
public function set lineRotation(value:String):void {
if (value == null) {
throw new TypeError("Error #2007: Parameter lineRotation must be non-null.", 2007);
@ -100,31 +103,54 @@ package flash.text.engine {
// TODO: This should validate that `value` is a member of TextRotation
this._lineRotation = value;
}
// Note: FP returns a copy of the Vector passed to it, so modifying the returned Vector doesn't affect the actual internal representation
public function get tabStops():Vector.<TabStop> {
return this._tabStops;
}
// Note: FP makes a copy of the Vector passed to it, then sets its internal representation to that
public function set tabStops(value:Vector.<TabStop>):void {
this._tabStops = value;
}
public function get textJustifier():TextJustifier {
return this._textJustifier;
}
public function set textJustifier(value:TextJustifier):void {
this._textJustifier = value;
}
public function get content():ContentElement {
return this._content;
}
public function set content(value:ContentElement):void {
this._content = value;
}
public native function createTextLine(previousLine:TextLine = null, width:Number = 1000000, lineOffset:Number = 0, fitSomething:Boolean = false):TextLine;
public function get textLineCreationResult():String {
return this._textLineCreationResult;
}
public function get firstLine():TextLine {
return this._firstLine;
}
public function get lastLine():TextLine {
return this._firstLine;
}
public function releaseLines(start:TextLine, end:TextLine):void {
if (start != end || end != this._firstLine) {
stub_method("flash.text.engine.TextBlock", "releaseLines", "with start != end or multiple lines");
return;
}
this._firstLine._validity = "invalid";
this._firstLine._textBlock = null;
}
}
}

View File

@ -1,6 +1,7 @@
package flash.text.engine {
import flash.events.EventDispatcher;
import __ruffle__.stub_setter;
import __ruffle__.stub_method;
public final class TextElement extends ContentElement {
public function TextElement(text:String = null, elementFormat:ElementFormat = null, eventMirror:EventDispatcher = null, textRotation:String = "rotate0") {
@ -9,7 +10,20 @@ package flash.text.engine {
// Contrary to the documentation, TextElement does not implement a getter here. It inherits the getter from ContentElement.
public function set text(value:String):void {
stub_setter("flash.text.engine.TextElement", "text");
this._text = value;
}
public function replaceText(beginIndex:int, endIndex:int, newText:String):void {
var realText:String = this.text;
if (realText == null) {
realText = "";
}
if (beginIndex < 0 || endIndex < 0 || beginIndex > realText.length || endIndex > realText.length) {
throw new RangeError("Error #2006: The supplied index is out of bounds.", 2006);
}
this.text = realText.slice(0, beginIndex) + newText + realText.slice(endIndex, realText.length);
}
}
}

View File

@ -1,14 +1,113 @@
package flash.text.engine {
import __ruffle__.stub_getter;
import __ruffle__.stub_setter;
import __ruffle__.stub_method;
import flash.display.DisplayObjectContainer;
import flash.geom.Rectangle;
[Ruffle(NativeInstanceInit)]
public final class TextLine extends DisplayObjectContainer {
internal var _specifiedWidth:Number = 0.0;
internal var _textBlock:TextBlock = null;
internal var _rawTextLength:int = 0;
internal var _validity:String = "valid";
public static const MAX_LINE_WIDTH:int = 1000000;
public var userData;
public function TextLine() {
throw new ArgumentError("Error #2012: TextLine$ class cannot be instantiated.", 2012);
}
public function get rawTextLength():int {
return this._rawTextLength;
}
public function get textBlockBeginIndex():int {
stub_getter("flash.text.engine.TextLine", "textBlockBeginIndex");
return 0;
}
public function get specifiedWidth():Number {
return this._specifiedWidth;
}
public function get textBlock():TextBlock {
return this._textBlock;
}
public function get ascent():Number {
stub_getter("flash.text.engine.TextLine", "ascent");
return 12.0;
}
public function get descent():Number {
stub_getter("flash.text.engine.TextLine", "descent");
return 3.0;
}
public function get unjustifiedTextWidth():Number {
stub_getter("flash.text.engine.TextLine", "unjustifiedTextWidth");
return this._specifiedWidth;
}
public function get textWidth():Number {
stub_getter("flash.text.engine.TextLine", "textWidth");
return this._specifiedWidth;
}
public function get textHeight():Number {
stub_getter("flash.text.engine.TextLine", "textHeight");
return 15.0;
}
public function get validity():String {
stub_getter("flash.text.engine.TextLine", "validity");
return this._validity;
}
public function set validity(value:String):void {
stub_setter("flash.text.engine.TextLine", "validity");
this._validity = value;
}
public function get hasGraphicElement():Boolean {
stub_getter("flash.text.engine.TextLine", "hasGraphicElement");
return false;
}
public function get atomCount():int {
stub_getter("flash.text.engine.TextLine", "atomCount");
return this._rawTextLength;
}
public function get nextLine():TextLine {
return null;
}
public function getBaselinePosition(baseline:String):Number {
stub_method("flash.text.engine.TextLine", "getBaselinePosition");
return 0.0;
}
public function hasTabs():Boolean {
stub_getter("flash.text.engine.TextLine", "hasTabs");
return false;
}
public function getAtomIndexAtPoint(stageX:Number, stageY:Number):int {
stub_method("flash.text.engine.TextLine", "getAtomIndexAtPoint");
return -1;
}
public function getAtomBounds(index:int):Rectangle {
stub_method("flash.text.engine.TextLine", "getAtomBounds");
return new Rectangle(0, 0, 0, 0);
}
// This function does nothing in Flash Player 32
public function flushAtomData():void { }
}
}

View File

@ -0,0 +1,147 @@
use crate::avm2::activation::Activation;
use crate::avm2::error::Error;
use crate::avm2::globals::flash::display::display_object::initialize_for_allocator;
use crate::avm2::object::{Object, TObject};
use crate::avm2::parameters::ParametersExt;
use crate::avm2::value::Value;
use crate::avm2::Multiname;
use crate::avm2_stub_method;
use crate::display_object::{EditText, TDisplayObject};
use crate::html::TextFormat;
use crate::string::WStr;
pub fn create_text_line<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm2_stub_method!(activation, "flash.text.TextBlock", "createTextLine");
let previous_text_line = args.try_get_object(activation, 0);
let width = args.get_f64(activation, 1)?;
let content = this.get_public_property("content", activation)?;
let content = if matches!(content, Value::Null) {
return Ok(Value::Null);
} else {
content.as_object().unwrap()
};
let text = match previous_text_line {
Some(_) => {
// Some SWFs rely on eventually getting `null` from createLineText.
this.set_property(
&Multiname::new(
activation.avm2().flash_text_engine_internal,
"_textLineCreationResult",
),
"complete".into(),
activation,
)?;
return Ok(Value::Null);
}
// Get the content element's text property.
// TODO: GraphicElement and GroupElement
None => content
.get_public_property("text", activation)
.and_then(|o| {
if matches!(o, Value::Null) {
Ok("".into())
} else {
o.coerce_to_string(activation)
}
})
.unwrap_or("".into()),
};
let class = activation.avm2().classes().textline;
let movie = activation.context.swf.clone();
// FIXME: TextLine should be its own DisplayObject
let display_object: EditText =
EditText::new(&mut activation.context, movie, 0.0, 0.0, width, 15.0).into();
display_object.set_text(text.as_wstr(), &mut activation.context);
let element_format = content
.get_public_property("elementFormat", activation)?
.as_object();
let new_height = apply_format(activation, display_object, text.as_wstr(), element_format)?;
display_object.set_height(activation.gc(), new_height);
let instance = initialize_for_allocator(activation, display_object.into(), class)?;
class.call_native_init(instance.into(), &[], activation)?;
instance.set_property(
&Multiname::new(activation.avm2().flash_text_engine_internal, "_textBlock"),
this.into(),
activation,
)?;
instance.set_property(
&Multiname::new(
activation.avm2().flash_text_engine_internal,
"_specifiedWidth",
),
args.get_value(1),
activation,
)?;
instance.set_property(
&Multiname::new(
activation.avm2().flash_text_engine_internal,
"_rawTextLength",
),
text.len().into(),
activation,
)?;
this.set_property(
&Multiname::new(
activation.avm2().flash_text_engine_internal,
"_textLineCreationResult",
),
"success".into(),
activation,
)?;
this.set_property(
&Multiname::new(activation.avm2().flash_text_engine_internal, "_firstLine"),
instance.into(),
activation,
)?;
Ok(instance.into())
}
fn apply_format<'gc>(
activation: &mut Activation<'_, 'gc>,
display_object: EditText<'gc>,
text: &WStr,
element_format: Option<Object<'gc>>,
) -> Result<f64, Error<'gc>> {
if let Some(element_format) = element_format {
let color = element_format
.get_public_property("color", activation)?
.coerce_to_u32(activation)?;
let size = element_format
.get_public_property("fontSize", activation)?
.coerce_to_number(activation)?;
let format = TextFormat {
color: Some(swf::Color::from_rgb(color, 0xFF)),
size: Some(size),
..TextFormat::default()
};
display_object.set_text_format(0, text.len(), format.clone(), &mut activation.context);
display_object.set_new_text_format(format, &mut activation.context);
return Ok(size * 1.2 + 3.0);
}
Ok(15.0)
}

View File

@ -349,8 +349,10 @@ include "flash/text/engine/DigitWidth.as"
include "flash/text/engine/ElementFormat.as"
include "flash/text/engine/FontDescription.as"
include "flash/text/engine/FontLookup.as"
include "flash/text/engine/FontMetrics.as"
include "flash/text/engine/FontPosture.as"
include "flash/text/engine/FontWeight.as"
include "flash/text/engine/GroupElement.as"
include "flash/text/engine/JustificationStyle.as"
include "flash/text/engine/Kerning.as"
include "flash/text/engine/LigatureLevel.as"