egui/egui_extras/src/table.rs

603 lines
20 KiB
Rust
Raw Normal View History

//! Table view with (optional) fixed header and scrolling body.
//! Cell widths are precalculated with given size hints so we can have tables like this:
//! | fixed size | all available space/minimum | 30% of available width | fixed size |
//! Takes all available height, so if you want something below the table, put it in a strip.
use crate::{
layout::{CellDirection, CellSize},
sizing::Sizing,
Size, StripLayout,
};
use egui::{Rect, Response, Ui, Vec2};
/// Builder for a [`Table`] with (optional) fixed header and scrolling body.
///
/// Cell widths are precalculated so we can have tables like this:
///
/// | fixed size | all available space/minimum | 30% of available width | fixed size |
///
/// In contrast to normal egui behavior, columns/rows do *not* grow with its children!
/// Takes all available height, so if you want something below the table, put it in a strip.
///
/// You must pre-allocate all columns with [`Self::column`]/[`Self::columns`].
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// use egui_extras::{TableBuilder, Size};
/// TableBuilder::new(ui)
/// .column(Size::remainder().at_least(100.0))
/// .column(Size::exact(40.0))
/// .header(20.0, |mut header| {
/// header.col(|ui| {
/// ui.heading("Growing");
/// });
/// header.col(|ui| {
/// ui.heading("Fixed");
/// });
/// })
/// .body(|mut body| {
/// body.row(30.0, |mut row| {
/// row.col(|ui| {
/// ui.label("first row growing cell");
/// });
/// row.col(|ui| {
/// ui.button("action");
/// });
/// });
/// });
/// # });
/// ```
pub struct TableBuilder<'a> {
ui: &'a mut Ui,
sizing: Sizing,
scroll: bool,
striped: bool,
resizable: bool,
clip: bool,
}
impl<'a> TableBuilder<'a> {
pub fn new(ui: &'a mut Ui) -> Self {
Self {
ui,
sizing: Default::default(),
scroll: true,
striped: false,
resizable: false,
clip: true,
}
}
/// Enable scrollview in body (default: true)
pub fn scroll(mut self, scroll: bool) -> Self {
self.scroll = scroll;
self
}
/// Enable striped row background (default: false)
pub fn striped(mut self, striped: bool) -> Self {
self.striped = striped;
self
}
/// Make the columns resizable by dragging.
///
/// If the _last_ column is [`Size::Remainder`], then it won't be resizable
/// (and instead use up the remainder).
///
/// Default is `false`.
///
/// If you have multiple [`Table`]:s in the same [`Ui`]
/// you will need to give them unique id:s with [`Ui::push_id`].
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
/// Should we clip the contents of each cell? Default: `true`.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Allocate space for one column.
pub fn column(mut self, width: Size) -> Self {
self.sizing.add(width);
self
}
/// Allocate space for several columns at once.
pub fn columns(mut self, size: Size, count: usize) -> Self {
for _ in 0..count {
self.sizing.add(size);
}
self
}
fn available_width(&self) -> f32 {
self.ui.available_rect_before_wrap().width()
- if self.scroll {
2022-04-01 12:43:43 +00:00
self.ui.spacing().item_spacing.x + self.ui.spacing().scroll_bar_width
} else {
0.0
}
}
/// Create a header row which always stays visible and at the top
pub fn header(self, height: f32, header: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
let available_width = self.available_width();
let Self {
ui,
sizing,
scroll,
striped,
resizable,
clip,
} = self;
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
let widths = read_table_widths(ui, resize_id)
.unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x));
let table_top = ui.cursor().top();
{
let mut layout = StripLayout::new(ui, CellDirection::Horizontal, clip);
header(TableRow {
layout: &mut layout,
widths: &widths,
striped: false,
height,
});
layout.allocate_rect();
}
Table {
ui,
table_top,
resize_id,
sizing,
available_width,
widths,
scroll,
striped,
clip,
}
}
/// Create table body without a header row
pub fn body<F>(self, body: F)
where
F: for<'b> FnOnce(TableBody<'b>),
{
let available_width = self.available_width();
let Self {
ui,
sizing,
scroll,
striped,
resizable,
clip,
} = self;
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
let widths = read_table_widths(ui, resize_id)
.unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x));
let table_top = ui.cursor().top();
Table {
ui,
table_top,
resize_id,
sizing,
available_width,
widths,
scroll,
striped,
clip,
}
.body(body);
}
}
fn read_table_widths(ui: &egui::Ui, resize_id: Option<egui::Id>) -> Option<Vec<f32>> {
if let Some(resize_id) = resize_id {
let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO);
ui.ctx().check_for_id_clash(resize_id, rect, "Table");
ui.data().get_persisted(resize_id)
} else {
None
}
}
/// Table struct which can construct a [`TableBody`].
///
/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`].
pub struct Table<'a> {
ui: &'a mut Ui,
table_top: f32,
resize_id: Option<egui::Id>,
sizing: Sizing,
available_width: f32,
widths: Vec<f32>,
scroll: bool,
striped: bool,
clip: bool,
}
impl<'a> Table<'a> {
/// Create table body after adding a header row
pub fn body<F>(self, body: F)
where
F: for<'b> FnOnce(TableBody<'b>),
{
let Table {
ui,
table_top,
resize_id,
sizing,
mut available_width,
widths,
scroll,
striped,
clip,
} = self;
let avail_rect = ui.available_rect_before_wrap();
let mut new_widths = widths.clone();
egui::ScrollArea::new([false, scroll])
.auto_shrink([true; 2])
.show(ui, move |ui| {
let layout = StripLayout::new(ui, CellDirection::Horizontal, clip);
body(TableBody {
layout,
widths,
striped,
row_nr: 0,
start_y: avail_rect.top(),
end_y: avail_rect.bottom(),
});
});
let bottom = ui.min_rect().bottom();
// TODO: fix frame-delay by interacting before laying out (but painting later).
if let Some(resize_id) = resize_id {
let spacing_x = ui.spacing().item_spacing.x;
let mut x = avail_rect.left() - spacing_x * 0.5;
for (i, width) in new_widths.iter_mut().enumerate() {
x += *width + spacing_x;
// If the last column is Size::Remainder, then let it fill the remainder!
let last_column = i + 1 == sizing.sizes.len();
if last_column {
if let Size::Remainder { range: (min, max) } = sizing.sizes[i] {
let eps = 0.1; // just to avoid some rounding errors.
*width = (available_width - eps).clamp(min, max);
break;
}
}
let resize_id = ui.id().with("__panel_resize").with(i);
let mut p0 = egui::pos2(x, table_top);
let mut p1 = egui::pos2(x, bottom);
let line_rect = egui::Rect::from_min_max(p0, p1)
.expand(ui.style().interaction.resize_grab_radius_side);
let mouse_over_resize_line = ui.rect_contains_pointer(line_rect);
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
&& mouse_over_resize_line
{
ui.memory().set_dragged_id(resize_id);
}
let is_resizing = ui.memory().is_being_dragged(resize_id);
if is_resizing {
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let new_width = *width + pointer.x - x;
let (min, max) = sizing.sizes[i].range();
let new_width = new_width.clamp(min, max);
let x = x - *width + new_width;
p0.x = x;
p1.x = x;
*width = new_width;
}
}
let dragging_something_else =
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
let resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = egui::CursorIcon::ResizeHorizontal;
}
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
} else if resize_hover {
ui.style().visuals.widgets.hovered.bg_stroke
} else {
// ui.visuals().widgets.inactive.bg_stroke
ui.visuals().widgets.noninteractive.bg_stroke
};
ui.painter().line_segment([p0, p1], stroke);
available_width -= *width + spacing_x;
}
ui.data().insert_persisted(resize_id, new_widths);
}
}
}
/// The body of a table.
/// Is created by calling `body` on a [`Table`] (after adding a header row) or [`TableBuilder`] (without a header row).
pub struct TableBody<'a> {
layout: StripLayout<'a>,
widths: Vec<f32>,
striped: bool,
row_nr: usize,
start_y: f32,
end_y: f32,
}
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 a single row with the given height.
///
/// If you have many thousands of row it can be more performant to instead use [`Self::rows]` or [`Self::heterogeneous_rows`].
pub fn row(&mut self, height: f32, row: impl FnOnce(TableRow<'a, '_>)) {
row(TableRow {
layout: &mut self.layout,
widths: &self.widths,
striped: self.striped && self.row_nr % 2 == 0,
height,
});
self.row_nr += 1;
}
/// Add many rows with same height.
///
/// Is a lot more performant than adding each individual row as non visible rows must not be rendered.
///
/// If you need many rows with different heights, use [`Self::heterogeneous_rows`] instead.
///
/// ### 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_height = 18.0;
/// let num_rows = 10_000;
/// body.rows(row_height, num_rows, |row_index, mut row| {
/// row.col(|ui| {
/// ui.label("First column");
/// });
/// });
/// });
/// # });
/// ```
pub fn rows(mut self, height: f32, rows: usize, mut row: impl FnMut(usize, TableRow<'_, '_>)) {
let y_progress = self.y_progress();
let mut start = 0;
if y_progress > 0.0 {
start = (y_progress / height).floor() as usize;
self.add_buffer(y_progress);
}
let max_height = self.end_y - self.start_y;
let count = (max_height / height).ceil() as usize;
let end = rows.min(start + count);
for idx in start..end {
row(
idx,
TableRow {
layout: &mut self.layout,
widths: &self.widths,
striped: self.striped && idx % 2 == 0,
height,
},
);
}
if rows - end > 0 {
let skip_height = (rows - end) as f32 * height;
self.add_buffer(skip_height);
}
}
/// 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
for (row_index, striped, height) in &mut striped_heights {
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;
for (row_index, striped, height) in &mut striped_heights {
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
for (_, _, height) in striped_heights {
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);
}
}
// 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> {
fn drop(&mut self) {
self.layout.allocate_rect();
}
}
/// The row of a table.
/// Is created by [`TableRow`] for each created [`TableBody::row`] or each visible row in rows created by calling [`TableBody::rows`].
pub struct TableRow<'a, 'b> {
layout: &'b mut StripLayout<'a>,
widths: &'b [f32],
striped: bool,
height: f32,
}
impl<'a, 'b> TableRow<'a, 'b> {
/// Add the contents of a column.
pub fn col(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response {
assert!(
!self.widths.is_empty(),
"Tried using more table columns than available."
);
let width = if self.widths.is_empty() {
if cfg!(debug_assertions) {
panic!("Added more `Table` columns than were allocated.");
} else {
#[cfg(feature = "tracing")]
tracing::error!("Added more `Table` columns than were allocated");
#[cfg(not(feature = "tracing"))]
eprintln!("egui_extras: Added more `Table` columns than were allocated");
8.0 // anything will look wrong, so pick something that is obviously wrong
}
} else {
let width = self.widths[0];
self.widths = &self.widths[1..];
width
};
let width = CellSize::Absolute(width);
let height = CellSize::Absolute(self.height);
if self.striped {
self.layout.add_striped(width, height, add_contents)
} else {
self.layout.add(width, height, add_contents)
}
}
}
impl<'a, 'b> Drop for TableRow<'a, 'b> {
fn drop(&mut self) {
self.layout.end_line();
}
}