diff --git a/core/src/avm2/globals/flash/display/displayobject.rs b/core/src/avm2/globals/flash/display/displayobject.rs index fd5b7fb04505..18920b9eb217 100644 --- a/core/src/avm2/globals/flash/display/displayobject.rs +++ b/core/src/avm2/globals/flash/display/displayobject.rs @@ -671,6 +671,10 @@ pub fn hit_test_point<'gc>( .coerce_to_boolean(); if shape_flag { + if !dobj.is_on_stage(&activation.context) { + return Ok(false.into()); + } + return Ok(dobj .hit_test_shape( &mut activation.context, diff --git a/core/src/display_object/avm2_button.rs b/core/src/display_object/avm2_button.rs index a327b4662207..2fe0fe91fb67 100644 --- a/core/src/display_object/avm2_button.rs +++ b/core/src/display_object/avm2_button.rs @@ -643,6 +643,20 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> { BoundingBox::default() } + fn bounds_with_transform(&self, matrix: &Matrix) -> BoundingBox { + // Get self bounds + let mut bounds = self.self_bounds().transform(matrix); + + // Add the bounds of the child, dictated by current state + let state = self.0.read().state; + if let Some(child) = self.get_state_child(state.into()) { + let child_bounds = child.bounds_with_transform(matrix); + bounds.union(&child_bounds); + } + + bounds + } + fn hit_test_shape( &self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -652,8 +666,14 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> { if !options.contains(HitTestOptions::SKIP_INVISIBLE) || self.visible() { let state = self.0.read().state; if let Some(child) = self.get_state_child(state.into()) { - // hit_area is not actually a child, so transform point into local space before passing it down. - let point = self.global_to_local(point); + //TODO: the if below should probably always be taken, why does the hit area + // sometimes have a parent? + let mut point = point; + if child.parent().is_none() { + // hit_area is not actually a child, so transform point into local space before passing it down. + point = self.global_to_local(point); + } + if child.hit_test_shape(context, point, options) { return true; } @@ -780,6 +800,7 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> { ClipEvent::Press => (ButtonState::Down, static_data.over_to_down_sound.as_ref()), ClipEvent::Release => (ButtonState::Over, static_data.down_to_over_sound.as_ref()), ClipEvent::ReleaseOutside => (ButtonState::Up, static_data.over_to_up_sound.as_ref()), + ClipEvent::MouseUpInside => (ButtonState::Up, static_data.over_to_up_sound.as_ref()), ClipEvent::RollOut { .. } => (ButtonState::Up, static_data.over_to_up_sound.as_ref()), ClipEvent::RollOver { .. } => { (ButtonState::Over, static_data.up_to_over_sound.as_ref()) @@ -814,14 +835,20 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> { .as_interactive() .and_then(|c| c.mouse_pick(context, point, require_button_mode)); if mouse_pick.is_some() { - return mouse_pick; + // Selecting a child of a button is equivalent to selecting the button itself + return Some((*self).into()); } } 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); + //TODO: the if below should probably always be taken, why does the hit area + // sometimes have a parent? + let mut point = point; + if hit_area.parent().is_none() { + // hit_area is not actually a child, so transform point into local space before passing it down. + point = self.global_to_local(point); + } if hit_area.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { return Some((*self).into()); } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index ec7e20d3a953..49b199dcca2d 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -2491,7 +2491,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { } else { clip_depth = child.clip_depth(); } - } else if child.depth() > clip_depth + } else if child.depth() >= clip_depth && child.hit_test_shape(context, point, options) { return true; @@ -2744,7 +2744,7 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { point: (Twips, Twips), require_button_mode: bool, ) -> Option> { - if self.visible() && self.mouse_enabled() { + if self.visible() { let this: InteractiveObject<'gc> = (*self).into(); if let Some(masker) = self.masker() { @@ -2753,7 +2753,12 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { } } - if self.world_bounds().contains(point) { + // In AVM2, mouse_enabled should only impact the ability to select the current clip + // but it should still be possible to select any children where child.mouse_enabled() is + // true. + // InteractiveObject.mouseEnabled: + // "Any children of this instance on the display list are not affected." + if self.mouse_enabled() && 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); @@ -2778,6 +2783,11 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { !require_button_mode || matches!(self.object2(), Avm2Value::Object(_)); for child in self.iter_render_list().rev() { + // Clicking static text is ignored + if matches!(child, DisplayObject::Text(_)) { + continue; + } + if child.clip_depth() > 0 { if result.is_some() && child.clip_depth() >= hit_depth { if child.hit_test_shape(context, point, HitTestOptions::MOUSE_PICK) { @@ -2789,7 +2799,9 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { } else if result.is_none() { if let Some(child) = child.as_interactive() { result = child.mouse_pick(context, point, require_button_mode); - } else if check_non_interactive && child.hit_test_shape(context, point, options) + } else if check_non_interactive + && self.mouse_enabled() + && child.hit_test_shape(context, point, options) { result = Some(this); } @@ -2804,8 +2816,8 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { return result; } - // Check drawing. - if check_non_interactive { + // Check drawing, because this selects the current clip, it must have mouse enabled + if self.mouse_enabled() && check_non_interactive { let local_matrix = self.global_to_local_matrix(); let point = local_matrix * point; if self.0.read().drawing.hit_test(point, &local_matrix) { diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index 0b1a327b2eda..cb36b627ca33 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -208,6 +208,7 @@ swf_tests! { (as3_boolean_constr, "avm2/boolean_constr", 1), (as3_boolean_negation, "avm2/boolean_negation", 1), (as3_boolean_tostring, "avm2/boolean_tostring", 1), + (as3_button_hittest, "avm2/button_hittest", 1), (as3_bytearray_readobject_amf0, "avm2/bytearray_readobject_amf0", 1), (as3_bytearray_readobject_amf3, "avm2/bytearray_readobject_amf3", 1), (as3_bytearray_writeobject, "avm2/bytearray_writeobject", 1), diff --git a/tests/tests/swfs/avm2/button_hittest/output.txt b/tests/tests/swfs/avm2/button_hittest/output.txt new file mode 100644 index 000000000000..da9cb5363cc4 --- /dev/null +++ b/tests/tests/swfs/avm2/button_hittest/output.txt @@ -0,0 +1,2 @@ +// MP.hitTestObject(Btn1) +true diff --git a/tests/tests/swfs/avm2/button_hittest/test.fla b/tests/tests/swfs/avm2/button_hittest/test.fla new file mode 100644 index 000000000000..90cf34799526 Binary files /dev/null and b/tests/tests/swfs/avm2/button_hittest/test.fla differ diff --git a/tests/tests/swfs/avm2/button_hittest/test.swf b/tests/tests/swfs/avm2/button_hittest/test.swf new file mode 100644 index 000000000000..700bf0f97ab3 Binary files /dev/null and b/tests/tests/swfs/avm2/button_hittest/test.swf differ diff --git a/tests/tests/swfs/avm2/displayobject_hittestpoint/output.txt b/tests/tests/swfs/avm2/displayobject_hittestpoint/output.txt index 370673769827..0c539bc0eaca 100644 --- a/tests/tests/swfs/avm2/displayobject_hittestpoint/output.txt +++ b/tests/tests/swfs/avm2/displayobject_hittestpoint/output.txt @@ -43,3 +43,7 @@ false false //this.symbol1_1.hitTestPoint(50.0, 50.0, true); false +//this.symbol1.hitTestPoint(50.0, 50.0, true); (after removing from stage) +false +//this.symbol1.hitTestPoint(50.0, 50.0, false); (after removing from stage) +true \ No newline at end of file diff --git a/tests/tests/swfs/avm2/displayobject_hittestpoint/test.fla b/tests/tests/swfs/avm2/displayobject_hittestpoint/test.fla index 08f0957f76ea..478eb1ff443e 100644 Binary files a/tests/tests/swfs/avm2/displayobject_hittestpoint/test.fla and b/tests/tests/swfs/avm2/displayobject_hittestpoint/test.fla differ diff --git a/tests/tests/swfs/avm2/displayobject_hittestpoint/test.swf b/tests/tests/swfs/avm2/displayobject_hittestpoint/test.swf index e404bc03ad78..1d17a25effc0 100644 Binary files a/tests/tests/swfs/avm2/displayobject_hittestpoint/test.swf and b/tests/tests/swfs/avm2/displayobject_hittestpoint/test.swf differ