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.
This commit is contained in:
parent
5dff1e42c6
commit
e81a92d32d
2 changed files with 132 additions and 43 deletions
|
@ -1,5 +1,5 @@
|
||||||
use egui::TextStyle;
|
use egui::TextStyle;
|
||||||
use egui_extras::{Size, StripBuilder, TableBuilder};
|
use egui_extras::{Size, StripBuilder, TableBuilder, TableRow, TableRowBuilder};
|
||||||
|
|
||||||
/// 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))]
|
||||||
|
@ -75,7 +75,7 @@ impl TableDemo {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.body(|mut body| {
|
.body(|body| {
|
||||||
if self.virtual_scroll {
|
if self.virtual_scroll {
|
||||||
body.rows(text_height, 100_000, |row_index, mut row| {
|
body.rows(text_height, 100_000, |row_index, mut row| {
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
|
@ -91,37 +91,71 @@ impl TableDemo {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for row_index in 0..20 {
|
let row_builder = DemoRowBuilder {
|
||||||
let thick = row_index % 6 == 0;
|
row_count: 100000,
|
||||||
let row_height = if thick { 30.0 } else { 18.0 };
|
};
|
||||||
body.row(row_height, |mut row| {
|
body.heterogenous_rows(row_builder);
|
||||||
row.col(|ui| {
|
|
||||||
ui.centered_and_justified(|ui| {
|
|
||||||
ui.label(row_index.to_string());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.centered_and_justified(|ui| {
|
|
||||||
ui.label(clock_emoji(row_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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DemoRowBuilder {
|
||||||
|
row_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DemoRows {
|
||||||
|
row_count: usize,
|
||||||
|
current_row: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableRowBuilder for DemoRowBuilder {
|
||||||
|
fn row_heights(&self, _: &Vec<f32>) -> Box<dyn Iterator<Item = f32>> {
|
||||||
|
Box::new(DemoRows{
|
||||||
|
row_count: self.row_count,
|
||||||
|
current_row: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_row(&self, 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -351,7 +351,65 @@ pub struct TableBody<'a> {
|
||||||
end_y: f32,
|
end_y: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait TableRowBuilder {
|
||||||
|
fn row_heights(&self, widths: &Vec<f32>) -> Box<dyn Iterator<Item = f32> + '_>;
|
||||||
|
fn populate_row(&self, index: usize, row: TableRow<'_, '_>);
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> TableBody<'a> {
|
impl<'a> TableBody<'a> {
|
||||||
|
pub fn heterogenous_rows(mut self, builder: impl TableRowBuilder) {
|
||||||
|
let mut heights = builder.row_heights(&self.widths);
|
||||||
|
|
||||||
|
let max_height = self.end_y - self.start_y;
|
||||||
|
let delta = self.start_y - self.layout.current_y();
|
||||||
|
|
||||||
|
// cumulative height of all rows above those being displayed
|
||||||
|
let mut height_above_visible = 0.0;
|
||||||
|
// cumulative height of all rows below those being displayed
|
||||||
|
let mut height_below_visible = 0.0;
|
||||||
|
|
||||||
|
let mut row_index = 0;
|
||||||
|
|
||||||
|
let mut above_buffer_set = false;
|
||||||
|
let mut current_height = 0.0; // used to track height of visible rows
|
||||||
|
while let Some(height) = heights.next() {
|
||||||
|
// when delta is greater than height above 0, we need to increment the row index and
|
||||||
|
// update the height above visble with the current height then continue
|
||||||
|
if height_above_visible < delta {
|
||||||
|
row_index += 1;
|
||||||
|
height_above_visible += height;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if height above visible is > 0 here then we need to add a buffer to allow the table
|
||||||
|
// to calculate the "virtual" scrollbar position, reset height_above_visisble to 0 to
|
||||||
|
// prevent doing this more than once
|
||||||
|
if height_above_visible > 0.0 && !above_buffer_set {
|
||||||
|
self.buffer(height_above_visible);
|
||||||
|
above_buffer_set = true; // only set the upper buffer once
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate visible rows
|
||||||
|
if current_height < max_height {
|
||||||
|
let tr = TableRow {
|
||||||
|
layout: &mut self.layout,
|
||||||
|
widths: &self.widths,
|
||||||
|
striped: self.striped && self.row_nr % 2 == 0,
|
||||||
|
height: height,
|
||||||
|
};
|
||||||
|
self.row_nr += 1;
|
||||||
|
builder.populate_row(row_index, tr);
|
||||||
|
row_index += 1;
|
||||||
|
current_height += height;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate below height
|
||||||
|
height_below_visible += height
|
||||||
|
}
|
||||||
|
self.buffer(height_below_visible);
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
|
@ -362,14 +420,7 @@ impl<'a> TableBody<'a> {
|
||||||
if delta < 0.0 {
|
if delta < 0.0 {
|
||||||
start = (-delta / height).floor() as usize;
|
start = (-delta / height).floor() as usize;
|
||||||
|
|
||||||
let skip_height = start as f32 * height;
|
self.buffer(-delta);
|
||||||
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 +442,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.buffer(skip_height);
|
||||||
layout: &mut self.layout,
|
|
||||||
widths: &self.widths,
|
|
||||||
striped: false,
|
|
||||||
height: skip_height,
|
|
||||||
}
|
|
||||||
.col(|_| ()); // advances the cursor
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,6 +457,16 @@ impl<'a> TableBody<'a> {
|
||||||
|
|
||||||
self.row_nr += 1;
|
self.row_nr += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn buffer(&mut self, height: f32) {
|
||||||
|
TableRow {
|
||||||
|
layout: &mut self.layout,
|
||||||
|
widths: &self.widths,
|
||||||
|
striped: false,
|
||||||
|
height: height,
|
||||||
|
}
|
||||||
|
.col(|_| ()); // advances the cursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Drop for TableBody<'a> {
|
impl<'a> Drop for TableBody<'a> {
|
||||||
|
|
Loading…
Reference in a new issue