use std::any::Any;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::fmt;
use bevy::ecs::relationship::RelatedSpawner;
use bevy::ecs::spawn::SpawnWith;
use bevy::input::ButtonInput;
use bevy::input::ButtonState;
use bevy::input::gamepad::{Gamepad, GamepadButton};
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::tab_navigation::TabIndex;
use bevy::input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy::picking::cursor::EntityCursor;
use bevy::picking::events::{
PointerCancel, PointerClick, PointerDrag, PointerDragEnd, PointerDragStart, PointerPress,
PointerRelease,
};
use bevy::picking::hover::Hovered;
use bevy::picking::pointer::PointerButton;
use bevy::prelude::*;
use bevy::ui::auto_directional_navigation::AutoDirectionalNavigation;
use bevy::ui::{InteractionDisabled, Pressed, Selected};
use bevy::ui_widgets::{Activate, ScrollArea, ValueChange, observe};
use bevy::window::SystemCursorIcon;
use crate::list::ListView;
use crate::model::{
MIN_TARGET_PX, PAD_REPEAT_DELAY, PAD_REPEAT_RATE, SPLIT_KEY_STEP_PX, TABLE_LEAST_COLUMN,
TableStep, column_resize, natural_cmp, repeat_steps, scroll_to_reveal, table_anchor,
table_step, table_window, tree_indent,
};
use crate::nav::show_focus;
use crate::skin::{GooeySkin, Role, SkinScope};
use crate::tree::{Lead, Tree, bind_lead, spawn_lead};
use crate::widgets::{ONE_LINE, list_rail};
const DEFAULT_ROWS: f32 = 8.5;
pub trait RowSource: Any + Send + Sync {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn cell(&self, row: usize, column: usize) -> Cow<'_, str>;
fn key(&self, row: usize) -> u64 {
row as u64
}
fn compare(&self, column: usize, a: usize, b: usize) -> Ordering {
natural_cmp(&self.cell(a, column), &self.cell(b, column))
}
}
impl RowSource for Vec<Vec<String>> {
fn len(&self) -> usize {
Vec::len(self)
}
fn cell(&self, row: usize, column: usize) -> Cow<'_, str> {
Cow::Borrowed(self[row].get(column).map_or("", String::as_str))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Column {
pub title: String,
pub width: f32,
pub sortable: bool,
}
impl Column {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
width: 8.0,
sortable: true,
}
}
pub fn width(mut self, width: f32) -> Self {
self.width = width;
self
}
pub fn unsortable(mut self) -> Self {
self.sortable = false;
self
}
}
#[derive(Component)]
#[require(
TableSort,
TableSelection,
SelectMode,
TableCursor,
TableWidths,
TableRows,
TableCache
)]
pub struct Table {
source: Box<dyn RowSource>,
columns: Vec<Column>,
}
impl Table {
pub fn new(columns: impl IntoIterator<Item = Column>, rows: impl RowSource) -> Self {
Self {
source: Box::new(rows),
columns: columns.into_iter().collect(),
}
}
pub fn columns(&self) -> &[Column] {
&self.columns
}
pub fn len(&self) -> usize {
self.source.len()
}
pub fn is_empty(&self) -> bool {
self.source.is_empty()
}
pub fn source<T: RowSource>(&self) -> Option<&T> {
(self.source.as_ref() as &dyn Any).downcast_ref()
}
pub fn source_mut<T: RowSource>(&mut self) -> Option<&mut T> {
(self.source.as_mut() as &mut dyn Any).downcast_mut()
}
pub fn set_source(&mut self, rows: impl RowSource) {
self.source = Box::new(rows);
}
}
impl fmt::Debug for Table {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Table")
.field("columns", &self.columns)
.field("rows", &self.source.len())
.finish()
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TableRows(pub f32);
impl Default for TableRows {
fn default() -> Self {
Self(DEFAULT_ROWS)
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TableSort {
pub column: Option<usize>,
pub descending: bool,
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Hash, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SelectMode {
None,
#[default]
Single,
Multi,
}
#[derive(Component, Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TableSelection(pub BTreeSet<u64>);
impl TableSelection {
pub fn of(key: u64) -> Self {
Self(BTreeSet::from([key]))
}
pub fn contains(&self, key: u64) -> bool {
self.0.contains(&key)
}
pub fn iter(&self) -> impl Iterator<Item = u64> + '_ {
self.0.iter().copied()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TableCursor(pub Option<u64>);
#[derive(Component, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TableWidths(pub Vec<f32>);
#[derive(EntityEvent, Clone, Copy, Debug)]
pub struct SortBy {
#[event_target]
pub entity: Entity,
pub column: usize,
pub descending: bool,
}
#[derive(EntityEvent, Clone, Copy, Debug)]
pub struct RowActivated {
#[event_target]
pub entity: Entity,
pub key: u64,
}
#[derive(Component)]
pub struct TableParts {
view: Entity,
rows: Entity,
headers: Vec<HeaderParts>,
}
pub(crate) struct HeaderParts {
pub(crate) cell: Entity,
pub(crate) edge: Option<Entity>,
pub(crate) up: Entity,
pub(crate) down: Entity,
}
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct TableHeaderCell {
pub(crate) table: Entity,
pub(crate) column: usize,
}
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct TableEdge {
table: Entity,
column: usize,
}
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct EdgeGrab(f32);
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct TableRow {
pub table: Entity,
pub index: usize,
pub key: u64,
pub cursor: bool,
}
struct PoolRow {
row: Entity,
cells: Vec<(Entity, Entity)>,
lead: Option<Lead>,
}
#[derive(Component, Default)]
pub struct TableCache {
built: bool,
order: Vec<usize>,
pub(crate) keys: Vec<u64>,
pool: Vec<PoolRow>,
first: usize,
pub(crate) anchor: Option<u64>,
follow: bool,
text_px: f32,
pitch: f32,
}
impl TableCache {
pub(crate) fn index_of(&self, key: Option<u64>) -> Option<usize> {
let key = key?;
self.keys.iter().position(|&k| k == key)
}
pub(crate) fn takes(&self, cursor: Option<u64>, up: bool) -> bool {
let Some(last) = self.keys.len().checked_sub(1) else {
return false;
};
match self.index_of(cursor) {
None => true,
Some(i) if up => i > 0,
Some(i) => i < last,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Pick {
Move,
Only,
Toggle,
Range,
}
pub(crate) type Chosen<'a, 'w> = (
&'a SelectMode,
&'a mut Mut<'w, TableSelection>,
&'a mut Mut<'w, TableCursor>,
&'a mut Mut<'w, TableCache>,
);
pub(crate) fn pick(
entity: Entity,
index: usize,
how: Pick,
follow: bool,
(mode, selection, cursor, cache): Chosen<'_, '_>,
commands: &mut Commands,
) {
let Some(&key) = cache.keys.get(index) else {
return;
};
let was = cache.index_of(cursor.0);
if cursor.0 != Some(key) {
cursor.0 = Some(key);
}
cache.follow = follow;
let mut next = selection.0.clone();
match (*mode, how) {
(SelectMode::None, _) | (_, Pick::Move) => {}
(SelectMode::Single, _) | (SelectMode::Multi, Pick::Only) => {
next = BTreeSet::from([key]);
cache.anchor = Some(key);
}
(SelectMode::Multi, Pick::Toggle) => {
if !next.remove(&key) {
next.insert(key);
}
cache.anchor = Some(key);
}
(SelectMode::Multi, Pick::Range) => {
let from = cache.index_of(cache.anchor).or(was).unwrap_or(index);
let (lo, hi) = (from.min(index), from.max(index));
next = cache.keys[lo..=hi].iter().copied().collect();
let begun = cache.keys[from];
cache.anchor.get_or_insert(begun);
}
}
if next != selection.0 {
selection.0 = next;
commands.trigger(ValueChange {
source: entity,
value: selection.clone(),
is_final: true,
});
}
}
pub(crate) fn row_on_click(
mut click: On<PointerClick>,
rows: Query<&TableRow>,
keys: Res<ButtonInput<KeyCode>>,
mut tables: Query<
(
&SelectMode,
&mut TableSelection,
&mut TableCursor,
&mut TableCache,
),
Without<InteractionDisabled>,
>,
mut commands: Commands,
) {
click.propagate(false);
if click.button != PointerButton::Primary {
return;
}
let Ok(row) = rows.get(click.entity) else {
return;
};
let Ok((mode, mut selection, mut cursor, mut cache)) = tables.get_mut(row.table) else {
return;
};
let held = |a, b| keys.pressed(a) || keys.pressed(b);
let how = if held(KeyCode::ControlLeft, KeyCode::ControlRight) {
Pick::Toggle
} else if held(KeyCode::ShiftLeft, KeyCode::ShiftRight) {
Pick::Range
} else {
Pick::Only
};
pick(
row.table,
row.index,
how,
false,
(mode, &mut selection, &mut cursor, &mut cache),
&mut commands,
);
if click.count == 2 {
commands.trigger(RowActivated {
entity: row.table,
key: row.key,
});
}
}
pub(crate) fn header_on_activate(
activate: On<Activate>,
headers: Query<&TableHeaderCell>,
mut tables: Query<(&Table, &mut TableSort), Without<InteractionDisabled>>,
mut commands: Commands,
) {
let Ok(header) = headers.get(activate.entity) else {
return;
};
let Ok((table, mut sort)) = tables.get_mut(header.table) else {
return;
};
if !table
.columns
.get(header.column)
.is_some_and(|column| column.sortable)
{
return;
}
let descending = sort.column == Some(header.column) && !sort.descending;
*sort = TableSort {
column: Some(header.column),
descending,
};
commands.trigger(SortBy {
entity: header.table,
column: header.column,
descending,
});
}
pub(crate) fn header_on_key_input(
mut input: On<FocusedInput<KeyboardInput>>,
headers: Query<&TableHeaderCell>,
keys: Res<ButtonInput<KeyCode>>,
skin: Res<GooeySkin>,
mut tables: Query<(&Table, &mut TableWidths)>,
) {
if input.input.state != ButtonState::Pressed
|| !(keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight))
{
return;
}
let sign = match input.input.key_code {
KeyCode::ArrowLeft => -1.0,
KeyCode::ArrowRight => 1.0,
_ => return,
};
input.propagate(false);
let Ok(header) = headers.get(input.focused_entity) else {
return;
};
let Ok((table, mut widths)) = tables.get_mut(header.table) else {
return;
};
let text_px = (skin.text_size * skin.text_scale).max(1.0);
let start = width_of(table, &widths, header.column);
resize(
&mut widths,
table,
header.column,
start,
sign * SPLIT_KEY_STEP_PX,
text_px,
);
}
fn width_of(table: &Table, widths: &TableWidths, column: usize) -> f32 {
widths
.0
.get(column)
.copied()
.or_else(|| table.columns.get(column).map(|c| c.width))
.unwrap_or(8.0)
.max(TABLE_LEAST_COLUMN)
}
fn resize(
widths: &mut Mut<TableWidths>,
table: &Table,
column: usize,
start: f32,
by: f32,
text_px: f32,
) {
let to = column_resize(start * text_px, by, TABLE_LEAST_COLUMN * text_px) / text_px;
if widths.0.len() < table.columns.len() {
let missing = table.columns.len() - widths.0.len();
let from = widths.0.len();
widths
.0
.extend((0..missing).map(|i| table.columns[from + i].width));
}
if let Some(slot) = widths.0.get_mut(column)
&& (*slot - to).abs() > f32::EPSILON
{
*slot = to;
}
}
pub(crate) fn edge_on_press(mut press: On<PointerPress>) {
press.propagate(false);
}
pub(crate) fn edge_on_release(mut release: On<PointerRelease>) {
release.propagate(false);
}
pub(crate) fn edge_on_click(mut click: On<PointerClick>) {
click.propagate(false);
}
pub(crate) fn edge_on_drag_start(
mut start: On<PointerDragStart>,
edges: Query<&TableEdge>,
tables: Query<(&Table, &TableWidths)>,
mut commands: Commands,
) {
start.propagate(false);
let Ok(edge) = edges.get(start.entity) else {
return;
};
let Ok((table, widths)) = tables.get(edge.table) else {
return;
};
commands
.entity(start.entity)
.insert((Pressed, EdgeGrab(width_of(table, widths, edge.column))));
}
pub(crate) fn edge_on_drag(
mut drag: On<PointerDrag>,
edges: Query<(&TableEdge, &EdgeGrab)>,
skin: Res<GooeySkin>,
ui_scale: Option<Res<UiScale>>,
mut tables: Query<(&Table, &mut TableWidths)>,
) {
drag.propagate(false);
let Ok((edge, grab)) = edges.get(drag.entity) else {
return;
};
let Ok((table, mut widths)) = tables.get_mut(edge.table) else {
return;
};
let text_px = (skin.text_size * skin.text_scale).max(1.0);
let along = drag.distance.x / ui_scale.map_or(1.0, |s| s.0);
resize(&mut widths, table, edge.column, grab.0, along, text_px);
}
pub(crate) fn edge_on_drag_end(mut end: On<PointerDragEnd>, mut commands: Commands) {
end.propagate(false);
commands.entity(end.entity).remove::<(Pressed, EdgeGrab)>();
}
pub(crate) fn edge_on_cancel(mut cancel: On<PointerCancel>, mut commands: Commands) {
cancel.propagate(false);
commands
.entity(cancel.entity)
.remove::<(Pressed, EdgeGrab)>();
}
pub(crate) fn table_bundle(
columns: impl IntoIterator<Item = Column>,
rows: impl RowSource,
outlined: bool,
) -> impl Bundle {
let table = Table::new(columns, rows);
let titles: Vec<(String, bool)> = table
.columns
.iter()
.map(|c| (c.title.clone(), c.sortable))
.collect();
(
Node {
flex_direction: FlexDirection::Column,
overflow: Overflow::clip_x(),
..default()
},
Role::Table,
table,
TabIndex(0),
AutoDirectionalNavigation::default(),
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
let root = parent.target_entity();
let strip = (!outlined).then(|| {
parent
.spawn((
Node {
flex_shrink: 0.0,
..default()
},
Role::TableHeader,
))
.id()
});
let last = titles.len().saturating_sub(1);
let mut headers = Vec::new();
for (column, (title, _)) in titles.iter().enumerate() {
let Some(strip) = strip else {
break;
};
let (cell, up, down) = {
let world = parent.world_mut();
let cell = world
.spawn((
Node {
align_items: AlignItems::Center,
flex_shrink: 0.0,
overflow: Overflow::clip(),
..default()
},
bevy::ui_widgets::Button,
Role::TableHeaderCell,
TableHeaderCell {
table: root,
column,
},
Hovered::default(),
TabIndex(0),
AutoDirectionalNavigation::default(),
observe(header_on_activate),
observe(header_on_key_input),
ChildOf(strip),
))
.id();
world.spawn((
Text::new(title.clone()),
ONE_LINE,
Role::ButtonText,
ChildOf(cell),
));
let mark = |role, shift| {
(
Node {
display: Display::None,
..default()
},
UiTransform {
translation: Val2::percent(0.0, shift),
rotation: Rot2::degrees(45.0),
..default()
},
role,
Pickable::IGNORE,
ChildOf(cell),
)
};
let up = world
.spawn(mark(Role::TableSortUp, crate::widgets::CHEVRON_INK_SHIFT))
.id();
let down = world
.spawn(mark(
Role::TableSortDown,
-crate::widgets::CHEVRON_INK_SHIFT,
))
.id();
(cell, up, down)
};
let edge = (column < last).then(|| {
parent
.world_mut()
.spawn((
Node {
position_type: PositionType::Absolute,
top: Val::Px(0.0),
bottom: Val::Px(0.0),
width: Val::Px(MIN_TARGET_PX),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
Role::TableEdge,
TableEdge {
table: root,
column,
},
Hovered::default(),
EntityCursor::System(SystemCursorIcon::ColResize),
(
observe(edge_on_press),
observe(edge_on_release),
observe(edge_on_click),
observe(edge_on_drag_start),
observe(edge_on_drag),
observe(edge_on_drag_end),
observe(edge_on_cancel),
),
ChildOf(strip),
children![(
Node {
width: Val::Px(3.0),
height: Val::Px(20.0),
..default()
},
Role::SplitGrip,
Pickable::IGNORE,
)],
))
.id()
});
headers.push(HeaderParts {
cell,
edge,
up,
down,
});
}
let body = parent
.spawn((
Node {
flex_direction: FlexDirection::Column,
..default()
},
Role::List,
))
.id();
let (view, rows) = {
let world = parent.world_mut();
let view = world
.spawn((
Node {
display: Display::Block,
overflow: Overflow::scroll_y(),
..default()
},
Role::TableView,
ListView { rows: DEFAULT_ROWS },
ScrollArea,
ChildOf(body),
))
.id();
let rows = world
.spawn((
Node {
flex_direction: FlexDirection::Column,
flex_shrink: 0.0,
..default()
},
Role::TableRows,
ChildOf(view),
))
.id();
world.spawn((list_rail(view), ChildOf(body)));
(view, rows)
};
parent.world_mut().entity_mut(root).insert(TableParts {
view,
rows,
headers,
});
})),
)
}
type Laid = (
Entity,
Ref<'static, Table>,
Ref<'static, TableSort>,
Ref<'static, TableWidths>,
&'static TableRows,
Mut<'static, TableSelection>,
Mut<'static, TableCursor>,
Mut<'static, TableCache>,
&'static TableParts,
);
fn skin_of<'a>(
entity: Entity,
global: &'a GooeySkin,
scopes: &'a Query<&SkinScope>,
parents: &Query<&ChildOf>,
) -> &'a GooeySkin {
if scopes.is_empty() {
return global;
}
std::iter::once(entity)
.chain(parents.iter_ancestors(entity))
.find_map(|e| scopes.get(e).ok())
.map_or(global, |scope| &scope.0)
}
fn cell_node(width: f32, last: bool) -> Node {
Node {
flex_basis: Val::Px(width),
flex_grow: if last { 1.0 } else { 0.0 },
flex_shrink: 0.0,
min_width: Val::Px(0.0),
overflow: Overflow::clip(),
align_items: AlignItems::Center,
..default()
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_tables(
skin: Res<GooeySkin>,
scopes: Query<&SkinScope>,
parents: Query<&ChildOf>,
mut tables: Query<Laid>,
mut nodes: Query<&mut Node>,
mut scrolls: Query<(&mut ScrollPosition, &mut ListView)>,
mut texts: Query<&mut Text>,
mut rows: Query<(&mut TableRow, Has<Selected>)>,
mut commands: Commands,
) {
for (entity, table, sort, widths, rows_tall, mut selection, mut cursor, mut cache, parts) in
&mut tables
{
let skin = skin_of(entity, &skin, &scopes, &parents);
let text_px = (skin.text_size * skin.text_scale).max(1.0);
let pitch = skin.table_pitch();
let len = table.source.len();
let columns = table.columns.len();
let tree = table.source::<Tree>();
let indent = tree_indent(text_px);
let cache = &mut *cache;
let fresh = !cache.built;
let rebuilt = fresh || table.is_changed() || sort.is_changed();
let Ok((mut scroll, mut view)) = scrolls.get_mut(parts.view) else {
continue;
};
if view.rows != rows_tall.0 {
view.rows = rows_tall.0;
}
let tall = rows_tall.0.max(0.0);
let view_h = (tall * pitch).min(len as f32 * pitch);
let max = (len as f32 * pitch - view_h).max(0.0);
let mut y = scroll.y.clamp(0.0, max);
if rebuilt {
let old_first = cache.first;
let old_first_key = cache.keys.get(old_first).copied();
let old_cursor = cache.index_of(cursor.0);
let mut order: Vec<usize> = (0..len).collect();
if let Some(column) = sort.column.filter(|&c| c < columns) {
let descending = sort.descending;
order.sort_by(|&a, &b| {
let by = table.source.compare(column, a, b);
(if descending { by.reverse() } else { by }).then(a.cmp(&b))
});
}
cache.keys = order.iter().map(|&row| table.source.key(row)).collect();
cache.order = order;
if !fresh {
if table.is_changed() {
let now = cache.index_of(old_first_key);
y = table_anchor(y, pitch, old_first, now, max);
if !selection.0.is_empty() {
let live: BTreeSet<u64> = cache.keys.iter().copied().collect();
let kept: BTreeSet<u64> = selection
.0
.iter()
.copied()
.filter(|k| live.contains(k) || tree.is_some_and(|t| t.contains(*k)))
.collect();
if kept != selection.0 {
selection.0 = kept;
}
}
if let Some(key) = cursor.0
&& !cache.keys.contains(&key)
{
cursor.0 = tree.and_then(|t| t.shown_ancestor(key)).or_else(|| {
old_cursor
.and_then(|i| cache.keys.get(i.min(len.saturating_sub(1))).copied())
});
}
if cache.anchor.is_some_and(|k| !cache.keys.contains(&k)) {
cache.anchor = None;
}
} else {
y = 0.0;
cache.follow = cursor.0.is_some();
}
}
cache.built = true;
}
if cache.follow {
cache.follow = false;
if let Some(i) = cache.index_of(cursor.0) {
let top = i as f32 * pitch;
y = scroll_to_reveal(top, top + pitch, view_h, y, max, pitch);
}
}
if (scroll.y - y).abs() > 0.01 {
scroll.y = y;
}
let pool = (tall.ceil() as usize + 1).min(len);
let first = table_window(y, pitch, pool, len);
let moved = first != cache.first || pool != cache.pool.len() || rebuilt;
let resized = cache.text_px != text_px || cache.pitch != pitch;
cache.first = first;
let width_px = |c: usize| width_of(&table, &widths, c) * text_px;
let last = columns.saturating_sub(1);
while cache.pool.len() > pool {
if let Some(gone) = cache.pool.pop() {
commands.entity(gone.row).despawn();
}
}
for slot in 0..cache.pool.len() {
if !(moved || selection.is_changed() || cursor.is_changed() || resized) {
break;
}
let index = first + slot;
let (Some(&key), Some(&source)) = (cache.keys.get(index), cache.order.get(index))
else {
continue;
};
let slot_row = &cache.pool[slot];
if let Ok((mut row, selected)) = rows.get_mut(slot_row.row) {
let want = TableRow {
table: entity,
index,
key,
cursor: cursor.0 == Some(key),
};
if *row != want {
*row = want;
}
let should = selection.contains(key);
if should != selected {
if should {
commands.entity(slot_row.row).insert(Selected);
} else {
commands.entity(slot_row.row).remove::<Selected>();
}
}
}
if moved {
for (c, &(_, text)) in slot_row.cells.iter().enumerate() {
if let Ok(mut text) = texts.get_mut(text) {
let now = table.source.cell(source, c);
if text.0 != *now {
text.0 = now.into_owned();
}
}
}
}
if (moved || resized)
&& let (Some(tree), Some(lead)) = (tree, &slot_row.lead)
{
bind_lead(lead, tree.outline(source), indent, &mut nodes);
}
}
while cache.pool.len() < pool {
let index = first + cache.pool.len();
let (Some(&key), Some(&source)) = (cache.keys.get(index), cache.order.get(index))
else {
break;
};
let row = commands
.spawn((
Node {
align_items: AlignItems::Center,
flex_shrink: 0.0,
..default()
},
Role::TableRow,
TableRow {
table: entity,
index,
key,
cursor: cursor.0 == Some(key),
},
Hovered::default(),
observe(row_on_click),
ChildOf(parts.rows),
))
.id();
if selection.contains(key) {
commands.entity(row).insert(Selected);
}
let mut lead = None;
let cells = (0..columns)
.map(|c| {
let mut node = cell_node(width_px(c), c == last);
node.align_self = if tree.is_some() && c == 0 {
AlignSelf::Stretch
} else {
AlignSelf::Auto
};
let cell = commands
.spawn((node, Role::TableCell, Pickable::IGNORE, ChildOf(row)))
.id();
if let (Some(tree), 0) = (tree, c) {
lead = Some(spawn_lead(
&mut commands,
cell,
tree.outline(source),
indent,
));
}
let text = commands
.spawn((
Text::new(table.source.cell(source, c).into_owned()),
ONE_LINE,
Role::TableCellText,
Pickable::IGNORE,
ChildOf(cell),
))
.id();
(cell, text)
})
.collect();
cache.pool.push(PoolRow { row, cells, lead });
}
if let Ok(mut column) = nodes.get_mut(parts.rows) {
let above = Val::Px(first as f32 * pitch);
let below = Val::Px(len.saturating_sub(first + cache.pool.len()) as f32 * pitch);
if column.padding.top != above || column.padding.bottom != below {
column.padding.top = above;
column.padding.bottom = below;
}
}
if rebuilt || widths.is_changed() || resized || moved {
let mut left = 0.0;
for (c, header) in parts.headers.iter().enumerate() {
let px = width_px(c);
left += px;
if let Ok(mut node) = nodes.get_mut(header.cell) {
let want = cell_node(px, c == last);
if node.flex_basis != want.flex_basis || node.flex_grow != want.flex_grow {
node.flex_basis = want.flex_basis;
node.flex_grow = want.flex_grow;
}
}
if let Some(edge) = header.edge
&& let Ok(mut node) = nodes.get_mut(edge)
{
let at = Val::Px(left - MIN_TARGET_PX / 2.0);
if node.left != at {
node.left = at;
}
}
let marked = sort.column == Some(c);
for (mark, shown) in [
(header.up, marked && !sort.descending),
(header.down, marked && sort.descending),
] {
if let Ok(mut node) = nodes.get_mut(mark) {
let want = if shown { Display::Flex } else { Display::None };
if node.display != want {
node.display = want;
}
}
}
for slot in &cache.pool {
if let Some(&(cell, _)) = slot.cells.get(c)
&& let Ok(mut node) = nodes.get_mut(cell)
{
let want = cell_node(px, c == last);
if node.flex_basis != want.flex_basis || node.flex_grow != want.flex_grow {
node.flex_basis = want.flex_basis;
node.flex_grow = want.flex_grow;
}
}
}
}
}
cache.text_px = text_px;
cache.pitch = pitch;
}
}
type Typed = (
&'static Table,
&'static TableRows,
&'static SelectMode,
&'static mut TableSelection,
&'static mut TableCursor,
&'static mut TableCache,
);
#[allow(clippy::too_many_arguments)]
pub fn table_by_input(
time: Res<Time<Real>>,
keys: Res<ButtonInput<KeyCode>>,
mut presses: MessageReader<KeyboardInput>,
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
gamepads: Query<&Gamepad>,
mut tables: Query<Typed, Without<InteractionDisabled>>,
mut held: Local<(i8, f32)>,
mut commands: Commands,
) {
let typed: Vec<KeyboardInput> = presses
.read()
.filter(|p| p.state == ButtonState::Pressed)
.cloned()
.collect();
let target = focus.get().filter(|&f| tables.contains(f));
let Some(entity) = target else {
*held = (0, 0.0);
return;
};
let Ok((table, rows_tall, mode, mut selection, mut cursor, mut cache)) = tables.get_mut(entity)
else {
return;
};
let page = (rows_tall.0.floor() as usize).saturating_sub(1).max(1);
let down = |button| gamepads.iter().any(|g| g.pressed(button));
let pad_pressed = |button| gamepads.iter().any(|g| g.just_pressed(button));
let held_key = |a, b| keys.pressed(a) || keys.pressed(b);
let ctrl = held_key(KeyCode::ControlLeft, KeyCode::ControlRight);
let shift = held_key(KeyCode::ShiftLeft, KeyCode::ShiftRight);
let how = if ctrl {
Pick::Move
} else if shift && *mode == SelectMode::Multi {
Pick::Range
} else {
Pick::Only
};
let mut steps: Vec<TableStep> = Vec::new();
let (mut toggle, mut all, mut open) = (false, false, false);
for press in &typed {
match press.key_code {
KeyCode::ArrowUp => steps.push(TableStep::Up),
KeyCode::ArrowDown => steps.push(TableStep::Down),
KeyCode::PageUp if !ctrl => steps.push(TableStep::PageUp),
KeyCode::PageDown if !ctrl => steps.push(TableStep::PageDown),
KeyCode::Home => steps.push(TableStep::First),
KeyCode::End => steps.push(TableStep::Last),
KeyCode::Space if !press.repeat => toggle = true,
KeyCode::KeyA if ctrl && !press.repeat => all = true,
KeyCode::Enter if !press.repeat => open = true,
_ => {}
}
}
let direction = down(GamepadButton::DPadDown) as i8 - down(GamepadButton::DPadUp) as i8;
if direction == 0 {
*held = (0, 0.0);
} else {
let count = if held.0 == direction {
let before = held.1;
held.1 += time.delta_secs();
repeat_steps(before, held.1, PAD_REPEAT_DELAY, PAD_REPEAT_RATE)
} else {
*held = (direction, 0.0);
1
};
let step = if direction > 0 {
TableStep::Down
} else {
TableStep::Up
};
steps.extend(std::iter::repeat_n(step, count as usize));
}
if pad_pressed(GamepadButton::LeftTrigger2) {
steps.push(TableStep::PageUp);
}
if pad_pressed(GamepadButton::RightTrigger2) {
steps.push(TableStep::PageDown);
}
open |= pad_pressed(GamepadButton::South);
let handled = !steps.is_empty() || toggle || all || open;
if !handled {
return;
}
show_focus(&mut shown);
let len = table.len();
for step in steps {
let from = cache.index_of(cursor.0);
if let Some(to) = table_step(from, step, len, page) {
let how = if matches!(step, TableStep::PageUp | TableStep::PageDown) && ctrl {
Pick::Move
} else {
how
};
pick(
entity,
to,
how,
true,
(mode, &mut selection, &mut cursor, &mut cache),
&mut commands,
);
}
}
if toggle {
let at = cache
.index_of(cursor.0)
.or(table_step(None, TableStep::Down, len, page));
if let Some(at) = at {
let how = if *mode == SelectMode::Multi {
Pick::Toggle
} else {
Pick::Only
};
pick(
entity,
at,
how,
true,
(mode, &mut selection, &mut cursor, &mut cache),
&mut commands,
);
}
}
if all && *mode == SelectMode::Multi {
let every: BTreeSet<u64> = cache.keys.iter().copied().collect();
if every != selection.0 {
selection.0 = every;
commands.trigger(ValueChange {
source: entity,
value: selection.clone(),
is_final: true,
});
}
}
if open && let Some(key) = cursor.0 {
commands.trigger(RowActivated { entity, key });
}
}