From 4e3e4e67caa371485a10338bfb430aa207a20a75 Mon Sep 17 00:00:00 2001 From: SamuelOsborne Date: Wed, 18 Dec 2024 18:17:20 +0100 Subject: [PATCH] fix: pointer enter / exit on pointer move working (#272) * fix: pointer enter / exit on pointer move working --- deps/modules/thorvg | 2 +- dotlottie-ffi/emscripten_bindings.cpp | 3 + dotlottie-ffi/src/dotlottie_player.udl | 4 + dotlottie-ffi/src/dotlottie_player_cpp.udl | 3 + dotlottie-ffi/src/ffi/mod.rs | 45 +++ dotlottie-rs/src/dotlottie_player.rs | 63 +++- .../src/state_machine_engine/actions/mod.rs | 43 +-- .../src/state_machine_engine/events/mod.rs | 22 ++ .../src/state_machine_engine/listeners/mod.rs | 9 +- dotlottie-rs/src/state_machine_engine/mod.rs | 297 ++++++++++++++---- .../src/state_machine_engine/states/mod.rs | 3 +- ...ter_enter_exit.json => pointer_enter.json} | 11 - .../listener_tests/pointer_exit.json | 257 +++++++++++++++ dotlottie-rs/tests/state_machine_actions.rs | 4 - dotlottie-rs/tests/state_machine_listeners.rs | 108 ++++++- .../src/bin/new-format/new-format.rs | 75 +++-- 16 files changed, 799 insertions(+), 150 deletions(-) rename dotlottie-rs/tests/fixtures/statemachines/listener_tests/{pointer_enter_exit.json => pointer_enter.json} (95%) create mode 100644 dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json diff --git a/deps/modules/thorvg b/deps/modules/thorvg index 3fdfe11c..babff908 160000 --- a/deps/modules/thorvg +++ b/deps/modules/thorvg @@ -1 +1 @@ -Subproject commit 3fdfe11cd7c2ce0dfdf4b11d088ad2ab9e17e14c +Subproject commit babff908cddcb95903ced747262cc595644c2ec5 diff --git a/dotlottie-ffi/emscripten_bindings.cpp b/dotlottie-ffi/emscripten_bindings.cpp index a4703d01..d9489678 100644 --- a/dotlottie-ffi/emscripten_bindings.cpp +++ b/dotlottie-ffi/emscripten_bindings.cpp @@ -179,6 +179,9 @@ EMSCRIPTEN_BINDINGS(DotLottiePlayer) .function("stateMachineSetNumericTrigger", &DotLottiePlayer::state_machine_set_numeric_trigger) .function("stateMachineSetStringTrigger", &DotLottiePlayer::state_machine_set_string_trigger) .function("stateMachineSetBooleanTrigger", &DotLottiePlayer::state_machine_set_boolean_trigger) + .function("stateMachineGetNumericTrigger", &DotLottiePlayer::state_machine_get_numeric_trigger) + .function("stateMachineGetStringTrigger", &DotLottiePlayer::state_machine_get_string_trigger) + .function("stateMachineGetBooleanTrigger", &DotLottiePlayer::state_machine_get_boolean_trigger) .function("getLayerBounds", &DotLottiePlayer::get_layer_bounds) .function("stateMachineCurrentState", &DotLottiePlayer::state_machine_current_state) .function("stateMachinePostPointerDownEvent", &DotLottiePlayer::state_machine_post_pointer_down_event) diff --git a/dotlottie-ffi/src/dotlottie_player.udl b/dotlottie-ffi/src/dotlottie_player.udl index 36f14ed7..c716566f 100644 --- a/dotlottie-ffi/src/dotlottie_player.udl +++ b/dotlottie-ffi/src/dotlottie_player.udl @@ -154,6 +154,7 @@ interface DotLottiePlayer { boolean state_machine_start(); boolean state_machine_stop(); sequence state_machine_framework_setup(); + i32 state_machine_post_event([ByRef] Event event); i32 state_machine_post_pointer_down_event(f32 x, f32 y); i32 state_machine_post_pointer_up_event(f32 x, f32 y); i32 state_machine_post_pointer_move_event(f32 x, f32 y); @@ -163,6 +164,9 @@ interface DotLottiePlayer { boolean state_machine_set_boolean_trigger([ByRef] string key, boolean value); boolean state_machine_set_string_trigger([ByRef] string key, [ByRef] string value); boolean state_machine_set_numeric_trigger([ByRef] string key, f32 value); + f32 state_machine_get_numeric_trigger([ByRef] string key); + string state_machine_get_string_trigger([ByRef] string key); + boolean state_machine_get_boolean_trigger([ByRef] string key); string state_machine_current_state(); boolean state_machine_subscribe(StateMachineObserver observer); boolean state_machine_unsubscribe([ByRef] StateMachineObserver observer); diff --git a/dotlottie-ffi/src/dotlottie_player_cpp.udl b/dotlottie-ffi/src/dotlottie_player_cpp.udl index 9cdad686..6674f560 100644 --- a/dotlottie-ffi/src/dotlottie_player_cpp.udl +++ b/dotlottie-ffi/src/dotlottie_player_cpp.udl @@ -100,4 +100,7 @@ interface DotLottiePlayer { boolean state_machine_set_boolean_trigger([ByRef] string key, boolean value); boolean state_machine_set_string_trigger([ByRef] string key, [ByRef] string value); boolean state_machine_set_numeric_trigger([ByRef] string key, f32 value); + f32 state_machine_get_numeric_trigger([ByRef] string key); + string state_machine_get_string_trigger([ByRef] string key); + boolean state_machine_get_boolean_trigger([ByRef] string key); }; diff --git a/dotlottie-ffi/src/ffi/mod.rs b/dotlottie-ffi/src/ffi/mod.rs index 9536c324..41e4c675 100644 --- a/dotlottie-ffi/src/ffi/mod.rs +++ b/dotlottie-ffi/src/ffi/mod.rs @@ -690,6 +690,51 @@ pub unsafe extern "C" fn dotlottie_state_machine_set_boolean_trigger( }) } +// #[no_mangle] +// pub unsafe extern "C" fn dotlottie_state_machine_get_boolean_trigger( +// ptr: *mut DotLottiePlayer, +// key: *const c_char, +// ) -> bool { +// exec_dotlottie_player_op(ptr, |dotlottie_player| { +// if let Ok(key) = DotLottieString::read(key) { +// dotlottie_player.state_machine_get_boolean_trigger(&key) +// } else { +// false +// } +// }) +// } + +// #[no_mangle] +// pub unsafe extern "C" fn dotlottie_state_machine_get_string_trigger( +// ptr: *mut DotLottiePlayer, +// key: *const c_char, +// result: *mut types::DotLottieString, +// ) -> i32 { +// exec_dotlottie_player_op(ptr, |dotlottie_player| { +// if let Ok(key) = DotLottieString::read(key) { +// dotlottie_player +// .state_machine_get_string_trigger(&key) +// .copy(result); +// } else { +// DOTLOTTIE_INVALID_PARAMETER +// } +// }) +// } + +// #[no_mangle] +// pub unsafe extern "C" fn dotlottie_state_machine_get_numeric_trigger( +// ptr: *mut DotLottiePlayer, +// key: *const c_char, +// ) -> f32 { +// exec_dotlottie_player_op(ptr, |dotlottie_player| { +// if let Ok(key) = DotLottieString::read(key) { +// dotlottie_player.state_machine_get_numeric_trigger(&key) +// } else { +// f32::MIN +// } +// }) +// } + #[no_mangle] pub unsafe extern "C" fn dotlottie_state_machine_framework_setup( ptr: *mut DotLottiePlayer, diff --git a/dotlottie-rs/src/dotlottie_player.rs b/dotlottie-rs/src/dotlottie_player.rs index 7f6af20b..e7a49fa9 100644 --- a/dotlottie-rs/src/dotlottie_player.rs +++ b/dotlottie-rs/src/dotlottie_player.rs @@ -1392,21 +1392,28 @@ impl DotLottiePlayer { listener_types.push("PointerDown".to_string()) } crate::listeners::Listener::PointerEnter { .. } => { - // Push PointerMove so that can determine if the pointer entered the layer - listener_types.push("PointerMove".to_string()) + // In case framework self detects pointer entering layers, push pointerExit + listener_types.push("PointerEnter".to_string()); + // We push PointerMove too so that we can do hit detection instead of the framework + listener_types.push("PointerMove".to_string()); } crate::listeners::Listener::PointerMove { .. } => { listener_types.push("PointerMove".to_string()) } crate::listeners::Listener::PointerExit { .. } => { - // Push PointerMove so that can determine if the pointer exited the layer - listener_types.push("PointerMove".to_string()) + // In case framework self detects pointer exiting layers, push pointerExit + listener_types.push("PointerExit".to_string()); + // We push PointerMove too so that we can do hit detection instead of the framework + listener_types.push("PointerMove".to_string()); } crate::listeners::Listener::OnComplete { .. } => { listener_types.push("OnComplete".to_string()) } } } + + listener_types.sort(); + listener_types.dedup(); listener_types } else { vec![] @@ -1518,6 +1525,53 @@ impl DotLottiePlayer { } } + pub fn state_machine_get_numeric_trigger(&self, key: &str) -> f32 { + match self.state_machine.try_read() { + Ok(state_machine) => { + if let Some(sm) = &*state_machine { + if let Some(value) = sm.get_numeric_trigger(key) { + return value; + } + } + } + Err(_) => { + return f32::MIN; + } + } + + f32::MIN + } + + pub fn state_machine_get_string_trigger(&self, key: &str) -> String { + match self.state_machine.try_write() { + Ok(mut state_machine) => { + if let Some(sm) = state_machine.as_mut() { + if let Some(value) = sm.get_string_trigger(key) { + return value; + } + } + } + Err(_) => return "".to_string(), + } + + "".to_string() + } + + pub fn state_machine_get_boolean_trigger(&self, key: &str) -> bool { + match self.state_machine.try_write() { + Ok(mut state_machine) => { + if let Some(sm) = state_machine.as_mut() { + if let Some(value) = sm.get_boolean_trigger(key) { + return value; + } + } + } + Err(_) => return false, + } + + false + } + pub fn state_machine_fire_event(&self, event: &str) { if let Ok(mut state_machine) = self.state_machine.try_write() { if let Some(sm) = state_machine.as_mut() { @@ -1810,6 +1864,7 @@ impl DotLottiePlayer { return "".to_string(); } } + "".to_string() } } diff --git a/dotlottie-rs/src/state_machine_engine/actions/mod.rs b/dotlottie-rs/src/state_machine_engine/actions/mod.rs index 67ad42d8..98f460a7 100644 --- a/dotlottie-rs/src/state_machine_engine/actions/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/actions/mod.rs @@ -23,6 +23,9 @@ pub trait ActionTrait { ) -> Result<(), StateMachineActionError>; } +// Todo: +// - FireCustomEvent +// - Reset #[derive(Debug, Deserialize, Clone)] #[serde(rename_all_fields = "camelCase")] #[serde(tag = "type")] @@ -30,9 +33,6 @@ pub enum Action { OpenUrl { url: String, }, - Theme { - theme_id: String, - }, Increment { trigger_name: String, value: Option, @@ -69,7 +69,7 @@ pub enum Action { value: f32, }, SetTheme { - theme_id: String, + value: String, }, SetFrame { value: StringNumber, @@ -77,7 +77,7 @@ pub enum Action { SetProgress { value: StringNumber, }, - SetSlot { + SetThemeData { value: String, }, FireCustomEvent { @@ -215,14 +215,14 @@ impl ActionTrait for Action { Ok(()) } - // Todo: Add support for setting a trigger to a trigger value Action::Fire { trigger_name } => { let _ = engine.fire(trigger_name, run_pipeline); Ok(()) } - Action::Reset { trigger_name } => { - todo!("Reset trigger {}", trigger_name); - // Ok(()) + Action::Reset { trigger_name: _ } => { + // todo!("Reset trigger {}", trigger_name); + + Ok(()) } Action::SetExpression { layer_name, @@ -239,15 +239,15 @@ impl ActionTrait for Action { ); // Ok(()) } - Action::SetTheme { theme_id } => { + Action::SetTheme { value } => { let read_lock = player.try_read(); match read_lock { Ok(player) => { - if !player.set_theme(theme_id) { + if !player.set_theme(value) { return Err(StateMachineActionError::ExecuteError(format!( "Error loading theme: {}", - theme_id + value ))); } } @@ -259,7 +259,7 @@ impl ActionTrait for Action { } Ok(()) } - Action::SetSlot { value } => { + Action::SetThemeData { value } => { let read_lock = player.read(); match read_lock { @@ -360,23 +360,6 @@ impl ActionTrait for Action { Ok(()) } - Action::Theme { theme_id } => { - let read_lock = player.read(); - - match read_lock { - Ok(player) => { - if !player.set_theme(theme_id) { - return Err(StateMachineActionError::ExecuteError( - "Error loading theme".to_string(), - )); - } - Ok(()) - } - Err(_) => Err(StateMachineActionError::ExecuteError( - "Error getting read lock on player".to_string(), - )), - } - } } } } diff --git a/dotlottie-rs/src/state_machine_engine/events/mod.rs b/dotlottie-rs/src/state_machine_engine/events/mod.rs index 4387a16b..176fec6e 100644 --- a/dotlottie-rs/src/state_machine_engine/events/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/events/mod.rs @@ -51,3 +51,25 @@ impl EventName for Event { } } } + +#[macro_export] +macro_rules! event_type_name { + (PointerDown) => { + "PointerDown" + }; + (PointerUp) => { + "PointerUp" + }; + (PointerMove) => { + "PointerMove" + }; + (PointerEnter) => { + "PointerEnter" + }; + (PointerExit) => { + "PointerExit" + }; + (OnComplete) => { + "OnComplete" + }; +} diff --git a/dotlottie-rs/src/state_machine_engine/listeners/mod.rs b/dotlottie-rs/src/state_machine_engine/listeners/mod.rs index a9d3eed4..4a2093e2 100644 --- a/dotlottie-rs/src/state_machine_engine/listeners/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/listeners/mod.rs @@ -35,7 +35,6 @@ pub enum Listener { actions: Vec, }, PointerMove { - layer_name: Option, actions: Vec, }, PointerExit { @@ -75,12 +74,8 @@ impl Display for Listener { .field("layer_name", layer_name) .field("action", actions) .finish(), - Self::PointerMove { - layer_name, - actions, - } => f + Self::PointerMove { actions } => f .debug_struct("PointerUp") - .field("layer_name", layer_name) .field("action", actions) .finish(), Self::PointerExit { @@ -109,7 +104,7 @@ impl ListenerTrait for Listener { Listener::PointerUp { layer_name, .. } => layer_name.clone(), Listener::PointerDown { layer_name, .. } => layer_name.clone(), Listener::PointerEnter { layer_name, .. } => layer_name.clone(), - Listener::PointerMove { layer_name, .. } => layer_name.clone(), + Listener::PointerMove { .. } => None, Listener::PointerExit { layer_name, .. } => layer_name.clone(), Listener::OnComplete { .. } => None, } diff --git a/dotlottie-rs/src/state_machine_engine/mod.rs b/dotlottie-rs/src/state_machine_engine/mod.rs index 3956da7e..0956b0e6 100644 --- a/dotlottie-rs/src/state_machine_engine/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/mod.rs @@ -24,8 +24,8 @@ use triggers::Trigger; use crate::state_machine_engine::listeners::Listener; use crate::{ - state_machine_state_check_pipeline, DotLottiePlayerContainer, EventName, PointerEvent, - StateMachineEngineSecurityError, + event_type_name, state_machine_state_check_pipeline, DotLottiePlayerContainer, EventName, + PointerEvent, StateMachineEngineSecurityError, }; use self::state_machine::state_machine_parse; @@ -78,8 +78,6 @@ pub enum StateMachineEngineError { } pub struct StateMachineEngine { - // pub listeners: Vec, - /* We keep references to the StateMachine's States. */ /* This prevents duplicating the data inside the engine. */ pub global_state: Option>, @@ -94,6 +92,10 @@ pub struct StateMachineEngine { event_trigger: HashMap, curr_event: Option, + // PointerEnter/PointerExit management + curr_entered_layer: String, + listened_layers: Vec<(String, String)>, + observers: RwLock>>, state_machine: StateMachine, @@ -116,6 +118,8 @@ impl Default for StateMachineEngine { boolean_trigger: HashMap::new(), event_trigger: HashMap::new(), curr_event: None, + curr_entered_layer: "".to_string(), + listened_layers: Vec::new(), status: StateMachineEngineStatus::Stopped, observers: RwLock::new(Vec::new()), state_history: Vec::new(), @@ -158,6 +162,8 @@ impl StateMachineEngine { boolean_trigger: HashMap::new(), event_trigger: HashMap::new(), curr_event: None, + curr_entered_layer: "".to_string(), + listened_layers: Vec::new(), status: StateMachineEngineStatus::Stopped, observers: RwLock::new(Vec::new()), state_history: Vec::new(), @@ -281,10 +287,10 @@ impl StateMachineEngine { let mut new_state_machine = StateMachineEngine::default(); if parsed_state_machine.is_err() { - // println!( - // "Error parsing state machine definition: {:?}", - // parsed_state_machine.err() - // ); + println!( + "Error parsing state machine definition: {:?}", + parsed_state_machine.err() + ); return Err(StateMachineEngineError::ParsingError { reason: "Failed to parse state machine definition".to_string(), }); @@ -329,6 +335,8 @@ impl StateMachineEngine { new_state_machine.player = Some(player.clone()); new_state_machine.state_machine = parsed_state_machine; + new_state_machine.init_listened_layers(); + // Run the security check pipeline let check_report = self.security_check_pipeline(&new_state_machine); @@ -398,9 +406,9 @@ impl StateMachineEngine { self.current_state.clone() } - pub fn listeners(&self, filter: Option) -> Vec<&Listener> { + pub fn listeners(&self, event_type_filter: Option) -> Vec<&Listener> { let mut listeners_clone = Vec::new(); - let filter = filter.unwrap_or("".to_string()); + let filter = event_type_filter.unwrap_or("".to_string()); if let Some(listeners) = &self.state_machine.listeners { for listener in listeners { @@ -420,6 +428,47 @@ impl StateMachineEngine { listeners_clone } + fn init_listened_layers(&mut self) { + let mut listeners = vec![]; + + listeners.extend(self.listeners(None)); + + let mut all_listened_layers: Vec<(String, String)> = vec![]; + + // Get every layer we listen to + for listener in listeners { + match listener { + Listener::PointerEnter { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerEnter).to_string())); + } + } + Listener::PointerExit { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerExit).to_string())) + } + } + Listener::PointerUp { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerUp).to_string())) + } + } + Listener::PointerDown { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerDown).to_string())) + } + } + _ => {} + } + } + + self.listened_layers = all_listened_layers; + } + fn get_state(&self, state_name: &str) -> Option> { if let Some(global_state) = &self.global_state { if global_state.name() == state_name { @@ -447,7 +496,14 @@ impl StateMachineEngine { let new_state = self.get_state(state_name); // We have a new state - if new_state.is_some() { + if let Some(new_state) = new_state { + // Emit transtion occured event + if let Ok(observers) = self.observers.try_read() { + for observer in observers.iter() { + observer.on_transition(self.get_current_state_name(), new_state.name()); + } + } + // Perform exit actions on the current state if there is one. if self.current_state.is_some() { let state = self.current_state.take(); @@ -466,8 +522,22 @@ impl StateMachineEngine { } } + // Emit transtion occured event + if let Ok(observers) = self.observers.try_read() { + for observer in observers.iter() { + observer.on_state_exit(self.get_current_state_name()); + } + } + // Assign the new state to the current_state - self.current_state = new_state; + self.current_state = Some(new_state); + + // Emit transtion occured event + if let Ok(observers) = self.observers.try_read() { + for observer in observers.iter() { + observer.on_state_entered(self.get_current_state_name()); + } + } // Perform entry actions // Execute its type of state @@ -622,7 +692,7 @@ impl StateMachineEngine { self.current_cycle_count += 1; if self.current_cycle_count >= self.max_cycle_count { - // println!("🚨 Infinite loop detected, ending state machine."); + println!("🚨 Infinite loop detected, ending state machine."); self.end(); return Err(StateMachineEngineError::InfiniteLoopError); } @@ -732,70 +802,129 @@ impl StateMachineEngine { None } - fn get_correct_pointer_actions_from_listener( - &self, - event: &Event, - layer_name: Option, - actions: &Vec, - x: f32, - y: f32, - ) -> Vec { - let mut actions_to_execute = Vec::new(); + fn manage_explicit_events(&mut self, event: &Event, x: f32, y: f32) { + let mut actions_to_execute: Vec = Vec::new(); + let listeners = self.listeners(None); + let mut entered_layer = self.curr_entered_layer.clone(); - // User defined a specific layer to check if hit - if let Some(layer) = layer_name { - // Check if the layer was hit, otherwise we ignore this listener - if let Some(rc_player) = &self.player { - let try_read_lock = rc_player.try_read(); - - if let Ok(player_container) = try_read_lock { - // If we have a pointer down event, we need to check if the pointer is outside of the layer - if let Event::PointerExit { x, y } = event { - if !player_container.hit_check(&layer, *x, *y) { - for action in actions { - actions_to_execute.push(action.clone()); - } - } - } else { - // Hit check will return true if the layer was hit - if player_container.hit_check(&layer, x, y) { - for action in actions { - actions_to_execute.push(action.clone()); + for listener in listeners { + if listener.type_name() == event.type_name() { + // User defined a specific layer to check if hit + if let Some(layer) = listener.get_layer_name() { + // Check if the layer was hit, otherwise we ignore this listener + if let Some(rc_player) = &self.player { + let try_read_lock = rc_player.try_read(); + + if let Ok(player_container) = try_read_lock { + // If we have a pointer down event, we need to check if the pointer is outside of the layer + if let Event::PointerExit { x, y } = event { + if self.curr_entered_layer == *layer + && !player_container.hit_check(&layer, *x, *y) + { + entered_layer = "".to_string(); + actions_to_execute.extend(listener.get_actions().clone()); + } + } else { + // Hit check will return true if the layer was hit + if player_container.hit_check(&layer, x, y) { + entered_layer = layer.clone(); + actions_to_execute.extend(listener.get_actions().clone()); + } } } } + } else { + // No layer was specified, add all actions + actions_to_execute.extend(listener.get_actions().clone()); } } - } else { - // No layer was specified, add all actions - for action in actions { - actions_to_execute.push(action.clone()); - } } - actions_to_execute + self.curr_entered_layer = entered_layer; + + for action in actions_to_execute { + // Run the pipeline because listeners are outside of the evaluation pipeline loop + if let Some(player_ref) = &self.player { + let _ = action.execute(self, player_ref.clone(), true); + } + } } - fn manage_pointer_event(&mut self, event: &Event, x: f32, y: f32) { - let listeners = self.listeners(Some(event.type_name())); + fn manage_cross_platform_events(&mut self, event: &Event, x: f32, y: f32) { + let mut actions_to_execute = Vec::new(); - if listeners.is_empty() { - return; + // Manage pointerMove listeners + if event.type_name() == event_type_name!(PointerMove).to_string() { + let pointer_move_listeners = + self.listeners(Some(event_type_name!(PointerMove).to_string())); + + for listener in pointer_move_listeners { + if let Listener::PointerMove { actions } = listener { + actions_to_execute.extend(actions.clone()); + } + } } - let mut actions_to_execute = Vec::new(); + // Check if we've moved the pointer over any of the pointerEnter/Exit listeners + // If we've changed layers, perform exit actions + // If we don't hit any layers, perform exit actions + if let Some(rc_player) = &self.player { + let try_read_lock = rc_player.try_read(); + + if let Ok(player_container) = try_read_lock { + let mut hit = false; + let old_layer = self.curr_entered_layer.clone(); + + // Loop through all layers we're listening to + for (layer, event_name) in &self.listened_layers { + // We're only interested in the listened layers that need enter / exit event + if event_name == event_type_name!(PointerEnter) + || event_name == event_type_name!(PointerExit) + { + if player_container.hit_check(&layer, x, y) { + hit = true; - for listener in listeners { - let action_vec = self.get_correct_pointer_actions_from_listener( - event, - listener.get_layer_name(), - listener.get_actions(), - x, - y, - ); + // If it's that same current layer, do nothing + if self.curr_entered_layer == *layer { + break; + } + + self.curr_entered_layer = layer.to_string(); - // Action vec was moved in to action_to_execute, it can't be used again - actions_to_execute.extend(action_vec); + // Get all pointer_enter listeners + let pointer_enter_listeners = + self.listeners(Some(event_type_name!(PointerEnter).to_string())); + + // Add their actions if their layer name matches the current layer name in loop + for listener in pointer_enter_listeners { + if let Some(listener_layer_name) = listener.get_layer_name() { + if *listener_layer_name == self.curr_entered_layer { + actions_to_execute.extend(listener.get_actions().clone()); + } + } + } + } + } + } + + // We didn't hit any listened layers + if !hit { + self.curr_entered_layer = "".to_string(); + + let pointer_exit_listeners = + self.listeners(Some(event_type_name!(PointerExit).to_string())); + + // Add the actions of every PointerExit listener that depended on the layer we've just exited + for listener in pointer_exit_listeners { + if let Some(listener_layer_name) = listener.get_layer_name() { + // We've exited the desired layer, add its actions to execute + if *listener_layer_name == old_layer { + actions_to_execute.extend(listener.get_actions().clone()); + } + } + } + } + } } for action in actions_to_execute { @@ -806,6 +935,45 @@ impl StateMachineEngine { } } + // How pointer event are managed depending on the listener's event and the sent event. + // Since we can't detect PointerMove on mobile, we can still check PointerDown/Up and see if it's entered or exited a layer. + // + // | -------------------------------- | ----------------------------- | ----------- | + // | Listener Event type | Web | Mobile | + // | -------------------------------- | ----------------------------- | ----------- | + // | PointerDown (No Layer) | PointerDown | PointerDown | + // | PointerDown (With Layer) | PointerDown | PointerDown | + // | PointerUp (No Layer) | PointerUp | PointerUp | + // | PointerUp (With Layer) | PointerUp | PointerUp | + // | PointerMove (No Layer) | PointerMove | PointerDown | + // | PointerEnter (No Layer) | PointerEnter | | + // | PointerEnter (With Layer) | PointerMove + PointerEnter | PointerDown | + // | PointerExit (No Layer) | PointerExit | PointerUp | + // | PointerExit (With Layer) | PointerMove + PointerExit | | + // | ---------------------------------|-------------------------------| ----------- | + + // Notes: + // Atm, PointerEnter/Exit without layers is not supported on mobile. + // This is because if we allow pointerDown to activate PointerEnter/Exit, + // It would override PointerDown with layers, which is not a great experience. + // With the current setup we can have an action that happens when the cursor is over the canvas + // and another action that happens when the cursor is over a specific layer. + fn manage_pointer_event(&mut self, event: &Event, x: f32, y: f32) { + // This will handle PointerDown, PointerUp, PointerEnter, PointerExit + if event.type_name() != "PointerMove" { + self.manage_explicit_events(event, x, y); + } + + // We're left with PointerMove + // Also performe checks for PointerDown and PointerUp, a mobile framework could of sent them and validate PointerEnter/Exit listeners. + if event.type_name() == "PointerMove" + || event.type_name() == "PointerDown" + || event.type_name() == "PointerUp" + { + self.manage_cross_platform_events(event, x, y); + } + } + fn manage_on_complete_event(&mut self, event: &Event) { let listeners = self.listeners(Some(event.type_name())); @@ -823,10 +991,7 @@ impl StateMachineEngine { { if let Some(current_state) = &self.current_state { if current_state.name() == *state_name { - for action in actions { - // Clones the reference to action - actions_to_execute.push(action.clone()); - } + actions_to_execute.extend(actions.clone()); } } } diff --git a/dotlottie-rs/src/state_machine_engine/states/mod.rs b/dotlottie-rs/src/state_machine_engine/states/mod.rs index a049caec..52b5043c 100644 --- a/dotlottie-rs/src/state_machine_engine/states/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/states/mod.rs @@ -121,7 +121,8 @@ impl StateTrait for State { let size = player_read.size(); // Todo compare against currently loaded animation - if !animation_id.is_empty() { + if !animation_id.is_empty() && player_read.active_animation_id() != *animation_id + { player_read.load_animation(animation_id, size.0, size.1); } diff --git a/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter_exit.json b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter.json similarity index 95% rename from dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter_exit.json rename to dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter.json index ad5bdb6c..ddba6181 100644 --- a/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter_exit.json +++ b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter.json @@ -190,17 +190,6 @@ "value": 5 } ] - }, - { - "type": "PointerExit", - "layerName": "star5", - "actions": [ - { - "type": "SetNumeric", - "triggerName": "rating", - "value": 0 - } - ] } ], "triggers": [ diff --git a/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json new file mode 100644 index 00000000..b5800820 --- /dev/null +++ b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json @@ -0,0 +1,257 @@ +{ + "descriptor": { + "id": "star-rating", + "initial": "global" + }, + "states": [ + { + "name": "global", + "type": "GlobalState", + "animationId": "", + "transitions": [ + { + "type": "Transition", + "toState": "star_0", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 0 + } + ] + }, + { + "type": "Transition", + "toState": "star_1", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 1 + } + ] + }, + { + "type": "Transition", + "toState": "star_2", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 2 + } + ] + }, + { + "type": "Transition", + "toState": "star_3", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 3 + } + ] + }, + { + "type": "Transition", + "toState": "star_4", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 4 + } + ] + }, + { + "type": "Transition", + "toState": "star_5", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 5 + } + ] + } + ] + }, + { + "type": "PlaybackState", + "name": "star_0", + "animationId": "", + "autoplay": true, + "segment": "star_0", + "transitions": [], + "entryActions": [] + }, + { + "type": "PlaybackState", + "name": "star_1", + "animationId": "", + "autoplay": true, + "segment": "star_1", + "transitions": [], + "entryActions": [] + }, + { + "type": "PlaybackState", + "name": "star_2", + "animationId": "", + "autoplay": true, + "segment": "star_2", + "transitions": [], + "entryActions": [] + }, + { + "type": "PlaybackState", + "name": "star_3", + "animationId": "", + "autoplay": true, + "segment": "star_3", + "transitions": [] + }, + { + "type": "PlaybackState", + "name": "star_4", + "animationId": "", + "autoplay": true, + "segment": "star_4", + "transitions": [] + }, + { + "type": "PlaybackState", + "name": "star_5", + "animationId": "", + "autoplay": true, + "segment": "star_5", + "transitions": [] + } + ], + "listeners": [ + { + "type": "PointerEnter", + "layerName": "star1", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 1 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star1", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 0 + } + ] + }, + { + "type": "PointerEnter", + "layerName": "star2", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 2 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star2", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 0 + } + ] + }, + { + "type": "PointerEnter", + "layerName": "star3", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 3 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star3", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 0 + } + ] + }, + { + "type": "PointerEnter", + "layerName": "star4", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 4 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star4", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 0 + } + ] + }, + { + "type": "PointerEnter", + "layerName": "star5", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 5 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star5", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 0 + } + ] + } + ], + "triggers": [ + { + "type": "Numeric", + "name": "rating", + "value": 0 + } + ] +} \ No newline at end of file diff --git a/dotlottie-rs/tests/state_machine_actions.rs b/dotlottie-rs/tests/state_machine_actions.rs index de363c61..1765131e 100644 --- a/dotlottie-rs/tests/state_machine_actions.rs +++ b/dotlottie-rs/tests/state_machine_actions.rs @@ -271,10 +271,6 @@ mod tests { } // TODO - #[test] - fn theme_action() { // todo!() - } - #[test] fn fire_custom_event() { // todo!() diff --git a/dotlottie-rs/tests/state_machine_listeners.rs b/dotlottie-rs/tests/state_machine_listeners.rs index 18baf230..6a90d11e 100644 --- a/dotlottie-rs/tests/state_machine_listeners.rs +++ b/dotlottie-rs/tests/state_machine_listeners.rs @@ -87,9 +87,8 @@ mod tests { } #[test] - pub fn pointer_enter_exit_test() { - let global_state = - include_str!("fixtures/statemachines/listener_tests/pointer_enter_exit.json"); + pub fn pointer_enter_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_enter.json"); let player = DotLottiePlayer::new(Config::default()); player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); let l = player.state_machine_load_data(global_state); @@ -120,18 +119,115 @@ mod tests { player.state_machine_post_event(&Event::PointerEnter { x: 75.0, y: 45.0 }); let curr_state_name = get_current_state_name(&player); assert_eq!(curr_state_name, "star_5"); + } + + #[test] + pub fn pointer_enter_via_move_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_enter.json"); + let player = DotLottiePlayer::new(Config::default()); + player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); + let l = player.state_machine_load_data(global_state); + let s = player.state_machine_start(); + + assert!(l); + assert!(s); - // This should keep rating at 5 since we're still in the last star - player.state_machine_post_event(&Event::PointerExit { x: 75.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 15.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_1"); + + player.state_machine_post_event(&Event::PointerMove { x: 30.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_2"); + + player.state_machine_post_event(&Event::PointerMove { x: 45.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_3"); + + player.state_machine_post_event(&Event::PointerMove { x: 60.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_4"); + + player.state_machine_post_event(&Event::PointerMove { x: 75.0, y: 45.0 }); let curr_state_name = get_current_state_name(&player); assert_eq!(curr_state_name, "star_5"); + } + + #[test] + pub fn pointer_exit_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_exit.json"); + let player = DotLottiePlayer::new(Config::default()); + player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); + let l = player.state_machine_load_data(global_state); + let s = player.state_machine_start(); - // This should no keep rating at 5 since we're not in the last star + assert!(l); + assert!(s); + + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerEnter { x: 15.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_1"); player.state_machine_post_event(&Event::PointerExit { x: 0.0, y: 0.0 }); let curr_state_name = get_current_state_name(&player); assert_eq!(curr_state_name, "star_0"); } + #[test] + pub fn pointer_exit_via_move_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_exit.json"); + let player = DotLottiePlayer::new(Config::default()); + player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); + let l = player.state_machine_load_data(global_state); + let s = player.state_machine_start(); + + assert!(l); + assert!(s); + + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 15.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_1"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 30.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_2"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 45.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_3"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 60.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_4"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 75.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_5"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + } + #[test] pub fn pointer_move_test() { let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_move.json"); diff --git a/examples/demo-state-machine/src/bin/new-format/new-format.rs b/examples/demo-state-machine/src/bin/new-format/new-format.rs index 42dfd70b..57062acb 100644 --- a/examples/demo-state-machine/src/bin/new-format/new-format.rs +++ b/examples/demo-state-machine/src/bin/new-format/new-format.rs @@ -6,8 +6,8 @@ use std::io::Read; use std::sync::{Arc, RwLock}; use std::time::Instant; -pub const WIDTH: usize = 125; -pub const HEIGHT: usize = 125; +pub const WIDTH: usize = 500; +pub const HEIGHT: usize = 500; pub const STATE_MACHINE_NAME: &str = "rating"; pub const ANIMATION_NAME: &str = "star_marked"; @@ -101,32 +101,64 @@ fn main() { let mut mx = 0.0; let mut my = 0.0; + let mut oo = false; + + let mut left_down = false; + + let mut entered = false; while window.is_open() && !window.is_key_down(Key::Escape) { - let left_down = window.get_mouse_down(MouseButton::Left); - if left_down { - // if left_down { - window.get_mouse_pos(minifb::MouseMode::Pass).map(|mouse| { - if mouse.0 != mx || mouse.1 != my { - mx = mouse.0; - my = mouse.1; + let tmp = window.get_mouse_down(MouseButton::Left); + let mouse_pos = window.get_mouse_pos(minifb::MouseMode::Pass); + mouse_pos.map(|mouse| { + if mouse.0 != mx || mouse.1 != my { + mx = mouse.0; + my = mouse.1; + } + + if mx >= 0.0 && mx <= WIDTH as f32 && my >= 0.0 && my <= HEIGHT as f32 { + println!("Sending pointer enter"); + if !entered { + let event = Event::PointerEnter { x: mx, y: my }; - let event = Event::PointerDown { x: mx, y: my }; + let p = &mut *locked_player.write().unwrap(); + let _m = p.state_machine_post_event(&event); + } + entered = true; + } else { + println!("Sending pointer Exit"); + if entered { + let event = Event::PointerExit { x: mx, y: my }; let p = &mut *locked_player.write().unwrap(); let _m = p.state_machine_post_event(&event); } - }); - // Get the coordinates - // let (x, y) = window.get_mouse_pos(minifb::MouseMode::Pass).unwrap(); + entered = false; + } + }); - // let pointer_event = Event::PointerDown { x, y }; + if !tmp && left_down { + let event = Event::PointerUp { x: mx, y: my }; - // let p = &mut *locked_player.write().unwrap(); + println!("Sending pointer up"); + let p = &mut *locked_player.write().unwrap(); + let _m = p.state_machine_post_event(&event); + } + + left_down = tmp; - // println!("PointerDown -> x: {}, y: {}", x, y); + // left_down = window.get_mouse_down(MouseButton::Left); + if left_down { + let event = Event::PointerDown { x: mx, y: my }; - // p.post_event(&pointer_event); + println!("Sending pointer down"); + let p = &mut *locked_player.write().unwrap(); + let _m = p.state_machine_post_event(&event); + } else { + println!("Sending pointer move {} {}", mx, my); + let event = Event::PointerMove { x: mx, y: my }; + let p = &mut *locked_player.write().unwrap(); + let _m = p.state_machine_post_event(&event); } timer.tick(&*locked_player.read().unwrap()); @@ -141,9 +173,12 @@ fn main() { if window.is_key_pressed(Key::Enter, minifb::KeyRepeat::No) { let p = &mut *locked_player.write().unwrap(); - rating += 1.0; - println!("current state: {}", p.state_machine_current_state()); - p.state_machine_set_numeric_trigger("rating", rating); + oo = !oo; + p.state_machine_set_boolean_trigger("OnOffSwitch", oo); + + // rating += 1.0; + // println!("current state: {}", p.state_machine_current_state()); + // p.state_machine_set_numeric_trigger("rating", rating); } let p = &mut *locked_player.write().unwrap();