| 140 |
140 |
|
pub header: Style,
|
| 141 |
141 |
|
/// The heading of the column the table is ordered by.
|
| 142 |
142 |
|
pub sorted: Style,
|
|
143 |
+ |
/// The heading of a column that offers to reorder and is not doing it now.
|
|
144 |
+ |
///
|
|
145 |
+ |
/// The middle of three tones (wiki `three-tone-convention`): it answers a
|
|
146 |
+ |
/// press, so it is neither the emphasised thing nor the inert one. A
|
|
147 |
+ |
/// heading that took [`header`](Self::header) here would be indistinguishable
|
|
148 |
+ |
/// from a column that cannot be reordered at all, which is the state this
|
|
149 |
+ |
/// separates it from.
|
|
150 |
+ |
pub sortable: Style,
|
| 143 |
151 |
|
/// A cell that is text.
|
| 144 |
152 |
|
pub value: Style,
|
| 145 |
153 |
|
/// A cell holding badges or chips. They carry their own tone, so this is
|
| 155 |
163 |
|
/// Cells between columns. Counted when deciding what fits, so a table that
|
| 156 |
164 |
|
/// narrows and a table that draws agree about the room available.
|
| 157 |
165 |
|
pub column_spacing: u16,
|
| 158 |
|
- |
/// Drawn after the heading of an ascending column.
|
|
166 |
+ |
/// The caret drawn after the heading of an ascending column.
|
|
167 |
+ |
///
|
|
168 |
+ |
/// Defaults to [`Sort::glyph`], which is where the spelling lives now: three
|
|
169 |
+ |
/// renderers holding the same literal agreed by coincidence. Still a knob,
|
|
170 |
+ |
/// because a terminal is the one host that may not be able to draw it — a
|
|
171 |
+ |
/// font without the geometric-shapes block leaves a box, and `"^"` is a
|
|
172 |
+ |
/// better caret than a tofu.
|
|
173 |
+ |
///
|
|
174 |
+ |
/// Bare, with no leading space: the gap is [`heading`]'s, written once for
|
|
175 |
+ |
/// all three states rather than baked into two strings and forgotten in the
|
|
176 |
+ |
/// third.
|
| 159 |
177 |
|
pub ascending: &'static str,
|
| 160 |
|
- |
/// Drawn after the heading of a descending column.
|
|
178 |
+ |
/// The caret drawn after the heading of a descending column.
|
| 161 |
179 |
|
pub descending: &'static str,
|
| 162 |
180 |
|
}
|
| 163 |
181 |
|
|
| 166 |
184 |
|
Self {
|
| 167 |
185 |
|
header: Style::new().add_modifier(Modifier::BOLD),
|
| 168 |
186 |
|
sorted: Style::new().add_modifier(Modifier::BOLD),
|
|
187 |
+ |
// Nothing of its own. A cell style patches the row's, so a colour
|
|
188 |
+ |
// is the only thing that could separate this from the header row it
|
|
189 |
+ |
// sits in, and the colourless default has none to spend: the idle
|
|
190 |
+ |
// caret is what says the heading answers a press. `from_theme` is
|
|
191 |
+ |
// where the three tones are real.
|
|
192 |
+ |
sortable: Style::new(),
|
| 169 |
193 |
|
value: Style::new(),
|
| 170 |
194 |
|
tokens: Style::new(),
|
| 171 |
195 |
|
actions: Style::new(),
|
| 172 |
196 |
|
link: Style::new().add_modifier(Modifier::UNDERLINED),
|
| 173 |
197 |
|
selected: Style::new().add_modifier(Modifier::REVERSED),
|
| 174 |
198 |
|
column_spacing: 1,
|
| 175 |
|
- |
// The pair audiofiles already draws, so a sorted column points the
|
| 176 |
|
- |
// same way in a terminal as it does in the egui browser.
|
| 177 |
|
- |
ascending: " \u{25B2}",
|
| 178 |
|
- |
descending: " \u{25BC}",
|
|
199 |
+ |
ascending: Sort::Ascending.glyph(),
|
|
200 |
+ |
descending: Sort::Descending.glyph(),
|
| 179 |
201 |
|
}
|
| 180 |
202 |
|
}
|
| 181 |
203 |
|
}
|
| 203 |
225 |
|
sorted: Style::new()
|
| 204 |
226 |
|
.fg(theme.content_primary)
|
| 205 |
227 |
|
.add_modifier(Modifier::BOLD),
|
|
228 |
+ |
sortable: Style::new().fg(theme.content_secondary),
|
| 206 |
229 |
|
value: Style::new().fg(theme.content_primary),
|
| 207 |
230 |
|
// A token paints its own background, and a tone underneath it would
|
| 208 |
231 |
|
// fight the one sitting on it. Secondary is what shows through the
|
| 216 |
239 |
|
.bg(theme.surface_raised)
|
| 217 |
240 |
|
.add_modifier(Modifier::BOLD),
|
| 218 |
241 |
|
column_spacing: 1,
|
| 219 |
|
- |
ascending: " \u{25B2}",
|
| 220 |
|
- |
descending: " \u{25BC}",
|
|
242 |
+ |
ascending: Sort::Ascending.glyph(),
|
|
243 |
+ |
descending: Sort::Descending.glyph(),
|
| 221 |
244 |
|
}
|
| 222 |
245 |
|
}
|
| 223 |
246 |
|
|
| 238 |
261 |
|
}
|
| 239 |
262 |
|
}
|
| 240 |
263 |
|
|
| 241 |
|
- |
/// The heading, with the caret if the table is ordered by this column.
|
|
264 |
+ |
/// The heading, with the caret if this column is ordered by or offers to be.
|
| 242 |
265 |
|
///
|
| 243 |
266 |
|
/// A column [`sorted`](Column::sorted) but not
|
| 244 |
267 |
|
/// [`sortable`](Column::sortable) still gets its caret. Both combinations mean
|
| 245 |
268 |
|
/// something, which is why the description holds the two fields apart: a list
|
| 246 |
269 |
|
/// ordered by a key the user cannot change is a real thing, and the caret is how
|
| 247 |
270 |
|
/// it says so.
|
|
271 |
+ |
///
|
|
272 |
+ |
/// A column sortable and *not* sorted draws the idle mark, in the ascending
|
|
273 |
+ |
/// spelling because that is the direction a first press takes. The tone is what
|
|
274 |
+ |
/// separates it from the column in force, and [`header`] picks that; here the
|
|
275 |
+ |
/// point is the width. This is what closes the reflow: pressing a heading used
|
|
276 |
+ |
/// to widen its column by two cells and shift every column after it, because
|
|
277 |
+ |
/// [`measure`] sizes from this function and the caret appeared with the press.
|
| 248 |
278 |
|
fn heading<'a>(column: &Column<'a>, style: &TableStyle) -> Line<'a> {
|
| 249 |
|
- |
match column.sorted {
|
| 250 |
|
- |
Some(Sort::Ascending) => Line::from(format!("{}{}", column.name, style.ascending)),
|
| 251 |
|
- |
Some(Sort::Descending) => Line::from(format!("{}{}", column.name, style.descending)),
|
| 252 |
|
- |
None => Line::from(column.name),
|
| 253 |
|
- |
}
|
|
279 |
+ |
let caret = match column.sorted {
|
|
280 |
+ |
Some(Sort::Ascending) => style.ascending,
|
|
281 |
+ |
Some(Sort::Descending) => style.descending,
|
|
282 |
+ |
None if column.sortable => style.ascending,
|
|
283 |
+ |
None => return Line::from(column.name),
|
|
284 |
+ |
};
|
|
285 |
+ |
// The gap, once, rather than inside each of the two style strings. A
|
|
286 |
+ |
// consumer swapping the glyph for an ASCII one does not have to remember to
|
|
287 |
+ |
// bring a space with it.
|
|
288 |
+ |
Line::from(format!("{} {caret}", column.name))
|
| 254 |
289 |
|
}
|
| 255 |
290 |
|
|
| 256 |
291 |
|
/// How wide a column wants to be, in cells, at its narrowest.
|
| 417 |
452 |
|
.iter()
|
| 418 |
453 |
|
.filter(|column| column.kept_at(cutoff))
|
| 419 |
454 |
|
.map(|column| {
|
| 420 |
|
- |
let tone = if column.sorted.is_some() {
|
| 421 |
|
- |
style.sorted
|
| 422 |
|
- |
} else {
|
| 423 |
|
- |
style.header
|
|
455 |
+ |
// Three states, three tones (wiki `three-tone-convention`). In
|
|
456 |
+ |
// force, offering, and not a control at all -- and the middle
|
|
457 |
+ |
// one is the state that had nowhere to be said, so a heading
|
|
458 |
+ |
// you could press looked exactly like one you could not.
|
|
459 |
+ |
let tone = match (column.sorted, column.sortable) {
|
|
460 |
+ |
(Some(_), _) => style.sorted,
|
|
461 |
+ |
(None, true) => style.sortable,
|
|
462 |
+ |
(None, false) => style.header,
|
| 424 |
463 |
|
};
|
| 425 |
464 |
|
TrackCell::from(heading(column, style)).style(tone)
|
| 426 |
465 |
|
})
|
| 553 |
592 |
|
.collect()
|
| 554 |
593 |
|
}
|
| 555 |
594 |
|
|
|
595 |
+ |
/// The foreground each of the three heading cells was drawn in.
|
|
596 |
+ |
///
|
|
597 |
+ |
/// Read off a rendered buffer for [`cell_text`]'s reason: a ratatui `Row`
|
|
598 |
+ |
/// hands nothing back, and what is asserted is what a user sees.
|
|
599 |
+ |
fn cell_colors(row: &Row<'_>) -> Vec<Option<ratatui::style::Color>> {
|
|
600 |
+ |
use ratatui::layout::Rect;
|
|
601 |
+ |
use ratatui::widgets::Widget;
|
|
602 |
+ |
let mut buf = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 60, 1));
|
|
603 |
+ |
Table::new(vec![row.clone()], [Constraint::Length(18); 3])
|
|
604 |
+ |
.column_spacing(1)
|
|
605 |
+ |
.render(Rect::new(0, 0, 60, 1), &mut buf);
|
|
606 |
+ |
(0..3).map(|i| buf[(i * 19, 0)].fg).map(Some).collect()
|
|
607 |
+ |
}
|
|
608 |
+ |
|
| 556 |
609 |
|
#[test]
|
| 557 |
610 |
|
fn cells_are_ordered_by_the_columns_and_not_by_the_row() {
|
| 558 |
611 |
|
// The row hands them over backwards. The table decides the order, which
|
| 766 |
819 |
|
}
|
| 767 |
820 |
|
|
| 768 |
821 |
|
#[test]
|
| 769 |
|
- |
fn the_ordered_column_draws_a_caret_and_the_others_do_not() {
|
|
822 |
+ |
fn a_heading_carries_a_caret_when_it_is_ordered_by_or_offers_to_be() {
|
| 770 |
823 |
|
let style = TableStyle::default();
|
| 771 |
824 |
|
let head = header(&columns(), &style, Priority::Optional);
|
| 772 |
825 |
|
assert_eq!(
|
| 773 |
826 |
|
cell_text(&head),
|
| 774 |
|
- |
vec!["name \u{25B2}", "size", "note"],
|
| 775 |
|
- |
"only the column in force carries one"
|
|
827 |
+ |
vec!["name \u{25B2}", "size \u{25B2}", "note"],
|
|
828 |
+ |
"in force and offering both carry one; not a control carries none"
|
| 776 |
829 |
|
);
|
| 777 |
830 |
|
}
|
| 778 |
831 |
|
|
|
832 |
+ |
#[test]
|
|
833 |
+ |
fn the_three_states_of_a_heading_are_three_tones() {
|
|
834 |
+ |
// wiki `three-tone-convention`. The middle state is the one that had
|
|
835 |
+ |
// nowhere to be said: a heading you can press looked exactly like one
|
|
836 |
+ |
// you cannot, and the idle caret alone does not separate them, because
|
|
837 |
+ |
// a sorted-but-unsortable column draws a caret too.
|
|
838 |
+ |
use ratatui::style::Color;
|
|
839 |
+ |
let style = TableStyle {
|
|
840 |
+ |
sorted: Style::new().fg(Color::Red),
|
|
841 |
+ |
sortable: Style::new().fg(Color::Green),
|
|
842 |
+ |
header: Style::new().fg(Color::Blue),
|
|
843 |
+ |
..TableStyle::default()
|
|
844 |
+ |
};
|
|
845 |
+ |
let drawn = cell_colors(&header(&columns(), &style, Priority::Optional));
|
|
846 |
+ |
assert_eq!(
|
|
847 |
+ |
drawn,
|
|
848 |
+ |
vec![Some(Color::Red), Some(Color::Green), Some(Color::Blue)]
|
|
849 |
+ |
);
|
|
850 |
+ |
|
|
851 |
+ |
// The colourless default separates them by the caret and nothing else,
|
|
852 |
+ |
// and that is the honest limit rather than an oversight: a cell style
|
|
853 |
+ |
// patches the row's, so a plain cell under a bold header row is drawn
|
|
854 |
+ |
// bold whatever it holds. Three tones need three colours, which is what
|
|
855 |
+ |
// `from_theme` is for.
|
|
856 |
+ |
let house = TableStyle::default();
|
|
857 |
+ |
let plain = cell_colors(&header(&columns(), &house, Priority::Optional));
|
|
858 |
+ |
assert_eq!(plain[0], plain[1], "no colour to spend, so none is claimed");
|
|
859 |
+ |
}
|
|
860 |
+ |
|
|
861 |
+ |
#[test]
|
|
862 |
+ |
fn pressing_a_heading_does_not_move_the_columns_after_it() {
|
|
863 |
+ |
// The reflow the idle caret closes. `measure` sizes from `heading`, so
|
|
864 |
+ |
// a caret that appeared with the press widened its own column by two
|
|
865 |
+ |
// cells and shifted the rest of the row sideways.
|
|
866 |
+ |
let style = TableStyle::default();
|
|
867 |
+ |
let offering = Column {
|
|
868 |
+ |
name: "size",
|
|
869 |
+ |
width: Width::Content,
|
|
870 |
+ |
priority: Priority::Secondary,
|
|
871 |
+ |
sortable: true,
|
|
872 |
+ |
sorted: None,
|
|
873 |
+ |
};
|
|
874 |
+ |
let in_force = Column {
|
|
875 |
+ |
sorted: Some(Sort::Descending),
|
|
876 |
+ |
..offering
|
|
877 |
+ |
};
|
|
878 |
+ |
let rows: Vec<Vec<Cell<'_>>> = vec![];
|
|
879 |
+ |
assert_eq!(
|
|
880 |
+ |
measure(&offering, &rows, &style),
|
|
881 |
+ |
measure(&in_force, &rows, &style)
|
|
882 |
+ |
);
|
|
883 |
+ |
|
|
884 |
+ |
// And the column that is not a control at all is narrower, which is the
|
|
885 |
+ |
// width that would be wrong to reserve: it has no caret to draw.
|
|
886 |
+ |
let inert = Column {
|
|
887 |
+ |
sortable: false,
|
|
888 |
+ |
..offering
|
|
889 |
+ |
};
|
|
890 |
+ |
assert!(measure(&inert, &rows, &style) < measure(&offering, &rows, &style));
|
|
891 |
+ |
}
|
|
892 |
+ |
|
| 779 |
893 |
|
#[test]
|
| 780 |
894 |
|
fn a_column_sorted_without_being_sortable_still_draws_its_caret() {
|
| 781 |
895 |
|
// A list ordered by a key the user cannot change is a real thing to
|