max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
11 files changed,
+573 insertions,
-357 deletions
| @@ -41,7 +41,7 @@ | |||
| 41 | 41 | use makeover_layout as layout; | |
| 42 | 42 | use makeover_webview::form::escape_into; | |
| 43 | 43 | use quasi_http::Serves; | |
| 44 | - | use quasi_router::screen::{Act, Cell, Cells, Column}; | |
| 44 | + | use quasi_router::screen::{Act, Cell, Column, Row}; | |
| 45 | 45 | use quasi_router::{Action, Node, RegionKind, Slot}; | |
| 46 | 46 | use quasi_webview::Webview; | |
| 47 | 47 | ||
| @@ -209,7 +209,7 @@ | |||
| 209 | 209 | ], | |
| 210 | 210 | rows: (0..d.buyer_rows()) | |
| 211 | 211 | .map(|r| { | |
| 212 | - | Cells::new([ | |
| 212 | + | Row::cells([ | |
| 213 | 213 | Cell::new(d.buyer(r, B::Username)) | |
| 214 | 214 | .activate(Action::get(format!("/u/{}", d.buyer(r, B::Username)))), | |
| 215 | 215 | Cell::new(d.buyer(r, B::Email)) | |
| @@ -236,7 +236,7 @@ | |||
| 236 | 236 | ], | |
| 237 | 237 | rows: (0..d.shared_rows()) | |
| 238 | 238 | .map(|r| { | |
| 239 | - | Cells::new([ | |
| 239 | + | Row::cells([ | |
| 240 | 240 | Cell::new(d.shared(r, S::Name)) | |
| 241 | 241 | .activate(Action::get(format!("/u/{}", d.shared(r, S::Username)))), | |
| 242 | 242 | Cell::acts([Act::new( |
| @@ -1551,8 +1551,8 @@ | |||
| 1551 | 1551 | // problem rather than a worth problem, and `budget` below is the | |
| 1552 | 1552 | // answer to it. | |
| 1553 | 1553 | let spacing = ui.spacing().item_spacing.x; | |
| 1554 | - | for (index, part) in row.parts.iter().enumerate() { | |
| 1555 | - | let after = tail_width(ui, pass.immediate, &row.parts[index + 1..], spacing); | |
| 1554 | + | for (index, part) in row.cells.iter().enumerate() { | |
| 1555 | + | let after = tail_width(ui, pass.immediate, &row.cells[index + 1..], spacing); | |
| 1556 | 1556 | row_part(pass, ui, part, after); | |
| 1557 | 1557 | } | |
| 1558 | 1558 | }); | |
| @@ -1672,10 +1672,10 @@ | |||
| 1672 | 1672 | /// rather than parts. Same rule and the same reason: it is where the row's name | |
| 1673 | 1673 | /// already is. | |
| 1674 | 1674 | fn cells_name(row: &quasi_router::Cells) -> String { | |
| 1675 | - | row.values | |
| 1675 | + | row.cells | |
| 1676 | 1676 | .iter() | |
| 1677 | 1677 | .find_map(|cell| { | |
| 1678 | - | let said = row_leaf_text(&cell.parts); | |
| 1678 | + | let said = row_leaf_text(&cell.content); | |
| 1679 | 1679 | (!said.is_empty()).then_some(said) | |
| 1680 | 1680 | }) | |
| 1681 | 1681 | .unwrap_or_default() | |
| @@ -1700,7 +1700,11 @@ | |||
| 1700 | 1700 | /// leaves. The first textual part is what `Row::new` takes and what a reader | |
| 1701 | 1701 | /// would call the row, so there is nothing to invent here. | |
| 1702 | 1702 | fn row_name(row: &quasi_router::Row) -> String { | |
| 1703 | - | let parts: Vec<Node> = row.parts.iter().map(|part| part.node.clone()).collect(); | |
| 1703 | + | let parts: Vec<Node> = row | |
| 1704 | + | .cells | |
| 1705 | + | .iter() | |
| 1706 | + | .flat_map(|cell| cell.content.iter().cloned()) | |
| 1707 | + | .collect(); | |
| 1704 | 1708 | row_leaf_text(&parts) | |
| 1705 | 1709 | } | |
| 1706 | 1710 | ||
| @@ -1710,24 +1714,30 @@ | |||
| 1710 | 1714 | /// where it sits in the run and the node says what it is. That is the property | |
| 1711 | 1715 | /// the containment migration bought every renderer, and it is why a row does not | |
| 1712 | 1716 | /// need a second switch over member types here. | |
| 1713 | - | fn row_part(pass: &mut Pass<'_>, ui: &mut Ui, part: &quasi_router::Part, after: Option<f32>) { | |
| 1717 | + | fn row_part(pass: &mut Pass<'_>, ui: &mut Ui, part: &quasi_router::Cell, after: Option<f32>) { | |
| 1714 | 1718 | // The cap, settled by `7bfb554a`: a run is a line, and a part takes the | |
| 1715 | 1719 | // lines its flow allows and no more. Only the text leaves need it -- a | |
| 1716 | 1720 | // token, a control and a meter are single-line widgets already, and putting | |
| 1717 | 1721 | // them through a galley would be saying something about them that the | |
| 1718 | 1722 | // description did not. | |
| 1719 | - | match &part.node { | |
| 1720 | - | Node::Text { text, .. } => capped(pass.immediate, ui, text, part.flow, after), | |
| 1721 | - | Node::Rich { source, .. } => { | |
| 1722 | - | capped( | |
| 1723 | - | pass.immediate, | |
| 1724 | - | ui, | |
| 1725 | - | &docengine::render_plain(source), | |
| 1726 | - | part.flow, | |
| 1727 | - | after, | |
| 1728 | - | ); | |
| 1723 | + | // A cell holds a run since the 2026-09-05 collapse, so this walks it. The | |
| 1724 | + | // flow is the cell's, which is the column's unless the description narrowed | |
| 1725 | + | // it, and it applies to every text leaf in the run. | |
| 1726 | + | let flow = part.room(); | |
| 1727 | + | for node in &part.content { | |
| 1728 | + | match node { | |
| 1729 | + | Node::Text { text, .. } => capped(pass.immediate, ui, text, flow, after), | |
| 1730 | + | Node::Rich { source, .. } => { | |
| 1731 | + | capped( | |
| 1732 | + | pass.immediate, | |
| 1733 | + | ui, | |
| 1734 | + | &docengine::render_plain(source), | |
| 1735 | + | flow, | |
| 1736 | + | after, | |
| 1737 | + | ); | |
| 1738 | + | } | |
| 1739 | + | other => draw(pass, ui, other), | |
| 1729 | 1740 | } | |
| 1730 | - | _ => draw(pass, ui, &part.node), | |
| 1731 | 1741 | } | |
| 1732 | 1742 | } | |
| 1733 | 1743 | ||
| @@ -1741,7 +1751,7 @@ | |||
| 1741 | 1751 | fn tail_width( | |
| 1742 | 1752 | ui: &Ui, | |
| 1743 | 1753 | immediate: &Immediate, | |
| 1744 | - | rest: &[quasi_router::Part], | |
| 1754 | + | rest: &[quasi_router::Cell], | |
| 1745 | 1755 | spacing: f32, | |
| 1746 | 1756 | ) -> Option<f32> { | |
| 1747 | 1757 | if rest.is_empty() { | |
| @@ -1754,9 +1764,22 @@ | |||
| 1754 | 1764 | Some(total) | |
| 1755 | 1765 | } | |
| 1756 | 1766 | ||
| 1757 | - | /// The width a part wants when nothing is squeezing it. | |
| 1758 | - | fn intrinsic_width(ui: &Ui, immediate: &Immediate, part: &quasi_router::Part) -> Option<f32> { | |
| 1759 | - | match &part.node { | |
| 1767 | + | /// The width a cell wants when nothing is squeezing it. | |
| 1768 | + | /// | |
| 1769 | + | /// A cell holds a run since the 2026-09-05 collapse, so this is the sum of its | |
| 1770 | + | /// leaves. `None` if any leaf cannot say, which is the same answer the single | |
| 1771 | + | /// node gave before: a run containing something unmeasurable is unmeasurable. | |
| 1772 | + | fn intrinsic_width(ui: &Ui, immediate: &Immediate, part: &quasi_router::Cell) -> Option<f32> { | |
| 1773 | + | let mut total = 0.0; | |
| 1774 | + | for node in &part.content { | |
| 1775 | + | total += leaf_width(ui, immediate, node)?; | |
| 1776 | + | } | |
| 1777 | + | Some(total) | |
| 1778 | + | } | |
| 1779 | + | ||
| 1780 | + | /// The width one leaf wants when nothing is squeezing it. | |
| 1781 | + | fn leaf_width(ui: &Ui, immediate: &Immediate, node: &Node) -> Option<f32> { | |
| 1782 | + | match node { | |
| 1760 | 1783 | Node::Text { text, .. } => Some(text_width(ui, text)), | |
| 1761 | 1784 | Node::Rich { source, .. } => Some(text_width(ui, &docengine::render_plain(source))), | |
| 1762 | 1785 | // A token is its words plus `token_padding` on both sides, which is | |
| @@ -2091,7 +2114,7 @@ | |||
| 2091 | 2114 | let Some(index) = columns.iter().position(|c| c.name == column.name) else { | |
| 2092 | 2115 | break 'cell; | |
| 2093 | 2116 | }; | |
| 2094 | - | let Some(cell) = row.values.get(index) else { | |
| 2117 | + | let Some(cell) = row.cells.get(index) else { | |
| 2095 | 2118 | break 'cell; | |
| 2096 | 2119 | }; | |
| 2097 | 2120 | // The outline, in the first column and nowhere else. A table | |
| @@ -2119,7 +2142,7 @@ | |||
| 2119 | 2142 | }; | |
| 2120 | 2143 | ui.label(RichText::new(sign).monospace().color(colour)); | |
| 2121 | 2144 | } | |
| 2122 | - | for node in &cell.parts { | |
| 2145 | + | for node in &cell.content { | |
| 2123 | 2146 | // A cell's contents are ordinary nodes, but a press inside one | |
| 2124 | 2147 | // cannot reach the pass from here. Only the two that carry an | |
| 2125 | 2148 | // address are collected; everything else draws. |
| @@ -9,7 +9,7 @@ | |||
| 9 | 9 | use makeover_immediate::Palette; | |
| 10 | 10 | use quasi_router::{ | |
| 11 | 11 | Act, Action, Address, Chrome, Consult, Field, Frame, Message, Method, Node, Outcome, | |
| 12 | - | RegionKind, Request, Response, Run, Screen, Slot, layout, | |
| 12 | + | RegionKind, Request, Response, Row, Run, Screen, Slot, layout, | |
| 13 | 13 | }; | |
| 14 | 14 | ||
| 15 | 15 | use crate::view::Asking; | |
| @@ -94,7 +94,7 @@ | |||
| 94 | 94 | Node::list([quasi_router::Row::new("One")]), | |
| 95 | 95 | Node::Table { | |
| 96 | 96 | columns: vec![quasi_router::Column::new("Name")], | |
| 97 | - | rows: vec![quasi_router::Cells::new([quasi_router::Cell::new("One")])], | |
| 97 | + | rows: vec![quasi_router::Row::cells([quasi_router::Cell::new("One")])], | |
| 98 | 98 | more: None, | |
| 99 | 99 | }, | |
| 100 | 100 | Node::Timeline { | |
| @@ -1066,14 +1066,14 @@ | |||
| 1066 | 1066 | // The node this renderer declined to draw until 2026-08-14. The narrowing | |
| 1067 | 1067 | // and the tracks are makeover-immediate's; what is asserted here is that a | |
| 1068 | 1068 | // described table reaches them at all, with a cell holding an ordinary node. | |
| 1069 | - | use quasi_router::{Cell, Cells, Column}; | |
| 1069 | + | use quasi_router::{Cell, Column}; | |
| 1070 | 1070 | ||
| 1071 | 1071 | let columns = vec![Column::new("name"), Column::new("bpm")]; | |
| 1072 | 1072 | let rows = vec![ | |
| 1073 | - | Cells::new(["kick.wav", "120"]), | |
| 1073 | + | Row::cells(["kick.wav", "120"]), | |
| 1074 | 1074 | // A cell holding a control rather than a value, which is what | |
| 1075 | 1075 | // `CellPart` exists to separate and what a file list actually has. | |
| 1076 | - | Cells::new([ | |
| 1076 | + | Row::cells([ | |
| 1077 | 1077 | Cell::new("snare.wav"), | |
| 1078 | 1078 | Cell::acts([Act::new("Play", Action::post("/play"))]), | |
| 1079 | 1079 | ]), | |
| @@ -1137,15 +1137,15 @@ | |||
| 1137 | 1137 | // `Node::Table` and this renderer is what draws it. The collecting slot the | |
| 1138 | 1138 | // menu uses is separate from the one `activate` uses, so this also covers | |
| 1139 | 1139 | // the case where a row carries both. | |
| 1140 | - | use quasi_router::{Cells, Column}; | |
| 1140 | + | use quasi_router::Column; | |
| 1141 | 1141 | ||
| 1142 | 1142 | let screen = screen_of([Node::Table { | |
| 1143 | 1143 | columns: vec![Column::new("Name"), Column::new("BPM")], | |
| 1144 | 1144 | rows: vec![ | |
| 1145 | - | Cells::new(["kick.wav", "120"]) | |
| 1145 | + | Row::cells(["kick.wav", "120"]) | |
| 1146 | 1146 | .activate(Action::post("/files/1/open")) | |
| 1147 | 1147 | .offers(Act::new("Preview", Action::post("/files/1/play"))), | |
| 1148 | - | Cells::new(["snare.wav", "140"]), | |
| 1148 | + | Row::cells(["snare.wav", "140"]), | |
| 1149 | 1149 | ], | |
| 1150 | 1150 | more: None, | |
| 1151 | 1151 | }]); | |
| @@ -1166,13 +1166,13 @@ | |||
| 1166 | 1166 | // because this renderer holds the set: how many are in it, and that a | |
| 1167 | 1167 | // control over none of them should not fire. Drawn rather than hidden, so | |
| 1168 | 1168 | // the affordance stays on screen and a reader learns bulk actions exist. | |
| 1169 | - | use quasi_router::{Cells, Column}; | |
| 1169 | + | use quasi_router::Column; | |
| 1170 | 1170 | ||
| 1171 | 1171 | let screen = Screen::sidebar_content("Tasks").selecting("chosen").with( | |
| 1172 | 1172 | Slot::new("main", RegionKind::Pane) | |
| 1173 | 1173 | .with(Node::Table { | |
| 1174 | 1174 | columns: vec![Column::new("title")], | |
| 1175 | - | rows: vec![Cells::new(["First"]).ticking("t-1", false)], | |
| 1175 | + | rows: vec![Row::cells(["First"]).ticking("t-1", false)], | |
| 1176 | 1176 | more: None, | |
| 1177 | 1177 | }) | |
| 1178 | 1178 | .with(Node::Act( | |
| @@ -1197,13 +1197,13 @@ | |||
| 1197 | 1197 | // so this renderer adds one. The assertion is that the added column does | |
| 1198 | 1198 | // not disturb the described ones: a two-column table with ticks still | |
| 1199 | 1199 | // reaches both of its own cells. | |
| 1200 | - | use quasi_router::{Cells, Column}; | |
| 1200 | + | use quasi_router::Column; | |
| 1201 | 1201 | ||
| 1202 | 1202 | let screen = screen_of([Node::Table { | |
| 1203 | 1203 | columns: vec![Column::new("name"), Column::new("bpm")], | |
| 1204 | 1204 | rows: vec![ | |
| 1205 | - | Cells::new(["kick.wav", "120"]).ticking("k", false), | |
| 1206 | - | Cells::new(["snare.wav", "140"]).ticking("s", true), | |
| 1205 | + | Row::cells(["kick.wav", "120"]).ticking("k", false), | |
| 1206 | + | Row::cells(["snare.wav", "140"]).ticking("s", true), | |
| 1207 | 1207 | ], | |
| 1208 | 1208 | more: None, | |
| 1209 | 1209 | }]); | |
| @@ -1253,7 +1253,7 @@ | |||
| 1253 | 1253 | quasi_router::Column::new("Kind").priority(layout::Priority::Secondary), | |
| 1254 | 1254 | quasi_router::Column::new("Added").priority(layout::Priority::Optional), | |
| 1255 | 1255 | ], | |
| 1256 | - | rows: vec![quasi_router::Cells::new([ | |
| 1256 | + | rows: vec![quasi_router::Row::cells([ | |
| 1257 | 1257 | "kick.wav", | |
| 1258 | 1258 | "sample", | |
| 1259 | 1259 | "2026-08-12", | |
| @@ -2035,13 +2035,13 @@ | |||
| 2035 | 2035 | // member was read into a parameter both callers passed `None` for, so the | |
| 2036 | 2036 | // count went undrawn, the press over nothing went through, and what it sent | |
| 2037 | 2037 | // travelled under the selection's name rather than under `Node::TICKED`. | |
| 2038 | - | use quasi_router::{Cells, Column}; | |
| 2038 | + | use quasi_router::Column; | |
| 2039 | 2039 | ||
| 2040 | 2040 | let screen = Screen::sidebar_content("Tasks").selecting("chosen").with( | |
| 2041 | 2041 | Slot::new("main", RegionKind::Pane) | |
| 2042 | 2042 | .with(Node::Table { | |
| 2043 | 2043 | columns: vec![Column::new("title")], | |
| 2044 | - | rows: vec![Cells::new(["First"]).ticking("t-1", false)], | |
| 2044 | + | rows: vec![Row::cells(["First"]).ticking("t-1", false)], | |
| 2045 | 2045 | more: None, | |
| 2046 | 2046 | }) | |
| 2047 | 2047 | .with(Node::Act( | |
| @@ -2072,13 +2072,13 @@ | |||
| 2072 | 2072 | // `033ff3ca`. The box stands beside the control here rather than behind a | |
| 2073 | 2073 | // disclosure, so what is asserted is the payload: the value the box holds | |
| 2074 | 2074 | // and the set the verb acts over, in one call. | |
| 2075 | - | use quasi_router::{Cells, Column, Field}; | |
| 2075 | + | use quasi_router::{Column, Field}; | |
| 2076 | 2076 | ||
| 2077 | 2077 | let screen = Screen::sidebar_content("Items").selecting("chosen").with( | |
| 2078 | 2078 | Slot::new("main", RegionKind::Pane) | |
| 2079 | 2079 | .with(Node::Table { | |
| 2080 | 2080 | columns: vec![Column::new("title")], | |
| 2081 | - | rows: vec![Cells::new(["First"]).ticking("i-1", false)], | |
| 2081 | + | rows: vec![Row::cells(["First"]).ticking("i-1", false)], | |
| 2082 | 2082 | more: None, | |
| 2083 | 2083 | }) | |
| 2084 | 2084 | .with(Node::Act( | |
| @@ -2171,15 +2171,15 @@ | |||
| 2171 | 2171 | // `Node::Table`. `makeover_immediate::table` answers a `Ui` per cell and no | |
| 2172 | 2172 | // row-wide rect, so the menu hangs off every cell of the row, and the | |
| 2173 | 2173 | // second column is what proves it rather than the first. | |
| 2174 | - | use quasi_router::{Cells, Column}; | |
| 2174 | + | use quasi_router::Column; | |
| 2175 | 2175 | ||
| 2176 | 2176 | let screen = screen_of([Node::Table { | |
| 2177 | 2177 | columns: vec![Column::new("Name"), Column::new("BPM")], | |
| 2178 | 2178 | rows: vec![ | |
| 2179 | - | Cells::new(["kick.wav", "120"]) | |
| 2179 | + | Row::cells(["kick.wav", "120"]) | |
| 2180 | 2180 | .activate(Action::post("/files/1/open")) | |
| 2181 | 2181 | .offers(Act::new("Preview", Action::post("/files/1/play"))), | |
| 2182 | - | Cells::new(["snare.wav", "140"]).activate(Action::post("/files/2/open")), | |
| 2182 | + | Row::cells(["snare.wav", "140"]).activate(Action::post("/files/2/open")), | |
| 2183 | 2183 | ], | |
| 2184 | 2184 | more: None, | |
| 2185 | 2185 | }]); | |
| @@ -2199,13 +2199,13 @@ | |||
| 2199 | 2199 | ||
| 2200 | 2200 | #[test] | |
| 2201 | 2201 | fn a_table_row_without_a_menu_still_opens() { | |
| 2202 | - | use quasi_router::{Cells, Column}; | |
| 2202 | + | use quasi_router::Column; | |
| 2203 | 2203 | ||
| 2204 | 2204 | let screen = screen_of([Node::Table { | |
| 2205 | 2205 | columns: vec![Column::new("Name")], | |
| 2206 | 2206 | rows: vec![ | |
| 2207 | - | Cells::new(["kick.wav"]).activate(Action::post("/files/1/open")), | |
| 2208 | - | Cells::new(["snare.wav"]).activate(Action::post("/files/2/open")), | |
| 2207 | + | Row::cells(["kick.wav"]).activate(Action::post("/files/1/open")), | |
| 2208 | + | Row::cells(["snare.wav"]).activate(Action::post("/files/2/open")), | |
| 2209 | 2209 | ], | |
| 2210 | 2210 | more: None, | |
| 2211 | 2211 | }]); | |
| @@ -2809,10 +2809,11 @@ | |||
| 2809 | 2809 | draw( | |
| 2810 | 2810 | &screen_of([Node::Table { | |
| 2811 | 2811 | columns: vec![quasi_router::Column::new("Task")], | |
| 2812 | - | rows: vec![quasi_router::Cells::new([ | |
| 2812 | + | rows: vec![quasi_router::Row::cells([ | |
| 2813 | 2813 | cell, | |
| 2814 | 2814 | quasi_router::Cell { | |
| 2815 | - | parts: vec![Node::since(started)], | |
| 2815 | + | content: vec![Node::since(started)], | |
| 2816 | + | ..quasi_router::Cell::default() | |
| 2816 | 2817 | }, | |
| 2817 | 2818 | ])], | |
| 2818 | 2819 | more: None, | |
| @@ -3428,7 +3429,7 @@ | |||
| 3428 | 3429 | quasi_router::Column::new("Tempo"), | |
| 3429 | 3430 | ], | |
| 3430 | 3431 | rows: vec![ | |
| 3431 | - | quasi_router::Cells::new(vec![ | |
| 3432 | + | quasi_router::Row::cells(vec![ | |
| 3432 | 3433 | quasi_router::Cell::new("kick.wav"), | |
| 3433 | 3434 | quasi_router::Cell::new("90"), | |
| 3434 | 3435 | ]) | |
| @@ -3450,7 +3451,7 @@ | |||
| 3450 | 3451 | fn a_table_row_that_only_lists_claims_nothing() { | |
| 3451 | 3452 | let screen = screen_of([Node::Table { | |
| 3452 | 3453 | columns: vec![quasi_router::Column::new("Name")], | |
| 3453 | - | rows: vec![quasi_router::Cells::new(vec![quasi_router::Cell::new( | |
| 3454 | + | rows: vec![quasi_router::Row::cells(vec![quasi_router::Cell::new( | |
| 3454 | 3455 | "kick.wav", | |
| 3455 | 3456 | )])], | |
| 3456 | 3457 | more: None, | |
| @@ -3884,12 +3885,12 @@ | |||
| 3884 | 3885 | quasi_router::Column::new("Size"), | |
| 3885 | 3886 | ], | |
| 3886 | 3887 | rows: vec![ | |
| 3887 | - | quasi_router::Cells::new([ | |
| 3888 | + | quasi_router::Row::cells([ | |
| 3888 | 3889 | quasi_router::Cell::new("kick.wav"), | |
| 3889 | 3890 | quasi_router::Cell::new("2.1 MB"), | |
| 3890 | 3891 | ]) | |
| 3891 | 3892 | .ticking("1", false), | |
| 3892 | - | quasi_router::Cells::new([ | |
| 3893 | + | quasi_router::Row::cells([ | |
| 3893 | 3894 | quasi_router::Cell::new("snare.wav"), | |
| 3894 | 3895 | quasi_router::Cell::new("1.4 MB"), | |
| 3895 | 3896 | ]) |
| @@ -49,7 +49,7 @@ | |||
| 49 | 49 | //! closed set, which is what `makeover_layout::Intent` already is. | |
| 50 | 50 | ||
| 51 | 51 | use crate::screen::{ | |
| 52 | - | Act, Cell, Cells, Choice, Column, Field, Figure, Meter, Node, Prose, RegionKind, Row, Slot, Tag, | |
| 52 | + | Act, Cell, Choice, Column, Field, Figure, Meter, Node, Prose, RegionKind, Row, Slot, Tag, | |
| 53 | 53 | }; | |
| 54 | 54 | ||
| 55 | 55 | /// What an element may hold. | |
| @@ -277,11 +277,8 @@ | |||
| 277 | 277 | } | |
| 278 | 278 | } | |
| 279 | 279 | ||
| 280 | - | impl Element for Cells { | |
| 281 | - | fn containment(&self) -> Containment { | |
| 282 | - | Containment::Collection(Of::Cells) | |
| 283 | - | } | |
| 284 | - | } | |
| 280 | + | // `Element for Cells` was deleted 2026-09-05: Cells is an alias for Row, | |
| 281 | + | // so the impl for Row is the impl for both. | |
| 285 | 282 | ||
| 286 | 283 | impl Element for Cell { | |
| 287 | 284 | fn containment(&self) -> Containment { |
| @@ -163,11 +163,11 @@ | |||
| 163 | 163 | }; | |
| 164 | 164 | pub use crate::router::{Handler, Router}; | |
| 165 | 165 | pub use crate::screen::{ | |
| 166 | - | Accepted, Act, Action, Adds, Answer, CUTOFFS, Candidate, Canvas, Cell, Cells, Choice, Choosing, | |
| 167 | - | Clock, Column, Consult, Curve, Destination, Discovery, Document, Feed, FeedKind, Field, Figure, | |
| 168 | - | Held, Image, Instance, Jump, Meter, Node, Outline, Part, Placed, Prefill, Progress, Prose, | |
| 169 | - | Question, Ranked, RegionKind, Repeat, Repeating, Replaces, Rest, Reveal, Richness, Row, Run, | |
| 170 | - | Screen, Slot, SocialKind, Table, Tag, ThemeChoice, Trust, folded, folded_by, | |
| 166 | + | Accepted, Act, Action, Adds, Answer, CUTOFFS, Candidate, Canvas, Cell, CellKey, Cells, Choice, | |
| 167 | + | Choosing, Clock, Column, Consult, Curve, Destination, Discovery, Document, Feed, FeedKind, | |
| 168 | + | Field, Figure, Held, Image, Instance, Jump, Meter, Node, Outline, Placed, Prefill, Progress, | |
| 169 | + | Prose, Question, Ranked, RegionKind, Repeat, Repeating, Replaces, Rest, Reveal, Richness, Row, | |
| 170 | + | Run, Screen, Slot, SocialKind, Table, Tag, ThemeChoice, Trust, folded, folded_by, | |
| 171 | 171 | writable_root_attr, | |
| 172 | 172 | }; | |
| 173 | 173 |
| @@ -6212,52 +6212,12 @@ | |||
| 6212 | 6212 | } | |
| 6213 | 6213 | } | |
| 6214 | 6214 | ||
| 6215 | - | /// One part of a row's run, and the role it takes. | |
| 6215 | + | /// `Part` was merged into [`Cell`] on 2026-09-05. | |
| 6216 | 6216 | /// | |
| 6217 | - | /// A cell's run entries carry no role because their kind already says which | |
| 6218 | - | /// part they are: text is the value, a [`Node::Link`] is the link, a | |
| 6219 | - | /// [`Node::Token`] is a chip, a [`Node::Act`] is a control. A row's | |
| 6220 | - | /// `primary`, `secondary` and `meta` are three *text* roles, and kind cannot | |
| 6221 | - | /// tell those apart, so a row says which one it means. | |
| 6222 | - | /// | |
| 6223 | - | /// The role is a style role and nothing else. [`layout::RowPart`] is unchanged | |
| 6224 | - | /// by the containment model: it says how a part is drawn, not what may sit in | |
| 6225 | - | /// it, and that is the half of it worth keeping. | |
| 6226 | - | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 6227 | - | pub struct Part { | |
| 6228 | - | /// Which of the row's roles this part takes. | |
| 6229 | - | pub role: layout::RowPart, | |
| 6230 | - | /// What is in it. A leaf, since a row is an inline run. | |
| 6231 | - | pub node: Node, | |
| 6232 | - | /// How much vertical room it may take. | |
| 6233 | - | /// | |
| 6234 | - | /// [`layout::Flow::Tight`] by default, which is one line and is what every | |
| 6235 | - | /// part did before this field existed. [`Row::relaxed`] is how a call site | |
| 6236 | - | /// asks for two, and the two apps that had written a two-line clamp into | |
| 6237 | - | /// their own stylesheets are why it exists. | |
| 6238 | - | /// | |
| 6239 | - | /// On the part rather than on the row: BB clamps a feed row's *title* while | |
| 6240 | - | /// its excerpt wraps freely underneath, so a row-wide setting would have | |
| 6241 | - | /// been wrong at the only site that asked for it. | |
| 6242 | - | pub flow: layout::Flow, | |
| 6243 | - | /// What it is worth when the run does not fit. | |
| 6244 | - | /// | |
| 6245 | - | /// `None` means the description did not say, and the role answers instead | |
| 6246 | - | /// ([`layout::RowPart::priority`]). An `Option` rather than a defaulted | |
| 6247 | - | /// value because there is no global default worth having: `Optional` would | |
| 6248 | - | /// make every part droppable and `Essential` would make the ladder inert, | |
| 6249 | - | /// and only the role knows which a part meant. [`Part::worth`] is what a | |
| 6250 | - | /// renderer should read. | |
| 6251 | - | pub priority: Option<layout::Priority>, | |
| 6252 | - | } | |
| 6253 | - | ||
| 6254 | - | impl Part { | |
| 6255 | - | /// What this part is worth, said or inherited from its role. | |
| 6256 | - | #[must_use] | |
| 6257 | - | pub fn worth(&self) -> layout::Priority { | |
| 6258 | - | self.priority.unwrap_or_else(|| self.role.priority()) | |
| 6259 | - | } | |
| 6260 | - | } | |
| 6217 | + | /// A part was a cell that carried its own role because a list had no columns to | |
| 6218 | + | /// carry it. Now a list declares columns like a table does, so the role is | |
| 6219 | + | /// [`CellKey::Role`] and there is one type. `Part::worth` is [`Cell::priority`] | |
| 6220 | + | /// falling back to its key. | |
| 6261 | 6221 | ||
| 6262 | 6222 | /// One row of a list. | |
| 6263 | 6223 | /// | |
| @@ -6329,12 +6289,19 @@ | |||
| 6329 | 6289 | ||
| 6330 | 6290 | #[derive(Debug, Clone, PartialEq, Eq, Default)] | |
| 6331 | 6291 | pub struct Row { | |
| 6332 | - | /// What is in the row, in order. | |
| 6292 | + | /// What is in the row, in order, one entry per column it says anything in. | |
| 6333 | 6293 | /// | |
| 6334 | 6294 | /// Built by [`Row::new`], [`secondary`](Row::secondary), | |
| 6335 | - | /// [`meta`](Row::meta), [`token`](Row::token), [`act`](Row::act) and | |
| 6336 | - | /// [`meter`](Row::meter). | |
| 6337 | - | pub parts: Vec<Part>, | |
| 6295 | + | /// [`meta`](Row::meta), [`token`](Row::token), [`act`](Row::act), | |
| 6296 | + | /// [`meter`](Row::meter) for the default column set, and by | |
| 6297 | + | /// [`cells`](Row::cells), [`at`](Row::at) and [`cell`](Row::cell) for a | |
| 6298 | + | /// declared one. | |
| 6299 | + | /// | |
| 6300 | + | /// A cell says which column it answers to through [`Cell::key`], so a row | |
| 6301 | + | /// is no longer two collections with a private staging vector between | |
| 6302 | + | /// them. A cell keyed [`CellKey::Named`] is unresolved until | |
| 6303 | + | /// [`Table::row`] sees it. | |
| 6304 | + | pub cells: Vec<Cell>, | |
| 6338 | 6305 | /// The route that selects this row, if selecting it does anything. | |
| 6339 | 6306 | pub activate: Option<Action>, | |
| 6340 | 6307 | /// Whether this is the row the detail side is currently showing. | |
| @@ -6496,6 +6463,23 @@ | |||
| 6496 | 6463 | /// | |
| 6497 | 6464 | /// [`Region::showing_at_most_one`]: Region::showing_at_most_one | |
| 6498 | 6465 | pub open: Option<bool>, | |
| 6466 | + | /// Which side of a change this row is on, when the table is a diff. | |
| 6467 | + | /// | |
| 6468 | + | /// Decision `19d7602d` (2026-09-02, option d). A diff is a table of lines, | |
| 6469 | + | /// and the only thing the vocabulary was missing was a way for a line to | |
| 6470 | + | /// say whether it was added, removed or unchanged. So it is a member here | |
| 6471 | + | /// rather than a `Node::Diff` carrying git's data model into a vocabulary | |
| 6472 | + | /// shared by a task manager and a sample browser. | |
| 6473 | + | /// | |
| 6474 | + | /// `None` is not [`Change::Context`]. `None` means this table is not a diff | |
| 6475 | + | /// and no renderer should tint it; `Some(Context)` means it is a diff and | |
| 6476 | + | /// this line did not change. Two facts, and a table of ordinary rows must | |
| 6477 | + | /// not read as a diff whose every line is context. | |
| 6478 | + | /// | |
| 6479 | + | /// What a renderer does with it is [`Change`]'s `Intent` impl and its own | |
| 6480 | + | /// palette. A terminal with two colours to spend gets the same three | |
| 6481 | + | /// answers a browser does. | |
| 6482 | + | pub change: Option<layout::Change>, | |
| 6499 | 6483 | } | |
| 6500 | 6484 | ||
| 6501 | 6485 | impl Row { | |
| @@ -6507,14 +6491,15 @@ | |||
| 6507 | 6491 | pub fn new(primary: impl Into<String>) -> Self { | |
| 6508 | 6492 | let primary = primary.into(); | |
| 6509 | 6493 | Self { | |
| 6510 | - | parts: if primary.is_empty() { | |
| 6494 | + | cells: if primary.is_empty() { | |
| 6511 | 6495 | Vec::new() | |
| 6512 | 6496 | } else { | |
| 6513 | - | vec![Part { | |
| 6514 | - | role: layout::RowPart::Primary, | |
| 6515 | - | node: Node::text(primary), | |
| 6516 | - | flow: layout::Flow::default(), | |
| 6497 | + | vec![Cell { | |
| 6498 | + | key: CellKey::Role(layout::RowPart::Primary), | |
| 6499 | + | content: vec![Node::text(primary)], | |
| 6500 | + | flow: None, | |
| 6517 | 6501 | priority: None, | |
| 6502 | + | span: 1, | |
| 6518 | 6503 | }] | |
| 6519 | 6504 | }, | |
| 6520 | 6505 | ..Self::default() | |
| @@ -6629,11 +6614,12 @@ | |||
| 6629 | 6614 | /// Add a token, chaining. | |
| 6630 | 6615 | #[must_use] | |
| 6631 | 6616 | pub fn token(mut self, tag: Tag) -> Self { | |
| 6632 | - | self.parts.push(Part { | |
| 6633 | - | role: layout::RowPart::Tokens, | |
| 6634 | - | node: Node::Token(tag), | |
| 6635 | - | flow: layout::Flow::default(), | |
| 6617 | + | self.cells.push(Cell { | |
| 6618 | + | key: CellKey::Role(layout::RowPart::Tokens), | |
| 6619 | + | content: vec![Node::Token(tag)], | |
| 6620 | + | flow: None, | |
| 6636 | 6621 | priority: None, | |
| 6622 | + | span: 1, | |
| 6637 | 6623 | }); | |
| 6638 | 6624 | self | |
| 6639 | 6625 | } | |
| @@ -6716,11 +6702,12 @@ | |||
| 6716 | 6702 | /// A control acting on this row. | |
| 6717 | 6703 | #[must_use] | |
| 6718 | 6704 | pub fn act(mut self, act: Act) -> Self { | |
| 6719 | - | self.parts.push(Part { | |
| 6720 | - | role: layout::RowPart::Actions, | |
| 6721 | - | node: Node::Act(act), | |
| 6722 | - | flow: layout::Flow::default(), | |
| 6705 | + | self.cells.push(Cell { | |
| 6706 | + | key: CellKey::Role(layout::RowPart::Actions), | |
| 6707 | + | content: vec![Node::Act(act)], | |
| 6708 | + | flow: None, | |
| 6723 | 6709 | priority: None, | |
| 6710 | + | span: 1, | |
| 6724 | 6711 | }); | |
| 6725 | 6712 | self | |
| 6726 | 6713 | } | |
| @@ -6750,11 +6737,12 @@ | |||
| 6750 | 6737 | "a row is an inline run and holds leaves; {node:?} holds {:?}", | |
| 6751 | 6738 | node.containment() | |
| 6752 | 6739 | ); | |
| 6753 | - | self.parts.push(Part { | |
| 6754 | - | role, | |
| 6755 | - | node, | |
| 6756 | - | flow: layout::Flow::default(), | |
| 6740 | + | self.cells.push(Cell { | |
| 6741 | + | key: CellKey::Role(role), | |
| 6742 | + | content: vec![node], | |
| 6743 | + | flow: None, | |
| 6757 | 6744 | priority: None, | |
| 6745 | + | span: 1, | |
| 6758 | 6746 | }); | |
| 6759 | 6747 | self | |
| 6760 | 6748 | } | |
| @@ -6775,8 +6763,8 @@ | |||
| 6775 | 6763 | /// costs each renderer is in that type's docs. | |
| 6776 | 6764 | #[must_use] | |
| 6777 | 6765 | pub fn relaxed(mut self) -> Self { | |
| 6778 | - | if let Some(part) = self.parts.last_mut() { | |
| 6779 | - | part.flow = layout::Flow::Relaxed; | |
| 6766 | + | if let Some(part) = self.cells.last_mut() { | |
| 6767 | + | part.flow = Some(layout::Flow::Relaxed); | |
| 6780 | 6768 | } | |
| 6781 | 6769 | self | |
| 6782 | 6770 | } | |
| @@ -6790,7 +6778,7 @@ | |||
| 6790 | 6778 | /// A no-op on an empty run rather than a panic, for `relaxed`'s reason. | |
| 6791 | 6779 | #[must_use] | |
| 6792 | 6780 | pub fn worth(mut self, priority: layout::Priority) -> Self { | |
| 6793 | - | if let Some(part) = self.parts.last_mut() { | |
| 6781 | + | if let Some(part) = self.cells.last_mut() { | |
| 6794 | 6782 | part.priority = Some(priority); | |
| 6795 | 6783 | } | |
| 6796 | 6784 | self | |
| @@ -6803,23 +6791,82 @@ | |||
| 6803 | 6791 | /// Building a row that calls `.meta` twice meant the second one won when | |
| 6804 | 6792 | /// `meta` was an `Option`, and it still does. | |
| 6805 | 6793 | fn set(&mut self, role: layout::RowPart, node: Node) { | |
| 6806 | - | match self.parts.iter_mut().find(|part| part.role == role) { | |
| 6807 | - | Some(part) => part.node = node, | |
| 6808 | - | None => self.parts.push(Part { | |
| 6809 | - | role, | |
| 6810 | - | node, | |
| 6811 | - | flow: layout::Flow::default(), | |
| 6794 | + | match self | |
| 6795 | + | .cells | |
| 6796 | + | .iter_mut() | |
| 6797 | + | .find(|cell| cell.key == CellKey::Role(role)) | |
| 6798 | + | { | |
| 6799 | + | Some(cell) => cell.content = vec![node], | |
| 6800 | + | None => self.cells.push(Cell { | |
| 6801 | + | key: CellKey::Role(role), | |
| 6802 | + | content: vec![node], | |
| 6803 | + | flow: None, | |
| 6812 | 6804 | priority: None, | |
| 6805 | + | span: 1, | |
| 6813 | 6806 | }), | |
| 6814 | 6807 | } | |
| 6815 | 6808 | } | |
| 6816 | 6809 | ||
| 6810 | + | /// A row of a declared table, its cells in column order. | |
| 6811 | + | /// | |
| 6812 | + | /// Was `Cells::new` before the 2026-09-05 collapse. It is a separate | |
| 6813 | + | /// constructor from [`new`](Self::new) rather than an overload of it | |
| 6814 | + | /// because the two say different things: `Row::new` names the primary | |
| 6815 | + | /// column of the default set, and this answers a column list positionally. | |
| 6816 | + | #[must_use] | |
| 6817 | + | pub fn cells(values: impl IntoIterator<Item = impl Into<Cell>>) -> Self { | |
| 6818 | + | Self { | |
| 6819 | + | cells: values | |
| 6820 | + | .into_iter() | |
| 6821 | + | .enumerate() | |
| 6822 | + | .map(|(at, cell)| { | |
| 6823 | + | let mut cell = cell.into(); | |
| 6824 | + | cell.key = CellKey::Column(at); | |
| 6825 | + | cell | |
| 6826 | + | }) | |
| 6827 | + | .collect(), | |
| 6828 | + | ..Self::default() | |
| 6829 | + | } | |
| 6830 | + | } | |
| 6831 | + | ||
| 6832 | + | /// A cell naming the column it sits in, chaining. | |
| 6833 | + | /// | |
| 6834 | + | /// Safer than counting to a column in every way but one, which is why | |
| 6835 | + | /// [`Table::row`] carries a debug assertion: a name no column has is | |
| 6836 | + | /// dropped, so a typo renders an empty column rather than failing to | |
| 6837 | + | /// compile. The row and the column list are usually written in different | |
| 6838 | + | /// functions, so nothing above `Table::row` has both in hand to check. | |
| 6839 | + | #[must_use] | |
| 6840 | + | pub fn at(mut self, column: impl Into<String>, cell: impl Into<Cell>) -> Self { | |
| 6841 | + | let mut cell = cell.into(); | |
| 6842 | + | cell.key = CellKey::Named(column.into()); | |
| 6843 | + | self.cells.push(cell); | |
| 6844 | + | self | |
| 6845 | + | } | |
| 6846 | + | ||
| 6847 | + | /// A cell in the next column along, chaining. | |
| 6848 | + | #[must_use] | |
| 6849 | + | pub fn cell(mut self, cell: impl Into<Cell>) -> Self { | |
| 6850 | + | let at = self.cells.len(); | |
| 6851 | + | let mut cell = cell.into(); | |
| 6852 | + | cell.key = CellKey::Column(at); | |
| 6853 | + | self.cells.push(cell); | |
| 6854 | + | self | |
| 6855 | + | } | |
| 6856 | + | ||
| 6857 | + | /// Which side of a change this row is on, when the table is a diff. | |
| 6858 | + | #[must_use] | |
| 6859 | + | pub const fn changed(mut self, change: layout::Change) -> Self { | |
| 6860 | + | self.change = Some(change); | |
| 6861 | + | self | |
| 6862 | + | } | |
| 6863 | + | ||
| 6817 | 6864 | /// The parts taking one role, in order. | |
| 6818 | 6865 | pub fn role(&self, role: layout::RowPart) -> impl Iterator<Item = &Node> { | |
| 6819 | - | self.parts | |
| 6866 | + | self.cells | |
| 6820 | 6867 | .iter() | |
| 6821 | - | .filter(move |part| part.role == role) | |
| 6822 | - | .map(|part| &part.node) | |
| 6868 | + | .filter(move |cell| cell.key == CellKey::Role(role)) | |
| 6869 | + | .flat_map(|cell| cell.content.iter()) | |
| 6823 | 6870 | } | |
| 6824 | 6871 | ||
| 6825 | 6872 | /// The row's primary text. | |
| @@ -6852,7 +6899,10 @@ | |||
| 6852 | 6899 | #[must_use] | |
| 6853 | 6900 | pub fn names(&self, id: &str) -> bool { | |
| 6854 | 6901 | self.menu.iter().any(|act| act.id.as_deref() == Some(id)) | |
| 6855 | - | || self.parts.iter().any(|part| part.node.names(id)) | |
| 6902 | + | || self | |
| 6903 | + | .cells | |
| 6904 | + | .iter() | |
| 6905 | + | .any(|cell| cell.content.iter().any(|node| node.names(id))) | |
| 6856 | 6906 | } | |
| 6857 | 6907 | ||
| 6858 | 6908 | pub fn acts(&self) -> impl Iterator<Item = &Act> { | |
| @@ -6865,8 +6915,10 @@ | |||
| 6865 | 6915 | ||
| 6866 | 6916 | /// Every kind of time-derived readout in this row's run, added to `found`. | |
| 6867 | 6917 | fn clocks_into(&self, found: &mut BTreeSet<Clock>) { | |
| 6868 | - | for part in &self.parts { | |
| 6869 | - | part.node.clocks_into(found); | |
| 6918 | + | for cell in &self.cells { | |
| 6919 | + | for node in &cell.content { | |
| 6920 | + | node.clocks_into(found); | |
| 6921 | + | } | |
| 6870 | 6922 | } | |
| 6871 | 6923 | } | |
| 6872 | 6924 | ||
| @@ -6907,21 +6959,112 @@ | |||
| 6907 | 6959 | /// A `Vec<Act>` and not a node: the ruling that a row holds no nodes holds | |
| 6908 | 6960 | /// here for the same reason. Acts carry their own tone, state and | |
| 6909 | 6961 | /// confirmation, and that is the whole of what these cells hold. | |
| 6910 | - | #[derive(Debug, Clone, PartialEq, Eq, Default)] | |
| 6962 | + | /// Which column a cell answers to. | |
| 6963 | + | /// | |
| 6964 | + | /// The 2026-09-05 collapse: a list's parts were addressed by role and a table's | |
| 6965 | + | /// values by column, and those were the same operation under two spellings, a | |
| 6966 | + | /// lookup into a declared key set. The only difference was who declares the | |
| 6967 | + | /// keys, and now that a list declares its own the difference is gone. | |
| 6968 | + | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 6969 | + | pub enum CellKey { | |
| 6970 | + | /// A column of the default set, which is what a container that declares no | |
| 6971 | + | /// columns gets. | |
| 6972 | + | /// | |
| 6973 | + | /// [`layout::RowPart`]'s six variants are that set. A container spelled as | |
| 6974 | + | /// a list is a table over them, which is why `list { row "x" { secondary | |
| 6975 | + | /// "y" } }` still says what it always said. | |
| 6976 | + | Role(layout::RowPart), | |
| 6977 | + | /// A declared column, by position, already resolved. | |
| 6978 | + | Column(usize), | |
| 6979 | + | /// A declared column, by name, pending resolution by [`Table::row`]. | |
| 6980 | + | /// | |
| 6981 | + | /// This replaces the private staging vector `Cells` used to carry. A named | |
| 6982 | + | /// cell now sits in the row with the others and says it is unresolved, | |
| 6983 | + | /// rather than living in a second collection that had to be drained. | |
| 6984 | + | Named(String), | |
| 6985 | + | } | |
| 6986 | + | ||
| 6987 | + | impl Default for CellKey { | |
| 6988 | + | fn default() -> Self { | |
| 6989 | + | Self::Role(layout::RowPart::Primary) | |
| 6990 | + | } | |
| 6991 | + | } | |
| 6992 | + | ||
| 6993 | + | /// One cell: a run of leaf nodes, addressed by a key. | |
| 6994 | + | /// | |
| 6995 | + | /// **The 2026-09-05 collapse merged `Part` into this type.** A part was a cell | |
| 6996 | + | /// that carried its own role because a list had no columns to carry it; a cell | |
| 6997 | + | /// was a part whose column carried the role instead. One type now, with | |
| 6998 | + | /// [`key`](Self::key) saying which column it answers to. | |
| 6999 | + | /// | |
| 7000 | + | /// A cell was a `String` before that, so a table whose rows carry a control | |
| 7001 | + | /// could not be described at all and had to become a [`Node::List`], losing its | |
| 7002 | + | /// column headers, which is what the MNW server's SSH-keys tab did and why it | |
| 7003 | + | /// read worse than the Askama original it replaced. | |
| 7004 | + | /// | |
| 7005 | + | /// # Why the acts sit on the cell and not on the row | |
| 7006 | + | /// | |
| 7007 | + | /// Counted across MNW's templates, 30 table rows carry a control. 25 put it | |
| 7008 | + | /// alone in the last cell, which a row-level `actions` list would have covered. | |
| 7009 | + | /// The other five put it *beside a value*: `project_content`'s position cell is | |
| 7010 | + | /// the number plus two reorder arrows, `project_synckit`'s slug cell is the slug | |
| 7011 | + | /// plus "Set slug", `promo_codes_list`'s use count is a number that is itself | |
| 7012 | + | /// the button opening the redemptions. A row-level list renders as an appended | |
| 7013 | + | /// cell and cannot say any of those, and neither can an actions *column*, since | |
| 7014 | + | /// a column is a column. The control belongs where it actually is. | |
| 7015 | + | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 6911 | 7016 | pub struct Cell { | |
| 6912 | 7017 | /// What is in it, in order. | |
| 6913 | 7018 | /// | |
| 6914 | - | /// An inline run: every part is a leaf, so the whole cell is drawable on | |
| 7019 | + | /// An inline run: every entry is a leaf, so the whole cell is drawable on | |
| 6915 | 7020 | /// one wrapped line without a renderer knowing what is in it. That is the | |
| 6916 | - | /// bound, and [`Cell::part`] is where it is enforced. | |
| 7021 | + | /// bound, and [`Cell::part`] is where it is enforced. It is also what | |
| 7022 | + | /// [`CellKey::Role`] leans on: several tokens in one row are one cell | |
| 7023 | + | /// holding several [`Node::Token`]s, not several cells fighting for a key. | |
| 6917 | 7024 | /// | |
| 6918 | 7025 | /// The run is what makes a meter in a cell or a figure in a cell sayable | |
| 6919 | 7026 | /// without a member each. | |
| 7027 | + | pub content: Vec<Node>, | |
| 7028 | + | /// Which column this cell answers to. | |
| 7029 | + | pub key: CellKey, | |
| 7030 | + | /// How much vertical room it may take, or `None` for its column's. | |
| 6920 | 7031 | /// | |
| 6921 | - | /// [`Cell::new`], [`tag`](Cell::tag), [`token`](Cell::token), | |
| 6922 | - | /// [`acts`](Cell::acts), [`act`](Cell::act) and | |
| 6923 | - | /// [`activate`](Cell::activate) build the run. | |
| 6924 | - | pub parts: Vec<Node>, | |
| 7032 | + | /// An override rather than a duplicate: the column states the default and a | |
| 7033 | + | /// cell may narrow it. BB clamps a feed row's title while its excerpt wraps | |
| 7034 | + | /// freely underneath, which is two columns rather than one override, but a | |
| 7035 | + | /// long value in one row of an otherwise tight column is the case that | |
| 7036 | + | /// keeps this here. | |
| 7037 | + | pub flow: Option<layout::Flow>, | |
| 7038 | + | /// What it is worth when the row does not fit, or `None` for its column's. | |
| 7039 | + | /// | |
| 7040 | + | /// `None` means the description did not say, and the key answers instead: | |
| 7041 | + | /// [`layout::RowPart::priority`] for a role, [`Column::priority`] for a | |
| 7042 | + | /// declared column. There is no global default worth having, which is why | |
| 7043 | + | /// this is an `Option` rather than a defaulted value. | |
| 7044 | + | pub priority: Option<layout::Priority>, | |
| 7045 | + | /// How many columns this cell covers. 1 is one column. | |
| 7046 | + | /// | |
| 7047 | + | /// **Defined and unread until the 2D work (`b1d4c5d7`).** It is here so | |
| 7048 | + | /// spanning does not cost a second breaking change; nothing honours it yet, | |
| 7049 | + | /// and a renderer meeting a value above 1 today should draw it as 1. | |
| 7050 | + | pub span: u16, | |
| 7051 | + | } | |
| 7052 | + | ||
| 7053 | + | impl Default for Cell { | |
| 7054 | + | /// An empty cell in the primary column, covering one column. | |
| 7055 | + | /// | |
| 7056 | + | /// `span` is 1 rather than 0 here, which is why this is written out: a | |
| 7057 | + | /// derived `Default` would produce a cell covering no columns, and nothing | |
| 7058 | + | /// downstream would say so. | |
| 7059 | + | fn default() -> Self { | |
| 7060 | + | Self { | |
| 7061 | + | content: Vec::new(), | |
| 7062 | + | key: CellKey::default(), | |
| 7063 | + | flow: None, | |
| 7064 | + | priority: None, | |
| 7065 | + | span: 1, | |
| 7066 | + | } | |
| 7067 | + | } | |
| 6925 | 7068 | } | |
| 6926 | 7069 | ||
| 6927 | 7070 | impl Cell { | |
| @@ -6933,7 +7076,11 @@ | |||
| 6933 | 7076 | pub fn new(value: impl Into<String>) -> Self { | |
| 6934 | 7077 | let value = value.into(); | |
| 6935 | 7078 | Self { | |
| 6936 | - | parts: if value.is_empty() { | |
| 7079 | + | key: CellKey::default(), | |
| 7080 | + | flow: None, | |
| 7081 | + | priority: None, | |
| 7082 | + | span: 1, | |
| 7083 | + | content: if value.is_empty() { | |
| 6937 | 7084 | Vec::new() | |
| 6938 | 7085 | } else { | |
| 6939 | 7086 | vec![Node::text(value)] | |
| @@ -6947,28 +7094,36 @@ | |||
| 6947 | 7094 | /// token would say the same thing and reads as an oversight. | |
| 6948 | 7095 | pub fn tag(tag: Tag) -> Self { | |
| 6949 | 7096 | Self { | |
| 6950 | - | parts: vec![Node::Token(tag)], | |
| 7097 | + | key: CellKey::default(), | |
| 7098 | + | flow: None, | |
| 7099 | + | priority: None, | |
| 7100 | + | span: 1, | |
| 7101 | + | content: vec![Node::Token(tag)], | |
| 6951 | 7102 | } | |
| 6952 | 7103 | } | |
| 6953 | 7104 | ||
| 6954 | 7105 | /// A tag in this cell, chaining. | |
| 6955 | 7106 | #[must_use] | |
| 6956 | 7107 | pub fn token(mut self, tag: Tag) -> Self { | |
| 6957 | - | self.parts.push(Node::Token(tag)); | |
| 7108 | + | self.content.push(Node::Token(tag)); | |
| 6958 | 7109 | self | |
| 6959 | 7110 | } | |
| 6960 | 7111 | ||
| 6961 | 7112 | /// A cell holding controls and no text. | |
| 6962 | 7113 | pub fn acts(actions: impl IntoIterator<Item = Act>) -> Self { | |
| 6963 | 7114 | Self { | |
| 6964 | - | parts: actions.into_iter().map(Node::Act).collect(), | |
| 7115 | + | key: CellKey::default(), | |
| 7116 | + | flow: None, | |
| 7117 | + | priority: None, | |
| 7118 | + | span: 1, | |
| 7119 | + | content: actions.into_iter().map(Node::Act).collect(), | |
| 6965 | 7120 | } | |
| 6966 | 7121 | } | |
| 6967 | 7122 | ||
| 6968 | 7123 | /// A control in this cell, chaining. | |
| 6969 | 7124 | #[must_use] | |
| 6970 | 7125 | pub fn act(mut self, act: Act) -> Self { | |
| 6971 | - | self.parts.push(Node::Act(act)); | |
| 7126 | + | self.content.push(Node::Act(act)); | |
| 6972 | 7127 | self | |
| 6973 | 7128 | } | |
| 6974 | 7129 | ||
| @@ -6982,7 +7137,7 @@ | |||
| 6982 | 7137 | /// supplier before this existed. goingson's task list is the site. | |
| 6983 | 7138 | #[must_use] | |
| 6984 | 7139 | pub fn meter(mut self, meter: Meter) -> Self { | |
| 6985 | - | self.parts.push(Node::Meter(meter)); | |
| 7140 | + | self.content.push(Node::Meter(meter)); | |
| 6986 | 7141 | self | |
| 6987 | 7142 | } | |
| 6988 | 7143 | ||
| @@ -6999,7 +7154,7 @@ | |||
| 6999 | 7154 | #[must_use] | |
| 7000 | 7155 | pub fn activate(mut self, action: Action) -> Self { | |
| 7001 | 7156 | if let Some(first) = self | |
| 7002 | - | .parts | |
| 7157 | + | .content | |
| 7003 | 7158 | .iter_mut() | |
| 7004 | 7159 | .find(|part| matches!(part, Node::Text { .. })) | |
| 7005 | 7160 | && let Node::Text { text, .. } = first | |
| @@ -7033,17 +7188,48 @@ | |||
| 7033 | 7188 | {:?}", | |
| 7034 | 7189 | node.containment() | |
| 7035 | 7190 | ); | |
| 7036 | - | self.parts.push(node); | |
| 7191 | + | self.content.push(node); | |
| 7037 | 7192 | self | |
| 7038 | 7193 | } | |
| 7039 | 7194 | ||
| 7195 | + | /// What this cell is worth when the row does not fit. | |
| 7196 | + | /// |
Lines truncated
| @@ -58,7 +58,7 @@ | |||
| 58 | 58 | ||
| 59 | 59 | use makeover_layout as layout; | |
| 60 | 60 | use quasi_router::{ | |
| 61 | - | Act, Action, Cells, Chrome, Consult, Field, Frame, Node, Part, Prefill, Row, Screen, Slot, | |
| 61 | + | Act, Action, Cells, Chrome, Consult, Field, Frame, Node, Prefill, Row, Screen, Slot, | |
| 62 | 62 | }; | |
| 63 | 63 | ||
| 64 | 64 | use crate::Local; | |
| @@ -661,7 +661,7 @@ | |||
| 661 | 661 | inside: inside(cells) | |
| 662 | 662 | .into_iter() | |
| 663 | 663 | .filter_map(|(column, part)| { | |
| 664 | - | inside_spot(&cells.values[column].parts[part]) | |
| 664 | + | inside_spot(&cells.cells[column].content[part]) | |
| 665 | 665 | }) | |
| 666 | 666 | .collect(), | |
| 667 | 667 | }); | |
| @@ -724,8 +724,8 @@ | |||
| 724 | 724 | /// [`Cell::parts`]: quasi_router::Cell | |
| 725 | 725 | pub(crate) fn inside(cells: &Cells) -> Vec<(usize, usize)> { | |
| 726 | 726 | let mut found = Vec::new(); | |
| 727 | - | for (column, cell) in cells.values.iter().enumerate() { | |
| 728 | - | for (part, node) in cell.parts.iter().enumerate() { | |
| 727 | + | for (column, cell) in cells.cells.iter().enumerate() { | |
| 728 | + | for (part, node) in cell.content.iter().enumerate() { | |
| 729 | 729 | if inside_spot(node).is_some() { | |
| 730 | 730 | found.push((column, part)); | |
| 731 | 731 | } | |
| @@ -960,7 +960,9 @@ | |||
| 960 | 960 | }, | |
| 961 | 961 | }); | |
| 962 | 962 | } | |
| 963 | - | for Part { node, .. } in &row.parts { | |
| 964 | - | node_spots(node, region, local, found); | |
| 963 | + | for cell in &row.cells { | |
| 964 | + | for node in &cell.content { | |
| 965 | + | node_spots(node, region, local, found); | |
| 966 | + | } | |
| 965 | 967 | } | |
| 966 | 968 | } |
| @@ -14,8 +14,7 @@ | |||
| 14 | 14 | use std::time::SystemTime; | |
| 15 | 15 | ||
| 16 | 16 | use quasi_router::{ | |
| 17 | - | Act, Action, Cell, Cells, Clock, Field, Figure, Image, Meter, Node, Outline, Part, Rest, Row, | |
| 18 | - | Tag, | |
| 17 | + | Act, Action, Cell, Cells, Clock, Field, Figure, Image, Meter, Node, Outline, Rest, Row, Tag, | |
| 19 | 18 | }; | |
| 20 | 19 | use ratatui::buffer::Buffer; | |
| 21 | 20 | use ratatui::layout::Rect; | |
| @@ -645,13 +644,15 @@ | |||
| 645 | 644 | ||
| 646 | 645 | /// Claim whatever the row's own run carries, one answer per part. | |
| 647 | 646 | fn claim_parts(pass: &mut Pass<'_>, row: &Row) -> Vec<bool> { | |
| 648 | - | row.parts | |
| 647 | + | row.cells | |
| 649 | 648 | .iter() | |
| 650 | - | .map(|Part { node, .. }| match node { | |
| 651 | - | Node::Act(act) => claim_act(pass, act), | |
| 652 | - | Node::Link { .. } => pass.claim(), | |
| 653 | - | Node::Token(tag) => claim_tag(pass, tag), | |
| 654 | - | _ => false, | |
| 649 | + | .map(|cell| { | |
| 650 | + | cell.content.iter().any(|node| match node { | |
| 651 | + | Node::Act(act) => claim_act(pass, act), | |
| 652 | + | Node::Link { .. } => pass.claim(), | |
| 653 | + | Node::Token(tag) => claim_tag(pass, tag), | |
| 654 | + | _ => false, | |
| 655 | + | }) | |
| 655 | 656 | }) | |
| 656 | 657 | .collect() | |
| 657 | 658 | } | |
| @@ -862,20 +863,26 @@ | |||
| 862 | 863 | /// for the callers that are measuring rather than drawing. | |
| 863 | 864 | /// The run as a line, from the parts a caller decided to keep. | |
| 864 | 865 | /// | |
| 865 | - | /// `kept` holds indices into `row.parts`, so focus stays keyed to the part it | |
| 866 | + | /// `kept` holds indices into `row.cells`, so focus stays keyed to the part it | |
| 866 | 867 | /// was claimed for even when parts before it were dropped. Claims happen before | |
| 867 | 868 | /// layout and must not be renumbered by it. | |
| 868 | 869 | fn row_line_of(tui: &Tui, row: &Row, focus: &[bool], kept: &[usize]) -> Line<'static> { | |
| 869 | 870 | let mut spans = Vec::new(); | |
| 870 | 871 | for &index in kept { | |
| 871 | - | let Some(Part { role, node, .. }) = row.parts.get(index) else { | |
| 872 | + | let Some(cell) = row.cells.get(index) else { | |
| 872 | 873 | continue; | |
| 873 | 874 | }; | |
| 874 | 875 | if !spans.is_empty() { | |
| 875 | 876 | spans.push(Span::raw(" ")); | |
| 876 | 877 | } | |
| 877 | 878 | let focused = focus.get(index).copied().unwrap_or(false); | |
| 878 | - | spans.extend(inline_spans(tui, node, part_style(tui, *role), focused)); | |
| 879 | + | let style = match &cell.key { | |
| 880 | + | quasi_router::CellKey::Role(role) => part_style(tui, *role), | |
| 881 | + | _ => part_style(tui, quasi_router::layout::RowPart::Primary), | |
| 882 | + | }; | |
| 883 | + | for node in &cell.content { | |
| 884 | + | spans.extend(inline_spans(tui, node, style, focused)); | |
| 885 | + | } | |
| 879 | 886 | } | |
| 880 | 887 | // `Row::menu` is not drawn, and that is the description's own instruction: | |
| 881 | 888 | // a menu is reached by right-click on a pointer host, long-press on a touch | |
| @@ -885,12 +892,12 @@ | |||
| 885 | 892 | ||
| 886 | 893 | /// Every part, in order: what a row draws when there is room for all of it. | |
| 887 | 894 | fn kept_all(row: &Row) -> Vec<usize> { | |
| 888 | - | (0..row.parts.len()).collect() | |
| 895 | + | (0..row.cells.len()).collect() | |
| 889 | 896 | } | |
| 890 | 897 | ||
| 891 | 898 | /// The parts that survive a run too tall for its cap. | |
| 892 | 899 | /// | |
| 893 | - | /// Drops by [`quasi_router::Part::worth`], least valuable first, and only when | |
| 900 | + | /// Drops by [`quasi_router::Cell::worth`], least valuable first, and only when | |
| 894 | 901 | /// dropping earns something: if the run still does not fit with every droppable | |
| 895 | 902 | /// part gone, the whole run comes back and the cap cuts it. That is the rule | |
| 896 | 903 | /// that keeps this honest. Dropping a badge to make room for a title that | |
| @@ -922,9 +929,9 @@ | |||
| 922 | 929 | let mut kept = all.clone(); | |
| 923 | 930 | for tier in [layout::Priority::Optional, layout::Priority::Secondary] { | |
| 924 | 931 | kept.retain(|&index| { | |
| 925 | - | row.parts | |
| 926 | - | .get(index) | |
| 927 | - | .is_none_or(|part| matches!(part.node, Node::Act(_)) || part.worth() != tier) | |
| 932 | + | row.cells.get(index).is_none_or(|cell| { | |
| 933 | + | cell.content.iter().any(|n| matches!(n, Node::Act(_))) || cell.worth() != tier | |
| 934 | + | }) | |
| 928 | 935 | }); | |
| 929 | 936 | if fits(&kept) { | |
| 930 | 937 | return kept; | |
| @@ -971,9 +978,9 @@ | |||
| 971 | 978 | /// `layout::Flow::lines` owns the numbers, so a tier added upstream arrives | |
| 972 | 979 | /// here without this function being edited. | |
| 973 | 980 | fn row_lines(row: &Row) -> u16 { | |
| 974 | - | row.parts | |
| 981 | + | row.cells | |
| 975 | 982 | .iter() | |
| 976 | - | .map(|part| u16::from(part.flow.lines())) | |
| 983 | + | .map(|cell| u16::from(cell.room().lines())) | |
| 977 | 984 | .max() | |
| 978 | 985 | .unwrap_or(1) | |
| 979 | 986 | .max(1) | |
| @@ -1733,88 +1740,88 @@ | |||
| 1733 | 1740 | fallback: 12, | |
| 1734 | 1741 | }; | |
| 1735 | 1742 | ||
| 1736 | - | let body: Vec<Vec<table::Cell<'_>>> = rows | |
| 1737 | - | .iter() | |
| 1738 | - | .enumerate() | |
| 1739 | - | .map(|(index, cells)| { | |
| 1740 | - | let mut row: Vec<table::Cell<'_>> = | |
| 1741 | - | Vec::with_capacity(columns.len() + usize::from(gutter)); | |
| 1742 | - | if gutter { | |
| 1743 | - | // The set the view is holding decides it, not the description, | |
| 1744 | - | // which is the rule `39057019` settled for a field and | |
| 1745 | - | // `draw_gutter` follows for a list row: the description says | |
| 1746 | - | // what arrived and the view says what the user has done since. | |
| 1747 | - | // A redraw reading the description would undo the tick the | |
| 1748 | - | // moment anything else on the screen changed. | |
| 1749 | - | let drawn = match (cells.selected, cells.value.as_deref()) { | |
| 1750 | - | (Some(_), Some(value)) if pass.view.is_ticked(value) => "[x]", | |
| 1751 | - | (Some(_), _) => "[ ]", | |
| 1752 | - | // A live selection is the description's own answer and not | |
| 1753 | - | // the view's, which is the whole difference from the two | |
| 1754 | - | // above: the app holds the set, so a redraw reading the | |
| 1755 | - | // description is reading the truth rather than undoing it. | |
| 1756 | - | (None, _) if cells.chosen == Some(true) => " * ", | |
| 1757 | - | (None, _) => "", | |
| 1758 | - | }; | |
| 1759 | - | row.push(table::Cell::new(TICK_COLUMN, Line::from(drawn))); | |
| 1760 | - | } | |
| 1761 | - | row.extend(columns.iter().zip(&cells.values).enumerate().map( | |
| 1762 | - | |(at, (column, cell))| { | |
| 1763 | - | // The lit part, and only in the row the caret is on: | |
| 1764 | - | // `lit` is already `None` unless this row is focused, so | |
| 1765 | - | // the column test is all that is left to do here. | |
| 1766 | - | let within = lit.filter(|(col, _)| Some(index) == focused && *col == at); | |
| 1767 | - | let mut line = cell_line(tui, cell, within.map(|(_, part)| part)); | |
| 1768 | - | // The outline, in the first column and nowhere else. A | |
| 1769 | - | // table has no gutter to indent in and the indent is not a | |
| 1770 | - | // value in the grid, so it rides in front of the row's | |
| 1771 | - | // leading text -- which is the cell the eye reads the | |
| 1772 | - | // hierarchy from anyway. | |
| 1773 | - | if at == 0 && branches { | |
| 1774 | - | let mark = match cells.open { | |
| 1775 | - | Some(described) => { | |
| 1776 | - | let open = pass.view.open(&cells.key(), described); | |
| 1777 | - | format!("{} ", crate::outline::chevron(open)) | |
| 1778 | - | } | |
| 1779 | - | None => " ".to_string(), | |
| 1780 | - | }; | |
| 1781 | - | let indent = " ".repeat( | |
| 1782 | - | usize::from(cells.depth.level) * usize::from(crate::outline::STEP), | |
| 1783 | - | ); | |
| 1784 | - | line.spans.insert(0, Span::raw(format!("{indent}{mark}"))); | |
| 1785 | - | } | |
| 1786 | - | // Which side of a change this line is on, when the table is | |
| 1787 | - | // a diff. `19d7602d`. A marker in front of the row rather | |
| 1788 | - | // than a tint behind it, which is what every terminal diff | |
| 1789 | - | // has always done and what a reader already reads: the sign | |
| 1790 | - | // survives a monochrome terminal, a colour does not, and | |
| 1791 | - | // this renderer has no per-row background to spend anyway. | |
| 1792 | - | // | |
| 1793 | - | // The colour goes on beside it, off `Change`'s own intent, | |
| 1794 | - | // so a terminal with status colours gets both. | |
| 1795 | - | if at == 0 | |
| 1796 | - | && let Some(change) = cells.change | |
| 1797 | - | { | |
| 1798 | - | let (sign, style) = match change { | |
| 1799 | - | layout::Change::Added => { | |
| 1800 | - | ("+", Style::default().fg(tui.theme().status_success)) | |
| 1801 | - | } | |
| 1802 | - | layout::Change::Removed => { | |
| 1803 | - | ("-", Style::default().fg(tui.theme().status_danger)) | |
| 1804 | - | } | |
| 1805 | - | // Including a kind this renderer has not learned: | |
| 1806 | - | // an unchanged line, which is the reading that | |
| 1807 | - | // draws the text and loses only the sign. | |
| 1808 | - | _ => (" ", Style::default()), | |
| 1809 | - | }; | |
| 1810 | - | line.spans.insert(0, Span::styled(sign, style)); | |
| 1811 | - | } | |
| 1812 | - | table::Cell::new(column.name.as_str(), line).part(cell_part(cell)) | |
| 1813 | - | }, | |
| 1814 | - | )); | |
| 1815 | - | row | |
| 1816 | - | }) | |
| 1817 | - | .collect(); | |
| 1743 | + | let body: Vec<Vec<table::Cell<'_>>> = | |
| 1744 | + | rows.iter() | |
| 1745 | + | .enumerate() | |
| 1746 | + | .map(|(index, cells)| { | |
| 1747 | + | let mut row: Vec<table::Cell<'_>> = | |
| 1748 | + | Vec::with_capacity(columns.len() + usize::from(gutter)); | |
| 1749 | + | if gutter { | |
| 1750 | + | // The set the view is holding decides it, not the description, | |
| 1751 | + | // which is the rule `39057019` settled for a field and | |
| 1752 | + | // `draw_gutter` follows for a list row: the description says | |
| 1753 | + | // what arrived and the view says what the user has done since. | |
| 1754 | + | // A redraw reading the description would undo the tick the | |
| 1755 | + | // moment anything else on the screen changed. | |
| 1756 | + | let drawn = match (cells.selected, cells.value.as_deref()) { | |
| 1757 | + | (Some(_), Some(value)) if pass.view.is_ticked(value) => "[x]", | |
| 1758 | + | (Some(_), _) => "[ ]", | |
| 1759 | + | // A live selection is the description's own answer and not | |
| 1760 | + | // the view's, which is the whole difference from the two | |
| 1761 | + | // above: the app holds the set, so a redraw reading the | |
| 1762 | + | // description is reading the truth rather than undoing it. | |
| 1763 | + | (None, _) if cells.chosen == Some(true) => " * ", | |
| 1764 | + | (None, _) => "", | |
| 1765 | + | }; | |
| 1766 | + | row.push(table::Cell::new(TICK_COLUMN, Line::from(drawn))); | |
| 1767 | + | } | |
| 1768 | + | row.extend(columns.iter().zip(&cells.cells).enumerate().map( | |
| 1769 | + | |(at, (column, cell))| { | |
| 1770 | + | // The lit part, and only in the row the caret is on: | |
| 1771 | + | // `lit` is already `None` unless this row is focused, so | |
| 1772 | + | // the column test is all that is left to do here. | |
| 1773 | + | let within = lit.filter(|(col, _)| Some(index) == focused && *col == at); | |
| 1774 | + | let mut line = cell_line(tui, cell, within.map(|(_, part)| part)); | |
| 1775 | + | // The outline, in the first column and nowhere else. A | |
| 1776 | + | // table has no gutter to indent in and the indent is not a | |
| 1777 | + | // value in the grid, so it rides in front of the row's | |
| 1778 | + | // leading text -- which is the cell the eye reads the | |
| 1779 | + | // hierarchy from anyway. | |
| 1780 | + | if at == 0 && branches { | |
| 1781 | + | let mark = match cells.open { | |
| 1782 | + | Some(described) => { | |
| 1783 | + | let open = pass.view.open(&cells.key(), described); | |
| 1784 | + | format!("{} ", crate::outline::chevron(open)) | |
| 1785 | + | } | |
| 1786 | + | None => " ".to_string(), | |
| 1787 | + | }; | |
| 1788 | + | let indent = " ".repeat( | |
| 1789 | + | usize::from(cells.depth.level) * usize::from(crate::outline::STEP), | |
| 1790 | + | ); | |
| 1791 | + | line.spans.insert(0, Span::raw(format!("{indent}{mark}"))); | |
| 1792 | + | } | |
| 1793 | + | // Which side of a change this line is on, when the table is | |
| 1794 | + | // a diff. `19d7602d`. A marker in front of the row rather | |
| 1795 | + | // than a tint behind it, which is what every terminal diff | |
| 1796 | + | // has always done and what a reader already reads: the sign | |
| 1797 | + | // survives a monochrome terminal, a colour does not, and | |
| 1798 | + | // this renderer has no per-row background to spend anyway. | |
| 1799 | + | // | |
| 1800 | + | // The colour goes on beside it, off `Change`'s own intent, | |
| 1801 | + | // so a terminal with status colours gets both. | |
| 1802 | + | if at == 0 | |
| 1803 | + | && let Some(change) = cells.change | |
| 1804 | + | { | |
| 1805 | + | let (sign, style) = match change { | |
| 1806 | + | layout::Change::Added => { | |
| 1807 | + | ("+", Style::default().fg(tui.theme().status_success)) | |
| 1808 | + | } | |
| 1809 | + | layout::Change::Removed => { | |
| 1810 | + | ("-", Style::default().fg(tui.theme().status_danger)) | |
| 1811 | + | } | |
| 1812 | + | // Including a kind this renderer has not learned: | |
| 1813 | + | // an unchanged line, which is the reading that | |
| 1814 | + | // draws the text and loses only the sign. | |
| 1815 | + | _ => (" ", Style::default()), | |
| 1816 | + | }; | |
| 1817 | + | line.spans.insert(0, Span::styled(sign, style)); | |
| 1818 | + | } | |
| 1819 | + | table::Cell::new(column.name.as_str(), line).part(cell_part(cell)) | |
| 1820 | + | }, | |
| 1821 | + | )); | |
| 1822 | + | row | |
| 1823 | + | }) | |
| 1824 | + | .collect(); | |
| 1818 | 1825 | ||
| 1819 | 1826 | let widget = table::table(&named, &body, &sizing, &tui.table, area.width); | |
| 1820 | 1827 | let height = table_height(columns, rows.len()).min(area.height); | |
| @@ -1845,7 +1852,7 @@ | |||
| 1845 | 1852 | /// style, so the reader can see which of a row's buttons Enter would press. | |
| 1846 | 1853 | fn cell_line(tui: &Tui, cell: &Cell, lit: Option<usize>) -> Line<'static> { | |
| 1847 | 1854 | let mut spans = Vec::new(); | |
| 1848 | - | for (at, part) in cell.parts.iter().enumerate() { | |
| 1855 | + | for (at, part) in cell.content.iter().enumerate() { | |
| 1849 | 1856 | if !spans.is_empty() { | |
| 1850 | 1857 | spans.push(Span::raw(" ")); | |
| 1851 | 1858 | } | |
| @@ -1866,17 +1873,21 @@ | |||
| 1866 | 1873 | /// A control wins, then a link, then a chip, then the value: a cell whose last | |
| 1867 | 1874 | /// word is a button should not be painted as prose. | |
| 1868 | 1875 | fn cell_part(cell: &Cell) -> layout::CellPart { | |
| 1869 | - | if cell.parts.iter().any(|part| matches!(part, Node::Act(_))) { | |
| 1876 | + | if cell.content.iter().any(|part| matches!(part, Node::Act(_))) { | |
| 1870 | 1877 | return layout::CellPart::Actions; | |
| 1871 | 1878 | } | |
| 1872 | 1879 | if cell | |
| 1873 | - | .parts | |
| 1880 | + | .content | |
| 1874 | 1881 | .iter() | |
| 1875 | 1882 | .any(|part| matches!(part, Node::Link { .. })) | |
| 1876 | 1883 | { | |
| 1877 | 1884 | return layout::CellPart::Link; | |
| 1878 | 1885 | } | |
| 1879 | - | if cell.parts.iter().any(|part| matches!(part, Node::Token(_))) { | |
| 1886 | + | if cell | |
| 1887 | + | .content | |
| 1888 | + | .iter() | |
| 1889 | + | .any(|part| matches!(part, Node::Token(_))) | |
| 1890 | + | { | |
| 1880 | 1891 | return layout::CellPart::Tokens; | |
| 1881 | 1892 | } | |
| 1882 | 1893 | layout::CellPart::Value |
| @@ -8,8 +8,8 @@ | |||
| 8 | 8 | use makeover_layout as layout; | |
| 9 | 9 | use makeover_tui::{Fidelity, Theme}; | |
| 10 | 10 | use quasi_router::{ | |
| 11 | - | Act, Action, Candidate, Cell, Cells, Choice, Column, Consult, Field, Figure, Meter, Node, | |
| 12 | - | RegionKind, Row, Run, Screen, Slot, Tag, | |
| 11 | + | Act, Action, Candidate, Cell, Choice, Column, Consult, Field, Figure, Meter, Node, RegionKind, | |
| 12 | + | Row, Run, Screen, Slot, Tag, | |
| 13 | 13 | }; | |
| 14 | 14 | use ratatui::buffer::Buffer; | |
| 15 | 15 | use ratatui::layout::Rect; | |
| @@ -159,7 +159,7 @@ | |||
| 159 | 159 | Node::list([Row::new("One")]), | |
| 160 | 160 | Node::Table { | |
| 161 | 161 | columns: vec![Column::new("Name")], | |
| 162 | - | rows: vec![Cells::new([Cell::new("One")])], | |
| 162 | + | rows: vec![Row::cells([Cell::new("One")])], | |
| 163 | 163 | more: None, | |
| 164 | 164 | }, | |
| 165 | 165 | Node::Timeline { | |
| @@ -767,7 +767,7 @@ | |||
| 767 | 767 | Column::new("Name").priority(layout::Priority::Essential), | |
| 768 | 768 | Column::new("Added").priority(layout::Priority::Optional), | |
| 769 | 769 | ], | |
| 770 | - | rows: vec![Cells::new(["kick.wav", "2026-08-12"])], | |
| 770 | + | rows: vec![Row::cells(["kick.wav", "2026-08-12"])], | |
| 771 | 771 | more: None, | |
| 772 | 772 | }; | |
| 773 | 773 | ||
| @@ -859,7 +859,7 @@ | |||
| 859 | 859 | .and_more(Rest::more(2, Action::get("/more"))), | |
| 860 | 860 | Node::Table { | |
| 861 | 861 | columns: vec![Column::new("Name")], | |
| 862 | - | rows: vec![Cells::new(["one"]).activate(Action::get("/row"))], | |
| 862 | + | rows: vec![Row::cells(["one"]).activate(Action::get("/row"))], | |
| 863 | 863 | more: None, | |
| 864 | 864 | }, | |
| 865 | 865 | ]); | |
| @@ -2135,8 +2135,8 @@ | |||
| 2135 | 2135 | .with(Node::Table { | |
| 2136 | 2136 | columns: vec![quasi_router::Column::new("title")], | |
| 2137 | 2137 | rows: vec![ | |
| 2138 | - | Cells::new(["First"]).ticking("t-1", false), | |
| 2139 | - | Cells::new(["Second"]).ticking("t-2", false), | |
| 2138 | + | Row::cells(["First"]).ticking("t-1", false), | |
| 2139 | + | Row::cells(["Second"]).ticking("t-2", false), | |
| 2140 | 2140 | ], | |
| 2141 | 2141 | more: None, | |
| 2142 | 2142 | }) | |
| @@ -2170,8 +2170,8 @@ | |||
| 2170 | 2170 | Node::Table { | |
| 2171 | 2171 | columns: vec![Column::new("Name")], | |
| 2172 | 2172 | rows: vec![ | |
| 2173 | - | Cells::new(["kick.wav"]).offers(Act::new("Preview", Action::post("/files/1/play"))), | |
| 2174 | - | Cells::new(["snare.wav"]), | |
| 2173 | + | Row::cells(["kick.wav"]).offers(Act::new("Preview", Action::post("/files/1/play"))), | |
| 2174 | + | Row::cells(["snare.wav"]), | |
| 2175 | 2175 | ], | |
| 2176 | 2176 | more: None, | |
| 2177 | 2177 | }, | |
| @@ -2204,8 +2204,8 @@ | |||
| 2204 | 2204 | .with(Node::Table { | |
| 2205 | 2205 | columns: vec![Column::new("title")], | |
| 2206 | 2206 | rows: vec![ | |
| 2207 | - | Cells::new(["First"]).ticking("t-1", false), | |
| 2208 | - | Cells::new(["Second"]).ticking("t-2", false), | |
| 2207 | + | Row::cells(["First"]).ticking("t-1", false), | |
| 2208 | + | Row::cells(["Second"]).ticking("t-2", false), | |
| 2209 | 2209 | ], | |
| 2210 | 2210 | more: None, | |
| 2211 | 2211 | }) | |
| @@ -2845,7 +2845,7 @@ | |||
| 2845 | 2845 | Column::new("Kind").priority(layout::Priority::Secondary), | |
| 2846 | 2846 | Column::new("Added").priority(layout::Priority::Optional), | |
| 2847 | 2847 | ], | |
| 2848 | - | rows: vec![Cells::new(["kick.wav", "sample", "2026-08-12"])], | |
| 2848 | + | rows: vec![Row::cells(["kick.wav", "sample", "2026-08-12"])], | |
| 2849 | 2849 | more: Some(Rest::more(1, Action::get("/samples?from=1"))), | |
| 2850 | 2850 | }) | |
| 2851 | 2851 | .with(Node::List { | |
| @@ -4098,7 +4098,7 @@ | |||
| 4098 | 4098 | /// an Edit and a Remove in its last cell. | |
| 4099 | 4099 | fn table_with_controls() -> Screen { | |
| 4100 | 4100 | let row = |id: &str, name: &str| { | |
| 4101 | - | Cells::new([ | |
| 4101 | + | Row::cells([ | |
| 4102 | 4102 | Cell::new(name), | |
| 4103 | 4103 | Cell::acts([ | |
| 4104 | 4104 | Act::new("Edit", Action::post(format!("/files/{id}/edit"))), | |
| @@ -4211,7 +4211,7 @@ | |||
| 4211 | 4211 | let mut runtime = Runtime::new(Screen::sidebar_content("Files").with( | |
| 4212 | 4212 | Slot::new("main", RegionKind::Pane).with(Node::Table { | |
| 4213 | 4213 | columns: vec![Column::new("name"), Column::new("")], | |
| 4214 | - | rows: vec![Cells::new([ | |
| 4214 | + | rows: vec![Row::cells([ | |
| 4215 | 4215 | Cell::new("kick.wav"), | |
| 4216 | 4216 | Cell::acts([Act::new("Remove", Action::post("/files/1/remove"))]), | |
| 4217 | 4217 | ])], | |
| @@ -4295,7 +4295,7 @@ | |||
| 4295 | 4295 | Slot::new("main", RegionKind::Pane).with(Node::Table { | |
| 4296 | 4296 | columns: vec![Column::new("name"), Column::new("")], | |
| 4297 | 4297 | rows: vec![ | |
| 4298 | - | Cells::new([ | |
| 4298 | + | Row::cells([ | |
| 4299 | 4299 | Cell::new("kick.wav"), | |
| 4300 | 4300 | Cell::acts([ | |
| 4301 | 4301 | Act::new("Restore", Action::post("/files/1/restore")).disabled(), | |
| @@ -4327,7 +4327,7 @@ | |||
| 4327 | 4327 | Slot::new("main", RegionKind::Pane) | |
| 4328 | 4328 | .with(Node::Table { | |
| 4329 | 4329 | columns: vec![Column::new("name"), Column::new("")], | |
| 4330 | - | rows: vec![Cells::new([ | |
| 4330 | + | rows: vec![Row::cells([ | |
| 4331 | 4331 | Cell::new("kick.wav"), | |
| 4332 | 4332 | Cell::acts([Act::new("Remove", Action::post("/files/1/remove"))]), | |
| 4333 | 4333 | ])], |