use std::fmt::Debug;
use anyhow::{anyhow, Result};
use bevy::{ecs::system::SystemParam, prelude::*};
pub mod prelude {
pub use super::{is_focused, Navigation};
}
#[mutants::skip]
#[tracing::instrument(skip_all)]
pub fn plugin(app: &mut App) {
trace!("Initializing plugin...");
app.add_observer(gc_popups);
trace!("Plugin initialized.");
}
#[derive(SystemParam)]
pub struct Navigation<'w, 's> {
breadcrumbs: Query<'w, 's, (Entity, &'static Breadcrumb)>,
root: Query<'w, 's, Entity, With<NavigationRoot>>,
focused: Query<'w, 's, Entity, With<Focus>>,
commands: Commands<'w, 's>,
}
impl Navigation<'_, '_> {
#[tracing::instrument(skip(self))]
pub fn is_focused(&self, entity: Entity) -> bool {
self.focused.get(entity).is_ok()
}
#[tracing::instrument(skip(self))]
pub fn spawn_popup(&mut self, bundle: impl Bundle + Debug) -> Result<()> {
let popup = self.commands.spawn((bundle, Popup)).id();
debug!("Spawned popup as entity {:?}", popup);
self.focus(popup)?;
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn focus_as_root(&mut self, entity: Entity) {
self.reset_stack();
for root in self.root.iter_mut() {
self.commands.entity(root).remove::<NavigationRoot>();
}
self.commands.entity(entity).insert((NavigationRoot, Focus));
debug!("Focused entity as root");
}
#[tracing::instrument(skip(self))]
pub fn focus(&mut self, entity: Entity) -> Result<()> {
if self.is_focused(entity) {
return Ok(());
}
let root = self.root()?;
self.reset_stack();
let mut entity_commands = self.commands.entity(entity);
entity_commands.insert(Focus);
if entity != root {
entity_commands.insert(Breadcrumb(root));
debug!("Focused entity with root {:?}", root);
} else {
debug!("Focused root entity");
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn go_to(&mut self, entity: Entity) -> Result<()> {
let current = self.focused()?;
(self.commands.entity(current))
.remove::<Focus>()
.insert(Breadcrumb(entity));
self.commands.entity(entity).insert(Focus);
debug!("Navigated from: {:?}", current);
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn go_back(&mut self) -> Result<()> {
let current = self.focused()?;
if let Ok((_, breadcrumb)) = self.breadcrumbs.get(current) {
self.commands
.entity(current)
.remove::<(Breadcrumb, Focus)>();
self.commands.entity(breadcrumb.0).insert(Focus);
debug!("Navigated back to: {:?}", breadcrumb.0);
} else {
debug!("Nothing to go back to, no breadcrumbs found.");
}
Ok(())
}
fn reset_stack(&mut self) {
for focused in self.focused.iter_mut() {
self.commands.entity(focused).remove::<Focus>();
debug!("Removed focus from: {:?}", focused);
}
for (entity, _) in self.breadcrumbs.iter_mut() {
self.commands.entity(entity).remove::<Breadcrumb>();
debug!("Removed breadcrumb from: {:?}", entity);
}
}
fn focused(&self) -> Result<Entity> {
self.focused
.get_single()
.map_err(|_| anyhow!("Zero or multiple focused entities found. This shouldn't happen."))
}
fn root(&self) -> Result<Entity> {
self.root
.get_single()
.map_err(|_| anyhow!("Zero or multiple navigation roots found. This shouldn't happen."))
}
}
pub fn is_focused<C: Component>(query: Query<Entity, (With<C>, With<Focus>)>) -> bool {
!query.is_empty()
}
#[derive(Component)]
pub struct NavigationRoot;
#[derive(Component)]
pub struct Focus;
#[derive(Component, Deref, DerefMut)]
pub struct Breadcrumb(pub Entity);
#[derive(Component)]
pub struct Popup;
#[mutants::skip]
#[tracing::instrument(skip_all)]
fn gc_popups(
unfocused: Trigger<OnRemove, Focus>,
popups: Query<Entity, With<Popup>>,
mut commands: Commands,
) {
if let Ok(popup) = popups.get(unfocused.entity()) {
commands.entity(popup).despawn_recursive();
debug!("Despawned popup entity {:?}", popup);
}
}
#[cfg(test)]
mod tests {
use bevy::ecs::system::RunSystemOnce;
use super::*;
#[test]
fn test_is_focused() -> Result<()> {
let mut app = App::new();
let entity = app.world_mut().spawn_empty().id();
(app.world_mut()).run_system_once(move |navigation: Navigation| {
assert!(!navigation.is_focused(entity));
})?;
app.world_mut().entity_mut(entity).insert(Focus);
(app.world_mut()).run_system_once(move |navigation: Navigation| {
assert!(navigation.is_focused(entity));
})?;
Ok(())
}
#[test]
fn test_focus_as_root() -> Result<()> {
let mut app = App::new();
let entity = app.world_mut().spawn_empty().id();
(app.world_mut()).run_system_once(move |mut navigation: Navigation| {
navigation.focus_as_root(entity.clone());
})?;
(app.world_mut()).run_system_once(move |q: Query<(&Focus, &NavigationRoot)>| {
let _ = q.get(entity).unwrap();
})?;
Ok(())
}
#[test]
fn test_focus() -> Result<()> {
let mut app = App::new();
let root = app.world_mut().spawn(NavigationRoot).id();
let entity = app.world_mut().spawn_empty().id();
(app.world_mut()).run_system_once(move |mut navigation: Navigation| {
navigation.focus(entity).unwrap();
})?;
(app.world_mut()).run_system_once(move |q: Query<&Breadcrumb, With<Focus>>| {
let breadcrumb = q.get(entity).unwrap();
assert_eq!(breadcrumb.0, root)
})?;
Ok(())
}
#[test]
fn test_go_to() -> Result<()> {
let mut app = App::new();
let current = app.world_mut().spawn(Focus).id();
let target = app.world_mut().spawn_empty().id();
(app.world_mut()).run_system_once(move |mut navigation: Navigation| {
navigation.go_to(target).unwrap();
})?;
assert!(!app.world_mut().entity(current).contains::<Focus>());
assert!(app.world_mut().entity(current).contains::<Breadcrumb>());
assert!(app.world_mut().entity(target).contains::<Focus>());
Ok(())
}
#[test]
fn test_go_back() -> Result<()> {
let mut app = App::new();
let previous = app.world_mut().spawn_empty().id();
let current = app.world_mut().spawn((Focus, Breadcrumb(previous))).id();
(app.world_mut()).run_system_once(|mut navigation: Navigation| {
navigation.go_back().unwrap();
})?;
assert!(!app.world_mut().entity(current).contains::<Focus>());
assert!(!app.world_mut().entity(current).contains::<Breadcrumb>());
assert!(app.world_mut().entity(previous).contains::<Focus>());
Ok(())
}
}