diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 0b60923fd..7b261feda 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -20,7 +20,7 @@ mod morph_shape; mod movie_clip; mod text; -use crate::events::{ButtonEvent, ButtonEventResult, ClipEvent}; +use crate::events::{ClipEvent, ClipEventResult}; pub use bitmap::Bitmap; pub use button::Button; pub use edit_text::EditText; @@ -720,26 +720,13 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug + Into> /// 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 propagate_button_event( + fn handle_clip_event( &self, - context: &mut UpdateContext<'_, 'gc, '_>, - event: ButtonEvent, - ) -> ButtonEventResult { - for child in self.children() { - if child.propagate_button_event(context, event) == ButtonEventResult::Handled { - return ButtonEventResult::Handled; - } - } - ButtonEventResult::NotHandled - } - - /// 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 propagate_clip_event(&self, context: &mut UpdateContext<'_, 'gc, '_>, event: ClipEvent) { - for child in self.children() { - child.propagate_clip_event(context, event); - } + _avm: &mut Avm1<'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _event: ClipEvent, + ) -> ClipEventResult { + ClipEventResult::NotHandled } fn run_frame(&mut self, _avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>) {} diff --git a/core/src/display_object/button.rs b/core/src/display_object/button.rs index 69da1a908..51c9bdfde 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -1,7 +1,7 @@ use crate::avm1::{Avm1, Object, StageObject, Value}; use crate::context::{ActionType, RenderContext, UpdateContext}; use crate::display_object::{DisplayObjectBase, TDisplayObject}; -use crate::events::{ButtonEvent, ButtonEventResult, ButtonKeyCode}; +use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult}; use crate::prelude::*; use crate::tag_utils::{SwfMovie, SwfSlice}; use gc_arena::{Collect, GcCell, MutationContext}; @@ -77,17 +77,6 @@ impl<'gc> Button<'gc> { )) } - pub fn handle_button_event( - &mut self, - avm: &mut Avm1<'gc>, - context: &mut crate::context::UpdateContext<'_, 'gc, '_>, - event: ButtonEvent, - ) { - self.0 - .write(context.gc_context) - .handle_button_event((*self).into(), avm, context, event) - } - 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); @@ -189,26 +178,6 @@ impl<'gc> TDisplayObject<'gc> for Button<'gc> { } } - fn propagate_button_event( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - event: ButtonEvent, - ) -> ButtonEventResult { - for child in self.children() { - if child.propagate_button_event(context, event) == ButtonEventResult::Handled { - return ButtonEventResult::Handled; - } - } - match event { - ButtonEvent::KeyPress { key_code } => self.0.write(context.gc_context).run_actions( - context, - swf::ButtonActionCondition::KeyPress, - Some(key_code), - ), - _ => ButtonEventResult::NotHandled, - } - } - fn object(&self) -> Value<'gc> { self.0 .read() @@ -224,6 +193,28 @@ impl<'gc> TDisplayObject<'gc> for Button<'gc> { fn allow_as_mask(&self) -> bool { !self.0.read().children.is_empty() } + + /// 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, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + event: ClipEvent, + ) -> ClipEventResult { + if event.propagates() { + for child in self.children() { + if child.handle_clip_event(avm, context, event) == ClipEventResult::Handled { + return ClipEventResult::Handled; + } + } + } + + self.0 + .write(context.gc_context) + .handle_clip_event((*self).into(), avm, context, event) + } } impl<'gc> ButtonData<'gc> { @@ -307,57 +298,57 @@ impl<'gc> ButtonData<'gc> { } } - fn handle_button_event( + fn handle_clip_event( &mut self, self_display_object: DisplayObject<'gc>, avm: &mut Avm1<'gc>, context: &mut crate::context::UpdateContext<'_, 'gc, '_>, - event: ButtonEvent, - ) { + event: ClipEvent, + ) -> ClipEventResult { + let mut handled = ClipEventResult::NotHandled; + + // Translate the clip event to a button event, based on how the button state changes. let cur_state = self.state; let new_state = match event { - ButtonEvent::RollOut => ButtonState::Up, - ButtonEvent::RollOver => ButtonState::Over, - ButtonEvent::Press => ButtonState::Down, - ButtonEvent::Release => ButtonState::Over, - ButtonEvent::KeyPress { key_code } => { - self.run_actions( + ClipEvent::RollOut => ButtonState::Up, + ClipEvent::RollOver => ButtonState::Over, + ClipEvent::Press => ButtonState::Down, + ClipEvent::Release => ButtonState::Over, + ClipEvent::KeyPress { key_code } => { + handled = self.run_actions( context, swf::ButtonActionCondition::KeyPress, Some(key_code), ); cur_state } + _ => return ClipEventResult::NotHandled, }; - let button_event_handler = match (cur_state, new_state) { + match (cur_state, new_state) { (ButtonState::Up, ButtonState::Over) => { self.run_actions(context, swf::ButtonActionCondition::IdleToOverUp, None); self.play_sound(context, self.static_data.read().up_to_over_sound.as_ref()); - Some("onRollOver") } (ButtonState::Over, ButtonState::Up) => { self.run_actions(context, swf::ButtonActionCondition::OverUpToIdle, None); self.play_sound(context, self.static_data.read().over_to_up_sound.as_ref()); - Some("onRollOut") } (ButtonState::Over, ButtonState::Down) => { self.run_actions(context, swf::ButtonActionCondition::OverUpToOverDown, None); self.play_sound(context, self.static_data.read().over_to_down_sound.as_ref()); - Some("onPress") } (ButtonState::Down, ButtonState::Over) => { self.run_actions(context, swf::ButtonActionCondition::OverDownToOverUp, None); self.play_sound(context, self.static_data.read().down_to_over_sound.as_ref()); - Some("onRelease") } - _ => None, + _ => (), }; // Queue ActionScript-defined event handlers after the SWF defined ones. // (e.g., clip.onRelease = foo). if context.swf.version() >= 6 { - if let Some(name) = button_event_handler { + if let Some(name) = event.method_name() { context.action_queue.queue_actions( self_display_object, ActionType::Method { @@ -371,6 +362,8 @@ impl<'gc> ButtonData<'gc> { } self.set_state(self_display_object, avm, context, new_state); + + handled } fn play_sound( @@ -393,16 +386,20 @@ impl<'gc> ButtonData<'gc> { context: &mut UpdateContext<'_, 'gc, '_>, condition: swf::ButtonActionCondition, key_code: Option, - ) -> ButtonEventResult { - let mut handled = ButtonEventResult::NotHandled; + ) -> 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::KeyPress || action.key_code == key_code) { + // KeyPress events are consumed when a button handles them. + if action.condition == swf::ButtonActionCondition::KeyPress { + handled = ClipEventResult::Handled; + } + // Note that AVM1 buttons run actions relative to their parent, not themselves. - handled = ButtonEventResult::Handled; context.action_queue.queue_actions( parent, ActionType::Normal { diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index e61152651..18b0eb24b 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -8,7 +8,7 @@ use crate::display_object::{ Bitmap, Button, DisplayObjectBase, EditText, Graphic, MorphShapeStatic, TDisplayObject, Text, }; use crate::drawing::Drawing; -use crate::events::{ButtonKeyCode, ClipEvent}; +use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult}; use crate::font::Font; use crate::prelude::*; use crate::shape_utils::DrawCommand; @@ -463,17 +463,9 @@ impl<'gc> MovieClip<'gc> { actions: SmallVec<[ClipAction; 2]>, ) { let mut mc = self.0.write(gc_context); - mc.has_button_clip_event = actions.iter().any(|a| { - a.events.iter().any(|e| match e { - ClipEvent::KeyPress { .. } - | ClipEvent::Press - | ClipEvent::Release - | ClipEvent::ReleaseOutside - | ClipEvent::RollOut - | ClipEvent::RollOver => true, - _ => false, - }) - }); + mc.has_button_clip_event = actions + .iter() + .any(|a| a.events.iter().copied().any(ClipEvent::is_button_event)); mc.set_clip_actions(actions); } @@ -601,14 +593,14 @@ impl<'gc> MovieClip<'gc> { mc.drawing.draw_command(command); } - pub fn run_clip_action( + pub fn run_clip_event( self, context: &mut crate::context::UpdateContext<'_, 'gc, '_>, event: ClipEvent, ) { self.0 .write(context.gc_context) - .run_clip_action(self.into(), context, event); + .run_clip_event(self.into(), context, event); } } @@ -633,10 +625,10 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { let mut mc = self.0.write(context.gc_context); let is_load_frame = !mc.initialized(); if is_load_frame { - mc.run_clip_action((*self).into(), context, ClipEvent::Load); + mc.run_clip_event((*self).into(), context, ClipEvent::Load); mc.set_initialized(true); } else { - mc.run_clip_action((*self).into(), context, ClipEvent::EnterFrame); + mc.run_clip_event((*self).into(), context, ClipEvent::EnterFrame); } // Run my SWF tags. @@ -645,7 +637,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { } if is_load_frame { - mc.run_clip_postaction((*self).into(), context, ClipEvent::Load); + mc.run_clip_postevent((*self).into(), context, ClipEvent::Load); } } @@ -671,27 +663,24 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { self_node: DisplayObject<'gc>, point: (Twips, Twips), ) -> Option> { - if self.visible() && self.world_bounds().contains(point) { - if self.0.read().has_button_clip_event { - return Some(self_node); - } - - if let Ok(object) = self.object().as_object() { - const MOUSE_EVENT_HANDLERS: [&str; 5] = [ - "onPress", - "onRelease", - "onReleaseOutside", - "onRollOut", - "onRollOver", - ]; - if MOUSE_EVENT_HANDLERS - .iter() - .any(|handler| object.has_property(avm, context, handler)) - { + if self.visible() { + if self.world_bounds().contains(point) { + if self.0.read().has_button_clip_event { return Some(self_node); } + + if let Ok(object) = self.object().as_object() { + if ClipEvent::BUTTON_EVENT_METHODS + .iter() + .any(|handler| object.has_property(avm, context, handler)) + { + return Some(self_node); + } + } } + // Maybe we could skip recursing down at all if !world_bounds.contains(point), + // but a child button can have an invisible hit area outside the parent's bounds. for child in self.0.read().children.values().rev() { let result = child.mouse_pick(avm, context, *child, point); if result.is_some() { @@ -703,13 +692,21 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { None } - fn propagate_clip_event(&self, context: &mut UpdateContext<'_, 'gc, '_>, event: ClipEvent) { - for child in self.children() { - child.propagate_clip_event(context, event); + fn handle_clip_event( + &self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + event: ClipEvent, + ) -> ClipEventResult { + if event.propagates() { + for child in self.children() { + if child.handle_clip_event(avm, context, event) == ClipEventResult::Handled { + return ClipEventResult::Handled; + } + } } - self.0 - .read() - .run_clip_action((*self).into(), context, event); + + self.0.read().run_clip_event((*self).into(), context, event) } fn as_movie_clip(&self) -> Option> { @@ -809,7 +806,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { { let mut mc = self.0.write(context.gc_context); mc.stop_audio_stream(context); - mc.run_clip_action((*self).into(), context, ClipEvent::Unload); + mc.run_clip_event((*self).into(), context, ClipEvent::Unload); } self.set_removed(context.gc_context, true); } @@ -1323,12 +1320,14 @@ impl<'gc> MovieClipData<'gc> { } /// Run all actions for the given clip event. - fn run_clip_action( + fn run_clip_event( &self, self_display_object: DisplayObject<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, event: ClipEvent, - ) { + ) -> ClipEventResult { + let mut handled = ClipEventResult::NotHandled; + // TODO: What's the behavior for loaded SWF files? if context.swf.version() >= 5 { for clip_action in self @@ -1336,6 +1335,10 @@ impl<'gc> MovieClipData<'gc> { .iter() .filter(|action| action.events.contains(&event)) { + // KeyPress events are consumed by a single instance. + if matches!(clip_action.event, ClipEvent::KeyPress { .. }) { + handled = ClipEventResult::Handled; + } context.action_queue.queue_actions( self_display_object, ActionType::Normal { @@ -1348,28 +1351,7 @@ impl<'gc> MovieClipData<'gc> { // Queue ActionScript-defined event handlers after the SWF defined ones. // (e.g., clip.onEnterFrame = foo). if context.swf.version() >= 6 { - let name = match event { - ClipEvent::Construct => None, - ClipEvent::Data => Some("onData"), - ClipEvent::DragOut => Some("onDragOut"), - ClipEvent::DragOver => Some("onDragOver"), - ClipEvent::EnterFrame => Some("onEnterFrame"), - ClipEvent::Initialize => None, - ClipEvent::KeyDown => Some("onKeyDown"), - ClipEvent::KeyPress { .. } => None, - ClipEvent::KeyUp => Some("onKeyUp"), - ClipEvent::Load => Some("onLoad"), - ClipEvent::MouseDown => Some("onMouseDown"), - ClipEvent::MouseMove => Some("onMouseMove"), - ClipEvent::MouseUp => Some("onMouseUp"), - ClipEvent::Press => Some("onPress"), - ClipEvent::RollOut => Some("onRollOut"), - ClipEvent::RollOver => Some("onRollOver"), - ClipEvent::Release => Some("onRelease"), - ClipEvent::ReleaseOutside => Some("onReleaseOutside"), - ClipEvent::Unload => Some("onUnload"), - }; - if let Some(name) = name { + if let Some(name) = event.method_name() { context.action_queue.queue_actions( self_display_object, ActionType::Method { @@ -1382,6 +1364,8 @@ impl<'gc> MovieClipData<'gc> { } } } + + handled } /// Run clip actions that trigger after the clip's own actions. @@ -1393,7 +1377,7 @@ impl<'gc> MovieClipData<'gc> { /// TODO: If it turns out other `Load` events need to be delayed, perhaps /// we should change which frame triggers a `Load` event, rather than /// making sure our actions run after the clip's. - fn run_clip_postaction( + fn run_clip_postevent( &self, self_display_object: DisplayObject<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, diff --git a/core/src/events.rs b/core/src/events.rs index e76584237..3c6b80faf 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -12,27 +12,9 @@ pub enum PlayerEvent { TextInput { codepoint: char }, } -/// The events that an AVM1 button can fire. -/// -/// In Flash, these are created using `on` code on the button instance: -/// ```ignore -/// on(release) { -/// trace("Button clicked"); -/// } -/// ``` -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[allow(dead_code)] -pub enum ButtonEvent { - Press, - Release, - RollOut, - RollOver, - KeyPress { key_code: ButtonKeyCode }, -} - /// Whether this button event was handled by some child. #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum ButtonEventResult { +pub enum ClipEventResult { NotHandled, Handled, } @@ -63,6 +45,57 @@ pub enum ClipEvent { Unload, } +impl ClipEvent { + /// Method names for button event handles. + pub const BUTTON_EVENT_METHODS: [&'static str; 7] = [ + "onDragOver", + "onDragOut", + "onPress", + "onRelease", + "onReleaseOutside", + "onRollOut", + "onRollOver", + ]; + + /// Indicates that the event should be propagated down to children. + pub fn propagates(self) -> bool { + matches!( + self, + Self::MouseUp | Self::MouseDown | Self::MouseMove | Self::KeyPress { .. } | Self::KeyDown | Self::KeyUp + ) + } + + /// Indicates whether this is an event type used by Buttons (i.e., on that can be used in an `on` handler in Flash). + pub fn is_button_event(self) -> bool { + matches!(self, Self::DragOut | Self::DragOver | Self::KeyPress { .. } | Self::Press | Self::RollOut | Self::RollOver | Self::Release | Self::ReleaseOutside) + } + + /// Returns the method name of the event handler for this event. + pub fn method_name(self) -> Option<&'static str> { + match self { + ClipEvent::Construct => None, + ClipEvent::Data => Some("onData"), + ClipEvent::DragOut => Some("onDragOut"), + ClipEvent::DragOver => Some("onDragOver"), + ClipEvent::EnterFrame => Some("onEnterFrame"), + ClipEvent::Initialize => None, + ClipEvent::KeyDown => Some("onKeyDown"), + ClipEvent::KeyPress { .. } => None, + ClipEvent::KeyUp => Some("onKeyUp"), + ClipEvent::Load => Some("onLoad"), + ClipEvent::MouseDown => Some("onMouseDown"), + ClipEvent::MouseMove => Some("onMouseMove"), + ClipEvent::MouseUp => Some("onMouseUp"), + ClipEvent::Press => Some("onPress"), + ClipEvent::RollOut => Some("onRollOut"), + ClipEvent::RollOver => Some("onRollOver"), + ClipEvent::Release => Some("onRelease"), + ClipEvent::ReleaseOutside => Some("onReleaseOutside"), + ClipEvent::Unload => Some("onUnload"), + } + } +} + /// Flash virtual keycode. #[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] #[repr(u8)] diff --git a/core/src/player.rs b/core/src/player.rs index e3c9ffedd..9a73aa757 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -7,9 +7,7 @@ use crate::backend::{ }; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::display_object::{MorphShape, MovieClip}; -use crate::events::{ - ButtonEvent, ButtonEventResult, ButtonKeyCode, ClipEvent, KeyCode, PlayerEvent, -}; +use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, PlayerEvent}; use crate::library::Library; use crate::loader::LoadManager; use crate::prelude::*; @@ -385,7 +383,7 @@ impl Player { PlayerEvent::TextInput { codepoint } if codepoint as u32 >= 32 && codepoint as u32 <= 126 => { - Some(ButtonEvent::KeyPress { + Some(ClipEvent::KeyPress { key_code: ButtonKeyCode::try_from(codepoint as u8).unwrap(), }) } @@ -393,7 +391,7 @@ impl Player { // Special keys have custom values for keyPress. PlayerEvent::KeyDown { key_code } => { if let Some(key_code) = crate::events::key_code_to_button_key_code(key_code) { - Some(ButtonEvent::KeyPress { key_code }) + Some(ClipEvent::KeyPress { key_code }) } else { None } @@ -402,12 +400,12 @@ impl Player { }; if button_event.is_some() { - self.mutate_with_update_context(|_avm, context| { + self.mutate_with_update_context(|avm, context| { let levels: Vec> = context.levels.values().copied().collect(); for level in levels { if let Some(button_event) = button_event { - let state = level.propagate_button_event(context, button_event); - if state == ButtonEventResult::Handled { + let state = level.handle_clip_event(avm, context, button_event); + if state == ClipEventResult::Handled { return; } } @@ -426,12 +424,12 @@ impl Player { }; if clip_event.is_some() || mouse_event_name.is_some() { - self.mutate_with_update_context(|_avm, context| { + self.mutate_with_update_context(|avm, context| { let levels: Vec> = context.levels.values().copied().collect(); for level in levels { if let Some(clip_event) = clip_event { - level.propagate_clip_event(context, clip_event); + level.handle_clip_event(avm, context, clip_event); } } @@ -452,40 +450,20 @@ impl Player { let mut is_mouse_down = self.is_mouse_down; self.mutate_with_update_context(|avm, context| { if let Some(node) = context.mouse_hovered_object { - if let Some(mut button) = node.clone().as_button() { - match event { - PlayerEvent::MouseDown { .. } => { - is_mouse_down = true; - needs_render = true; - button.handle_button_event(avm, context, ButtonEvent::Press); - } - - PlayerEvent::MouseUp { .. } => { - is_mouse_down = false; - needs_render = true; - button.handle_button_event(avm, context, ButtonEvent::Release); - } - - _ => (), + match event { + PlayerEvent::MouseDown { .. } => { + is_mouse_down = true; + needs_render = true; + node.handle_clip_event(avm, context, ClipEvent::Press); } - } - if let Some(clip) = node.clone().as_movie_clip() { - match event { - PlayerEvent::MouseDown { .. } => { - is_mouse_down = true; - needs_render = true; - clip.run_clip_action(context, ClipEvent::Press); - } - - PlayerEvent::MouseUp { .. } => { - is_mouse_down = false; - needs_render = true; - clip.run_clip_action(context, ClipEvent::Release); - } - - _ => (), + PlayerEvent::MouseUp { .. } => { + is_mouse_down = false; + needs_render = true; + node.handle_clip_event(avm, context, ClipEvent::Release); } + + _ => (), } } @@ -550,30 +528,14 @@ impl Player { if cur_hovered.map(|d| d.as_ptr()) != new_hovered.map(|d| d.as_ptr()) { // RollOut of previous node. if let Some(node) = cur_hovered { - match node { - DisplayObject::Button(mut button) => { - button.handle_button_event(avm, context, ButtonEvent::RollOut); - } - DisplayObject::MovieClip(clip) => { - clip.run_clip_action(context, ClipEvent::RollOut); - } - _ => (), - } + node.handle_clip_event(avm, context, ClipEvent::RollOut); } // RollOver on new node. new_cursor = MouseCursor::Arrow; if let Some(node) = new_hovered { new_cursor = MouseCursor::Hand; - match node { - DisplayObject::Button(mut button) => { - button.handle_button_event(avm, context, ButtonEvent::RollOver); - } - DisplayObject::MovieClip(clip) => { - clip.run_clip_action(context, ClipEvent::RollOver); - } - _ => (), - } + node.handle_clip_event(avm, context, ClipEvent::RollOver); } context.mouse_hovered_object = new_hovered;