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, video::VideoBackend,
}; };
use crate::context_menu::ContextMenuState; 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::external::ExternalInterface;
use crate::focus_tracker::FocusTracker; use crate::focus_tracker::FocusTracker;
use crate::library::Library; use crate::library::Library;
@ -98,10 +98,10 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> {
pub stage: Stage<'gc>, pub stage: Stage<'gc>,
/// The display object that the mouse is currently hovering over. /// 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. /// 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. /// The input manager, tracking keys state.
pub input: &'a InputManager, pub input: &'a InputManager,

View File

@ -37,7 +37,6 @@ mod text;
mod video; mod video;
use crate::avm1::activation::Activation; use crate::avm1::activation::Activation;
use crate::backend::ui::MouseCursor;
pub use crate::display_object::container::{ pub use crate::display_object::container::{
DisplayObjectContainer, Lists, TDisplayObjectContainer, DisplayObjectContainer, Lists, TDisplayObjectContainer,
}; };
@ -1279,16 +1278,6 @@ pub trait TDisplayObject<'gc>:
self.hit_test_bounds(pos) 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( fn post_instantiation(
&self, &self,
context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
@ -1323,11 +1312,6 @@ pub trait TDisplayObject<'gc>:
true 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 /// Obtain the top-most non-Stage parent of the display tree hierarchy, if
/// a suitable object exists. If none such object exists, this function /// a suitable object exists. If none such object exists, this function
/// yields an AVM1 error (which shouldn't happen in normal usage). /// 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 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> { fn object(&self) -> Value<'gc> {
self.0 self.0
.read() .read()
@ -549,6 +517,40 @@ impl<'gc> TInteractiveObject<'gc> for Avm1Button<'gc> {
ClipEventResult::NotHandled 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> { impl<'gc> Avm1ButtonData<'gc> {

View File

@ -640,44 +640,6 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> {
false 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> { fn object2(&self) -> Avm2Value<'gc> {
self.0 self.0
.read() .read()
@ -808,6 +770,46 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> {
self.event_dispatch_to_avm2(context, event) 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> { impl<'gc> Avm2ButtonData<'gc> {

View File

@ -1762,27 +1762,6 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> {
self.set_removed(context.gc_context, true); 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) { fn on_focus_changed(&self, gc_context: MutationContext<'gc, '_>, focused: bool) {
let mut text = self.0.write(gc_context); let mut text = self.0.write(gc_context);
text.has_focus = focused; text.has_focus = focused;
@ -1839,6 +1818,27 @@ impl<'gc> TInteractiveObject<'gc> for EditText<'gc> {
ClipEventResult::Handled 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. /// Static data shared between all instances of a text object.

View File

@ -1,6 +1,7 @@
//! Interactive object enumtrait //! Interactive object enumtrait
use crate::avm2::{Avm2, Event as Avm2Event, EventData as Avm2EventData, Value as Avm2Value}; use crate::avm2::{Avm2, Event as Avm2Event, EventData as Avm2EventData, Value as Avm2Value};
use crate::backend::ui::MouseCursor;
use crate::context::UpdateContext; use crate::context::UpdateContext;
use crate::display_object::avm1_button::Avm1Button; use crate::display_object::avm1_button::Avm1Button;
use crate::display_object::avm2_button::Avm2Button; use crate::display_object::avm2_button::Avm2Button;
@ -16,6 +17,7 @@ use gc_arena::{Collect, MutationContext};
use ruffle_macros::enum_trait_object; use ruffle_macros::enum_trait_object;
use std::cell::{Ref, RefMut}; use std::cell::{Ref, RefMut};
use std::fmt::Debug; use std::fmt::Debug;
use swf::Twips;
bitflags! { bitflags! {
/// Boolean state flags used by `InteractiveObject`. /// Boolean state flags used by `InteractiveObject`.
@ -250,12 +252,40 @@ pub trait TInteractiveObject<'gc>:
self.event_dispatch(context, event) 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> { impl<'gc> InteractiveObject<'gc> {
pub fn ptr_eq<T: TInteractiveObject<'gc>>(a: T, b: T) -> bool { pub fn ptr_eq<T: TInteractiveObject<'gc>>(a: T, b: T) -> bool {
a.as_displayobject().as_ptr() == b.as_displayobject().as_ptr() 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> { impl<'gc> PartialEq for InteractiveObject<'gc> {

View File

@ -1924,84 +1924,6 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
false 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>> { fn as_movie_clip(&self) -> Option<MovieClip<'gc>> {
Some(*self) Some(*self)
} }
@ -2210,6 +2132,86 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> {
handled 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> { impl<'gc> MovieClipData<'gc> {

View File

@ -19,8 +19,8 @@ use crate::config::Letterbox;
use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext};
use crate::context_menu::{ContextMenuCallback, ContextMenuItem, ContextMenuState}; use crate::context_menu::{ContextMenuCallback, ContextMenuItem, ContextMenuState};
use crate::display_object::{ use crate::display_object::{
EditText, MorphShape, MovieClip, Stage, StageAlign, StageDisplayState, StageQuality, EditText, InteractiveObject, MorphShape, MovieClip, Stage, StageAlign, StageDisplayState,
StageScaleMode, TInteractiveObject, StageQuality, StageScaleMode, TInteractiveObject,
}; };
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent}; use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent};
use crate::external::Value as ExternalValue; use crate::external::Value as ExternalValue;
@ -65,10 +65,10 @@ struct GcRootData<'gc> {
stage: Stage<'gc>, stage: Stage<'gc>,
/// The display object that the mouse is currently hovering over. /// 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. /// 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. /// The object being dragged via a `startDrag` action.
drag_object: Option<DragObject<'gc>>, drag_object: Option<DragObject<'gc>>,
@ -1087,9 +1087,14 @@ impl Player {
.iter_depth_list() .iter_depth_list()
.rev() .rev()
.find_map(|(_depth, level)| { .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); display_object.set_visible(context.gc_context, was_visible);
} }
} }
@ -1110,37 +1115,41 @@ impl Player {
.iter_depth_list() .iter_depth_list()
.rev() .rev()
.find_map(|(_depth, level)| { .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(); Default::default();
// Cancel hover if an object is removed from the stage. // Cancel hover if an object is removed from the stage.
if let Some(hovered) = context.mouse_over_object { if let Some(hovered) = context.mouse_over_object {
if hovered.removed() { if hovered.as_displayobject().removed() {
context.mouse_over_object = None; context.mouse_over_object = None;
} }
} }
if let Some(pressed) = context.mouse_down_object { if let Some(pressed) = context.mouse_down_object {
if pressed.removed() { if pressed.as_displayobject().removed() {
context.mouse_down_object = None; context.mouse_down_object = None;
} }
} }
let cur_over_object = context.mouse_over_object; let cur_over_object = context.mouse_over_object;
// Check if a new object has been hovered over. // 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 // If the mouse button is down, the object the user clicked on grabs the focus
// and fires "drag" events. Other objects are ignroed. // and fires "drag" events. Other objects are ignroed.
if context.input.is_mouse_down() { if context.input.is_mouse_down() {
context.mouse_over_object = new_over_object; context.mouse_over_object = new_over_object;
if let Some(down_object) = context.mouse_down_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. // Dragged from outside the clicked object to the inside.
events.push((down_object, ClipEvent::DragOut)); events.push((down_object, ClipEvent::DragOut));
} else if DisplayObject::option_ptr_eq( } else if InteractiveObject::option_ptr_eq(
context.mouse_down_object, context.mouse_down_object,
new_over_object, new_over_object,
) { ) {
@ -1155,7 +1164,7 @@ impl Player {
events.push(( events.push((
cur_over_object, cur_over_object,
ClipEvent::RollOut { ClipEvent::RollOut {
to: new_over_object.and_then(|d| d.as_interactive()), to: new_over_object,
}, },
)); ));
} }
@ -1165,7 +1174,7 @@ impl Player {
events.push(( events.push((
new_over_object, new_over_object,
ClipEvent::RollOver { ClipEvent::RollOver {
from: cur_over_object.and_then(|d| d.as_interactive()), from: cur_over_object,
}, },
)); ));
} else { } else {
@ -1192,7 +1201,7 @@ impl Player {
events.push((context.stage.into(), ClipEvent::MouseUpInside)); 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_down_object,
context.mouse_over_object, context.mouse_over_object,
); );
@ -1216,7 +1225,7 @@ impl Player {
events.push(( events.push((
over_object, over_object,
ClipEvent::RollOver { ClipEvent::RollOver {
from: cur_over_object.and_then(|d| d.as_interactive()), from: cur_over_object,
}, },
)); ));
} else { } else {
@ -1232,10 +1241,8 @@ impl Player {
false false
} else { } else {
for (object, event) in events { for (object, event) in events {
if !object.removed() { if !object.as_displayobject().removed() {
if let Some(interactive) = object.as_interactive() { object.handle_clip_event(context, event);
interactive.handle_clip_event(context, event);
}
} }
} }
true true