Improve `egui_extras::Table` layout (#4755)
Mostly a refactor, but some minor fixes to how it works. Mostly preparing for a few bigger changes.
This commit is contained in:
parent
f0e2bd8b00
commit
753412193c
|
|
@ -107,12 +107,12 @@ impl Default for Tests {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::from_demos(vec![
|
Self::from_demos(vec![
|
||||||
Box::<super::tests::CursorTest>::default(),
|
Box::<super::tests::CursorTest>::default(),
|
||||||
|
Box::<super::tests::GridTest>::default(),
|
||||||
Box::<super::tests::IdTest>::default(),
|
Box::<super::tests::IdTest>::default(),
|
||||||
Box::<super::tests::InputEventHistory>::default(),
|
Box::<super::tests::InputEventHistory>::default(),
|
||||||
Box::<super::tests::InputTest>::default(),
|
Box::<super::tests::InputTest>::default(),
|
||||||
Box::<super::tests::LayoutTest>::default(),
|
Box::<super::tests::LayoutTest>::default(),
|
||||||
Box::<super::tests::ManualLayoutTest>::default(),
|
Box::<super::tests::ManualLayoutTest>::default(),
|
||||||
Box::<super::tests::TableTest>::default(),
|
|
||||||
Box::<super::tests::WindowResizeTest>::default(),
|
Box::<super::tests::WindowResizeTest>::default(),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct TableTest {
|
pub struct GridTest {
|
||||||
num_cols: usize,
|
num_cols: usize,
|
||||||
num_rows: usize,
|
num_rows: usize,
|
||||||
min_col_width: f32,
|
min_col_width: f32,
|
||||||
|
|
@ -7,7 +7,7 @@ pub struct TableTest {
|
||||||
text_length: usize,
|
text_length: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TableTest {
|
impl Default for GridTest {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
num_cols: 4,
|
num_cols: 4,
|
||||||
|
|
@ -19,9 +19,9 @@ impl Default for TableTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::Demo for TableTest {
|
impl crate::Demo for GridTest {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"Table Test"
|
"Grid Test"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||||
|
|
@ -32,7 +32,7 @@ impl crate::Demo for TableTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::View for TableTest {
|
impl crate::View for GridTest {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.add(
|
ui.add(
|
||||||
egui::Slider::new(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"),
|
egui::Slider::new(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"),
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
mod cursor_test;
|
mod cursor_test;
|
||||||
|
mod grid_test;
|
||||||
mod id_test;
|
mod id_test;
|
||||||
mod input_event_history;
|
mod input_event_history;
|
||||||
mod input_test;
|
mod input_test;
|
||||||
mod layout_test;
|
mod layout_test;
|
||||||
mod manual_layout_test;
|
mod manual_layout_test;
|
||||||
mod table_test;
|
|
||||||
mod window_resize_test;
|
mod window_resize_test;
|
||||||
|
|
||||||
pub use cursor_test::CursorTest;
|
pub use cursor_test::CursorTest;
|
||||||
|
pub use grid_test::GridTest;
|
||||||
pub use id_test::IdTest;
|
pub use id_test::IdTest;
|
||||||
pub use input_event_history::InputEventHistory;
|
pub use input_event_history::InputEventHistory;
|
||||||
pub use input_test::InputTest;
|
pub use input_test::InputTest;
|
||||||
pub use layout_test::LayoutTest;
|
pub use layout_test::LayoutTest;
|
||||||
pub use manual_layout_test::ManualLayoutTest;
|
pub use manual_layout_test::ManualLayoutTest;
|
||||||
pub use table_test::TableTest;
|
|
||||||
pub use window_resize_test::WindowResizeTest;
|
pub use window_resize_test::WindowResizeTest;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ impl<'a> DatePickerPopup<'a> {
|
||||||
let height = 20.0;
|
let height = 20.0;
|
||||||
let spacing = 2.0;
|
let spacing = 2.0;
|
||||||
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
|
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
|
||||||
|
|
||||||
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Don't wrap any text
|
||||||
|
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.clip(false)
|
.clip(false)
|
||||||
.sizes(
|
.sizes(
|
||||||
|
|
|
||||||
|
|
@ -191,12 +191,12 @@ impl<'l> StripLayout<'l> {
|
||||||
fn cell(
|
fn cell(
|
||||||
&mut self,
|
&mut self,
|
||||||
flags: StripLayoutFlags,
|
flags: StripLayoutFlags,
|
||||||
rect: Rect,
|
max_rect: Rect,
|
||||||
child_ui_id_source: egui::Id,
|
child_ui_id_source: egui::Id,
|
||||||
add_cell_contents: impl FnOnce(&mut Ui),
|
add_cell_contents: impl FnOnce(&mut Ui),
|
||||||
) -> Ui {
|
) -> Ui {
|
||||||
let mut child_ui = self.ui.child_ui_with_id_source(
|
let mut child_ui = self.ui.child_ui_with_id_source(
|
||||||
rect,
|
max_rect,
|
||||||
self.cell_layout,
|
self.cell_layout,
|
||||||
child_ui_id_source,
|
child_ui_id_source,
|
||||||
Some(egui::UiStackInfo::new(egui::UiKind::TableCell)),
|
Some(egui::UiStackInfo::new(egui::UiKind::TableCell)),
|
||||||
|
|
@ -205,7 +205,7 @@ impl<'l> StripLayout<'l> {
|
||||||
if flags.clip {
|
if flags.clip {
|
||||||
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
|
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
|
||||||
let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
|
let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
|
||||||
let clip_rect = rect.expand2(margin);
|
let clip_rect = max_rect.expand2(margin);
|
||||||
child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect()));
|
child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,26 +49,20 @@ impl Size {
|
||||||
/// Won't shrink below this size (in points).
|
/// Won't shrink below this size (in points).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn at_least(mut self, minimum: f32) -> Self {
|
pub fn at_least(mut self, minimum: f32) -> Self {
|
||||||
match &mut self {
|
self.range_mut().min = minimum;
|
||||||
Self::Absolute { range, .. }
|
|
||||||
| Self::Relative { range, .. }
|
|
||||||
| Self::Remainder { range, .. } => {
|
|
||||||
range.min = minimum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Won't grow above this size (in points).
|
/// Won't grow above this size (in points).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn at_most(mut self, maximum: f32) -> Self {
|
pub fn at_most(mut self, maximum: f32) -> Self {
|
||||||
match &mut self {
|
self.range_mut().max = maximum;
|
||||||
Self::Absolute { range, .. }
|
self
|
||||||
| Self::Relative { range, .. }
|
|
||||||
| Self::Remainder { range, .. } => {
|
|
||||||
range.max = maximum;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_range(mut self, range: Rangef) -> Self {
|
||||||
|
*self.range_mut() = range;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +74,29 @@ impl Size {
|
||||||
| Self::Remainder { range, .. } => range,
|
| Self::Remainder { range, .. } => range,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn range_mut(&mut self) -> &mut Rangef {
|
||||||
|
match self {
|
||||||
|
Self::Absolute { range, .. }
|
||||||
|
| Self::Relative { range, .. }
|
||||||
|
| Self::Remainder { range, .. } => range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_absolute(&self) -> bool {
|
||||||
|
matches!(self, Self::Absolute { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_relative(&self) -> bool {
|
||||||
|
matches!(self, Self::Relative { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_remainder(&self) -> bool {
|
||||||
|
matches!(self, Self::Remainder { .. })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|
@ -97,7 +114,7 @@ impl Sizing {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remainders = 0;
|
let mut num_remainders = 0;
|
||||||
let sum_non_remainder = self
|
let sum_non_remainder = self
|
||||||
.sizes
|
.sizes
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -108,28 +125,28 @@ impl Sizing {
|
||||||
range.clamp(length * fraction)
|
range.clamp(length * fraction)
|
||||||
}
|
}
|
||||||
Size::Remainder { .. } => {
|
Size::Remainder { .. } => {
|
||||||
remainders += 1;
|
num_remainders += 1;
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sum::<f32>()
|
.sum::<f32>()
|
||||||
+ spacing * (self.sizes.len() - 1) as f32;
|
+ spacing * (self.sizes.len() - 1) as f32;
|
||||||
|
|
||||||
let avg_remainder_length = if remainders == 0 {
|
let avg_remainder_length = if num_remainders == 0 {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
let mut remainder_length = length - sum_non_remainder;
|
let mut remainder_length = length - sum_non_remainder;
|
||||||
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
|
let avg_remainder_length = 0.0f32.max(remainder_length / num_remainders as f32).floor();
|
||||||
self.sizes.iter().for_each(|&size| {
|
for &size in &self.sizes {
|
||||||
if let Size::Remainder { range } = size {
|
if let Size::Remainder { range } = size {
|
||||||
if avg_remainder_length < range.min {
|
if avg_remainder_length < range.min {
|
||||||
remainder_length -= range.min;
|
remainder_length -= range.min;
|
||||||
remainders -= 1;
|
num_remainders -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if remainders > 0 {
|
if num_remainders > 0 {
|
||||||
0.0f32.max(remainder_length / remainders as f32)
|
0.0f32.max(remainder_length / num_remainders as f32)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -401,13 +401,8 @@ impl<'a> TableBuilder<'a> {
|
||||||
|
|
||||||
fn available_width(&self) -> f32 {
|
fn available_width(&self) -> f32 {
|
||||||
self.ui.available_rect_before_wrap().width()
|
self.ui.available_rect_before_wrap().width()
|
||||||
- if self.scroll_options.vscroll {
|
- (self.scroll_options.vscroll as i32 as f32)
|
||||||
self.ui.spacing().scroll.bar_inner_margin
|
* self.ui.spacing().scroll.allocated_width()
|
||||||
+ self.ui.spacing().scroll.bar_width
|
|
||||||
+ self.ui.spacing().scroll.bar_outer_margin
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a header row which always stays visible and at the top
|
/// Create a header row which always stays visible and at the top
|
||||||
|
|
@ -428,17 +423,13 @@ impl<'a> TableBuilder<'a> {
|
||||||
|
|
||||||
let state_id = ui.id().with("__table_state");
|
let state_id = ui.id().with("__table_state");
|
||||||
|
|
||||||
let initial_widths =
|
let (is_sizing_pass, state) = TableState::load(ui, state_id, &columns, available_width);
|
||||||
to_sizing(&columns).to_lengths(available_width, ui.spacing().item_spacing.x);
|
|
||||||
let mut max_used_widths = vec![0.0; initial_widths.len()];
|
|
||||||
let (had_state, state) = TableState::load(ui, initial_widths, state_id);
|
|
||||||
let is_first_frame = !had_state;
|
|
||||||
let first_frame_auto_size_columns = is_first_frame && columns.iter().any(|c| c.is_auto());
|
|
||||||
|
|
||||||
|
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 first_frame_auto_size_columns {
|
if is_sizing_pass {
|
||||||
// Hide first-frame-jitters when auto-sizing.
|
// Hide first-frame-jitters when auto-sizing.
|
||||||
ui.set_sizing_pass();
|
ui.set_sizing_pass();
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +459,7 @@ impl<'a> TableBuilder<'a> {
|
||||||
available_width,
|
available_width,
|
||||||
state,
|
state,
|
||||||
max_used_widths,
|
max_used_widths,
|
||||||
first_frame_auto_size_columns,
|
is_sizing_pass,
|
||||||
resizable,
|
resizable,
|
||||||
striped,
|
striped,
|
||||||
cell_layout,
|
cell_layout,
|
||||||
|
|
@ -498,13 +489,9 @@ impl<'a> TableBuilder<'a> {
|
||||||
|
|
||||||
let state_id = ui.id().with("__table_state");
|
let state_id = ui.id().with("__table_state");
|
||||||
|
|
||||||
let initial_widths =
|
let (is_sizing_pass, state) = TableState::load(ui, state_id, &columns, available_width);
|
||||||
to_sizing(&columns).to_lengths(available_width, ui.spacing().item_spacing.x);
|
|
||||||
let max_used_widths = vec![0.0; initial_widths.len()];
|
|
||||||
let (had_state, state) = TableState::load(ui, initial_widths, state_id);
|
|
||||||
let is_first_frame = !had_state;
|
|
||||||
let first_frame_auto_size_columns = is_first_frame && columns.iter().any(|c| c.is_auto());
|
|
||||||
|
|
||||||
|
let max_used_widths = vec![0.0; columns.len()];
|
||||||
let table_top = ui.cursor().top();
|
let table_top = ui.cursor().top();
|
||||||
|
|
||||||
Table {
|
Table {
|
||||||
|
|
@ -515,7 +502,7 @@ impl<'a> TableBuilder<'a> {
|
||||||
available_width,
|
available_width,
|
||||||
state,
|
state,
|
||||||
max_used_widths,
|
max_used_widths,
|
||||||
first_frame_auto_size_columns,
|
is_sizing_pass,
|
||||||
resizable,
|
resizable,
|
||||||
striped,
|
striped,
|
||||||
cell_layout,
|
cell_layout,
|
||||||
|
|
@ -535,24 +522,30 @@ struct TableState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableState {
|
impl TableState {
|
||||||
/// Returns `true` if it did load.
|
/// Return true if we should do a sizing pass.
|
||||||
fn load(ui: &egui::Ui, default_widths: Vec<f32>, state_id: egui::Id) -> (bool, Self) {
|
fn load(ui: &Ui, state_id: egui::Id, 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");
|
||||||
|
|
||||||
if let Some(state) = ui.data_mut(|d| d.get_persisted::<Self>(state_id)) {
|
let state = ui
|
||||||
|
.data_mut(|d| d.get_persisted::<Self>(state_id))
|
||||||
|
.filter(|state| {
|
||||||
// make sure that the stored widths aren't out-dated
|
// make sure that the stored widths aren't out-dated
|
||||||
if state.column_widths.len() == default_widths.len() {
|
state.column_widths.len() == columns.len()
|
||||||
return (true, state);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
let is_sizing_pass =
|
||||||
false,
|
ui.is_sizing_pass() || state.is_none() && columns.iter().any(|c| c.is_auto());
|
||||||
|
|
||||||
|
let state = state.unwrap_or_else(|| {
|
||||||
|
let initial_widths =
|
||||||
|
to_sizing(columns).to_lengths(available_width, ui.spacing().item_spacing.x);
|
||||||
Self {
|
Self {
|
||||||
column_widths: default_widths,
|
column_widths: initial_widths,
|
||||||
},
|
}
|
||||||
)
|
});
|
||||||
|
|
||||||
|
(is_sizing_pass, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
|
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
|
||||||
|
|
@ -576,7 +569,8 @@ pub struct Table<'a> {
|
||||||
/// Accumulated maximum used widths for each column.
|
/// Accumulated maximum used widths for each column.
|
||||||
max_used_widths: Vec<f32>,
|
max_used_widths: Vec<f32>,
|
||||||
|
|
||||||
first_frame_auto_size_columns: bool,
|
/// During the sizing pass we calculate the width of columns with [`Column::auto`].
|
||||||
|
is_sizing_pass: bool,
|
||||||
resizable: bool,
|
resizable: bool,
|
||||||
striped: bool,
|
striped: bool,
|
||||||
cell_layout: egui::Layout,
|
cell_layout: egui::Layout,
|
||||||
|
|
@ -608,7 +602,7 @@ impl<'a> Table<'a> {
|
||||||
mut available_width,
|
mut available_width,
|
||||||
mut state,
|
mut state,
|
||||||
mut max_used_widths,
|
mut max_used_widths,
|
||||||
first_frame_auto_size_columns,
|
is_sizing_pass,
|
||||||
striped,
|
striped,
|
||||||
cell_layout,
|
cell_layout,
|
||||||
scroll_options,
|
scroll_options,
|
||||||
|
|
@ -653,7 +647,7 @@ impl<'a> Table<'a> {
|
||||||
|
|
||||||
// Hide first-frame-jitters when auto-sizing.
|
// Hide first-frame-jitters when auto-sizing.
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
if first_frame_auto_size_columns {
|
if is_sizing_pass {
|
||||||
ui.set_sizing_pass();
|
ui.set_sizing_pass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,9 +717,8 @@ impl<'a> Table<'a> {
|
||||||
|
|
||||||
x += *column_width + spacing_x;
|
x += *column_width + spacing_x;
|
||||||
|
|
||||||
if column.is_auto() && (first_frame_auto_size_columns || !column_is_resizable) {
|
if column.is_auto() && (is_sizing_pass || !column_is_resizable) {
|
||||||
*column_width = max_used_widths[i];
|
*column_width = width_range.clamp(max_used_widths[i]);
|
||||||
*column_width = width_range.clamp(*column_width);
|
|
||||||
} else if column_is_resizable {
|
} else if column_is_resizable {
|
||||||
let column_resize_id = ui.id().with("resize_column").with(i);
|
let column_resize_id = ui.id().with("resize_column").with(i);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue