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
|
||||
}
|
||||
|
||||
/// 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?
|
||||
///
|
||||
/// Overrides [`crate::style::Interaction::selectable_labels`].
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ const NUM_MANUAL_ROWS: usize = 20;
|
|||
|
||||
impl crate::View for TableDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let mut reset = false;
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
reset = ui.button("Reset").clicked();
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
|
@ -115,7 +119,7 @@ impl crate::View for TableDemo {
|
|||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
egui::ScrollArea::horizontal().show(ui, |ui| {
|
||||
self.table_ui(ui);
|
||||
self.table_ui(ui, reset);
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
|
|
@ -128,7 +132,7 @@ impl crate::View for 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};
|
||||
|
||||
let text_height = egui::TextStyle::Body
|
||||
|
|
@ -142,9 +146,14 @@ impl TableDemo {
|
|||
.resizable(self.resizable)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Column::auto())
|
||||
.column(
|
||||
Column::remainder()
|
||||
.at_least(40.0)
|
||||
.clip(true)
|
||||
.resizable(true),
|
||||
)
|
||||
.column(Column::auto())
|
||||
.column(Column::initial(100.0).range(40.0..=300.0))
|
||||
.column(Column::initial(100.0).at_least(40.0).clip(true))
|
||||
.column(Column::remainder())
|
||||
.column(Column::remainder())
|
||||
.min_scrolled_height(0.0)
|
||||
.max_scroll_height(available_height);
|
||||
|
|
@ -157,19 +166,23 @@ impl TableDemo {
|
|||
table = table.scroll_to_row(row_index, None);
|
||||
}
|
||||
|
||||
if reset {
|
||||
table.reset();
|
||||
}
|
||||
|
||||
table
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.strong("Row");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Interaction");
|
||||
ui.strong("Clipped text");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Expanding content");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Clipped text");
|
||||
ui.strong("Interaction");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Content");
|
||||
|
|
@ -187,13 +200,13 @@ impl TableDemo {
|
|||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
ui.label(long_text(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(row_index));
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
|
@ -217,13 +230,13 @@ impl TableDemo {
|
|||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
ui.label(long_text(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(row_index));
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.add(
|
||||
|
|
@ -245,13 +258,13 @@ impl TableDemo {
|
|||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
ui.label(long_text(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(row_index));
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
|
|
@ -280,14 +293,7 @@ impl TableDemo {
|
|||
}
|
||||
|
||||
fn expanding_content(ui: &mut egui::Ui) {
|
||||
let width = ui.available_width().clamp(20.0, 200.0);
|
||||
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()),
|
||||
);
|
||||
ui.add(egui::Separator::default().horizontal());
|
||||
}
|
||||
|
||||
fn long_text(row_index: usize) -> String {
|
||||
|
|
|
|||
|
|
@ -150,14 +150,16 @@ impl<'l> StripLayout<'l> {
|
|||
|
||||
let used_rect = child_ui.min_rect();
|
||||
|
||||
self.set_pos(max_rect);
|
||||
|
||||
let allocation_rect = if flags.clip {
|
||||
let allocation_rect = if self.ui.is_sizing_pass() {
|
||||
used_rect
|
||||
} else if flags.clip {
|
||||
max_rect
|
||||
} else {
|
||||
max_rect.union(used_rect)
|
||||
};
|
||||
|
||||
self.set_pos(allocation_rect);
|
||||
|
||||
self.ui.advance_cursor_after_rect(allocation_rect);
|
||||
|
||||
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::Remainder => Size::remainder(),
|
||||
}
|
||||
.at_least(column.width_range.min)
|
||||
.at_most(column.width_range.max);
|
||||
.with_range(column.width_range);
|
||||
sizing.add(size);
|
||||
}
|
||||
sizing
|
||||
|
|
@ -405,6 +404,12 @@ impl<'a> TableBuilder<'a> {
|
|||
* 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
|
||||
pub fn header(self, height: f32, add_header_row: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
|
||||
let available_width = self.available_width();
|
||||
|
|
@ -423,14 +428,14 @@ impl<'a> TableBuilder<'a> {
|
|||
|
||||
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 table_top = ui.cursor().top();
|
||||
|
||||
ui.scope(|ui| {
|
||||
if is_sizing_pass {
|
||||
// Hide first-frame-jitters when auto-sizing.
|
||||
ui.set_sizing_pass();
|
||||
}
|
||||
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 (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 table_top = ui.cursor().top();
|
||||
|
|
@ -519,11 +525,21 @@ impl<'a> TableBuilder<'a> {
|
|||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct TableState {
|
||||
column_widths: Vec<f32>,
|
||||
|
||||
/// If known from previous frame
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
max_used_widths: Vec<f32>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
/// 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);
|
||||
ui.ctx().check_for_id_clash(state_id, rect, "Table");
|
||||
|
||||
|
|
@ -537,20 +553,56 @@ impl TableState {
|
|||
let is_sizing_pass =
|
||||
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 =
|
||||
to_sizing(columns).to_lengths(available_width, ui.spacing().item_spacing.x);
|
||||
Self {
|
||||
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)
|
||||
}
|
||||
|
||||
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
|
||||
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();
|
||||
|
||||
// Hide first-frame-jitters when auto-sizing.
|
||||
ui.scope(|ui| {
|
||||
if is_sizing_pass {
|
||||
ui.set_sizing_pass();
|
||||
|
|
@ -695,16 +746,11 @@ impl<'a> Table<'a> {
|
|||
let column_is_resizable = column.resizable.unwrap_or(resizable);
|
||||
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();
|
||||
|
||||
if is_last_column && column.initial_width == InitialColumnSize::Remainder {
|
||||
if is_last_column
|
||||
&& column.initial_width == InitialColumnSize::Remainder
|
||||
&& !ui.is_sizing_pass()
|
||||
{
|
||||
// If the last column is 'remainder', then let it fill the remainder!
|
||||
let eps = 0.1; // just to avoid some rounding errors.
|
||||
*column_width = available_width - eps;
|
||||
|
|
@ -715,6 +761,20 @@ impl<'a> Table<'a> {
|
|||
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;
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
available_width -= *column_width + spacing_x;
|
||||
}
|
||||
|
||||
state.max_used_widths = max_used_widths;
|
||||
|
||||
state.store(ui, state_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue