Compare commits

...

16 commits

Author SHA1 Message Date
Emil Ernerfeldt
ea5941c3a1 Simplify table demo 2022-04-05 08:27:26 +02:00
Wayne Warren
a17519fb34 egui_extras: address CI failures 2022-04-04 18:31:21 -06:00
Wayne Warren
603ce062b3 egui_extras: clean up visible table bounds calculation a little 2022-04-04 08:10:28 -06:00
wayne
1d7f698669
Update egui_extras/src/table.rs
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-04-04 03:16:24 +00:00
Wayne Warren
84e0700459 egui_extras: use slice instead of vec ref 2022-04-03 20:52:41 -06:00
Wayne Warren
2d6ca4265f egui_extras: minor indexing fix, address PR feedback 2022-04-03 20:51:27 -06:00
Wayne Warren
b82fc89462 egui_extras: rename TableBody::delta to TableBody::y_progress 2022-04-03 20:26:50 -06:00
Wayne Warren
51a98d586b egui_extras: fix row indexing 2022-04-03 20:03:11 -06:00
Wayne Warren
91d78fa2e9 egui_extras: clean up virtual scroll table demo settings a little 2022-04-03 20:02:50 -06:00
Wayne Warren
1c5f93322d egui_extras: restore TableBody::row example 2022-04-03 12:16:23 -06:00
Wayne Warren
7706a91974 egui_extras: add heterogeneous_rows example 2022-04-03 12:11:12 -06:00
Wayne Warren
2f22f41185 egui_extras: cleanup TableBody.heterogeneous_rows a little
* use f64 when accumulating virtual height above and below visible
  region
* break the big loop iterating over heights into three loops, one for
  each non-visible region, and one for the visible region
* retain each row's stripe color using an enumeration over the given
  heights iterator
* use a VIRTUAL_EXTENSION constant to extend the "visible" region of the
  table above and below the actual visible region to provide the
  illusion of rows sliding into and out of sight
2022-04-03 12:00:27 -06:00
Wayne Warren
c2df572dd1 egui_extras: add comments to new TableBody methods 2022-04-03 11:25:38 -06:00
Wayne Warren
44bd8c1cc4 egui_extras: follow closure-based conventions for TableBody.heterogeneous_rows 2022-04-03 11:18:18 -06:00
Wayne Warren
ab4930fde7 egui_extras: make number of rows in table demo configurable 2022-04-03 10:59:10 -06:00
Wayne Warren
e81a92d32d egui_extras: enable virtual scroll for heterogenous rows
Introduce `TableBody.heterogenous_rows` and `TableRowBuilder`, the former of which takes as an argument the latter. `TableRowBuilder` provides two methods that enable virtual scrolling for rows with non-uniform heights. Those methods are:

* `TableRowBuilder.row_heights` which returns an iterator over `f32` to allow incremental virtual scroll buffer calculation.
* `TableRowBuilder.populate_row` which `TableBody.heterogenous_rows` uses to allow `TableRowBuilder` implementations to, you guessed it, populate rows that are visible.

One thought that occurs to me while writing this description is that
`TableBody.heterogenous_rows` could look more like the following:

```
    pub fn heterogenous_rows(
        mut self,
        row_heights: impl Iterator<Item = f32> + '_,
        mut row: impl FnMut(usize, TableRow<'_, '_>),
    )
```

This could potentially be easier to use, considering all the trouble I had coming up with and implementing the trait. Happy to make this change if the maintainers prefer.
2022-04-03 00:18:17 -06:00
2 changed files with 227 additions and 34 deletions

View file

