use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy_ecs::prelude::{
ChildOf, Children, Commands, Component, Entity, Mut, On, Query, Ref, Res, ResMut,
};
use bevy_ecs::system::SystemParam;
use bevy_input::keyboard::{Key, KeyboardInput};
use bevy_input_focus::FocusedInput;
use plurimus::core::ratatui_core::layout::{Constraint, Rect, Size};
use plurimus::core::ratatui_core::style::{Modifier, Style};
use plurimus::core::ratatui_core::text::Line;
use plurimus::term::bevy_compat::HeldModifiers;
use plurimus::ui::{ComputedWidgetArea, KeyBinding, ScrollArea, UiStyle, first_bound};
use plurimus::widgets::{
ActiveDescendant, TableAction, TableColumns, TableGeometry, TableKeys, TablePosition,
TableSelection, TableStripe, ValueChange, table_header, table_row,
};
use super::domain::{ListOps, Ops};
use super::draft::Draft;
use super::editing::{self, Slot};
use super::sort::{self, Sort};
use super::widths::Laid;
use crate::commands::tui::layout;
use crate::commands::tui::motion::{Cues, Play};
use crate::commands::tui::nav::{ActivePage, Page, ShownSurface};
use crate::commands::tui::theme::Theme;
#[derive(Component)]
pub struct DomainTable {
pub ops: Ops,
pub list: ListOps,
pub sort: Option<Sort>,
pub wanted: Option<Row>,
pub is_applied: bool,
}
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
pub struct Row(pub usize);
#[derive(SystemParam)]
pub struct Turn<'w, 's> {
active: ResMut<'w, ActivePage>,
tables: Query<'w, 's, &'static mut DomainTable>,
}
impl Turn<'_, '_> {
pub fn to(&mut self, page: Page, index: Option<usize>) {
self.active.0 = page;
let Some(index) = index else {
return;
};
let mut tables = self.tables.iter_mut();
if let Some(mut table) = tables.find(|table| table.ops.surface == Some(page)) {
table.wanted = Some(Row(index));
}
}
}
pub(super) const OPEN_KEYS: &[(KeyBinding, ())] = &[(KeyBinding::new(Key::Enter), ())];
pub fn table_keys() -> TableKeys {
TableKeys(vec![
(Key::ArrowUp.into(), TableAction::RowPrev),
(Key::ArrowDown.into(), TableAction::RowNext),
(Key::Home.into(), TableAction::RowFirst),
(Key::End.into(), TableAction::RowLast),
(Key::PageUp.into(), TableAction::PageUp),
(Key::PageDown.into(), TableAction::PageDown),
])
}
pub fn table_bundle() -> impl bevy_ecs::bundle::Bundle {
(
plurimus::widgets::table([]),
TableSelection::Row,
layout::table_cursor(),
table_keys(),
TableStripe(Style::new()),
ScrollArea::new(Size::default()),
)
}
#[derive(SystemParam)]
pub struct TableView<'w> {
draft: Res<'w, Draft>,
theme: Res<'w, Theme>,
shown: ShownSurface<'w>,
}
pub fn rebuild_rows(
view: TableView,
mut tables: Query<(
Entity,
&mut DomainTable,
&ActiveDescendant,
Ref<ComputedWidgetArea>,
&ScrollArea,
)>,
rows: Query<&Row>,
mut commands: Commands,
) {
let (draft, theme) = (&view.draft, &view.theme);
let is_turned = view.shown.is_changed();
let surface = view.shown.surface();
for (table, mut domain_table, cursor, area, scroll) in &mut tables {
let given = scroll.content_width(area.0.width);
let is_stale =
draft.is_changed() || is_turned || domain_table.is_changed() || area.is_changed();
if domain_table.ops.surface != surface || !is_stale {
continue;
}
let kept = cursor_row(*cursor, &rows);
let table_ref = domain_table.bypass_change_detection();
let wanted = table_ref.wanted.take().or(kept);
commands.entity(table).despawn_related::<Children>();
let list = table_ref.list;
let mut items: Vec<sort::Item> = (list.rows)(&draft.plan).into_iter().enumerate().collect();
if let Some(sort) = table_ref.sort {
items = sort.order(items);
}
if items.is_empty() {
spawn_nothing_yet(&mut commands, table, list.purpose, theme);
commands.entity(table).insert(ActiveDescendant(None));
continue;
}
let laid = Laid::of(table_ref, &items, given);
commands.entity(table).insert(laid.columns());
commands.spawn((
table_header(laid.header()),
UiStyle(Style::new().add_modifier(Modifier::BOLD)),
ChildOf(table),
));
let placed = spawn_body(&mut commands, table, &laid, items, wanted);
commands.entity(table).insert(ActiveDescendant(placed));
}
}
fn spawn_body(
commands: &mut Commands,
table: Entity,
laid: &Laid,
items: Vec<sort::Item>,
wanted: Option<Row>,
) -> Option<Entity> {
let mut cursor_row = None;
let mut first_row = None;
for (index, cells) in items {
let row = Row(index);
let spawned = commands
.spawn((table_row(laid.row(cells)), row, ChildOf(table)))
.id();
first_row.get_or_insert(spawned);
if Some(row) == wanted {
cursor_row = Some(spawned);
}
}
cursor_row.or(first_row)
}
fn spawn_nothing_yet(commands: &mut Commands, table: Entity, purpose: &str, theme: &Theme) {
commands
.entity(table)
.insert(TableColumns(vec![Constraint::Fill(1)]));
let indent = usize::from(layout::CURSOR_COLS);
commands.spawn((
table_header(vec![Line::from(format!("{:indent$}{purpose}", ""))]),
UiStyle(theme.dimmed()),
ChildOf(table),
));
}
pub fn cue_receipt(
mut tables: Query<(
Entity,
Mut<DomainTable>,
&ActiveDescendant,
&ComputedWidgetArea,
)>,
geometry: TableGeometry,
theme: Res<Theme>,
mut cues: ResMut<Cues>,
) {
for (table, mut domain_table, cursor, area) in &mut tables {
if !domain_table.is_applied || domain_table.is_changed() {
continue;
}
domain_table.bypass_change_detection().is_applied = false;
let row = cursor.0.and_then(|row| geometry.cell_rect(table, row, 0));
if let Some(row) = row {
let flashed = Rect::new(area.0.x, row.y, area.0.width, 1);
cues.play(Play::Receipt(theme.dim), flashed);
}
}
}
pub fn cursor_row(cursor: ActiveDescendant, rows: &Query<&Row>) -> Option<Row> {
cursor.0.and_then(|row| rows.get(row).ok()).copied()
}
pub fn handle_table_key(
mut input: On<FocusedInput<KeyboardInput>>,
held: HeldModifiers,
tables: Query<(&DomainTable, &ActiveDescendant)>,
rows: Query<&Row>,
mut commands: Commands,
) {
let table = input.focused_entity;
let Ok((domain, cursor)) = tables.get(table) else {
return;
};
if first_bound(OPEN_KEYS, &input.input, held.get()).is_none() {
return;
}
input.propagate(false);
if let Some(row) = cursor_row(*cursor, &rows) {
commands
.run_system_cached_with(editing::open_item, (domain.ops, Some(table), Slot::At(row)));
}
}
pub fn handle_row_select(
select: On<ValueChange<TablePosition>>,
tables: Query<&DomainTable>,
rows: Query<&Row>,
mut commands: Commands,
) {
let Ok(domain) = tables.get(select.source) else {
return;
};
if let Some(&row) = select.value.row.and_then(|row| rows.get(row).ok()) {
let opened = (domain.ops, Some(select.source), Slot::At(row));
commands.run_system_cached_with(editing::open_item, opened);
}
}
#[cfg(test)]
mod tests {
#[test]
fn an_empty_domain_says_what_it_is_for_instead_of_heading_no_rows() {
let lists = super::super::SCREENS.iter().filter_map(|ops| ops.list);
for list in lists {
assert!(!list.purpose.is_empty(), "{} says nothing", list.singular);
}
}
}