Browse Source

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.
pull/4757/head
Emil Ernerfeldt 4 months ago
committed by GitHub
parent
commit
8ef0e85b85
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      crates/egui/src/widgets/label.rs
  2. 46
      crates/egui_demo_lib/src/demo/table_demo.rs
  3. 8
      crates/egui_extras/src/layout.rs
  4. 98
      crates/egui_extras/src/table.rs

8
crates/egui/src/widgets/label.rs

@ -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`].

46
crates/egui_demo_lib/src/demo/table_demo.rs

@ -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 {

8
crates/egui_extras/src/layout.rs

@ -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);

98
crates/egui_extras/src/table.rs

@ -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…
Cancel
Save