@ -1,12 +1,24 @@
use egui::TextStyle; use egui::TextStyle;
use egui_extras::{Size, StripBuilder, TableBuilder}; use egui_extras::{Size, StripBuilder, TableBuilder, TableRow};
/// Shows off a table with dynamic layout /// Shows off a table with dynamic layout
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Default)]
pub struct TableDemo { pub struct TableDemo {
heterogeneous_rows: bool,
virtual_scroll: bool, virtual_scroll: bool,
resizable: bool, resizable: bool,
num_rows: usize,
}
impl Default for TableDemo {
fn default() -> Self {
Self {
heterogeneous_rows: true,
virtual_scroll: false,
resizable: true,
num_rows: 100,
}
}
} }
impl super::Demo for TableDemo { impl super::Demo for TableDemo {
@ -28,8 +40,23 @@ impl super::Demo for TableDemo {
impl super::View for TableDemo { impl super::View for TableDemo {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
ui.checkbox(&mut self.virtual_scroll, "Virtual scroll"); ui.horizontal(|ui| {
ui.checkbox(&mut self.resizable, "Resizable columns"); ui.vertical(|ui| {
ui.checkbox(&mut self.resizable, "Resizable columns");
ui.checkbox(&mut self.virtual_scroll, "Virtual Scroll");
if self.virtual_scroll {
ui.checkbox(&mut self.heterogeneous_rows, "Heterogeneous row heights");
}
});
if self.virtual_scroll {
ui.add(
egui::Slider::new(&mut self.num_rows, 0..=100_000)
.logarithmic(true)
.text("Num rows"),
);
}
});
// Leave room for the source code link after the table demo: // Leave room for the source code link after the table demo:
StripBuilder::new(ui) StripBuilder::new(ui)
@ -77,19 +104,25 @@ impl TableDemo {
}) })
.body(|mut body| { .body(|mut body| {
if self.virtual_scroll { if self.virtual_scroll {
body.rows(text_height, 100_000, |row_index, mut row| { if !self.heterogeneous_rows {
row.col(|ui| { body.rows(text_height, self.num_rows, |row_index, mut row| {
ui.label(row_index.to_string()); row.col(|ui| {
ui.label(row_index.to_string());
});
row.col(|ui| {
ui.label(clock_emoji(row_index));
});
row.col(|ui| {
ui.add(
egui::Label::new("Thousands of rows of even height")
.wrap(false),
);
});
}); });
row.col(|ui| { } else {
ui.label(clock_emoji(row_index)); let rows = DemoRows::new(self.num_rows);
}); body.heterogeneous_rows(rows, DemoRows::populate_row);
row.col(|ui| { }
ui.add(
egui::Label::new("Thousands of rows of even height").wrap(false),
);
});
});
} else { } else {
for row_index in 0..20 { for row_index in 0..20 {
let thick = row_index % 6 == 0; let thick = row_index % 6 == 0;
@ -122,6 +155,58 @@ impl TableDemo {
} }
} }
struct DemoRows {
row_count: usize,
current_row: usize,
}
impl DemoRows {
fn new(row_count: usize) -> Self {
Self {
row_count,
current_row: 0,
}
}
fn populate_row(index: usize, mut row: TableRow<'_, '_>) {
let thick = index % 6 == 0;
row.col(|ui| {
ui.centered_and_justified(|ui| {
ui.label(index.to_string());
});
});
row.col(|ui| {
ui.centered_and_justified(|ui| {
ui.label(clock_emoji(index));
});
});
row.col(|ui| {
ui.centered_and_justified(|ui| {
ui.style_mut().wrap = Some(false);
if thick {
ui.heading("Extra thick row");
} else {
ui.label("Normal row");
}
});
});
}
}
impl Iterator for DemoRows {
type Item = f32;
fn next(&mut self) -> Option<Self::Item> {
if self.current_row < self.row_count {
let thick = self.current_row % 6 == 0;
self.current_row += 1;
Some(if thick { 30.0 } else { 18.0 })
} else {
None
}
}
}
fn clock_emoji(row_index: usize) -> String { fn clock_emoji(row_index: usize) -> String {
char::from_u32(0x1f550 + row_index as u32 % 24) char::from_u32(0x1f550 + row_index as u32 % 24)
.unwrap() .unwrap()

View file

@ -352,24 +352,126 @@ pub struct TableBody<'a> {
} }
impl<'a> TableBody<'a> { impl<'a> TableBody<'a> {
fn y_progress(&self) -> f32 {
self.start_y - self.layout.current_y()
}
/// Return a vector containing all column widths for this table body.
///
/// This is primarily meant for use with [`TableBody::heterogeneous_rows`] in cases where row
/// heights are expected to according to the width of one or more cells -- for example, if text
/// is wrapped rather than clippped within the cell.
pub fn widths(&self) -> &[f32] {
&self.widths
}
/// Add rows with varying heights.
///
/// This takes a very slight performance hit compared to [`TableBody::rows`] due to the need to
/// iterate over all row heights in to calculate the virtual table height above and below the
/// visible region, but it is many orders of magnitude more performant than adding individual
/// heterogenously-sized rows using [`TableBody::row`] at the cost of the additional complexity
/// that comes with pre-calculating row heights and representing them as an iterator.
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// use egui_extras::{TableBuilder, Size};
/// TableBuilder::new(ui)
/// .column(Size::remainder().at_least(100.0))
/// .body(|mut body| {
/// let row_heights: Vec<f32> = vec![60.0, 18.0, 31.0, 240.0];
/// body.heterogeneous_rows(row_heights.into_iter(), |row_index, mut row| {
/// let thick = row_index % 6 == 0;
/// row.col(|ui| {
/// ui.centered_and_justified(|ui| {
/// ui.label(row_index.to_string());
/// });
/// });
/// });
/// });
/// # });
/// ```
pub fn heterogeneous_rows(
&mut self,
heights: impl Iterator<Item = f32>,
mut populate_row: impl FnMut(usize, TableRow<'_, '_>),
) {
// in order for each row to retain its striped color as the table is scrolled, we need an
// iterator with the boolean built in based on the enumerated index of the iterator element
let mut striped_heights = heights
.enumerate()
.map(|(index, height)| (index, index % 2 == 0, height));
let max_height = self.end_y - self.start_y;
let y_progress = self.y_progress();
// cumulative height of all rows above those being displayed
let mut height_above_visible: f64 = 0.0;
// cumulative height of all rows below those being displayed
let mut height_below_visible: f64 = 0.0;
// calculate height above visible table range and populate the first non-virtual row.
// because this row is meant to slide under the top bound of the visual table we calculate
// height_of_first_row + height_above_visible >= y_progress as our break condition rather
// than just height_above_visible >= y_progress
while let Some((row_index, striped, height)) = striped_heights.next() {
if height as f64 + height_above_visible >= y_progress as f64 {
self.add_buffer(height_above_visible as f32);
let tr = TableRow {
layout: &mut self.layout,
widths: &self.widths,
striped: self.striped && striped,
height,
};
self.row_nr += 1;
populate_row(row_index, tr);
break;
}
height_above_visible += height as f64;
}
// populate visible rows, including the final row that should slide under the bottom bound
// of the visible table.
let mut current_height: f64 = 0.0;
while let Some((row_index, striped, height)) = striped_heights.next() {
if height as f64 + current_height > max_height as f64 {
break;
}
let tr = TableRow {
layout: &mut self.layout,
widths: &self.widths,
striped: self.striped && striped,
height,
};
self.row_nr += 1;
populate_row(row_index, tr);
current_height += height as f64;
}
// calculate height below the visible table range
while let Some((_, _, height)) = striped_heights.next() {
height_below_visible += height as f64
}
// if height below visible is > 0 here then we need to add a buffer to allow the table to
// accurately calculate the "virtual" scrollbar position
if height_below_visible > 0.0 {
self.add_buffer(height_below_visible as f32);
}
}
/// Add rows with same height. /// Add rows with same height.
/// ///
/// Is a lot more performant than adding each individual row as non visible rows must not be rendered /// Is a lot more performant than adding each individual row as non visible rows must not be rendered
pub fn rows(mut self, height: f32, rows: usize, mut row: impl FnMut(usize, TableRow<'_, '_>)) { pub fn rows(mut self, height: f32, rows: usize, mut row: impl FnMut(usize, TableRow<'_, '_>)) {
let delta = self.layout.current_y() - self.start_y; let y_progress = self.y_progress();
let mut start = 0; let mut start = 0;
if delta < 0.0 { if y_progress > 0.0 {
start = (-delta / height).floor() as usize; start = (y_progress / height).floor() as usize;
let skip_height = start as f32 * height; self.add_buffer(y_progress);
TableRow {
layout: &mut self.layout,
widths: &self.widths,
striped: false,
height: skip_height,
}
.col(|_| ()); // advances the cursor
} }
let max_height = self.end_y - self.start_y; let max_height = self.end_y - self.start_y;
@ -391,13 +493,7 @@ impl<'a> TableBody<'a> {
if rows - end > 0 { if rows - end > 0 {
let skip_height = (rows - end) as f32 * height; let skip_height = (rows - end) as f32 * height;
TableRow { self.add_buffer(skip_height);
layout: &mut self.layout,
widths: &self.widths,
striped: false,
height: skip_height,
}
.col(|_| ()); // advances the cursor
} }
} }
@ -412,6 +508,18 @@ impl<'a> TableBody<'a> {
self.row_nr += 1; self.row_nr += 1;
} }
// Create a table row buffer of the given height to represent the non-visible portion of the
// table.
fn add_buffer(&mut self, height: f32) {
TableRow {
layout: &mut self.layout,
widths: &self.widths,
striped: false,
height,
}
.col(|_| ()); // advances the cursor
}
} }
impl<'a> Drop for TableBody<'a> { impl<'a> Drop for TableBody<'a> {