From 494083673c49261d80e432d6adf28ced9043aba4 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 23 Apr 2021 21:16:27 -0400 Subject: [PATCH] core: Add AVM2 version of Button --- core/src/display_object.rs | 6 + core/src/display_object/avm2_button.rs | 642 +++++++++++++++++++++++++ 2 files changed, 648 insertions(+) create mode 100644 core/src/display_object/avm2_button.rs diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 0f1fe7a8b..88f37bdaa 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -21,6 +21,7 @@ use std::sync::Arc; use swf::Fixed8; mod avm1_button; +mod avm2_button; mod bitmap; mod container; mod edit_text; @@ -38,6 +39,7 @@ pub use crate::display_object::container::{ }; use crate::events::{ClipEvent, ClipEventResult}; pub use avm1_button::Avm1Button; +pub use avm2_button::Avm2Button; pub use bitmap::Bitmap; pub use edit_text::{AutoSizeMode, EditText, TextSelection}; pub use graphic::Graphic; @@ -467,6 +469,7 @@ pub fn render_base<'gc>(this: DisplayObject<'gc>, context: &mut RenderContext<'_ Stage(Stage<'gc>), Bitmap(Bitmap<'gc>), Avm1Button(Avm1Button<'gc>), + Avm2Button(Avm2Button<'gc>), EditText(EditText<'gc>), Graphic(Graphic<'gc>), MorphShape(MorphShape<'gc>), @@ -999,6 +1002,9 @@ pub trait TDisplayObject<'gc>: fn as_avm1_button(&self) -> Option> { None } + fn as_avm2_button(&self) -> Option> { + None + } fn as_movie_clip(&self) -> Option> { None } diff --git a/core/src/display_object/avm2_button.rs b/core/src/display_object/avm2_button.rs new file mode 100644 index 000000000..42ec5c059 --- /dev/null +++ b/core/src/display_object/avm2_button.rs @@ -0,0 +1,642 @@ +use crate::avm1::Object as Avm1Object; +use crate::avm2::{Object as Avm2Object, StageObject as Avm2StageObject, Value as Avm2Value}; +use crate::backend::ui::MouseCursor; +use crate::context::{ActionType, RenderContext, UpdateContext}; +use crate::display_object::container::{dispatch_added_event, dispatch_removed_event}; +use crate::display_object::{DisplayObjectBase, MovieClip, TDisplayObject}; +use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult}; +use crate::prelude::*; +use crate::tag_utils::{SwfMovie, SwfSlice}; +use crate::types::{Degrees, Percent}; +use crate::vminterface::Instantiator; +use gc_arena::{Collect, GcCell, MutationContext}; +use std::convert::TryFrom; +use std::sync::Arc; +use swf::ButtonActionCondition; + +#[derive(Clone, Debug, Collect, Copy)] +#[collect(no_drop)] +pub struct Avm2Button<'gc>(GcCell<'gc, Avm2ButtonData<'gc>>); + +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct Avm2ButtonData<'gc> { + base: DisplayObjectBase<'gc>, + static_data: GcCell<'gc, ButtonStatic>, + + /// The current button state to render. + state: ButtonState, + + /// The display object tree to render when the button is in the UP state. + up_state: Option>, + + /// The display object tree to render when the button is in the OVER state. + over_state: Option>, + + /// The display object tree to render when the button is in the DOWN state. + down_state: Option>, + + /// The display object tree to use for mouse hit checks. + hit_area: Option>, + + /// The current tracking mode of this button. + tracking: ButtonTracking, + + /// The AVM2 representation of this button. + object: Option>, + has_focus: bool, + enabled: bool, + use_hand_cursor: bool, +} + +impl<'gc> Avm2Button<'gc> { + pub fn from_swf_tag( + button: &swf::Button, + source_movie: &SwfSlice, + _library: &crate::library::Library<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Self { + let mut actions = vec![]; + for action in &button.actions { + let action_data = source_movie + .to_unbounded_subslice(action.action_data) + .unwrap(); + let bits = action.conditions.bits(); + let mut bit = 1u16; + while bits & !(bit - 1) != 0 { + if bits & bit != 0 { + actions.push(ButtonAction { + action_data: action_data.clone(), + condition: ButtonActionCondition::from_bits_truncate(bit), + key_code: action + .key_code + .and_then(|k| ButtonKeyCode::try_from(k).ok()), + }); + } + bit <<= 1; + } + } + + let static_data = ButtonStatic { + swf: source_movie.movie.clone(), + id: button.id, + records: button.records.clone(), + actions, + up_to_over_sound: None, + over_to_down_sound: None, + down_to_over_sound: None, + over_to_up_sound: None, + }; + + Avm2Button(GcCell::allocate( + context.gc_context, + Avm2ButtonData { + base: Default::default(), + static_data: GcCell::allocate(context.gc_context, static_data), + state: self::ButtonState::Up, + hit_area: None, + up_state: None, + over_state: None, + down_state: None, + object: None, + tracking: if button.is_track_as_menu { + ButtonTracking::Menu + } else { + ButtonTracking::Push + }, + has_focus: false, + enabled: true, + use_hand_cursor: true, + }, + )) + } + + pub fn set_sounds(self, gc_context: MutationContext<'gc, '_>, sounds: swf::ButtonSounds) { + let button = self.0.write(gc_context); + let mut static_data = button.static_data.write(gc_context); + static_data.up_to_over_sound = sounds.up_to_over_sound; + static_data.over_to_down_sound = sounds.over_to_down_sound; + static_data.down_to_over_sound = sounds.down_to_over_sound; + static_data.over_to_up_sound = sounds.over_to_up_sound; + } + + /// Handles the ancient DefineButtonCxform SWF tag. + /// Set the color transform for all children of each state. + pub fn set_colors( + self, + gc_context: MutationContext<'gc, '_>, + color_transforms: &[swf::ColorTransform], + ) { + let button = self.0.write(gc_context); + let mut static_data = button.static_data.write(gc_context); + + // This tag isn't documented well in SWF19. It is only used in very old SWF<=2 content. + // It applies color transforms to every character in a button, in sequence(?). + for (record, color_transform) in static_data.records.iter_mut().zip(color_transforms.iter()) + { + record.color_transform = color_transform.clone(); + } + } + + /// Construct a given state of the button and return it's containing + /// DisplayObject. + /// + /// In contrast to AVM1Button, the AVM2 variety constructs all of it's + /// children once and stores them in four named slots, either on their own + /// (if they are singular) or in `Sprite`s created specifically to store + /// button children. This means that, for example, a child that exists in + /// multiple states in the SWF will actually be instantiated multiple + /// times. + fn create_state( + self, + context: &mut UpdateContext<'_, 'gc, '_>, + swf_state: swf::ButtonState, + ) -> DisplayObject<'gc> { + let movie = self + .movie() + .expect("All SWF-defined buttons should have movies"); + let mut children = Vec::new(); + let static_data = self.0.read().static_data; + + for record in static_data.read().records.iter() { + if record.states.contains(swf_state) { + match context + .library + .library_for_movie_mut(movie.clone()) + .instantiate_by_id(record.id, context.gc_context) + { + Ok(child) => { + child.set_matrix(context.gc_context, &record.matrix); + child.set_depth(context.gc_context, record.depth.into()); + + if swf_state != swf::ButtonState::HIT_TEST { + child.set_color_transform( + context.gc_context, + &record.color_transform.clone().into(), + ); + } + + children.push((child, record.depth)); + } + Err(error) => { + log::error!( + "Button ID {}: could not instantiate child ID {}: {}", + static_data.read().id, + record.id, + error + ); + } + }; + } + } + + if children.len() > 1 { + let child = children.first().cloned().unwrap().0; + + child.set_parent(context.gc_context, Some(self.into())); + child.post_instantiation(context, child, None, Instantiator::Movie, false); + child.run_frame(context); + + child + } else { + let empty_slice = SwfSlice::empty(movie); + let sprite_constr = context.avm2.prototypes().sprite; + let state_sprite = MovieClip::new(empty_slice, context.gc_context); + + state_sprite.set_avm2_constructor(context.gc_context, Some(sprite_constr)); + state_sprite.construct_frame(context); + + for (child, depth) in children { + let removed_child = state_sprite.replace_at_depth(context, child, depth.into()); + + child.set_parent(context.gc_context, Some(self.into())); + child.post_instantiation(context, child, None, Instantiator::Movie, false); + child.run_frame(context); + + dispatch_added_event(self.into(), child, false, context); + if let Some(removed_child) = removed_child { + dispatch_removed_event(removed_child, context); + } + } + + state_sprite.into() + } + } + + /// Change the rendered state of the button. + pub fn set_state(self, context: &mut UpdateContext<'_, 'gc, '_>, state: ButtonState) { + self.0.write(context.gc_context).state = state; + } + + /// Get the display object that represents a particular button state. + pub fn get_state_child(self, state: ButtonState) -> Option> { + match state { + ButtonState::Up => self.0.read().up_state, + ButtonState::Over => self.0.read().over_state, + ButtonState::Down => self.0.read().down_state, + } + } + + pub fn enabled(self) -> bool { + self.0.read().enabled + } + + pub fn set_enabled(self, context: &mut UpdateContext<'_, 'gc, '_>, enabled: bool) { + self.0.write(context.gc_context).enabled = enabled; + if !enabled { + self.set_state(context, ButtonState::Up); + } + } + + pub fn use_hand_cursor(self) -> bool { + self.0.read().use_hand_cursor + } + + pub fn set_use_hand_cursor( + self, + context: &mut UpdateContext<'_, 'gc, '_>, + use_hand_cursor: bool, + ) { + self.0.write(context.gc_context).use_hand_cursor = use_hand_cursor; + } +} + +impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> { + impl_display_object!(base); + + fn id(&self) -> CharacterId { + self.0.read().static_data.read().id + } + + fn movie(&self) -> Option> { + Some(self.0.read().static_data.read().swf.clone()) + } + + fn post_instantiation( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + display_object: DisplayObject<'gc>, + _init_object: Option>, + _instantiated_by: Instantiator, + run_frame: bool, + ) { + self.set_default_instance_name(context); + + let up_state = self.create_state(context, swf::ButtonState::UP); + let over_state = self.create_state(context, swf::ButtonState::OVER); + let down_state = self.create_state(context, swf::ButtonState::DOWN); + let hit_area = self.create_state(context, swf::ButtonState::HIT_TEST); + + let mut write = self.0.write(context.gc_context); + write.up_state = Some(up_state); + write.over_state = Some(over_state); + write.down_state = Some(down_state); + write.hit_area = Some(hit_area); + + if write.object.is_none() { + let object = Avm2StageObject::for_display_object( + context.gc_context, + display_object, + context.avm2.prototypes().simplebutton, + ); + write.object = Some(object.into()); + + drop(write); + + if run_frame { + self.run_frame(context); + } + } else { + drop(write); + } + + self.set_state(context, ButtonState::Up); + } + + fn run_frame(&self, context: &mut UpdateContext<'_, 'gc, '_>) { + let up_state = self.0.read().up_state; + if let Some(up_state) = up_state { + up_state.run_frame(context); + } + + let over_state = self.0.read().over_state; + if let Some(over_state) = over_state { + over_state.run_frame(context); + } + + let down_state = self.0.read().up_state; + if let Some(down_state) = down_state { + down_state.run_frame(context); + } + + let hit_area = self.0.read().hit_area; + if let Some(hit_area) = hit_area { + hit_area.run_frame(context); + } + } + + fn render_self(&self, context: &mut RenderContext<'_, 'gc>) { + let state = self.0.read().state; + let current_state = self.get_state_child(state); + + if let Some(state) = current_state { + state.render(context); + } + } + + fn self_bounds(&self) -> BoundingBox { + // No inherent bounds; contains child DisplayObjects. + BoundingBox::default() + } + + fn hit_test_shape( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + point: (Twips, Twips), + options: HitTestOptions, + ) -> bool { + let hit_area = self.0.read().hit_area; + + if let Some(hit_area) = hit_area { + hit_area.hit_test_shape(context, point, options) + } else { + false + } + } + + fn mouse_pick( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + self_node: DisplayObject<'gc>, + point: (Twips, Twips), + ) -> Option> { + // The button is hovered if the mouse is over any child nodes. + if self.visible() { + let state = self.0.read().state; + let state_child = self.get_state_child(state); + + if let Some(state_child) = state_child { + let mouse_pick = state_child.mouse_pick(context, state_child, point); + if mouse_pick.is_some() { + return mouse_pick; + } + } + + let hit_area = self.0.read().hit_area; + if let Some(hit_area) = hit_area { + if hit_area.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { + return Some(self_node); + } + } + } + None + } + + fn mouse_cursor(&self) -> MouseCursor { + if self.use_hand_cursor() { + MouseCursor::Hand + } else { + MouseCursor::Arrow + } + } + + fn object2(&self) -> Avm2Value<'gc> { + self.0 + .read() + .object + .map(Avm2Value::from) + .unwrap_or(Avm2Value::Undefined) + } + + fn as_avm2_button(&self) -> Option { + Some(*self) + } + + fn allow_as_mask(&self) -> bool { + let state = self.0.read().state; + let current_state = self.get_state_child(state); + + if let Some(current_state) = current_state.and_then(|cs| cs.as_container()) { + current_state.is_empty() + } else { + false + } + } + + /// Executes and propagates the given clip event. + /// Events execute inside-out; the deepest child will react first, followed by its parent, and + /// so forth. + fn handle_clip_event( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + event: ClipEvent, + ) -> ClipEventResult { + if !self.visible() { + return ClipEventResult::NotHandled; + } + + if !self.enabled() && !matches!(event, ClipEvent::KeyPress { .. }) { + return ClipEventResult::NotHandled; + } + + if event.propagates() { + let state = self.0.read().state; + let current_state = self.get_state_child(state); + + if let Some(current_state) = current_state { + if current_state.handle_clip_event(context, event) == ClipEventResult::Handled { + return ClipEventResult::Handled; + } + } + } + + let mut handled = ClipEventResult::NotHandled; + let mut write = self.0.write(context.gc_context); + + // Translate the clip event to a button event, based on how the button state changes. + let cur_state = write.state; + let new_state = match event { + ClipEvent::RollOut => ButtonState::Up, + ClipEvent::RollOver => ButtonState::Over, + ClipEvent::Press => ButtonState::Down, + ClipEvent::Release => ButtonState::Over, + ClipEvent::KeyPress { key_code } => { + handled = write.run_actions( + context, + swf::ButtonActionCondition::KEY_PRESS, + Some(key_code), + ); + cur_state + } + _ => return ClipEventResult::NotHandled, + }; + + match (cur_state, new_state) { + (ButtonState::Up, ButtonState::Over) => { + write.run_actions(context, swf::ButtonActionCondition::IDLE_TO_OVER_UP, None); + write.play_sound(context, write.static_data.read().up_to_over_sound.as_ref()); + } + (ButtonState::Over, ButtonState::Up) => { + write.run_actions(context, swf::ButtonActionCondition::OVER_UP_TO_IDLE, None); + write.play_sound(context, write.static_data.read().over_to_up_sound.as_ref()); + } + (ButtonState::Over, ButtonState::Down) => { + write.run_actions( + context, + swf::ButtonActionCondition::OVER_UP_TO_OVER_DOWN, + None, + ); + write.play_sound( + context, + write.static_data.read().over_to_down_sound.as_ref(), + ); + } + (ButtonState::Down, ButtonState::Over) => { + write.run_actions( + context, + swf::ButtonActionCondition::OVER_DOWN_TO_OVER_UP, + None, + ); + write.play_sound( + context, + write.static_data.read().down_to_over_sound.as_ref(), + ); + } + _ => (), + }; + + // Queue ActionScript-defined event handlers after the SWF defined ones. + // (e.g., clip.onRelease = foo). + /*if context.swf.version() >= 6 { + if let Some(name) = event.method_name() { + context.action_queue.queue_actions( + self_display_object, + ActionType::Method { + object: write.object.unwrap(), + name, + args: vec![], + }, + false, + ); + } + }*/ + + if write.state != new_state { + drop(write); + self.set_state(context, new_state); + } + + handled + } + + fn is_focusable(&self) -> bool { + true + } + + fn on_focus_changed(&self, gc_context: MutationContext<'gc, '_>, focused: bool) { + self.0.write(gc_context).has_focus = focused; + } + + fn unload(&self, context: &mut UpdateContext<'_, 'gc, '_>) { + let had_focus = self.0.read().has_focus; + if had_focus { + let tracker = context.focus_tracker; + tracker.set(None, context); + } + if let Some(node) = self.maskee() { + node.set_masker(context.gc_context, None, true); + } else if let Some(node) = self.masker() { + node.set_maskee(context.gc_context, None, true); + } + self.set_removed(context.gc_context, true); + } +} + +impl<'gc> Avm2ButtonData<'gc> { + fn play_sound( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + sound: Option<&swf::ButtonSound>, + ) { + if let Some((id, sound_info)) = sound { + if let Some(sound_handle) = context + .library + .library_for_movie_mut(self.movie()) + .get_sound(*id) + { + let _ = context.start_sound(sound_handle, sound_info, None, None); + } + } + } + fn run_actions( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + condition: swf::ButtonActionCondition, + key_code: Option, + ) -> ClipEventResult { + let mut handled = ClipEventResult::NotHandled; + if let Some(parent) = self.base.parent { + for action in &self.static_data.read().actions { + if action.condition == condition + && (action.condition != swf::ButtonActionCondition::KEY_PRESS + || action.key_code == key_code) + { + // Note that AVM1 buttons run actions relative to their parent, not themselves. + handled = ClipEventResult::Handled; + context.action_queue.queue_actions( + parent, + ActionType::Normal { + bytecode: action.action_data.clone(), + }, + false, + ); + } + } + } + handled + } + + fn movie(&self) -> Arc { + self.static_data.read().swf.clone() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Collect)] +#[collect(require_static)] +#[allow(dead_code)] +pub enum ButtonState { + Up, + Over, + Down, +} + +#[derive(Clone, Debug)] +struct ButtonAction { + action_data: crate::tag_utils::SwfSlice, + condition: swf::ButtonActionCondition, + key_code: Option, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Collect)] +#[collect(require_static)] +enum ButtonTracking { + Push, + Menu, +} + +/// Static data shared between all instances of a button. +#[allow(dead_code)] +#[derive(Clone, Debug, Collect)] +#[collect(require_static)] +struct ButtonStatic { + swf: Arc, + id: CharacterId, + records: Vec, + actions: Vec, + + /// The sounds to play on state changes for this button. + up_to_over_sound: Option, + over_to_down_sound: Option, + down_to_over_sound: Option, + over_to_up_sound: Option, +}