| 108 |
108 |
|
/// for it, and a renderer that turned it on would be adding a claim the
|
| 109 |
109 |
|
/// other two cannot make.
|
| 110 |
110 |
|
///
|
| 111 |
|
- |
/// The only knob here that is not a metric, and it is the only one because
|
| 112 |
|
- |
/// egui_extras already answers the rest. A sticky heading, for one: it is
|
| 113 |
|
- |
/// what `TableBuilder::header` does and there is no version that does not,
|
| 114 |
|
- |
/// so a field offering the choice would be offering one this renderer cannot
|
| 115 |
|
- |
/// make.
|
|
111 |
+ |
/// Not every setting egui_extras has becomes a field here. A sticky heading
|
|
112 |
+ |
/// is what `TableBuilder::header` does and there is no version that does
|
|
113 |
+ |
/// not, so the knob 0.12.0 briefly carried for it offered a choice this
|
|
114 |
+ |
/// renderer cannot make. This one and [`resizable`](Self::resizable) are the
|
|
115 |
+ |
/// two that pass that test.
|
| 116 |
116 |
|
pub striped: bool,
|
|
117 |
+ |
/// Whether the user can drag the divider between two columns.
|
|
118 |
+ |
///
|
|
119 |
+ |
/// The second knob that is not a metric, and it passes the same test
|
|
120 |
+ |
/// `sticky_header` failed: egui_extras offers both settings and a renderer
|
|
121 |
+ |
/// can honestly make either choice. Off by default for `striped`'s reason:
|
|
122 |
+ |
/// the description has no word for it, so a default that turned it on would
|
|
123 |
+ |
/// be this renderer adding a claim the other two cannot make.
|
|
124 |
+ |
///
|
|
125 |
+ |
/// It does not fight the narrowing. A drag moves a track for the frames it
|
|
126 |
+ |
/// is held; [`cutoff_for`] still decides which columns exist, off the widths
|
|
127 |
+ |
/// the app declared in [`Sizing`], so a resize can never drop a column.
|
|
128 |
+ |
pub resizable: bool,
|
| 117 |
129 |
|
}
|
| 118 |
130 |
|
|
| 119 |
131 |
|
impl Default for TableStyle {
|
| 126 |
138 |
|
ascending: " \u{25B2}",
|
| 127 |
139 |
|
descending: " \u{25BC}",
|
| 128 |
140 |
|
striped: false,
|
|
141 |
+ |
resizable: false,
|
| 129 |
142 |
|
}
|
| 130 |
143 |
|
}
|
| 131 |
144 |
|
}
|
| 132 |
145 |
|
|
|
146 |
+ |
/// The body's own facts for this frame: how many rows, which are selected, and
|
|
147 |
+ |
/// which one to bring into view.
|
|
148 |
+ |
///
|
|
149 |
+ |
/// Held apart from [`TableStyle`] because none of it is style and none of it
|
|
150 |
+ |
/// survives the frame: a row count changes when a folder does, a selection when
|
|
151 |
+ |
/// the user clicks, and a scroll request exists for exactly one frame. Held
|
|
152 |
+ |
/// apart from the [`Column`] slice because none of it is description either.
|
|
153 |
+ |
/// The description says what a table *is*, and this says what it holds right
|
|
154 |
+ |
/// now.
|
|
155 |
+ |
///
|
|
156 |
+ |
/// Both of the optional fields are here rather than left to the app because
|
|
157 |
+ |
/// egui_extras answers them on a handle the app never sees: `set_selected` is a
|
|
158 |
+ |
/// method on the row, and `scroll_to_row` a method on the builder, and this
|
|
159 |
+ |
/// crate owns both. That is the same reason [`cell`] exists.
|
|
160 |
+ |
#[derive(Default)]
|
|
161 |
+ |
pub struct Body<'a> {
|
|
162 |
+ |
/// How many rows to draw.
|
|
163 |
+ |
pub rows: usize,
|
|
164 |
+ |
/// Whether a row is selected, by index.
|
|
165 |
+ |
///
|
|
166 |
+ |
/// A predicate rather than a set, so an app whose selection is a range, a
|
|
167 |
+ |
/// bitmap or a single index does not have to build a collection to be asked.
|
|
168 |
+ |
/// `None` is a table no row of which is selected, which is not the same
|
|
169 |
+ |
/// claim as a predicate that always answers false and costs nothing to make.
|
|
170 |
+ |
pub selected: Option<&'a dyn Fn(usize) -> bool>,
|
|
171 |
+ |
/// A row to bring into view this frame.
|
|
172 |
+ |
///
|
|
173 |
+ |
/// Set it from a request the app then clears, the way a keyboard cursor
|
|
174 |
+ |
/// moving off-screen raises one: held rather than taken, it would fight
|
|
175 |
+ |
/// every scroll the user makes with the mouse.
|
|
176 |
+ |
pub scroll_to: Option<usize>,
|
|
177 |
+ |
}
|
|
178 |
+ |
|
|
179 |
+ |
impl std::fmt::Debug for Body<'_> {
|
|
180 |
+ |
// Hand-written because `selected` is a closure and `#[derive(Debug)]` will
|
|
181 |
+ |
// not have it. What is worth printing is whether one was supplied.
|
|
182 |
+ |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
183 |
+ |
f.debug_struct("Body")
|
|
184 |
+ |
.field("rows", &self.rows)
|
|
185 |
+ |
.field("selected", &self.selected.is_some())
|
|
186 |
+ |
.field("scroll_to", &self.scroll_to)
|
|
187 |
+ |
.finish()
|
|
188 |
+ |
}
|
|
189 |
+ |
}
|
|
190 |
+ |
|
| 133 |
191 |
|
/// The colour a cell of this part takes.
|
| 134 |
192 |
|
///
|
| 135 |
193 |
|
/// [`CellPart`] is `#[non_exhaustive]`, and a member added upstream lands on
|
| 245 |
303 |
|
|
| 246 |
304 |
|
/// A described table, narrowed for the width available.
|
| 247 |
305 |
|
///
|
| 248 |
|
- |
/// `rows` is a count and `draw` is called once per cell of each kept column, in
|
| 249 |
|
- |
/// column order. Taking a closure rather than a slice of contents is what keeps
|
| 250 |
|
- |
/// the app's own data borrowed one cell at a time, which is
|
|
306 |
+ |
/// `draw` is called once per cell of each kept column, in column order, for each
|
|
307 |
+ |
/// of [`Body::rows`] rows. Taking a closure rather than a slice of contents is
|
|
308 |
+ |
/// what keeps the app's own data borrowed one cell at a time, which is
|
| 251 |
309 |
|
/// [`group`](crate::group)'s reasoning and immediate mode's habit.
|
| 252 |
310 |
|
///
|
|
311 |
+ |
/// `body` is borrowed immutably and `draw` is `FnMut`, which is the split a
|
|
312 |
+ |
/// caller has to plan for: a selection read by [`Body::selected`] cannot be the
|
|
313 |
+ |
/// same value `draw` mutates. Snapshot it before the call. That is not this
|
|
314 |
+ |
/// crate imposing anything. It is the borrow the app already takes when it
|
|
315 |
+ |
/// clones its row list to hand egui a closure.
|
|
316 |
+ |
///
|
| 253 |
317 |
|
/// Returns the sortable column whose heading was pressed this frame, if any. The
|
| 254 |
318 |
|
/// app owns the ordering, so this reports the press and changes nothing: what a
|
| 255 |
319 |
|
/// press *calls* is an address, and the description names none. That is
|
| 261 |
325 |
|
pub fn table<'a>(
|
| 262 |
326 |
|
ui: &mut Ui,
|
| 263 |
327 |
|
columns: &'a [Column<'a>],
|
| 264 |
|
- |
rows: usize,
|
|
328 |
+ |
body: &Body<'_>,
|
| 265 |
329 |
|
sizing: &Sizing<'_>,
|
| 266 |
330 |
|
palette: &Palette,
|
| 267 |
331 |
|
style: &TableStyle,
|
| 277 |
341 |
|
return None;
|
| 278 |
342 |
|
}
|
| 279 |
343 |
|
|
| 280 |
|
- |
let mut builder = TableBuilder::new(ui).striped(style.striped);
|
|
344 |
+ |
let mut builder = TableBuilder::new(ui)
|
|
345 |
+ |
.striped(style.striped)
|
|
346 |
+ |
.resizable(style.resizable)
|
|
347 |
+ |
// Not a knob, because there is no second honest answer: a cell's
|
|
348 |
+ |
// contents sit on the row's centre line. CSS says `vertical-align:
|
|
349 |
+ |
// middle` and a terminal row is one line tall, so a field offering the
|
|
350 |
+ |
// choice would be offering one only this renderer could take. egui's own
|
|
351 |
+ |
// default is top-aligned, which is why it has to be said at all.
|
|
352 |
+ |
.cell_layout(egui::Layout::left_to_right(egui::Align::Center));
|
| 281 |
353 |
|
for column in &kept {
|
| 282 |
354 |
|
builder = builder.column(track(column, sizing));
|
| 283 |
355 |
|
}
|
|
356 |
+ |
if let Some(row) = body.scroll_to {
|
|
357 |
+ |
builder = builder.scroll_to_row(row, None);
|
|
358 |
+ |
}
|
| 284 |
359 |
|
|
| 285 |
360 |
|
// Written through a Cell rather than returned, because egui_extras hands the
|
| 286 |
361 |
|
// header and the body their own closures and neither can return a value past
|
| 297 |
372 |
|
});
|
| 298 |
373 |
|
}
|
| 299 |
374 |
|
})
|
| 300 |
|
- |
.body(|body| {
|
| 301 |
|
- |
body.rows(style.row_height, rows, |mut row| {
|
|
375 |
+ |
.body(|table_body| {
|
|
376 |
+ |
table_body.rows(style.row_height, body.rows, |mut row| {
|
| 302 |
377 |
|
let index = row.index();
|
|
378 |
+ |
if let Some(selected) = body.selected {
|
|
379 |
+ |
// Before the cells, and on the row rather than on any of
|
|
380 |
+ |
// them: a selection marks the whole row, and a renderer that
|
|
381 |
+ |
// tinted each cell would leave the gaps between them
|
|
382 |
+ |
// unpainted.
|
|
383 |
+ |
row.set_selected(selected(index));
|
|
384 |
+ |
}
|
| 303 |
385 |
|
for column in &kept {
|
| 304 |
386 |
|
row.col(|ui| draw(ui, column, index));
|
| 305 |
387 |
|
}
|
| 517 |
599 |
|
// egui_extras offers it and the other two renderers cannot say it. A
|
| 518 |
600 |
|
// default that turned it on would be this renderer adding a claim.
|
| 519 |
601 |
|
assert!(!TableStyle::default().striped);
|
|
602 |
+ |
// Same test, same answer, and the reason `sticky_header` failed it: that
|
|
603 |
+ |
// one had no second setting to offer.
|
|
604 |
+ |
assert!(!TableStyle::default().resizable);
|
|
605 |
+ |
}
|
|
606 |
+ |
|
|
607 |
+ |
#[test]
|
|
608 |
+ |
fn a_body_claims_nothing_until_it_is_asked_to() {
|
|
609 |
+ |
// The default is a table of no rows, no selection and no scroll
|
|
610 |
+ |
// request. All three absences are the honest reading of an app that has
|
|
611 |
+ |
// not said otherwise, which is why they are `Option` and not a
|
|
612 |
+ |
// predicate that always answers false.
|
|
613 |
+ |
let body = Body::default();
|
|
614 |
+ |
assert_eq!(body.rows, 0);
|
|
615 |
+ |
assert!(body.selected.is_none());
|
|
616 |
+ |
assert!(body.scroll_to.is_none());
|
|
617 |
+ |
}
|
|
618 |
+ |
|
|
619 |
+ |
#[test]
|
|
620 |
+ |
fn a_selection_is_asked_per_row_and_not_collected() {
|
|
621 |
+ |
// A predicate, so an app whose selection is a range or a single index
|
|
622 |
+ |
// does not build a set to be asked. Exercised the way `table` asks it:
|
|
623 |
+ |
// once per row index, in order.
|
|
624 |
+ |
let selected = |index: usize| index.is_multiple_of(2);
|
|
625 |
+ |
let body = Body {
|
|
626 |
+ |
rows: 4,
|
|
627 |
+ |
selected: Some(&selected),
|
|
628 |
+ |
scroll_to: None,
|
|
629 |
+ |
};
|
|
630 |
+ |
let f = body.selected.expect("a predicate was supplied");
|
|
631 |
+ |
assert_eq!(
|
|
632 |
+ |
(0..body.rows).map(f).collect::<Vec<_>>(),
|
|
633 |
+ |
vec![true, false, true, false]
|
|
634 |
+ |
);
|
|
635 |
+ |
}
|
|
636 |
+ |
|
|
637 |
+ |
#[test]
|
|
638 |
+ |
fn narrowing_reads_the_declared_widths_and_not_a_dragged_track() {
|
|
639 |
+ |
// `resizable` lets the user move a divider, and `cutoff_for` must not
|
|
640 |
+ |
// hear about it: a drag that could drop a column would make the
|
|
641 |
+ |
// narrowing a thing the user does by accident rather than a property of
|
|
642 |
+ |
// the description. That `cutoff_for` takes no `TableStyle` at all is the
|
|
643 |
+ |
// structural half of the guarantee; this is the behavioural half, and it
|
|
644 |
+ |
// is what would fail if a measured width were ever threaded in beside
|
|
645 |
+ |
// the declared one.
|
|
646 |
+ |
let (cols, sz) = (columns(), sizing());
|
|
647 |
+ |
assert_eq!(cutoff_for(&cols, &sz, 300.0), Priority::Optional);
|
|
648 |
+ |
assert_eq!(cutoff_for(&cols, &sz, 200.0), Priority::Secondary);
|
| 520 |
649 |
|
}
|
| 521 |
650 |
|
}
|