use std::fmt::Debug;
use anyhow::{anyhow, Result};
use bevy::{ecs::system::SystemParam, prelude::*};
pub mod prelude {
pub use super::{is_focused, Navigation};
}
#[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;
#[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);
}
}