diff --git a/core/src/context.rs b/core/src/context.rs index b3a79613d..3602a14f2 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -16,7 +16,7 @@ use crate::backend::{ video::VideoBackend, }; use crate::context_menu::ContextMenuState; -use crate::display_object::{EditText, MovieClip, SoundTransform, Stage}; +use crate::display_object::{EditText, InteractiveObject, MovieClip, SoundTransform, Stage}; use crate::external::ExternalInterface; use crate::focus_tracker::FocusTracker; use crate::library::Library; @@ -98,10 +98,10 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { pub stage: Stage<'gc>, /// The display object that the mouse is currently hovering over. - pub mouse_over_object: Option>, + pub mouse_over_object: Option>, /// If the mouse is down, the display object that the mouse is currently pressing. - pub mouse_down_object: Option>, + pub mouse_down_object: Option>, /// The input manager, tracking keys state. pub input: &'a InputManager, diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 99fde33e7..d41e1d720 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -37,7 +37,6 @@ mod text; mod video; use crate::avm1::activation::Activation; -use crate::backend::ui::MouseCursor; pub use crate::display_object::container::{ DisplayObjectContainer, Lists, TDisplayObjectContainer, }; @@ -1279,16 +1278,6 @@ pub trait TDisplayObject<'gc>: self.hit_test_bounds(pos) } - #[allow(unused_variables)] - fn mouse_pick( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - pos: (Twips, Twips), - require_button_mode: bool, - ) -> Option> { - None - } - fn post_instantiation( &self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -1323,11 +1312,6 @@ pub trait TDisplayObject<'gc>: true } - /// The cursor to use when this object is the hovered element under a mouse. - fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { - MouseCursor::Hand - } - /// Obtain the top-most non-Stage parent of the display tree hierarchy, if /// a suitable object exists. If none such object exists, this function /// yields an AVM1 error (which shouldn't happen in normal usage). diff --git a/core/src/display_object/avm1_button.rs b/core/src/display_object/avm1_button.rs index 1f6ca8286..9c1ec7fbe 100644 --- a/core/src/display_object/avm1_button.rs +++ b/core/src/display_object/avm1_button.rs @@ -358,38 +358,6 @@ impl<'gc> TDisplayObject<'gc> for Avm1Button<'gc> { false } - fn mouse_pick( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - point: (Twips, Twips), - require_button_mode: bool, - ) -> Option> { - // The button is hovered if the mouse is over any child nodes. - if self.visible() { - for child in self.iter_render_list().rev() { - let result = child.mouse_pick(context, point, require_button_mode); - if result.is_some() { - return result; - } - } - - for child in self.0.read().hit_area.values() { - if child.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { - return Some((*self).into()); - } - } - } - None - } - - fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { - if self.use_hand_cursor() { - MouseCursor::Hand - } else { - MouseCursor::Arrow - } - } - fn object(&self) -> Value<'gc> { self.0 .read() @@ -549,6 +517,40 @@ impl<'gc> TInteractiveObject<'gc> for Avm1Button<'gc> { ClipEventResult::NotHandled } + + fn mouse_pick( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + point: (Twips, Twips), + require_button_mode: bool, + ) -> Option> { + // The button is hovered if the mouse is over any child nodes. + if self.visible() { + for child in self.iter_render_list().rev() { + let result = child + .as_interactive() + .and_then(|c| c.mouse_pick(context, point, require_button_mode)); + if result.is_some() { + return result; + } + } + + for child in self.0.read().hit_area.values() { + if child.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { + return Some((*self).into()); + } + } + } + None + } + + fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { + if self.use_hand_cursor() { + MouseCursor::Hand + } else { + MouseCursor::Arrow + } + } } impl<'gc> Avm1ButtonData<'gc> { diff --git a/core/src/display_object/avm2_button.rs b/core/src/display_object/avm2_button.rs index 6e55194a4..b15d59e4b 100644 --- a/core/src/display_object/avm2_button.rs +++ b/core/src/display_object/avm2_button.rs @@ -640,44 +640,6 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> { false } - fn mouse_pick( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - point: (Twips, Twips), - require_button_mode: bool, - ) -> 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.into()); - - if let Some(state_child) = state_child { - let mouse_pick = state_child.mouse_pick(context, point, require_button_mode); - if mouse_pick.is_some() { - return mouse_pick; - } - } - - let hit_area = self.0.read().hit_area; - if let Some(hit_area) = hit_area { - // hit_area is not actually a child, so transform point into local space before passing it down. - let point = self.global_to_local(point); - if hit_area.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { - return Some((*self).into()); - } - } - } - None - } - - fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { - if self.use_hand_cursor() { - MouseCursor::Hand - } else { - MouseCursor::Arrow - } - } - fn object2(&self) -> Avm2Value<'gc> { self.0 .read() @@ -808,6 +770,46 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> { self.event_dispatch_to_avm2(context, event) } + + fn mouse_pick( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + point: (Twips, Twips), + require_button_mode: bool, + ) -> 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.into()); + + if let Some(state_child) = state_child { + let mouse_pick = state_child + .as_interactive() + .and_then(|c| c.mouse_pick(context, point, require_button_mode)); + if mouse_pick.is_some() { + return mouse_pick; + } + } + + let hit_area = self.0.read().hit_area; + if let Some(hit_area) = hit_area { + // hit_area is not actually a child, so transform point into local space before passing it down. + let point = self.global_to_local(point); + if hit_area.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { + return Some((*self).into()); + } + } + } + None + } + + fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { + if self.use_hand_cursor() { + MouseCursor::Hand + } else { + MouseCursor::Arrow + } + } } impl<'gc> Avm2ButtonData<'gc> { diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index e491d9ed3..302166196 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -1762,27 +1762,6 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { self.set_removed(context.gc_context, true); } - fn mouse_pick( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - point: (Twips, Twips), - _require_button_mode: bool, - ) -> Option> { - // The button is hovered if the mouse is over any child nodes. - if self.visible() - && self.is_selectable() - && self.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) - { - Some((*self).into()) - } else { - None - } - } - - fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { - MouseCursor::IBeam - } - fn on_focus_changed(&self, gc_context: MutationContext<'gc, '_>, focused: bool) { let mut text = self.0.write(gc_context); text.has_focus = focused; @@ -1839,6 +1818,27 @@ impl<'gc> TInteractiveObject<'gc> for EditText<'gc> { ClipEventResult::Handled } + + fn mouse_pick( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + point: (Twips, Twips), + _require_button_mode: bool, + ) -> Option> { + // The button is hovered if the mouse is over any child nodes. + if self.visible() + && self.is_selectable() + && self.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) + { + Some((*self).into()) + } else { + None + } + } + + fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { + MouseCursor::IBeam + } } /// Static data shared between all instances of a text object. diff --git a/core/src/display_object/interactive.rs b/core/src/display_object/interactive.rs index 3c859064c..6494a1339 100644 --- a/core/src/display_object/interactive.rs +++ b/core/src/display_object/interactive.rs @@ -1,6 +1,7 @@ //! Interactive object enumtrait use crate::avm2::{Avm2, Event as Avm2Event, EventData as Avm2EventData, Value as Avm2Value}; +use crate::backend::ui::MouseCursor; use crate::context::UpdateContext; use crate::display_object::avm1_button::Avm1Button; use crate::display_object::avm2_button::Avm2Button; @@ -16,6 +17,7 @@ use gc_arena::{Collect, MutationContext}; use ruffle_macros::enum_trait_object; use std::cell::{Ref, RefMut}; use std::fmt::Debug; +use swf::Twips; bitflags! { /// Boolean state flags used by `InteractiveObject`. @@ -250,12 +252,40 @@ pub trait TInteractiveObject<'gc>: self.event_dispatch(context, event) } + + /// Determine the bottom-most interactive display object under the given + /// mouse cursor. + /// + /// Only objects capable of handling mouse input should flag themselves as + /// mouse-pickable, as doing so will make them eligible to recieve targeted + /// mouse events. As a result of this, the returned object will always be + /// an `InteractiveObject`. + fn mouse_pick( + &self, + _context: &mut UpdateContext<'_, 'gc, '_>, + _pos: (Twips, Twips), + _require_button_mode: bool, + ) -> Option> { + None + } + + /// The cursor to use when this object is the hovered element under a mouse. + fn mouse_cursor(self, _context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { + MouseCursor::Hand + } } impl<'gc> InteractiveObject<'gc> { pub fn ptr_eq>(a: T, b: T) -> bool { a.as_displayobject().as_ptr() == b.as_displayobject().as_ptr() } + + pub fn option_ptr_eq( + a: Option>, + b: Option>, + ) -> bool { + a.map(|o| o.as_displayobject().as_ptr()) == b.map(|o| o.as_displayobject().as_ptr()) + } } impl<'gc> PartialEq for InteractiveObject<'gc> { diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 75e292ba5..e8ae22480 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -1924,84 +1924,6 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { false } - fn mouse_pick( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - point: (Twips, Twips), - require_button_mode: bool, - ) -> Option> { - if self.visible() { - let this: DisplayObject<'gc> = (*self).into(); - - if let Some(masker) = self.masker() { - if !masker.hit_test_shape(context, point, HitTestOptions::SKIP_INVISIBLE) { - return None; - } - } - - if self.world_bounds().contains(point) { - // This MovieClip operates in "button mode" if it has a mouse handler, - // either via on(..) or via property mc.onRelease, etc. - let is_button_mode = self.is_button_mode(context); - - if is_button_mode { - let mut options = HitTestOptions::SKIP_INVISIBLE; - options.set(HitTestOptions::SKIP_MASK, self.maskee().is_none()); - if self.hit_test_shape(context, point, options) { - return Some(this); - } - } - } - - // 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. - let mut hit_depth = 0; - let mut result = None; - - for child in self.iter_render_list().rev() { - if child.clip_depth() > 0 { - if result.is_some() && child.clip_depth() >= hit_depth { - if child.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { - return result; - } else { - result = None; - } - } - } else if result.is_none() { - result = child.mouse_pick(context, point, require_button_mode); - - if result.is_some() { - hit_depth = child.depth(); - } - } - } - - if result.is_some() { - return result; - } - - // AVM2 allows movie clips to recieve mouse events without - // explicitly enabling button mode. - if !require_button_mode || matches!(self.object2(), Avm2Value::Object(_)) { - let mut options = HitTestOptions::SKIP_INVISIBLE; - options.set(HitTestOptions::SKIP_MASK, self.maskee().is_none()); - if self.hit_test_shape(context, point, options) { - return Some(this); - } - } - } - - None - } - - fn mouse_cursor(self, context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { - if self.use_hand_cursor() && self.is_button_mode(context) { - MouseCursor::Hand - } else { - MouseCursor::Arrow - } - } - fn as_movie_clip(&self) -> Option> { Some(*self) } @@ -2210,6 +2132,86 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { handled } + + fn mouse_pick( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + point: (Twips, Twips), + require_button_mode: bool, + ) -> Option> { + if self.visible() { + let this: InteractiveObject<'gc> = (*self).into(); + + if let Some(masker) = self.masker() { + if !masker.hit_test_shape(context, point, HitTestOptions::SKIP_INVISIBLE) { + return None; + } + } + + if self.world_bounds().contains(point) { + // This MovieClip operates in "button mode" if it has a mouse handler, + // either via on(..) or via property mc.onRelease, etc. + let is_button_mode = self.is_button_mode(context); + + if is_button_mode { + let mut options = HitTestOptions::SKIP_INVISIBLE; + options.set(HitTestOptions::SKIP_MASK, self.maskee().is_none()); + if self.hit_test_shape(context, point, options) { + return Some(this); + } + } + } + + // 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. + let mut hit_depth = 0; + let mut result = None; + + for child in self.iter_render_list().rev() { + if child.clip_depth() > 0 { + if result.is_some() && child.clip_depth() >= hit_depth { + if child.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { + return result; + } else { + result = None; + } + } + } else if result.is_none() { + result = child + .as_interactive() + .and_then(|c| c.mouse_pick(context, point, require_button_mode)); + + if result.is_some() { + hit_depth = child.depth(); + } + } + } + + if result.is_some() { + return result; + } + + // AVM2 allows movie clips to recieve mouse events without + // explicitly enabling button mode. + if !require_button_mode || matches!(self.object2(), Avm2Value::Object(_)) { + let mut options = HitTestOptions::SKIP_INVISIBLE; + options.set(HitTestOptions::SKIP_MASK, self.maskee().is_none()); + if self.hit_test_shape(context, point, options) { + return Some(this); + } + } + } + + None + } + + fn mouse_cursor(self, context: &mut UpdateContext<'_, 'gc, '_>) -> MouseCursor { + if self.use_hand_cursor() && self.is_button_mode(context) { + MouseCursor::Hand + } else { + MouseCursor::Arrow + } + } } impl<'gc> MovieClipData<'gc> { diff --git a/core/src/player.rs b/core/src/player.rs index fb7a03c0d..43ec63790 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -19,8 +19,8 @@ use crate::config::Letterbox; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::context_menu::{ContextMenuCallback, ContextMenuItem, ContextMenuState}; use crate::display_object::{ - EditText, MorphShape, MovieClip, Stage, StageAlign, StageDisplayState, StageQuality, - StageScaleMode, TInteractiveObject, + EditText, InteractiveObject, MorphShape, MovieClip, Stage, StageAlign, StageDisplayState, + StageQuality, StageScaleMode, TInteractiveObject, }; use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent}; use crate::external::Value as ExternalValue; @@ -65,10 +65,10 @@ struct GcRootData<'gc> { stage: Stage<'gc>, /// The display object that the mouse is currently hovering over. - mouse_hovered_object: Option>, + mouse_hovered_object: Option>, /// If the mouse is down, the display object that the mouse is currently pressing. - mouse_pressed_object: Option>, + mouse_pressed_object: Option>, /// The object being dragged via a `startDrag` action. drag_object: Option>, @@ -1087,9 +1087,14 @@ impl Player { .iter_depth_list() .rev() .find_map(|(_depth, level)| { - level.mouse_pick(context, *context.mouse_position, false) + level.as_interactive().and_then(|l| { + l.mouse_pick(context, *context.mouse_position, false) + }) }); - movie_clip.set_drop_target(context.gc_context, drop_target_object); + movie_clip.set_drop_target( + context.gc_context, + drop_target_object.map(|d| d.as_displayobject()), + ); display_object.set_visible(context.gc_context, was_visible); } } @@ -1110,37 +1115,41 @@ impl Player { .iter_depth_list() .rev() .find_map(|(_depth, level)| { - level.mouse_pick(context, *context.mouse_position, true) + level + .as_interactive() + .and_then(|l| l.mouse_pick(context, *context.mouse_position, true)) }); - let mut events: smallvec::SmallVec<[(DisplayObject<'_>, ClipEvent); 2]> = + let mut events: smallvec::SmallVec<[(InteractiveObject<'_>, ClipEvent); 2]> = Default::default(); // Cancel hover if an object is removed from the stage. if let Some(hovered) = context.mouse_over_object { - if hovered.removed() { + if hovered.as_displayobject().removed() { context.mouse_over_object = None; } } if let Some(pressed) = context.mouse_down_object { - if pressed.removed() { + if pressed.as_displayobject().removed() { context.mouse_down_object = None; } } let cur_over_object = context.mouse_over_object; // Check if a new object has been hovered over. - if !DisplayObject::option_ptr_eq(cur_over_object, new_over_object) { + if !InteractiveObject::option_ptr_eq(cur_over_object, new_over_object) { // If the mouse button is down, the object the user clicked on grabs the focus // and fires "drag" events. Other objects are ignroed. if context.input.is_mouse_down() { context.mouse_over_object = new_over_object; if let Some(down_object) = context.mouse_down_object { - if DisplayObject::option_ptr_eq(context.mouse_down_object, cur_over_object) - { + if InteractiveObject::option_ptr_eq( + context.mouse_down_object, + cur_over_object, + ) { // Dragged from outside the clicked object to the inside. events.push((down_object, ClipEvent::DragOut)); - } else if DisplayObject::option_ptr_eq( + } else if InteractiveObject::option_ptr_eq( context.mouse_down_object, new_over_object, ) { @@ -1155,7 +1164,7 @@ impl Player { events.push(( cur_over_object, ClipEvent::RollOut { - to: new_over_object.and_then(|d| d.as_interactive()), + to: new_over_object, }, )); } @@ -1165,7 +1174,7 @@ impl Player { events.push(( new_over_object, ClipEvent::RollOver { - from: cur_over_object.and_then(|d| d.as_interactive()), + from: cur_over_object, }, )); } else { @@ -1192,7 +1201,7 @@ impl Player { events.push((context.stage.into(), ClipEvent::MouseUpInside)); } - let released_inside = DisplayObject::option_ptr_eq( + let released_inside = InteractiveObject::option_ptr_eq( context.mouse_down_object, context.mouse_over_object, ); @@ -1216,7 +1225,7 @@ impl Player { events.push(( over_object, ClipEvent::RollOver { - from: cur_over_object.and_then(|d| d.as_interactive()), + from: cur_over_object, }, )); } else { @@ -1232,10 +1241,8 @@ impl Player { false } else { for (object, event) in events { - if !object.removed() { - if let Some(interactive) = object.as_interactive() { - interactive.handle_clip_event(context, event); - } + if !object.as_displayobject().removed() { + object.handle_clip_event(context, event); } } true