use bevy_app::{App, Update};
use bevy_ecs::change_detection::DetectChangesMut;
use bevy_ecs::hierarchy::{ChildOf, Children};
use bevy_ecs::prelude::{Added, Entity, IntoScheduleConfigs, Query, ResMut, Resource};
use bevy_ecs::system::SystemParam;
use bevy_input::keyboard::Key;
use bevy_input_focus::InputFocus;
use bevy_input_focus::directional_navigation::DirectionalNavigationMap;
use bevy_math::CompassOctant;
use super::command::{self, Outcome};
use super::nav::{FocusStop, Page, PageSystems, ShownSurface, SurfaceRoot};
use super::overlay;
use super::sidebar::SidebarCursor;
pub fn plugin(app: &mut App) {
app.init_resource::<SidebarLed>();
app.add_systems(
Update,
(settle_focus.in_set(PageSystems::Show), block_bound_arrows),
);
}
#[derive(Resource, Default, Debug)]
pub struct SidebarLed(pub bool);
#[derive(SystemParam)]
pub struct Ring<'w, 's> {
pub(super) shown: ShownSurface<'w>,
roots: Query<'w, 's, (Entity, &'static SurfaceRoot)>,
children: Query<'w, 's, &'static Children>,
stops: Query<'w, 's, (), bevy_ecs::prelude::With<FocusStop>>,
pub(super) sidebar: SidebarCursor<'w, 's>,
}
impl Ring<'_, '_> {
fn sidebar_list(&self) -> Option<Entity> {
self.shown
.surface()
.and_then(Page::group)
.and_then(|group| self.sidebar.list(group))
}
fn panes(&self) -> impl Iterator<Item = Entity> {
let shown = self.shown.surface();
let root = self
.roots
.iter()
.find(|(_, root)| root.0 == shown)
.map(|(entity, _)| entity);
root.into_iter()
.flat_map(|root| self.children.iter_descendants_depth_first(root))
.filter(|&entity| self.stops.contains(entity))
}
pub fn stops(&self) -> Vec<Entity> {
self.sidebar_list()
.into_iter()
.chain(self.panes())
.collect()
}
pub fn has_many(&self) -> bool {
let panes = self.panes().take(2).count();
usize::from(self.sidebar_list().is_some()) + panes > 1
}
}
#[derive(SystemParam)]
pub struct PageFocus<'w, 's> {
pub(super) ring: Ring<'w, 's>,
led: ResMut<'w, SidebarLed>,
focus: ResMut<'w, InputFocus>,
overlays: ResMut<'w, overlay::Focus>,
}
impl PageFocus<'_, '_> {
pub fn enter_page(&mut self) {
let first = self.ring.panes().next();
self.set(first);
}
pub(super) fn holder(&self) -> Option<Entity> {
self.overlays.base(&self.focus)
}
pub(super) fn set(&mut self, holder: Option<Entity>) {
let Self {
overlays, focus, ..
} = self;
overlays.rebase(holder, focus);
}
fn step(&mut self, step: isize) {
let stops = self.ring.stops();
let holder = self.holder();
let Some(held) = stops.iter().position(|&stop| Some(stop) == holder) else {
return;
};
let next = (held as isize + step).rem_euclid(stops.len() as isize) as usize;
if next != held {
self.set(Some(stops[next]));
}
}
fn settle(&mut self, is_led: bool) {
let sidebar = is_led.then(|| self.ring.sidebar_list()).flatten();
match sidebar {
Some(list) => self.set(Some(list)),
None => self.enter_page(),
}
}
}
pub fn settle_focus(mut focus: PageFocus) {
let is_led = std::mem::take(&mut focus.led.bypass_change_detection().0);
if focus.ring.shown.is_changed() {
focus.settle(is_led);
}
}
pub fn enter_sidebar(mut focus: PageFocus) -> Outcome {
if let Some(page) = focus.ring.shown.surface()
&& let Some(group) = page.group()
{
focus.ring.sidebar.point_at(page);
let list = focus.ring.sidebar.list(group);
focus.set(list);
}
Outcome::Done
}
fn octant(key: &Key) -> Option<CompassOctant> {
match key {
Key::ArrowUp => Some(CompassOctant::North),
Key::ArrowDown => Some(CompassOctant::South),
Key::ArrowLeft => Some(CompassOctant::West),
Key::ArrowRight => Some(CompassOctant::East),
_ => None,
}
}
fn block_bound_arrows(
stops: Query<Entity, Added<FocusStop>>,
parents: Query<&ChildOf>,
roots: Query<&SurfaceRoot>,
mut map: ResMut<DirectionalNavigationMap>,
) {
for stop in &stops {
let page = parents
.iter_ancestors(stop)
.find_map(|ancestor| roots.get(ancestor).ok())
.and_then(|root| root.0);
let bound = page.into_iter().flat_map(command::arrows_bound_on);
for direction in bound.filter_map(octant) {
map.block_edge(stop, direction);
}
}
}
pub fn focus_next(mut focus: PageFocus) -> Outcome {
focus.step(1);
Outcome::Done
}
pub fn focus_previous(mut focus: PageFocus) -> Outcome {
focus.step(-1);
Outcome::Done
}
#[cfg(test)]
mod tests {
use bevy_ecs::system::SystemState;
use plurimus::bui::ComputedNodeRect;
use super::*;
use crate::commands::tui::hints::Hints;
use crate::commands::tui::nav::Page;
use crate::commands::tui::support::{SIZE, headless_app, show};
fn ring(app: &mut bevy_app::App) -> Vec<Entity> {
let mut state = SystemState::<Ring>::new(app.world_mut());
let ring = state.get_mut(app.world_mut()).expect("a valid param");
ring.stops()
}
fn named(app: &bevy_app::App, stops: &[Entity]) -> Vec<&'static str> {
stops
.iter()
.map(|&stop| {
app.world()
.get::<Hints>(stop)
.and_then(|hints| hints.0.first())
.map_or("", |(_, word)| word)
})
.collect()
}
#[test]
fn every_page_walks_its_panes_in_the_order_they_are_drawn() {
let mut app = headless_app(SIZE);
for page in Page::ALL {
show(&mut app, page);
let stops = ring(&mut app);
let panes = &stops[usize::from(page.group().is_some())..];
let placed: Vec<(u16, u16, Entity)> = panes
.iter()
.map(|&stop| {
let rect = app.world().get::<ComputedNodeRect>(stop).expect("laid out");
(rect.visible.y, rect.visible.x, stop)
})
.collect();
let mut by_place = placed.clone();
by_place.sort_unstable();
assert_eq!(placed, by_place, "{page:?} is walked out of drawn order");
}
}
#[test]
fn the_tool_pages_are_walked_from_the_left() {
let mut app = headless_app(SIZE);
show(&mut app, Page::RothConversions);
let stops = ring(&mut app);
assert_eq!(named(&app, &stops), ["tool", "edit", "option", "year"]);
show(&mut app, Page::MonteCarlo);
let stops = ring(&mut app);
assert_eq!(named(&app, &stops), ["tool", "assumption", "run"]);
}
}