core: Move `mouse_pick` and `mouse_cursor` to `InteractiveObject` as no non-interactive object implements them.

This also cascades into other places, ultimately resulting in more things being marked as `InteractiveObject`.
This commit is contained in:
David Wendt 2021-12-07 21:40:35 -05:00 committed by Mike Welsh
parent d0ef15503c
commit 353a5a78d6
8 changed files with 237 additions and 210 deletions

View File

@ -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<DisplayObject<'gc>>,
pub mouse_over_object: Option<InteractiveObject<'gc>>,
/// If the mouse is down, the display object that the mouse is currently pressing.
pub mouse_down_object: Option<DisplayObject<'gc>>,
pub mouse_down_object: Option<InteractiveObject<'gc>>,
/// The input manager, tracking keys state.
pub input: &'a InputManager,

View File

@ -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<DisplayObject<'gc>> {
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).

View File

@ -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<DisplayObject<'gc>> {
// 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<InteractiveObject<'gc>> {
// 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> {

View File

@ -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<DisplayObject<'gc>> {
// 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<InteractiveObject<'gc>> {
// 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> {

View File

@ -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<DisplayObject<'gc>> {
// 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<InteractiveObject<'gc>> {
// 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.

View File

@ -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<InteractiveObject<'gc>> {
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<T: TInteractiveObject<'gc>>(a: T, b: T) -> bool {
a.as_displayobject().as_ptr() == b.as_displayobject().as_ptr()
}
pub fn option_ptr_eq(
a: Option<InteractiveObject<'gc>>,
b: Option<InteractiveObject<'gc>>,
) -> bool {
a.map(|o| o.as_displayobject().as_ptr()) == b.map(|o| o.as_displayobject().as_ptr())
}
}
impl<'gc> PartialEq for InteractiveObject<'gc> {

View File

@ -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<DisplayObject<'gc>> {
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<MovieClip<'gc>> {
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<InteractiveObject<'gc>> {
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> {

View File

@ -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<DisplayObject<'gc>>,
mouse_hovered_object: Option<InteractiveObject<'gc>>,
/// If the mouse is down, the display object that the mouse is currently pressing.
mouse_pressed_object: Option<DisplayObject<'gc>>,
mouse_pressed_object: Option<InteractiveObject<'gc>>,
/// The object being dragged via a `startDrag` action.
drag_object: Option<DragObject<'gc>>,
@ -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