use std::sync::{Arc, Mutex};
use bevy::prelude::*;
pub use noesis_runtime::input::{FocusNavigationDirection, ModifierKeys};
use noesis_runtime::view::Key;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FocusMove {
pub from: String,
pub direction: FocusNavigationDirection,
pub wrapped: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FocusEngage {
pub name: String,
pub engage: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyBindingSpec {
pub name: String,
pub key: Key,
pub modifiers: ModifierKeys,
}
impl KeyBindingSpec {
#[must_use]
pub fn ident(&self) -> (String, i32, i32) {
(self.name.clone(), self.key as i32, self.modifiers.bits())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FocusPredict {
pub from: String,
pub direction: FocusNavigationDirection,
pub expect: Option<String>,
}
impl FocusPredict {
#[must_use]
pub fn ident(&self) -> (String, i32, Option<String>) {
(
self.from.clone(),
self.direction as i32,
self.expect.clone(),
)
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisFocusControl {
pub moves: Vec<FocusMove>,
pub engages: Vec<FocusEngage>,
pub bindings: Vec<KeyBindingSpec>,
pub predicts: Vec<FocusPredict>,
}
impl NoesisFocusControl {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn move_focus(
mut self,
from: impl Into<String>,
direction: FocusNavigationDirection,
wrapped: bool,
) -> Self {
self.moves.push(FocusMove {
from: from.into(),
direction,
wrapped,
});
self
}
#[must_use]
pub fn engage(mut self, name: impl Into<String>, engage: bool) -> Self {
self.engages.push(FocusEngage {
name: name.into(),
engage,
});
self
}
#[must_use]
pub fn key_binding(
mut self,
name: impl Into<String>,
key: Key,
modifiers: ModifierKeys,
) -> Self {
self.bindings.push(KeyBindingSpec {
name: name.into(),
key,
modifiers,
});
self
}
#[must_use]
pub fn predict(mut self, from: impl Into<String>, direction: FocusNavigationDirection) -> Self {
self.predicts.push(FocusPredict {
from: from.into(),
direction,
expect: None,
});
self
}
#[must_use]
pub fn predict_to(
mut self,
from: impl Into<String>,
direction: FocusNavigationDirection,
expect: impl Into<String>,
) -> Self {
self.predicts.push(FocusPredict {
from: from.into(),
direction,
expect: Some(expect.into()),
});
self
}
pub fn request_move(
&mut self,
from: impl Into<String>,
direction: FocusNavigationDirection,
wrapped: bool,
) {
self.moves.push(FocusMove {
from: from.into(),
direction,
wrapped,
});
}
pub fn request_engage(&mut self, name: impl Into<String>, engage: bool) {
self.engages.push(FocusEngage {
name: name.into(),
engage,
});
}
pub fn add_key_binding(&mut self, name: impl Into<String>, key: Key, modifiers: ModifierKeys) {
self.bindings.push(KeyBindingSpec {
name: name.into(),
key,
modifiers,
});
}
pub fn watch_predict(&mut self, from: impl Into<String>, direction: FocusNavigationDirection) {
self.predicts.push(FocusPredict {
from: from.into(),
direction,
expect: None,
});
}
pub fn watch_predict_to(
&mut self,
from: impl Into<String>,
direction: FocusNavigationDirection,
expect: impl Into<String>,
) {
self.predicts.push(FocusPredict {
from: from.into(),
direction,
expect: Some(expect.into()),
});
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisFocusBindingFired {
pub view: Entity,
pub name: String,
pub key: Key,
pub modifiers: ModifierKeys,
}
#[derive(Message, Debug, Clone)]
pub struct NoesisFocusPredicted {
pub view: Entity,
pub from: String,
pub direction: FocusNavigationDirection,
pub candidate: bool,
pub predicted_name: Option<String>,
pub matches_expected: bool,
}
#[derive(Resource, Clone, Default)]
pub struct SharedFocusBindingQueue(pub(crate) Arc<Mutex<Vec<(Entity, String, Key, ModifierKeys)>>>);
impl SharedFocusBindingQueue {
pub(crate) fn push(&self, view: Entity, name: String, key: Key, modifiers: ModifierKeys) {
self.0
.lock()
.expect("SharedFocusBindingQueue poisoned")
.push((view, name, key, modifiers));
}
fn drain(&self) -> Vec<(Entity, String, Key, ModifierKeys)> {
let mut guard = self.0.lock().expect("SharedFocusBindingQueue poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_focus_control(
mut views: Query<(Entity, Mut<NoesisFocusControl>)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, mut ctl) in &mut views {
if (!ctl.is_changed()
&& !state.scene_rebuilt_this_frame(entity)
&& !state.panel_mounted_this_frame(entity))
|| (ctl.moves.is_empty() && ctl.engages.is_empty())
{
continue;
}
let moves_applied = state.apply_focus_moves_for(entity, &ctl.moves);
let engages_applied = state.apply_focus_engages_for(entity, &ctl.engages);
if moves_applied && engages_applied {
let ctl = ctl.bypass_change_detection();
ctl.moves.clear();
ctl.engages.clear();
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_focus_bindings(
views: Query<(Entity, &NoesisFocusControl)>,
queue: Res<SharedFocusBindingQueue>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, ctl) in &views {
state.sync_key_bindings_for(entity, &ctl.bindings, &queue);
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn poll_focus_predictions(
views: Query<(Entity, &NoesisFocusControl)>,
mut messages: MessageWriter<NoesisFocusPredicted>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, ctl) in &views {
for (from, direction, candidate, predicted_name, matches_expected) in
state.poll_focus_predictions_for(entity, &ctl.predicts)
{
messages.write(NoesisFocusPredicted {
view: entity,
from,
direction,
candidate,
predicted_name,
matches_expected,
});
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn drain_focus_binding_queue(
queue: Res<SharedFocusBindingQueue>,
mut messages: MessageWriter<NoesisFocusBindingFired>,
) {
for (view, name, key, modifiers) in queue.drain() {
messages.write(NoesisFocusBindingFired {
view,
name,
key,
modifiers,
});
}
}
pub struct NoesisFocusControlPlugin;
impl Plugin for NoesisFocusControlPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisFocusBindingFired>()
.add_message::<NoesisFocusPredicted>()
.insert_resource(SharedFocusBindingQueue::default())
.add_systems(PreUpdate, drain_focus_binding_queue)
.add_systems(
PostUpdate,
(
sync_focus_control,
sync_focus_bindings,
poll_focus_predictions,
)
.in_set(NoesisSet::Apply)
.after(crate::panel::sync_panels),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_specs() {
let c = NoesisFocusControl::new()
.move_focus("First", FocusNavigationDirection::Right, false)
.engage("Pad", true)
.key_binding("Console", Key::Return, ModifierKeys::CONTROL)
.predict_to("First", FocusNavigationDirection::Right, "Second");
assert_eq!(c.moves.len(), 1);
assert_eq!(c.moves[0].from, "First");
assert_eq!(c.moves[0].direction, FocusNavigationDirection::Right);
assert!(c.engages[0].engage);
assert_eq!(c.bindings[0].key, Key::Return);
assert_eq!(c.bindings[0].modifiers, ModifierKeys::CONTROL);
assert_eq!(c.predicts[0].expect.as_deref(), Some("Second"));
}
#[test]
fn idents_are_stable() {
let b = KeyBindingSpec {
name: "X".into(),
key: Key::A,
modifiers: ModifierKeys::CONTROL,
};
assert_eq!(
b.ident(),
("X".to_string(), Key::A as i32, ModifierKeys::CONTROL.bits())
);
}
}