pub mod status;
use bevy_app::{App, Startup, Update};
use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy_ecs::hierarchy::ChildOf;
use bevy_ecs::prelude::{
Commands, Component, Entity, Has, IntoScheduleConfigs, On, Query, Res, ResMut, With,
};
use bevy_ecs::system::EntityCommands;
use bevy_ui::{Node, Val};
use plurimus::core::Edge;
use plurimus::core::ratatui_core::style::Modifier;
use plurimus::core::ratatui_core::text::{Line, Span};
use plurimus::ui::{Checked, InteractionDisabled, PressFocusDisabled, UiLabel, ValueChange};
use plurimus::widgets::ratatui_widgets::borders::BorderType;
use plurimus::widgets::{TabBarActiveStyle, TabBarLook, tab_bar, tab_item};
use super::layout::{self, TabRow, placed};
use super::nav::{self, ActivePage, LastShown, TAB_COUNT};
use super::session::Session;
use super::theme::{Repainted, Theme};
pub fn plugin(app: &mut App) {
app.add_systems(Startup, spawn_tab_row.after(layout::spawn_frame));
app.add_systems(Update, (light_the_active_tab, disable_the_dead_tabs));
app.add_systems(Update, repaint_tabs.in_set(Repainted));
app.add_observer(handle_tab_chosen);
app.add_plugins(status::plugin);
}
const TAB_DECORATION: u16 = 2;
const DIGIT_COLS: u16 = 2;
pub const TABS_COLS: u16 = tabs_cols();
const _: () = assert!(TAB_COUNT < 10, "a tab is labelled by one digit");
const fn tabs_cols() -> u16 {
let mut cols = 0;
let mut tab = 0;
while tab < TAB_COUNT {
cols += TAB_DECORATION + DIGIT_COLS + nav::tab_title(tab).len() as u16;
tab += 1;
}
cols
}
#[derive(Component, Debug)]
struct ShellTabs;
#[derive(Component, Clone, Copy, Debug)]
struct BarTab(usize);
fn look() -> TabBarLook {
TabBarLook::default()
.with_border(Some(BorderType::Rounded))
.with_joined(Some(Edge::Bottom))
.with_padding(0)
}
fn spawn_tab_row(rows: Query<Entity, With<TabRow>>, theme: Res<Theme>, mut commands: Commands) {
let Ok(row) = rows.single() else {
return;
};
let bar = commands
.spawn((
ShellTabs,
Node {
flex_grow: 1.0,
height: Val::Percent(100.0),
..Node::default()
},
tab_bar(),
look(),
TabBarActiveStyle(active_style(&theme)),
PressFocusDisabled,
placed(),
ChildOf(row),
))
.id();
for tab in 0..TAB_COUNT {
commands.spawn((tab_item(tab_label(tab, &theme)), BarTab(tab), ChildOf(bar)));
}
status::spawn(&mut commands, row);
}
fn active_style(theme: &Theme) -> plurimus::core::ratatui_core::style::Style {
theme.accented().add_modifier(Modifier::BOLD)
}
fn tab_label(tab: usize, theme: &Theme) -> Line<'static> {
Line::from(vec![
Span::styled(format!("{} ", nav::tab_digit(tab)), theme.dimmed()),
Span::raw(nav::tab_title(tab)),
])
}
fn repaint_tabs(
theme: Res<Theme>,
mut bars: Query<&mut TabBarActiveStyle, With<ShellTabs>>,
mut items: Query<(&BarTab, &mut UiLabel)>,
) {
if !theme.is_changed() {
return;
}
for mut style in &mut bars {
style.0 = active_style(&theme);
}
for (tab, mut label) in &mut items {
label.0 = tab_label(tab.0, &theme);
}
}
fn light_the_active_tab(
active: Res<ActivePage>,
items: Query<(Entity, &BarTab, Has<Checked>)>,
mut commands: Commands,
) {
if !active.is_changed() {
return;
}
let lit = active.0.tab();
for (entity, tab, is_marked) in &items {
mark(commands.entity(entity), Checked, tab.0 == lit, is_marked);
}
}
fn mark<Marker: Component>(mut item: EntityCommands, marker: Marker, should_be: bool, is: bool) {
if should_be == is {
return;
}
if should_be {
item.insert(marker);
} else {
item.remove::<Marker>();
}
}
fn disable_the_dead_tabs(
session: Res<Session>,
items: Query<(Entity, &BarTab, Has<InteractionDisabled>)>,
mut commands: Commands,
) {
if !session.is_changed() {
return;
}
let is_empty = session.is_empty();
for (entity, tab, is_marked) in &items {
let is_dead = is_empty && tab.0 != nav::Group::Plan.tab();
mark(
commands.entity(entity),
InteractionDisabled,
is_dead,
is_marked,
);
}
}
fn handle_tab_chosen(
chosen: On<ValueChange<Entity>>,
bars: Query<(), With<ShellTabs>>,
items: Query<&BarTab>,
last: Res<LastShown>,
mut active: ResMut<ActivePage>,
) {
if !bars.contains(chosen.source) {
return;
}
let Ok(tab) = items.get(chosen.value) else {
return;
};
active.set_if_neq(ActivePage(nav::entering(tab.0, *last)));
}
#[cfg(test)]
mod tests;