egui_extras: Improve the auto-sizing of `Table` (#4756)
This makes the sizing pass of an `egui_table` ensure the table uses as little width as possible. Subsequently, it will redistribute all non-resizable columns on the available space, so that a table better follow the parent container as it is resized. I also added `table.reset()` for forgetting the current column widths.
This commit is contained in:
parent
753412193c
commit
8ef0e85b85
|
|
@ -68,6 +68,14 @@ impl Label {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set [`Self::wrap_mode`] to [`TextWrapMode::Extend`],
|
||||||
|
/// disabling wrapping and truncating, and instead expanding the parent [`Ui`].
|
||||||
|
#[inline]
|
||||||
|
pub fn extend(mut self) -> Self {
|
||||||
|
self.wrap_mode = Some(TextWrapMode::Extend);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Can the user select the text with the mouse?
|
/// Can the user select the text with the mouse?
|
||||||
///
|
///
|
||||||
/// Overrides [`crate::style::Interaction::selectable_labels`].
|
/// Overrides [`crate::style::Interaction::selectable_labels`].
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ const NUM_MANUAL_ROWS: usize = 20;
|
||||||
|
|
||||||
impl crate::View for TableDemo {
|
impl crate::View for TableDemo {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let mut reset = false;
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.checkbox(&mut self.striped, "Striped");
|
ui.checkbox(&mut self.striped, "Striped");
|
||||||
|
|
@ -102,6 +104,8 @@ impl crate::View for TableDemo {
|
||||||
self.scroll_to_row = Some(self.scroll_to_row_slider);
|
self.scroll_to_row = Some(self.scroll_to_row_slider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset = ui.button("Reset").clicked();
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
@ -115,7 +119,7 @@ impl crate::View for TableDemo {
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
egui::ScrollArea::horizontal().show(ui, |ui| {
|
egui::ScrollArea::horizontal().show(ui, |ui| {
|
||||||
self.table_ui(ui);
|
self.table_ui(ui, reset);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
|
|
@ -128,7 +132,7 @@ impl crate::View for TableDemo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableDemo {
|
impl TableDemo {
|
||||||
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
fn table_ui(&mut self, ui: &mut egui::Ui, reset: bool) {
|
||||||
use egui_extras::{Column, TableBuilder};
|
use egui_extras::{Column, TableBuilder};
|
||||||
|
|
||||||
let text_height = egui::TextStyle::Body
|
let text_height = egui::TextStyle::Body
|
||||||
|
|
@ -142,9 +146,14 @@ impl TableDemo {
|
||||||
.resizable(self.resizable)
|
.resizable(self.resizable)
|
||||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||||
.column(Column::auto())
|
.column(Column::auto())
|
||||||
|
.column(
|
||||||
|
Column::remainder()
|
||||||
|
.at_least(40.0)
|
||||||
|
.clip(true)
|
||||||
|
.resizable(true),
|
||||||
|
)
|
||||||
.column(Column::auto())
|
.column(Column::auto())
|
||||||
.column(Column::initial(100.0).range(40.0..=300.0))
|
.column(Column::remainder())
|
||||||
.column(Column::initial(100.0).at_least(40.0).clip(true))
|
|
||||||
.column(Column::remainder())
|
.column(Column::remainder())
|
||||||
.min_scrolled_height(0.0)
|
.min_scrolled_height(0.0)
|
||||||
.max_scroll_height(available_height);
|
.max_scroll_height(available_height);
|
||||||
|
|
@ -157,19 +166,23 @@ impl TableDemo {
|
||||||
table = table.scroll_to_row(row_index, None);
|
table = table.scroll_to_row(row_index, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if reset {
|
||||||
|
table.reset();
|
||||||
|
}
|
||||||
|
|
||||||
table
|
table
|
||||||
.header(20.0, |mut header| {
|
.header(20.0, |mut header| {
|
||||||
header.col(|ui| {
|
header.col(|ui| {
|
||||||
ui.strong("Row");
|
ui.strong("Row");
|
||||||
});
|
});
|
||||||
header.col(|ui| {
|
header.col(|ui| {
|
||||||
ui.strong("Interaction");
|
ui.strong("Clipped text");
|
||||||
});
|
});
|
||||||
header.col(|ui| {
|
header.col(|ui| {
|
||||||
ui.strong("Expanding content");
|
ui.strong("Expanding content");
|
||||||
});
|
});
|
||||||
header.col(|ui| {
|
header.col(|ui| {
|
||||||
ui.strong("Clipped text");
|
ui.strong("Interaction");
|
||||||
});
|
});
|
||||||
header.col(|ui| {
|
header.col(|ui| {
|
||||||
ui.strong("Content");
|
ui.strong("Content");
|
||||||
|
|
@ -187,13 +200,13 @@ impl TableDemo {
|
||||||
ui.label(row_index.to_string());
|
ui.label(row_index.to_string());
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.checkbox(&mut self.checked, "Click me");
|
ui.label(long_text(row_index));
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
expanding_content(ui);
|
expanding_content(ui);
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.label(long_text(row_index));
|
ui.checkbox(&mut self.checked, "Click me");
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
@ -217,13 +230,13 @@ impl TableDemo {
|
||||||
ui.label(row_index.to_string());
|
ui.label(row_index.to_string());
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.checkbox(&mut self.checked, "Click me");
|
ui.label(long_text(row_index));
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
expanding_content(ui);
|
expanding_content(ui);
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.label(long_text(row_index));
|
ui.checkbox(&mut self.checked, "Click me");
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.add(
|
ui.add(
|
||||||
|
|
@ -245,13 +258,13 @@ impl TableDemo {
|
||||||
ui.label(row_index.to_string());
|
ui.label(row_index.to_string());
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.checkbox(&mut self.checked, "Click me");
|
ui.label(long_text(row_index));
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
expanding_content(ui);
|
expanding_content(ui);
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.label(long_text(row_index));
|
ui.checkbox(&mut self.checked, "Click me");
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col(|ui| {
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
@ -280,14 +293,7 @@ impl TableDemo {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expanding_content(ui: &mut egui::Ui) {
|
fn expanding_content(ui: &mut egui::Ui) {
|
||||||
let width = ui.available_width().clamp(20.0, 200.0);
|
ui.add(egui::Separator::default().horizontal());
|
||||||
let height = ui.available_height();
|
|
||||||
let (rect, _response) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover());
|
|
||||||
ui.painter().hline(
|
|
||||||
rect.x_range(),
|
|
||||||
rect.center().y,
|
|
||||||
(1.0, ui.visuals().text_color()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn long_text(row_index: usize) -> String {
|
fn long_text(row_index: usize) -> String {
|
||||||
|
|
|
||||||
|
|
@ -150,14 +150,16 @@ impl<'l> StripLayout<'l> {
|
||||||
|
|
||||||
let used_rect = child_ui.min_rect();
|
let used_rect = child_ui.min_rect();
|
||||||
|
|
||||||
self.set_pos(max_rect);
|
let allocation_rect = if self.ui.is_sizing_pass() {
|
||||||
|
used_rect
|
||||||
let allocation_rect = if flags.clip {
|
} else if flags.clip {
|
||||||
max_rect
|
max_rect
|
||||||
} else {
|
} else {
|
||||||
max_rect.union(used_rect)
|
max_rect.union(used_rect)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.set_pos(allocation_rect);
|
||||||
|
|
||||||
self.ui.advance_cursor_after_rect(allocation_rect);
|
self.ui.advance_cursor_after_rect(allocation_rect);
|
||||||
|
|
||||||
let response = child_ui.interact(max_rect, child_ui.id(), self.sense);
|
let response = child_ui.interact(max_rect, child_ui.id(), self.sense);
|
||||||
|
|
|
||||||
|
|
@ -156,8 +156,7 @@ fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing {
|
||||||
InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width),
|
InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width),
|
||||||
InitialColumnSize::Remainder => Size::remainder(),
|
InitialColumnSize::Remainder => Size::remainder(),
|
||||||
}
|
}
|
||||||
.at_least(column.width_range.min)
|
.with_range(column.width_range);
|
||||||
.at_most(column.width_range.max);
|
|
||||||
sizing.add(size);
|
sizing.add(size);
|
||||||
}
|
}
|
||||||
sizing
|
sizing
|
||||||
|
|
@ -405,6 +404,12 @@ impl<'a> TableBuilder<'a> {
|
||||||
* self.ui.spacing().scroll.allocated_width()
|
* self.ui.spacing().scroll.allocated_width()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset all column widths.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
let state_id = self.ui.id().with("__table_state");
|
||||||
|
TableState::reset(self.ui, state_id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a header row which always stays visible and at the top
|
/// Create a header row which always stays visible and at the top
|
||||||
pub fn header(self, height: f32, add_header_row: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
|
pub fn header(self, height: f32, add_header_row: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
|
||||||
let available_width = self.available_width();
|
let available_width = self.available_width();
|
||||||
|
|
@ -423,14 +428,14 @@ impl<'a> TableBuilder<'a> {
|
||||||
|
|
||||||
let state_id = ui.id().with("__table_state");
|
let state_id = ui.id().with("__table_state");
|
||||||
|
|
||||||
let (is_sizing_pass, state) = TableState::load(ui, state_id, &columns, available_width);
|
let (is_sizing_pass, state) =
|
||||||
|
TableState::load(ui, state_id, resizable, &columns, available_width);
|
||||||
|
|
||||||
let mut max_used_widths = vec![0.0; columns.len()];
|
let mut max_used_widths = vec![0.0; columns.len()];
|
||||||
let table_top = ui.cursor().top();
|
let table_top = ui.cursor().top();
|
||||||
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
if is_sizing_pass {
|
if is_sizing_pass {
|
||||||
// Hide first-frame-jitters when auto-sizing.
|
|
||||||
ui.set_sizing_pass();
|
ui.set_sizing_pass();
|
||||||
}
|
}
|
||||||
let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense);
|
let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense);
|
||||||
|
|
@ -489,7 +494,8 @@ impl<'a> TableBuilder<'a> {
|
||||||
|
|
||||||
let state_id = ui.id().with("__table_state");
|
let state_id = ui.id().with("__table_state");
|
||||||
|
|
||||||
let (is_sizing_pass, state) = TableState::load(ui, state_id, &columns, available_width);
|
let (is_sizing_pass, state) =
|
||||||
|
TableState::load(ui, state_id, resizable, &columns, available_width);
|
||||||
|
|
||||||
let max_used_widths = vec![0.0; columns.len()];
|
let max_used_widths = vec![0.0; columns.len()];
|
||||||
let table_top = ui.cursor().top();
|
let table_top = ui.cursor().top();
|
||||||
|
|
@ -519,11 +525,21 @@ impl<'a> TableBuilder<'a> {
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
struct TableState {
|
struct TableState {
|
||||||
column_widths: Vec<f32>,
|
column_widths: Vec<f32>,
|
||||||
|
|
||||||
|
/// If known from previous frame
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
max_used_widths: Vec<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableState {
|
impl TableState {
|
||||||
/// Return true if we should do a sizing pass.
|
/// Return true if we should do a sizing pass.
|
||||||
fn load(ui: &Ui, state_id: egui::Id, columns: &[Column], available_width: f32) -> (bool, Self) {
|
fn load(
|
||||||
|
ui: &Ui,
|
||||||
|
state_id: egui::Id,
|
||||||
|
resizable: bool,
|
||||||
|
columns: &[Column],
|
||||||
|
available_width: f32,
|
||||||
|
) -> (bool, Self) {
|
||||||
let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO);
|
let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO);
|
||||||
ui.ctx().check_for_id_clash(state_id, rect, "Table");
|
ui.ctx().check_for_id_clash(state_id, rect, "Table");
|
||||||
|
|
||||||
|
|
@ -537,20 +553,56 @@ impl TableState {
|
||||||
let is_sizing_pass =
|
let is_sizing_pass =
|
||||||
ui.is_sizing_pass() || state.is_none() && columns.iter().any(|c| c.is_auto());
|
ui.is_sizing_pass() || state.is_none() && columns.iter().any(|c| c.is_auto());
|
||||||
|
|
||||||
let state = state.unwrap_or_else(|| {
|
let mut state = state.unwrap_or_else(|| {
|
||||||
let initial_widths =
|
let initial_widths =
|
||||||
to_sizing(columns).to_lengths(available_width, ui.spacing().item_spacing.x);
|
to_sizing(columns).to_lengths(available_width, ui.spacing().item_spacing.x);
|
||||||
Self {
|
Self {
|
||||||
column_widths: initial_widths,
|
column_widths: initial_widths,
|
||||||
|
max_used_widths: Default::default(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if !is_sizing_pass && state.max_used_widths.len() == columns.len() {
|
||||||
|
// Make sure any non-resizable `remainder` columns are updated
|
||||||
|
// to take up the remainder of the current available width.
|
||||||
|
// Also handles changing item spacing.
|
||||||
|
let mut sizing = crate::sizing::Sizing::default();
|
||||||
|
for ((prev_width, max_used), column) in state
|
||||||
|
.column_widths
|
||||||
|
.iter()
|
||||||
|
.zip(&state.max_used_widths)
|
||||||
|
.zip(columns)
|
||||||
|
{
|
||||||
|
use crate::Size;
|
||||||
|
|
||||||
|
let column_resizable = column.resizable.unwrap_or(resizable);
|
||||||
|
let size = if column_resizable {
|
||||||
|
// Resiable columns keep their width:
|
||||||
|
Size::exact(*prev_width)
|
||||||
|
} else {
|
||||||
|
match column.initial_width {
|
||||||
|
InitialColumnSize::Absolute(width) => Size::exact(width),
|
||||||
|
InitialColumnSize::Automatic(_) => Size::exact(*prev_width),
|
||||||
|
InitialColumnSize::Remainder => Size::remainder(),
|
||||||
|
}
|
||||||
|
.at_least(column.width_range.min.max(*max_used))
|
||||||
|
.at_most(column.width_range.max)
|
||||||
|
};
|
||||||
|
sizing.add(size);
|
||||||
|
}
|
||||||
|
state.column_widths = sizing.to_lengths(available_width, ui.spacing().item_spacing.x);
|
||||||
|
}
|
||||||
|
|
||||||
(is_sizing_pass, state)
|
(is_sizing_pass, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
|
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
|
||||||
ui.data_mut(|d| d.insert_persisted(state_id, self));
|
ui.data_mut(|d| d.insert_persisted(state_id, self));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset(ui: &egui::Ui, state_id: egui::Id) {
|
||||||
|
ui.data_mut(|d| d.remove::<Self>(state_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
@ -645,7 +697,6 @@ impl<'a> Table<'a> {
|
||||||
|
|
||||||
let clip_rect = ui.clip_rect();
|
let clip_rect = ui.clip_rect();
|
||||||
|
|
||||||
// Hide first-frame-jitters when auto-sizing.
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
if is_sizing_pass {
|
if is_sizing_pass {
|
||||||
ui.set_sizing_pass();
|
ui.set_sizing_pass();
|
||||||
|
|
@ -695,16 +746,11 @@ impl<'a> Table<'a> {
|
||||||
let column_is_resizable = column.resizable.unwrap_or(resizable);
|
let column_is_resizable = column.resizable.unwrap_or(resizable);
|
||||||
let width_range = column.width_range;
|
let width_range = column.width_range;
|
||||||
|
|
||||||
if !column.clip {
|
|
||||||
// Unless we clip we don't want to shrink below the
|
|
||||||
// size that was actually used:
|
|
||||||
*column_width = column_width.at_least(max_used_widths[i]);
|
|
||||||
}
|
|
||||||
*column_width = width_range.clamp(*column_width);
|
|
||||||
|
|
||||||
let is_last_column = i + 1 == columns.len();
|
let is_last_column = i + 1 == columns.len();
|
||||||
|
if is_last_column
|
||||||
if is_last_column && column.initial_width == InitialColumnSize::Remainder {
|
&& column.initial_width == InitialColumnSize::Remainder
|
||||||
|
&& !ui.is_sizing_pass()
|
||||||
|
{
|
||||||
// If the last column is 'remainder', then let it fill the remainder!
|
// If the last column is 'remainder', then let it fill the remainder!
|
||||||
let eps = 0.1; // just to avoid some rounding errors.
|
let eps = 0.1; // just to avoid some rounding errors.
|
||||||
*column_width = available_width - eps;
|
*column_width = available_width - eps;
|
||||||
|
|
@ -715,6 +761,20 @@ impl<'a> Table<'a> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ui.is_sizing_pass() {
|
||||||
|
if column.clip {
|
||||||
|
// If we clip, we don't need to be as wide as the max used width
|
||||||
|
*column_width = column_width.min(max_used_widths[i]);
|
||||||
|
} else {
|
||||||
|
*column_width = max_used_widths[i];
|
||||||
|
}
|
||||||
|
} else if !column.clip {
|
||||||
|
// Unless we clip we don't want to shrink below the
|
||||||
|
// size that was actually used:
|
||||||
|
*column_width = column_width.at_least(max_used_widths[i]);
|
||||||
|
}
|
||||||
|
*column_width = width_range.clamp(*column_width);
|
||||||
|
|
||||||
x += *column_width + spacing_x;
|
x += *column_width + spacing_x;
|
||||||
|
|
||||||
if column.is_auto() && (is_sizing_pass || !column_is_resizable) {
|
if column.is_auto() && (is_sizing_pass || !column_is_resizable) {
|
||||||
|
|
@ -775,11 +835,13 @@ impl<'a> Table<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.painter().line_segment([p0, p1], stroke);
|
ui.painter().line_segment([p0, p1], stroke);
|
||||||
};
|
}
|
||||||
|
|
||||||
available_width -= *column_width + spacing_x;
|
available_width -= *column_width + spacing_x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.max_used_widths = max_used_widths;
|
||||||
|
|
||||||
state.store(ui, state_id);
|
state.store(ui, state_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue