diff --git a/core/src/avm1/globals/text_field.rs b/core/src/avm1/globals/text_field.rs index 6192d259b..069a4ce1d 100644 --- a/core/src/avm1/globals/text_field.rs +++ b/core/src/avm1/globals/text_field.rs @@ -72,6 +72,42 @@ macro_rules! with_text_field { }}; } +pub fn text_width<'gc>( + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + if let Some(etext) = this + .as_display_object() + .and_then(|dobj| dobj.as_edit_text()) + { + let metrics = etext.measure_text(context); + + return Ok(metrics.0.into()); + } + + Ok(Value::Undefined.into()) +} + +pub fn text_height<'gc>( + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + if let Some(etext) = this + .as_display_object() + .and_then(|dobj| dobj.as_edit_text()) + { + let metrics = etext.measure_text(context); + + return Ok(metrics.1.into()); + } + + Ok(Value::Undefined.into()) +} + pub fn create_proto<'gc>( gc_context: MutationContext<'gc, '_>, proto: Object<'gc>, @@ -103,6 +139,21 @@ pub fn create_proto<'gc>( } ); + object.add_property( + gc_context, + "textWidth", + Executable::Native(text_width), + None, + ReadOnly.into(), + ); + object.add_property( + gc_context, + "textHeight", + Executable::Native(text_height), + None, + ReadOnly.into(), + ); + object.into() } diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 22e69851f..8c021756c 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -3,6 +3,7 @@ use crate::avm1::globals::text_field::attach_virtual_properties; use crate::avm1::{Avm1, Object, ScriptObject, StageObject, TObject, Value}; use crate::context::{RenderContext, UpdateContext}; use crate::display_object::{DisplayObjectBase, TDisplayObject}; +use crate::font::{Font, Glyph}; use crate::prelude::*; use crate::transform::Transform; use gc_arena::{Collect, Gc, GcCell, MutationContext}; @@ -386,6 +387,103 @@ impl<'gc> EditText<'gc> { pub fn set_new_text_format(self, tf: TextFormat, gc_context: MutationContext<'gc, '_>) { self.0.write(gc_context).new_format = tf; } + + /// Measure the width and height of the `EditText`'s current text load. + /// + /// The returned tuple should be interpreted as width, then height. + pub fn measure_text(self, context: &mut UpdateContext<'_, 'gc, '_>) -> (f32, f32) { + let edit_text = self.0.read(); + // TODO: This is a stub implementation to just get some dynamic text rendering. + let static_data = &edit_text.static_data.0; + let font_id = static_data.font_id.unwrap_or(0); + + let mut size = (0.0, 0.0); + + // If the font can't be found or has no glyph information, use the "device font" instead. + // We're cheating a bit and not actually rendering text using the OS/web. + // Instead, we embed an SWF version of Noto Sans to use as the "device font", and render + // it the same as any other SWF outline text. + if let Some(font) = context + .library + .get_font(font_id) + .filter(|font| font.has_glyphs()) + .or_else(|| context.library.device_font()) + { + self.evaluate_font(font, &edit_text.text, |transform, glyph: &Glyph| { + //This is actually terrible matrix math. + let tx = transform.matrix.tx * transform.matrix.a; + let ty = transform.matrix.ty * transform.matrix.d; + size.0 = f32::max(size.0, tx + (glyph.advance as f32)); + size.1 = f32::max(size.1, ty); + }); + } + + size + } + + /// Evaluate an SWF font on a glyph-by-glyph basis. + /// + /// This function takes a font and a text string to evaluate, as well as a + /// `glyph_func`. The given `glyph_func` will be called once for each glyph + /// that the font expects to be rendered. It's arguments will be the + /// glyph's transform, and then the glyph itself. + fn evaluate_font(self, font: Font<'gc>, text: &str, mut glyph_func: FGlyph) + where + FGlyph: FnMut(&Transform, &Glyph), + { + let edit_text = self.0.read(); + // TODO: This is a stub implementation to just get some dynamic text rendering. + let static_data = &edit_text.static_data.0; + + // TODO: Many of these properties should change be instance members instead + // of static data, because they can be altered via ActionScript. + let color = static_data.color.as_ref().unwrap_or_else(|| &swf::Color { + r: 0, + g: 0, + b: 0, + a: 255, + }); + + let mut transform: Transform = Default::default(); + transform.color_transform.r_mult = f32::from(color.r) / 255.0; + transform.color_transform.g_mult = f32::from(color.g) / 255.0; + transform.color_transform.b_mult = f32::from(color.b) / 255.0; + transform.color_transform.a_mult = f32::from(color.a) / 255.0; + + let scale = if let Some(height) = static_data.height { + transform.matrix.ty += f32::from(height); + f32::from(height) / font.scale() + } else { + 1.0 + }; + if let Some(layout) = &static_data.layout { + transform.matrix.ty -= layout.leading.get() as f32; + } + transform.matrix.a = scale; + transform.matrix.d = scale; + let mut chars = text.chars().peekable(); + let has_kerning_info = font.has_kerning_info(); + 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 edit_text.static_data.0.is_html && c == '<' { + // Skip characters until we see a close bracket. + chars.by_ref().skip_while(|&x| x != '>').next(); + } else if let Some(glyph) = font.get_glyph_for_char(c) { + glyph_func(&transform, &glyph); + // Step horizontally. + let mut advance = f32::from(glyph.advance); + if has_kerning_info { + advance += font + .get_kerning_offset(c, chars.peek().cloned().unwrap_or('\0')) + .get() as f32; + } + transform.matrix.tx += advance * scale; + } + } + } } impl<'gc> TDisplayObject<'gc> for EditText<'gc> { @@ -428,25 +526,14 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { .unwrap_or(Value::Undefined) } - fn render(&self, context: &mut RenderContext) { + fn render(&self, context: &mut RenderContext<'_, 'gc>) { + context.transform_stack.push(&*self.transform()); + let edit_text = self.0.read(); // TODO: This is a stub implementation to just get some dynamic text rendering. - context.transform_stack.push(&*self.transform()); let static_data = &edit_text.static_data.0; let font_id = static_data.font_id.unwrap_or(0); - // TODO: Many of these properties should change be instance members instead - // of static data, because they can be altered via ActionScript. - let color = static_data.color.as_ref().unwrap_or_else(|| &swf::Color { - r: 0, - g: 0, - b: 0, - a: 255, - }); - let mut transform: Transform = Default::default(); - transform.color_transform.r_mult = f32::from(color.r) / 255.0; - transform.color_transform.g_mult = f32::from(color.g) / 255.0; - transform.color_transform.b_mult = f32::from(color.b) / 255.0; - transform.color_transform.a_mult = f32::from(color.a) / 255.0; + // If the font can't be found or has no glyph information, use the "device font" instead. // We're cheating a bit and not actually rendering text using the OS/web. // Instead, we embed an SWF version of Noto Sans to use as the "device font", and render @@ -457,45 +544,16 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { .filter(|font| font.has_glyphs()) .or_else(|| context.library.device_font()) { - let scale = if let Some(height) = static_data.height { - transform.matrix.ty += f32::from(height); - f32::from(height) / font.scale() - } else { - 1.0 - }; - if let Some(layout) = &static_data.layout { - transform.matrix.ty -= layout.leading.get() as f32; - } - transform.matrix.a = scale; - transform.matrix.d = scale; - let mut chars = edit_text.text.chars().peekable(); - let has_kerning_info = font.has_kerning_info(); - 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 edit_text.static_data.0.is_html && c == '<' { - // Skip characters until we see a close bracket. - chars.by_ref().skip_while(|&x| x != '>').next(); - } else if let Some(glyph) = font.get_glyph_for_char(c) { - // Render glyph. - context.transform_stack.push(&transform); - context - .renderer - .render_shape(glyph.shape, context.transform_stack.transform()); - context.transform_stack.pop(); - // Step horizontally. - let mut advance = f32::from(glyph.advance); - if has_kerning_info { - advance += font - .get_kerning_offset(c, chars.peek().cloned().unwrap_or('\0')) - .get() as f32; - } - transform.matrix.tx += advance * scale; - } - } + self.evaluate_font(font, &edit_text.text, |transform, glyph: &Glyph| { + // Render glyph. + context.transform_stack.push(transform); + context + .renderer + .render_shape(glyph.shape, context.transform_stack.transform()); + context.transform_stack.pop(); + }); } + context.transform_stack.pop(); }