use std::sync::{Arc, Mutex};
use bevy::prelude::*;
pub use noesis_runtime::view::Key;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Message, Debug, Clone)]
pub struct NoesisClicked {
pub view: Entity,
pub name: String,
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisClickWatch {
pub names: Vec<String>,
}
impl NoesisClickWatch {
pub fn new(names: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
names: names.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Resource, Clone, Default)]
pub struct SharedClickQueue(pub(crate) Arc<Mutex<Vec<(Entity, String)>>>);
impl SharedClickQueue {
pub(crate) fn push(&self, view: Entity, name: String) {
self.0
.lock()
.expect("SharedClickQueue poisoned")
.push((view, name));
}
fn drain(&self) -> Vec<(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>) {
for (view, name) in queue.drain() {
messages.write(NoesisClicked { 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.names, &queue);
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisKeyDown {
pub view: Entity,
pub name: String,
pub key: Key,
}
#[derive(Clone, Debug)]
pub struct KeyDownWatchEntry {
pub name: String,
pub swallow: Vec<Key>,
}
impl KeyDownWatchEntry {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
swallow: Vec::new(),
}
}
#[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
}
}
#[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, String, Key)>>>);
impl SharedKeyDownQueue {
pub(crate) fn push(&self, view: Entity, name: String, key: Key) {
self.0
.lock()
.expect("SharedKeyDownQueue poisoned")
.push((view, name, key));
}
fn drain(&self) -> Vec<(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>,
) {
for (view, name, key) in queue.drain() {
messages.write(NoesisKeyDown { 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);
}
}
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),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shared_click_queue_drain_takes_all_and_resets() {
let q = SharedClickQueue::default();
let v = Entity::PLACEHOLDER;
q.push(v, "Alpha".into());
q.push(v, "Beta".into());
let drained = q.drain();
assert_eq!(
drained,
vec![(v, "Alpha".to_string()), (v, "Beta".to_string())]
);
assert!(q.drain().is_empty());
}
#[test]
fn click_watch_constructor_normalizes_into_strings() {
let w = NoesisClickWatch::new(["a", "b", "c"]);
assert_eq!(
w.names,
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
#[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]);
}
}