use std::sync::{Arc, Mutex};
use bevy::prelude::*;
pub use noesis_runtime::view::Key;
use crate::render::{NoesisRenderState, NoesisSet, ReapOnRemove, add_bridge_reap};
#[derive(Message, Debug, Clone)]
pub struct NoesisClicked {
pub view: Entity,
pub name: String,
}
#[derive(EntityEvent, Debug, Clone)]
pub struct UiClicked {
pub entity: Entity,
pub view: Entity,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClickWatchEntry {
pub name: String,
pub target: Option<Entity>,
}
impl ClickWatchEntry {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
target: None,
}
}
#[must_use]
pub fn target(mut self, target: Entity) -> Self {
self.target = Some(target);
self
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisClickWatch {
pub entries: Vec<ClickWatchEntry>,
}
impl NoesisClickWatch {
pub fn new(names: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
entries: names.into_iter().map(ClickWatchEntry::new).collect(),
}
}
pub fn from_entries(entries: impl IntoIterator<Item = ClickWatchEntry>) -> Self {
Self {
entries: entries.into_iter().collect(),
}
}
pub fn watch(&mut self, name: impl Into<String>) -> &mut Self {
self.entries.push(ClickWatchEntry::new(name));
self
}
pub fn extend_names(
&mut self,
names: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.entries
.extend(names.into_iter().map(ClickWatchEntry::new));
self
}
}
#[derive(Resource, Clone, Default)]
pub struct SharedClickQueue(pub(crate) Arc<Mutex<Vec<(Entity, Entity, String)>>>);
impl SharedClickQueue {
pub(crate) fn push(&self, view: Entity, target: Entity, name: String) {
self.0
.lock()
.expect("SharedClickQueue poisoned")
.push((view, target, name));
}
fn drain(&self) -> Vec<(Entity, Entity, String)> {
let mut guard = self.0.lock().expect("SharedClickQueue poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn drain_click_queue(
queue: Res<SharedClickQueue>,
mut messages: MessageWriter<NoesisClicked>,
mut commands: Commands,
) {
for (view, target, name) in queue.drain() {
messages.write(NoesisClicked {
view,
name: name.clone(),
});
commands.trigger(UiClicked {
entity: target,
view,
name,
});
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_click_subscriptions(
views: Query<(Entity, &NoesisClickWatch)>,
queue: Res<SharedClickQueue>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, watch) in &views {
state.sync_click_subscriptions_for(entity, &watch.entries, &queue);
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisKeyDown {
pub view: Entity,
pub name: String,
pub key: Key,
}
#[derive(EntityEvent, Debug, Clone)]
pub struct UiKeyDown {
pub entity: Entity,
pub view: Entity,
pub name: String,
pub key: Key,
}
#[derive(Clone, Debug)]
pub struct KeyDownWatchEntry {
pub name: String,
pub swallow: Vec<Key>,
pub target: Option<Entity>,
}
impl KeyDownWatchEntry {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
swallow: Vec::new(),
target: None,
}
}
#[must_use]
pub fn swallow(mut self, key: Key) -> Self {
self.swallow.push(key);
self
}
#[must_use]
pub fn swallow_all<I>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = Key>,
{
self.swallow.extend(keys);
self
}
#[must_use]
pub fn target(mut self, target: Entity) -> Self {
self.target = Some(target);
self
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisKeyDownWatch {
pub entries: Vec<KeyDownWatchEntry>,
}
impl NoesisKeyDownWatch {
pub fn new(entries: impl IntoIterator<Item = KeyDownWatchEntry>) -> Self {
Self {
entries: entries.into_iter().collect(),
}
}
}
#[derive(Resource, Clone, Default)]
pub struct SharedKeyDownQueue(pub(crate) Arc<Mutex<Vec<(Entity, Entity, String, Key)>>>);
impl SharedKeyDownQueue {
pub(crate) fn push(&self, view: Entity, target: Entity, name: String, key: Key) {
self.0
.lock()
.expect("SharedKeyDownQueue poisoned")
.push((view, target, name, key));
}
fn drain(&self) -> Vec<(Entity, Entity, String, Key)> {
let mut guard = self.0.lock().expect("SharedKeyDownQueue poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn drain_keydown_queue(
queue: Res<SharedKeyDownQueue>,
mut messages: MessageWriter<NoesisKeyDown>,
mut commands: Commands,
) {
for (view, target, name, key) in queue.drain() {
messages.write(NoesisKeyDown {
view,
name: name.clone(),
key,
});
commands.trigger(UiKeyDown {
entity: target,
view,
name,
key,
});
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_keydown_subscriptions(
views: Query<(Entity, &NoesisKeyDownWatch)>,
queue: Res<SharedKeyDownQueue>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, watch) in &views {
state.sync_keydown_subscriptions_for(entity, &watch.entries, &queue);
}
}
impl ReapOnRemove for NoesisClickWatch {
fn reap(state: &mut NoesisRenderState, entity: Entity) {
state.reap_click_watch_for(entity);
}
}
impl ReapOnRemove for NoesisKeyDownWatch {
fn reap(state: &mut NoesisRenderState, entity: Entity) {
state.reap_keydown_watch_for(entity);
}
}
pub struct NoesisEventsPlugin;
impl Plugin for NoesisEventsPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisClicked>()
.add_message::<NoesisKeyDown>()
.insert_resource(SharedClickQueue::default())
.insert_resource(SharedKeyDownQueue::default())
.add_systems(PreUpdate, (drain_click_queue, drain_keydown_queue))
.add_systems(
PostUpdate,
(sync_click_subscriptions, sync_keydown_subscriptions).in_set(NoesisSet::Apply),
);
add_bridge_reap::<NoesisClickWatch>(app);
add_bridge_reap::<NoesisKeyDownWatch>(app);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shared_click_queue_drain_takes_all_and_resets() {
let q = SharedClickQueue::default();
let v = Entity::PLACEHOLDER;
let t = Entity::PLACEHOLDER;
q.push(v, t, "Alpha".into());
q.push(v, t, "Beta".into());
let drained = q.drain();
assert_eq!(
drained,
vec![(v, t, "Alpha".to_string()), (v, t, "Beta".to_string())]
);
assert!(q.drain().is_empty());
}
#[test]
fn click_watch_constructor_normalizes_into_entries() {
let w = NoesisClickWatch::new(["a", "b", "c"]);
let names: Vec<&str> = w.entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["a", "b", "c"]);
assert!(w.entries.iter().all(|e| e.target.is_none()));
}
#[test]
fn click_watch_entry_target_builder() {
let e = ClickWatchEntry::new("Row").target(Entity::PLACEHOLDER);
assert_eq!(e.name, "Row");
assert_eq!(e.target, Some(Entity::PLACEHOLDER));
}
#[test]
fn keydown_entry_swallow_builder() {
let e = KeyDownWatchEntry::new("Input").swallow(Key::Return);
assert_eq!(e.name, "Input");
assert_eq!(e.swallow, vec![Key::Return]);
assert_eq!(e.target, None);
}
}