use bevy_app::{App, Startup, Update};
use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy_ecs::hierarchy::ChildOf;
use bevy_ecs::prelude::{
Changed, Commands, Component, Entity, IntoScheduleConfigs, On, Query, Res, ResMut, With, World,
};
use bevy_ecs::system::SystemParam;
use bevy_input::keyboard::Key;
use bevy_ui::Node;
use plurimus::core::ratatui_core::layout::Size;
use plurimus::core::ratatui_core::text::Line;
use plurimus::ui::ScrollArea;
use plurimus::widgets::{
ActiveDescendant, ListBoxAction, ListBoxKeys, ListItemTrailing, ValueChange, list_item, listbox,
};
use super::command::Outcome;
use super::edit::{self, Draft};
use super::focus::{self, PageFocus, SidebarLed};
use super::hints::Hints;
use super::layout::{self, Body, filling, placed};
use super::nav::{self, ActivePage, Group, LastShown, Page, PageSystems, ShownSurface};
use super::pane::Pane;
use super::theme::{Repainted, Theme};
pub const SIDEBAR_COLS: u16 = BORDER_COLS + layout::CURSOR_COLS + widest_row();
const BORDER_COLS: u16 = 2;
const COUNT_COLS: usize = 4;
const fn widest_row() -> u16 {
let mut widest = 0;
let mut at = 0;
while at < Page::ALL.len() {
let page = Page::ALL[at];
let count = if page.is_domain() { COUNT_COLS } else { 0 };
let width = page.title().len() + count;
if page.group().is_some() && width > widest {
widest = width;
}
at += 1;
}
widest as u16
}
pub fn plugin(app: &mut App) {
app.add_systems(Startup, spawn_sidebars.after(layout::spawn_frame));
app.add_systems(Update, turn_to_the_cursor.in_set(PageSystems::Turn));
app.add_systems(
Update,
(
show_the_sidebar,
point_at_the_page.after(focus::settle_focus),
)
.in_set(PageSystems::Show),
);
app.add_systems(Update, refresh_counts.in_set(Repainted));
app.add_observer(handle_page_entered);
}
#[derive(Component, Debug)]
struct SidebarPane(Group);
#[derive(Component, Clone, Copy, Debug)]
pub struct Sidebar(Group);
#[derive(Component, Clone, Copy, Debug)]
pub struct SidebarRow(Page);
fn sidebar_keys() -> ListBoxKeys {
let mut keys = ListBoxKeys::default();
keys.0.push((Key::ArrowRight.into(), ListBoxAction::Select));
keys
}
const fn hints(group: Group) -> Hints {
match group {
Group::Tools => Hints(&[("↑↓", "tool"), ("→", "into")]),
Group::Plan => Hints(&[("↑↓", "domain"), ("→", "into")]),
}
}
fn spawn_sidebars(bodies: Query<Entity, With<Body>>, mut commands: Commands) {
let Ok(body) = bodies.single() else {
return;
};
for group in Group::ALL {
let pane = Pane::new(group.title())
.wide(f32::from(SIDEBAR_COLS))
.spawn(&mut commands, body);
commands.entity(pane).insert(SidebarPane(group));
commands.entity(body).insert_children(0, &[pane]);
let list = commands
.spawn((
Sidebar(group),
layout::Rests,
hints(group),
listbox(),
sidebar_keys(),
layout::list_cursor(),
ScrollArea::new(Size::default()),
filling(),
placed(),
ChildOf(pane),
))
.id();
for page in Page::ALL
.into_iter()
.filter(|page| page.group() == Some(group))
{
commands.spawn((list_item(page.title()), SidebarRow(page), ChildOf(list)));
}
}
}
#[derive(SystemParam)]
pub struct SidebarCursor<'w, 's> {
lists: Query<'w, 's, (Entity, &'static Sidebar, &'static mut ActiveDescendant)>,
rows: Query<'w, 's, (Entity, &'static SidebarRow)>,
}
impl SidebarCursor<'_, '_> {
fn of(&self, group: Group) -> Option<(Entity, &ActiveDescendant)> {
self.lists
.iter()
.find(|(_, sidebar, _)| sidebar.0 == group)
.map(|(list, _, cursor)| (list, cursor))
}
pub fn list(&self, group: Group) -> Option<Entity> {
self.of(group).map(|(list, _)| list)
}
pub fn point_at(&mut self, page: Page) {
let Some(group) = page.group() else {
return;
};
let row = self.rows.iter().find(|(_, row)| row.0 == page);
let Some((row, _)) = row else {
return;
};
let list = self
.lists
.iter_mut()
.find(|(_, sidebar, _)| sidebar.0 == group);
if let Some((_, _, mut cursor)) = list
&& cursor.0 != Some(row)
{
cursor.0 = Some(row);
}
}
}
fn turn_to_the_cursor(
moved: Query<(&Sidebar, &ActiveDescendant), Changed<ActiveDescendant>>,
rows: Query<&SidebarRow>,
mut active: ResMut<ActivePage>,
mut led: ResMut<SidebarLed>,
) {
for (sidebar, cursor) in &moved {
if active.0.group() != Some(sidebar.0) {
continue;
}
if let Some(row) = cursor.0.and_then(|row| rows.get(row).ok()) {
active.set_if_neq(ActivePage(row.0));
led.0 = true;
}
}
}
fn point_at_the_page(shown: ShownSurface, mut cursor: SidebarCursor) {
if let Some(page) = shown.surface() {
cursor.point_at(page);
}
}
fn show_the_sidebar(shown: ShownSurface, mut panes: Query<(&SidebarPane, &mut Node)>) {
if !shown.is_changed() {
return;
}
let group = shown.surface().and_then(Page::group);
for (pane, mut node) in &mut panes {
layout::set_display(&mut node, group == Some(pane.0));
}
}
fn refresh_counts(
draft: Res<Draft>,
theme: Res<Theme>,
rows: Query<(Entity, &SidebarRow, Option<&ListItemTrailing>)>,
mut commands: Commands,
) {
if !draft.is_changed() && !theme.is_changed() {
return;
}
for (entity, row, drawn) in &rows {
let Some(count) = edit::item_count(row.0, &draft.plan) else {
continue;
};
let trailing = Line::styled(count.to_string(), theme.dimmed());
if drawn.is_none_or(|drawn| drawn.0 != trailing) {
commands.entity(entity).insert(ListItemTrailing(trailing));
}
}
}
fn handle_page_entered(
chosen: On<ValueChange<Entity>>,
lists: Query<(), With<Sidebar>>,
mut focus: PageFocus,
) {
if lists.contains(chosen.source) {
focus.enter_page();
}
}
pub fn enter(world: &mut World, group: Group) -> Outcome {
let last = *world.resource::<LastShown>();
world
.resource_mut::<ActivePage>()
.set_if_neq(ActivePage(nav::entering(group.tab(), last)));
world.resource_mut::<SidebarLed>().0 = true;
let _ = world.run_system_cached(focus::enter_sidebar);
Outcome::Done
}
#[cfg(test)]
mod tests;