use std::sync::{Arc, Mutex};
use bevy::prelude::*;
pub use noesis_runtime::events::{EventArgs, RoutedEvent};
pub use noesis_runtime::view::{Key, MouseButton};
use crate::render::{NoesisRenderState, NoesisSet, ReapOnRemove, add_bridge_reap};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct RoutedEventSnapshot {
pub position: Option<(f32, f32)>,
pub mouse_button: Option<MouseButton>,
pub wheel_delta: Option<i32>,
pub key: Option<Key>,
pub text_char: Option<char>,
pub new_size: Option<(f32, f32)>,
}
impl RoutedEventSnapshot {
#[must_use]
pub(crate) fn capture(args: &EventArgs) -> Self {
Self {
position: args.position(),
mouse_button: args.mouse_button(),
wheel_delta: args.wheel_delta(),
key: args.key(),
text_char: args.text_char(),
new_size: args.new_size(),
}
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisRoutedEvent {
pub view: Entity,
pub name: String,
pub event: RoutedEvent,
pub args: RoutedEventSnapshot,
}
#[derive(EntityEvent, Debug, Clone)]
pub struct UiRoutedEvent {
pub entity: Entity,
pub view: Entity,
pub name: String,
pub event: RoutedEvent,
pub args: RoutedEventSnapshot,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EventWatchEntry {
pub name: String,
pub event: RoutedEvent,
pub mark_handled: bool,
pub handled_too: bool,
pub target: Option<Entity>,
}
impl EventWatchEntry {
pub fn new(name: impl Into<String>, event: RoutedEvent) -> Self {
Self {
name: name.into(),
event,
mark_handled: false,
handled_too: false,
target: None,
}
}
#[must_use]
pub fn mark_handled(mut self) -> Self {
self.mark_handled = true;
self
}
#[must_use]
pub fn handled_too(mut self) -> Self {
self.handled_too = true;
self
}
#[must_use]
pub fn target(mut self, target: Entity) -> Self {
self.target = Some(target);
self
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisEventWatch {
pub entries: Vec<EventWatchEntry>,
}
impl NoesisEventWatch {
pub fn new(entries: impl IntoIterator<Item = EventWatchEntry>) -> Self {
Self {
entries: entries.into_iter().collect(),
}
}
}
#[derive(Resource, Clone, Default)]
pub struct SharedRoutedEventQueue(
pub(crate) Arc<Mutex<Vec<(Entity, Entity, String, RoutedEvent, RoutedEventSnapshot)>>>,
);
impl SharedRoutedEventQueue {
pub(crate) fn push(
&self,
view: Entity,
target: Entity,
name: String,
event: RoutedEvent,
args: RoutedEventSnapshot,
) {
self.0
.lock()
.expect("SharedRoutedEventQueue poisoned")
.push((view, target, name, event, args));
}
fn drain(&self) -> Vec<(Entity, Entity, String, RoutedEvent, RoutedEventSnapshot)> {
let mut guard = self.0.lock().expect("SharedRoutedEventQueue poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn drain_routed_event_queue(
queue: Res<SharedRoutedEventQueue>,
mut messages: MessageWriter<NoesisRoutedEvent>,
mut commands: Commands,
) {
for (view, target, name, event, args) in queue.drain() {
messages.write(NoesisRoutedEvent {
view,
name: name.clone(),
event,
args: args.clone(),
});
commands.trigger(UiRoutedEvent {
entity: target,
view,
name,
event,
args,
});
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_event_subscriptions(
views: Query<(Entity, &NoesisEventWatch)>,
queue: Res<SharedRoutedEventQueue>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, watch) in &views {
state.sync_event_subscriptions_for(entity, &watch.entries, &queue);
}
}
impl ReapOnRemove for NoesisEventWatch {
fn reap(state: &mut NoesisRenderState, entity: Entity) {
state.reap_event_watch_for(entity);
}
}
pub struct NoesisRoutedEventsPlugin;
impl Plugin for NoesisRoutedEventsPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisRoutedEvent>()
.insert_resource(SharedRoutedEventQueue::default())
.add_systems(PreUpdate, drain_routed_event_queue)
.add_systems(
PostUpdate,
sync_event_subscriptions.in_set(NoesisSet::Apply),
);
add_bridge_reap::<NoesisEventWatch>(app);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shared_routed_queue_drain_takes_all_and_resets() {
let q = SharedRoutedEventQueue::default();
let v = Entity::PLACEHOLDER;
let t = Entity::PLACEHOLDER;
q.push(
v,
t,
"Alpha".into(),
RoutedEvent::MouseDown,
RoutedEventSnapshot::default(),
);
q.push(
v,
t,
"Beta".into(),
RoutedEvent::MouseUp,
RoutedEventSnapshot::default(),
);
let drained = q.drain();
assert_eq!(drained.len(), 2);
assert_eq!(drained[0].2, "Alpha");
assert_eq!(drained[0].3, RoutedEvent::MouseDown);
assert_eq!(drained[1].3, RoutedEvent::MouseUp);
assert!(q.drain().is_empty());
}
#[test]
fn event_watch_entry_builders() {
let e = EventWatchEntry::new("Box", RoutedEvent::PreviewKeyDown)
.mark_handled()
.handled_too();
assert_eq!(e.name, "Box");
assert_eq!(e.event, RoutedEvent::PreviewKeyDown);
assert!(e.mark_handled);
assert!(e.handled_too);
let d = EventWatchEntry::new("Target", RoutedEvent::MouseDown);
assert!(!d.mark_handled);
assert!(!d.handled_too);
}
#[test]
fn event_watch_constructor_collects_entries() {
let w = NoesisEventWatch::new([
EventWatchEntry::new("A", RoutedEvent::MouseEnter),
EventWatchEntry::new("B", RoutedEvent::MouseLeave),
]);
assert_eq!(w.entries.len(), 2);
assert_eq!(w.entries[0].name, "A");
assert_eq!(w.entries[1].event, RoutedEvent::MouseLeave);
}
}