| 6938 |
6938 |
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
| 6939 |
6939 |
|
pub struct Cells {
|
| 6940 |
6940 |
|
/// One entry per column, in the table's column order.
|
|
6941 |
+ |
///
|
|
6942 |
+ |
/// Filled either directly by [`Cells::new`] or by [`Table::row`] resolving
|
|
6943 |
+ |
/// [`at`](Cells::at)'s named cells against the columns it owns.
|
| 6941 |
6944 |
|
pub values: Vec<Cell>,
|
|
6945 |
+ |
/// Cells that named their column instead of taking a position.
|
|
6946 |
+ |
///
|
|
6947 |
+ |
/// Private, and empty once [`Table::row`] has resolved them into
|
|
6948 |
+ |
/// [`values`](Self::values). A row that names its columns is not a row
|
|
6949 |
+ |
/// until a table has told it what the columns are, which is why this
|
|
6950 |
+ |
/// cannot be reached from outside.
|
|
6951 |
+ |
placed: Vec<(String, Cell)>,
|
| 6942 |
6952 |
|
/// The route that opens this row.
|
| 6943 |
6953 |
|
pub activate: Option<Action>,
|
| 6944 |
6954 |
|
/// Whether this is the row currently being shown elsewhere.
|
| 7110 |
7120 |
|
pub fn new(values: impl IntoIterator<Item = impl Into<Cell>>) -> Self {
|
| 7111 |
7121 |
|
Self {
|
| 7112 |
7122 |
|
values: values.into_iter().map(Into::into).collect(),
|
|
7123 |
+ |
placed: Vec::new(),
|
| 7113 |
7124 |
|
activate: None,
|
| 7114 |
7125 |
|
current: false,
|
| 7115 |
7126 |
|
chosen: None,
|
| 7122 |
7133 |
|
}
|
| 7123 |
7134 |
|
}
|
| 7124 |
7135 |
|
|
|
7136 |
+ |
/// Put a cell in the column of this name.
|
|
7137 |
+ |
///
|
|
7138 |
+ |
/// The alternative to [`new`](Self::new)'s column order, and the one that
|
|
7139 |
+ |
/// survives a cell that is only sometimes there. A row built by position
|
|
7140 |
+ |
/// has to emit every cell to keep the count right, so a conditional cell
|
|
7141 |
+ |
/// is written as a matched pair and a mistake in the pair silently shifts
|
|
7142 |
+ |
/// every column after it. Named, an absent cell is an empty cell in its
|
|
7143 |
+ |
/// own column and the shift cannot be written.
|
|
7144 |
+ |
///
|
|
7145 |
+ |
/// [`Column::name`] already called itself "the name the cell is addressed
|
|
7146 |
+ |
/// by"; this is the constructor that makes that true.
|
|
7147 |
+ |
///
|
|
7148 |
+ |
/// Resolved by [`Table::row`], which is the only thing that knows the
|
|
7149 |
+ |
/// columns. Naming a column the table does not have is dropped there
|
|
7150 |
+ |
/// rather than here, for the same reason: this end cannot see them.
|
|
7151 |
+ |
#[must_use]
|
|
7152 |
+ |
pub fn at(mut self, column: impl Into<String>, cell: impl Into<Cell>) -> Self {
|
|
7153 |
+ |
self.placed.push((column.into(), cell.into()));
|
|
7154 |
+ |
self
|
|
7155 |
+ |
}
|
|
7156 |
+ |
|
|
7157 |
+ |
/// This is the row being shown elsewhere.
|
|
7158 |
+ |
///
|
|
7159 |
+ |
/// Was reachable only by assigning the field, which is why a row built by
|
|
7160 |
+ |
/// a chain had to fall out of the chain to say it. [`Row`] carries the same
|
|
7161 |
+ |
/// fact under the same name.
|
|
7162 |
+ |
#[must_use]
|
|
7163 |
+ |
pub const fn current(mut self, current: bool) -> Self {
|
|
7164 |
+ |
self.current = current;
|
|
7165 |
+ |
self
|
|
7166 |
+ |
}
|
|
7167 |
+ |
|
|
7168 |
+ |
/// Offer these acts on the row itself, rather than in a cell.
|
|
7169 |
+ |
///
|
|
7170 |
+ |
/// The plural of [`offers`](Self::offers), and the other half of the pair
|
|
7171 |
+ |
/// that was assignment-only. A context menu is a property of the row and
|
|
7172 |
+ |
/// belongs in the chain that builds it.
|
|
7173 |
+ |
#[must_use]
|
|
7174 |
+ |
pub fn menu(mut self, acts: impl IntoIterator<Item = Act>) -> Self {
|
|
7175 |
+ |
self.menu.extend(acts);
|
|
7176 |
+ |
self
|
|
7177 |
+ |
}
|
|
7178 |
+ |
|
| 7125 |
7179 |
|
/// The route that opens this row.
|
| 7126 |
7180 |
|
#[must_use]
|
| 7127 |
7181 |
|
pub fn activate(mut self, action: Action) -> Self {
|
| 7222 |
7276 |
|
}
|
| 7223 |
7277 |
|
}
|
| 7224 |
7278 |
|
|
|
7279 |
+ |
/// A table: the columns, and the rows that answer to them.
|
|
7280 |
+ |
///
|
|
7281 |
+ |
/// The columns arrive with the table, so a row is never built against a column
|
|
7282 |
+ |
/// list that does not exist yet. That is the rule [`Run::new`] holds for a
|
|
7283 |
+ |
/// shared row's fallback, here for the same reason: a table's invariant is that
|
|
7284 |
+ |
/// cells line up with columns, and the point a row is put in is the only place
|
|
7285 |
+ |
/// with enough context to keep that true.
|
|
7286 |
+ |
///
|
|
7287 |
+ |
/// # Why a cell is addressed by name
|
|
7288 |
+ |
///
|
|
7289 |
+ |
/// [`Column`] documented its `name` as "the heading, and the name the cell is
|
|
7290 |
+ |
/// addressed by ... what replaces addressing columns by position" while
|
|
7291 |
+ |
/// [`Cells`] stayed positional, so the file held both answers at once. Position
|
|
7292 |
+ |
/// is the one that loses. A cell that appears on some rows and not others
|
|
7293 |
+ |
/// shifts every cell after it, so a conditional cell had to be written as a
|
|
7294 |
+ |
/// matched pair and nothing but care kept the pair matched. Under
|
|
7295 |
+ |
/// [`Cells::at`] an absent cell is an empty cell in its own column, and a row
|
|
7296 |
+ |
/// whose arity depends on a runtime flag is an ordinary row.
|
|
7297 |
+ |
///
|
|
7298 |
+ |
/// A row built by [`Cells::new`] is still positional and still works. The two
|
|
7299 |
+ |
/// are not mixed in one row: if a row names any column, the names are the row.
|
|
7300 |
+ |
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
7301 |
+ |
pub struct Table {
|
|
7302 |
+ |
columns: Vec<Column>,
|
|
7303 |
+ |
rows: Vec<Cells>,
|
|
7304 |
+ |
more: Option<Rest>,
|
|
7305 |
+ |
}
|
|
7306 |
+ |
|
|
7307 |
+ |
impl Table {
|
|
7308 |
+ |
/// A table with these columns and no rows yet.
|
|
7309 |
+ |
pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
|
|
7310 |
+ |
Self {
|
|
7311 |
+ |
columns: columns.into_iter().collect(),
|
|
7312 |
+ |
rows: Vec::new(),
|
|
7313 |
+ |
more: None,
|
|
7314 |
+ |
}
|
|
7315 |
+ |
}
|
|
7316 |
+ |
|
|
7317 |
+ |
/// Add a row, resolving any cell that named its column.
|
|
7318 |
+ |
///
|
|
7319 |
+ |
/// A named cell whose column this table does not have is dropped: the
|
|
7320 |
+ |
/// column list is the table's statement of what a row may say, and a row
|
|
7321 |
+ |
/// saying more than that is answered by the columns rather than by
|
|
7322 |
+ |
/// widening them. A column no cell named is empty in this row.
|
|
7323 |
+ |
#[must_use]
|
|
7324 |
+ |
pub fn row(mut self, mut row: Cells) -> Self {
|
|
7325 |
+ |
if !row.placed.is_empty() {
|
|
7326 |
+ |
let placed = std::mem::take(&mut row.placed);
|
|
7327 |
+ |
row.values = self
|
|
7328 |
+ |
.columns
|
|
7329 |
+ |
.iter()
|
|
7330 |
+ |
.map(|column| {
|
|
7331 |
+ |
placed
|
|
7332 |
+ |
.iter()
|
|
7333 |
+ |
.find(|(name, _)| *name == column.name)
|
|
7334 |
+ |
.map_or_else(|| Cell::new(String::new()), |(_, cell)| cell.clone())
|
|
7335 |
+ |
})
|
|
7336 |
+ |
.collect();
|
|
7337 |
+ |
}
|
|
7338 |
+ |
self.rows.push(row);
|
|
7339 |
+ |
self
|
|
7340 |
+ |
}
|
|
7341 |
+ |
|
|
7342 |
+ |
/// Add several rows, chaining.
|
|
7343 |
+ |
#[must_use]
|
|
7344 |
+ |
pub fn rows(mut self, rows: impl IntoIterator<Item = Cells>) -> Self {
|
|
7345 |
+ |
for row in rows {
|
|
7346 |
+ |
self = self.row(row);
|
|
7347 |
+ |
}
|
|
7348 |
+ |
self
|
|
7349 |
+ |
}
|
|
7350 |
+ |
|
|
7351 |
+ |
/// Say what is not shown, and how to ask for it.
|
|
7352 |
+ |
#[must_use]
|
|
7353 |
+ |
pub fn more(mut self, rest: Rest) -> Self {
|
|
7354 |
+ |
self.more = Some(rest);
|
|
7355 |
+ |
self
|
|
7356 |
+ |
}
|
|
7357 |
+ |
|
|
7358 |
+ |
/// The columns, in order.
|
|
7359 |
+ |
#[must_use]
|
|
7360 |
+ |
pub fn columns(&self) -> &[Column] {
|
|
7361 |
+ |
&self.columns
|
|
7362 |
+ |
}
|
|
7363 |
+ |
}
|
|
7364 |
+ |
|
|
7365 |
+ |
impl From<Table> for Node {
|
|
7366 |
+ |
fn from(table: Table) -> Self {
|
|
7367 |
+ |
Self::Table {
|
|
7368 |
+ |
columns: table.columns,
|
|
7369 |
+ |
rows: table.rows,
|
|
7370 |
+ |
more: table.more,
|
|
7371 |
+ |
}
|
|
7372 |
+ |
}
|
|
7373 |
+ |
}
|
|
7374 |
+ |
|
| 7225 |
7375 |
|
/// A row that says where it sits in a hierarchy.
|
| 7226 |
7376 |
|
///
|
| 7227 |
7377 |
|
/// [`Row`] and [`Cells`] carry [`depth`](Row::depth) and [`open`](Row::open)
|
| 9346 |
9496 |
|
|
| 9347 |
9497 |
|
#[cfg(test)]
|
| 9348 |
9498 |
|
mod tests {
|
|
9499 |
+ |
use super::{Cell, Cells, Column, Node, Table};
|
|
9500 |
+ |
|
|
9501 |
+ |
/// The whole reason a cell names its column.
|
|
9502 |
+ |
///
|
|
9503 |
+ |
/// `git_repos` builds a visibility cell only for an owner, by pushing onto
|
|
9504 |
+ |
/// a `Vec` whose length has to agree with a column list built under a
|
|
9505 |
+ |
/// second, separate conditional. Named, the two cannot disagree: the row
|
|
9506 |
+ |
/// answers the columns the table has.
|
|
9507 |
+ |
#[test]
|
|
9508 |
+ |
fn a_conditional_cell_does_not_shift_the_columns_after_it() {
|
|
9509 |
+ |
let columns = || {
|
|
9510 |
+ |
[
|
|
9511 |
+ |
Column::new("Name"),
|
|
9512 |
+ |
Column::new("Visibility"),
|
|
9513 |
+ |
Column::new("Description"),
|
|
9514 |
+ |
]
|
|
9515 |
+ |
};
|
|
9516 |
+ |
let row = |is_owner: bool| {
|
|
9517 |
+ |
let mut cells = Cells::default()
|
|
9518 |
+ |
.at("Name", Cell::new("quasi"))
|
|
9519 |
+ |
.at("Description", Cell::new("the app stack"));
|
|
9520 |
+ |
if is_owner {
|
|
9521 |
+ |
cells = cells.at("Visibility", Cell::new("public"));
|
|
9522 |
+ |
}
|
|
9523 |
+ |
cells
|
|
9524 |
+ |
};
|
|
9525 |
+ |
|
|
9526 |
+ |
let owner = Table::new(columns()).row(row(true));
|
|
9527 |
+ |
let stranger = Table::new(columns()).row(row(false));
|
|
9528 |
+ |
|
|
9529 |
+ |
let read = |table: &Table| {
|
|
9530 |
+ |
table.rows[0]
|
|
9531 |
+ |
.values
|
|
9532 |
+ |
.iter()
|
|
9533 |
+ |
.map(Cell::text)
|
|
9534 |
+ |
.collect::<Vec<_>>()
|
|
9535 |
+ |
};
|
|
9536 |
+ |
|
|
9537 |
+ |
assert_eq!(read(&owner), ["quasi", "public", "the app stack"]);
|
|
9538 |
+ |
// The description stays in its own column rather than sliding left.
|
|
9539 |
+ |
assert_eq!(read(&stranger), ["quasi", "", "the app stack"]);
|
|
9540 |
+ |
}
|
|
9541 |
+ |
|
|
9542 |
+ |
/// A column nothing named is empty, and a name no column has is dropped.
|
|
9543 |
+ |
#[test]
|
|
9544 |
+ |
fn the_columns_decide_what_a_row_may_say() {
|
|
9545 |
+ |
let table = Table::new([Column::new("Name")]).row(
|
|
9546 |
+ |
Cells::default()
|
|
9547 |
+ |
.at("Name", Cell::new("kick.wav"))
|
|
9548 |
+ |
.at("Nonexistent", Cell::new("ignored")),
|
|
9549 |
+ |
);
|
|
9550 |
+ |
|
|
9551 |
+ |
assert_eq!(table.rows[0].values.len(), 1);
|
|
9552 |
+ |
assert_eq!(table.rows[0].values[0].text(), "kick.wav");
|
|
9553 |
+ |
}
|
|
9554 |
+ |
|
|
9555 |
+ |
/// A row built by position still works and is untouched by resolution.
|
|
9556 |
+ |
#[test]
|
|
9557 |
+ |
fn a_positional_row_is_left_alone() {
|
|
9558 |
+ |
let table = Table::new([Column::new("Name"), Column::new("Size")])
|
|
9559 |
+ |
.row(Cells::new(["kick.wav", "2.1 MB"]));
|
|
9560 |
+ |
|
|
9561 |
+ |
assert_eq!(table.rows[0].values.len(), 2);
|
|
9562 |
+ |
assert_eq!(table.rows[0].values[1].text(), "2.1 MB");
|
|
9563 |
+ |
}
|
|
9564 |
+ |
|
|
9565 |
+ |
/// The table is the node, so nothing reaches for the variant by hand.
|
|
9566 |
+ |
#[test]
|
|
9567 |
+ |
fn a_table_becomes_its_node() {
|
|
9568 |
+ |
let node = Node::from(Table::new([Column::new("Name")]).row(Cells::new(["kick.wav"])));
|
|
9569 |
+ |
|
|
9570 |
+ |
match node {
|
|
9571 |
+ |
Node::Table { columns, rows, .. } => {
|
|
9572 |
+ |
assert_eq!(columns.len(), 1);
|
|
9573 |
+ |
assert_eq!(rows.len(), 1);
|
|
9574 |
+ |
}
|
|
9575 |
+ |
other => panic!("expected a table, got {other:?}"),
|
|
9576 |
+ |
}
|
|
9577 |
+ |
}
|
|
9578 |
+ |
|
| 9349 |
9579 |
|
use super::*;
|
| 9350 |
9580 |
|
|
| 9351 |
9581 |
|
fn frames(count: usize) -> Vec<Node> {
|