core: Initial EditText implementation
This commit is contained in:
parent
782174aa75
commit
de1cedb653
|
@ -1,4 +1,5 @@
|
|||
pub enum Character<'gc> {
|
||||
EditText(Box<crate::edit_text::EditText<'gc>>),
|
||||
Graphic(Box<crate::graphic::Graphic<'gc>>),
|
||||
MovieClip(Box<crate::movie_clip::MovieClip<'gc>>),
|
||||
Bitmap(crate::backend::render::BitmapHandle),
|
||||
|
@ -13,6 +14,7 @@ unsafe impl<'gc> gc_arena::Collect for Character<'gc> {
|
|||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
match self {
|
||||
Character::EditText(c) => c.trace(cc),
|
||||
Character::Graphic(c) => c.trace(cc),
|
||||
Character::MovieClip(c) => c.trace(cc),
|
||||
Character::Bitmap(c) => c.trace(cc),
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
//! `EditText` display object and support code.
|
||||
use crate::display_object::{DisplayObject, DisplayObjectBase};
|
||||
use crate::player::{RenderContext, UpdateContext};
|
||||
use crate::prelude::*;
|
||||
use crate::transform::Transform;
|
||||
|
||||
/// A dynamic text field.
|
||||
/// The text in this text field can be changed dynamically.
|
||||
/// It may be selectable or editable by the user, depending on the text field properties.
|
||||
///
|
||||
/// In the Flash IDE, this is created by changing the text field type to "Dynamic".
|
||||
/// In AS2, this is created using `MovieClip.createTextField`.
|
||||
/// In AS3, this is created with the `TextField` class. (https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/text/TextField.html)
|
||||
///
|
||||
/// (SWF19 DefineEditText pp. 171-174)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EditText<'gc> {
|
||||
/// DisplayObject common properties.
|
||||
base: DisplayObjectBase<'gc>,
|
||||
|
||||
/// Static data shared among all instances of this `EditText`.
|
||||
static_data: gc_arena::Gc<'gc, EditTextStatic>,
|
||||
|
||||
/// The current text displayed by this text field.
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl<'gc> EditText<'gc> {
|
||||
/// Creates a new `EditText` from an SWF `DefineEditText` tag.
|
||||
pub fn from_swf_tag(context: &mut UpdateContext<'_, 'gc, '_>, swf_tag: swf::EditText) -> Self {
|
||||
Self {
|
||||
base: Default::default(),
|
||||
text: swf_tag.initial_text.clone().unwrap_or_default(),
|
||||
static_data: gc_arena::Gc::allocate(context.gc_context, EditTextStatic(swf_tag)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> DisplayObject<'gc> for EditText<'gc> {
|
||||
impl_display_object!(base);
|
||||
|
||||
fn id(&self) -> CharacterId {
|
||||
self.static_data.0.id
|
||||
}
|
||||
|
||||
fn run_frame(&mut self, _context: &mut UpdateContext) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
fn render(&self, context: &mut RenderContext) {
|
||||
// TODO: This is a stub implementation to just get some dynamic text rendering.
|
||||
context.transform_stack.push(self.transform());
|
||||
let static_data = &self.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 let Some(font) = context.library.get_font(font_id) {
|
||||
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 = self.text.chars().peekable();
|
||||
let has_kerning_info = font.has_kerning_info();
|
||||
while let Some(c) = chars.next() {
|
||||
if let Some(glyph) = font.get_glyph_for_char(c) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// context.transform_stack.pop();
|
||||
context.transform_stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for EditText<'gc> {
|
||||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.base.trace(cc);
|
||||
self.static_data.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Static data shared between all instances of a text object.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct EditTextStatic(swf::EditText);
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for EditTextStatic {
|
||||
#[inline]
|
||||
fn needs_trace() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
|
@ -1,37 +1,104 @@
|
|||
use crate::backend::render::{RenderBackend, ShapeHandle};
|
||||
use crate::prelude::*;
|
||||
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
|
||||
pub struct Font {
|
||||
glyphs: Vec<ShapeHandle>,
|
||||
/// The list of glyphs defined in the font.
|
||||
/// Used directly by `DefineText` tags.
|
||||
glyphs: Vec<Glyph>,
|
||||
|
||||
/// A map from a Unicode code point to glyph in the `glyphs` array.
|
||||
/// Used by `DefineEditText` tags.
|
||||
code_point_to_glyph: fnv::FnvHashMap<u16, usize>,
|
||||
|
||||
/// The scaling applied to the font height to render at the proper size.
|
||||
/// This depends on the DefineFont tag version.
|
||||
scale: f32,
|
||||
|
||||
/// Kerning infomration.
|
||||
/// Maps from a pair of unicode code points to horizontal offset value.
|
||||
kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn from_swf_tag(renderer: &mut dyn RenderBackend, tag: &swf::Font) -> Result<Font, Error> {
|
||||
let mut glyphs = vec![];
|
||||
for glyph in &tag.glyphs {
|
||||
let shape_handle = renderer.register_glyph_shape(glyph);
|
||||
glyphs.push(shape_handle);
|
||||
let mut code_point_to_glyph = fnv::FnvHashMap::default();
|
||||
for swf_glyph in &tag.glyphs {
|
||||
let glyph = Glyph {
|
||||
shape: renderer.register_glyph_shape(swf_glyph),
|
||||
advance: swf_glyph.advance.unwrap_or(0),
|
||||
};
|
||||
let index = glyphs.len();
|
||||
glyphs.push(glyph);
|
||||
code_point_to_glyph.insert(swf_glyph.code, index);
|
||||
}
|
||||
let kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips> = if let Some(layout) = &tag.layout {
|
||||
layout
|
||||
.kerning
|
||||
.iter()
|
||||
.map(|kerning| ((kerning.left_code, kerning.right_code), kerning.adjustment))
|
||||
.collect()
|
||||
} else {
|
||||
fnv::FnvHashMap::default()
|
||||
};
|
||||
Ok(Font {
|
||||
glyphs,
|
||||
code_point_to_glyph,
|
||||
|
||||
/// DefineFont3 stores coordinates at 20x the scale of DefineFont1/2.
|
||||
/// (SWF19 p.164)
|
||||
scale: if tag.version >= 3 { 20480.0 } else { 1024.0 },
|
||||
kerning_pairs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_glyph(&self, i: usize) -> Option<ShapeHandle> {
|
||||
/// Returns a glyph entry by index.
|
||||
/// Used by `Text` display objects.
|
||||
pub fn get_glyph(&self, i: usize) -> Option<Glyph> {
|
||||
self.glyphs.get(i).cloned()
|
||||
}
|
||||
|
||||
/// Returns a glyph entry by character.
|
||||
/// Used by `EditText` display objects.
|
||||
pub fn get_glyph_for_char(&self, c: char) -> Option<Glyph> {
|
||||
// TODO: Properly handle UTF-16/out-of-bounds code points.
|
||||
let code_point = c as u16;
|
||||
if let Some(index) = self.code_point_to_glyph.get(&code_point) {
|
||||
self.get_glyph(*index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a pair of characters, applies the offset that should be applied
|
||||
/// to the advance value between these two characters.
|
||||
/// Returns 0 twips if no kerning offset exists between these two characters.
|
||||
pub fn get_kerning_offset(&self, left: char, right: char) -> Twips {
|
||||
// TODO: Properly handle UTF-16/out-of-bounds code points.
|
||||
let left_code_point = left as u16;
|
||||
let right_code_point = right as u16;
|
||||
self.kerning_pairs
|
||||
.get(&(left_code_point, right_code_point))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns whether this font contains kerning information.
|
||||
#[inline]
|
||||
pub fn has_kerning_info(&self) -> bool {
|
||||
!self.kerning_pairs.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn scale(&self) -> f32 {
|
||||
self.scale
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Glyph {
|
||||
pub shape: ShapeHandle,
|
||||
pub advance: i16,
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ mod bounding_box;
|
|||
mod button;
|
||||
mod character;
|
||||
mod color_transform;
|
||||
mod edit_text;
|
||||
mod events;
|
||||
mod font;
|
||||
mod graphic;
|
||||
|
|
|
@ -47,6 +47,7 @@ impl<'gc> Library<'gc> {
|
|||
gc_context: MutationContext<'gc, '_>,
|
||||
) -> Result<DisplayNode<'gc>, Box<dyn std::error::Error>> {
|
||||
let obj: Box<dyn DisplayObject<'gc>> = match self.characters.get(&id) {
|
||||
Some(Character::EditText(edit_text)) => edit_text.clone(),
|
||||
Some(Character::Graphic(graphic)) => graphic.clone(),
|
||||
Some(Character::MorphShape(morph_shape)) => morph_shape.clone(),
|
||||
Some(Character::MovieClip(movie_clip)) => movie_clip.clone(),
|
||||
|
|
|
@ -617,6 +617,7 @@ impl<'gc, 'a> MovieClip<'gc> {
|
|||
TagCode::DefineBitsLossless2 => self.define_bits_lossless(context, reader, 2),
|
||||
TagCode::DefineButton => self.define_button_1(context, reader),
|
||||
TagCode::DefineButton2 => self.define_button_2(context, reader),
|
||||
TagCode::DefineEditText => self.define_edit_text(context, reader),
|
||||
TagCode::DefineFont => self.define_font_1(context, reader),
|
||||
TagCode::DefineFont2 => self.define_font_2(context, reader),
|
||||
TagCode::DefineFont3 => self.define_font_3(context, reader),
|
||||
|
@ -938,6 +939,21 @@ impl<'gc, 'a> MovieClip<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Defines a dynamic text field character.
|
||||
#[inline]
|
||||
fn define_edit_text(
|
||||
&mut self,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
reader: &mut SwfStream<&'a [u8]>,
|
||||
) -> DecodeResult {
|
||||
let swf_edit_text = reader.read_define_edit_text()?;
|
||||
let edit_text = crate::edit_text::EditText::from_swf_tag(context, swf_edit_text);
|
||||
context
|
||||
.library
|
||||
.register_character(edit_text.id(), Character::EditText(Box::new(edit_text)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn define_font_1(
|
||||
&mut self,
|
||||
|
|
|
@ -75,7 +75,7 @@ impl<'gc> DisplayObject<'gc> for Text<'gc> {
|
|||
context.transform_stack.push(&transform);
|
||||
context
|
||||
.renderer
|
||||
.render_shape(glyph, context.transform_stack.transform());
|
||||
.render_shape(glyph.shape, context.transform_stack.transform());
|
||||
context.transform_stack.pop();
|
||||
transform.matrix.tx += c.advance as f32;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue