From 3777b8d2741f298eaa1409dc08062902f7541990 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 29 Aug 2024 10:38:19 +0200 Subject: [PATCH] Truncate text in clipped `Table` columns (#5023) * Closes https://github.com/emilk/egui/issues/5013 * Columns with `clip = true` will have `TextWrapMode::Truncate` set * Added setting `Column::auto_size_this_frame` (acts like a double-click on column resizer) * Set `sizing_pass` on all cells in a column that is being auto-sized (e.g. on double-click) ![image](https://github.com/user-attachments/assets/360f6b59-c9a9-468b-8919-4b7e4fc6661a) --- crates/egui/src/ui_builder.rs | 2 +- crates/egui_extras/src/layout.rs | 25 ++++++++---- crates/egui_extras/src/table.rs | 70 ++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 7f2e8b92..166210b9 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -11,7 +11,7 @@ use crate::Ui; /// except for `max_rect` which by default is set to /// the parent [`Ui::available_rect_before_wrap`]. #[must_use] -#[derive(Default)] +#[derive(Clone, Default)] pub struct UiBuilder { pub id_source: Option, pub ui_stack_info: UiStackInfo, diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 6eb951c8..6d2b10c3 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -33,6 +33,9 @@ pub(crate) struct StripLayoutFlags { pub(crate) striped: bool, pub(crate) hovered: bool, pub(crate) selected: bool, + + /// Used when we want to accruately measure the size of this cell. + pub(crate) sizing_pass: bool, } /// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`] @@ -197,19 +200,27 @@ impl<'l> StripLayout<'l> { child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> Ui { - let mut child_ui = self.ui.new_child( - UiBuilder::new() - .id_source(child_ui_id_source) - .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell)) - .max_rect(max_rect) - .layout(self.cell_layout), - ); + let mut ui_builder = UiBuilder::new() + .id_source(child_ui_id_source) + .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell)) + .max_rect(max_rect) + .layout(self.cell_layout); + if flags.sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + + let mut child_ui = self.ui.new_child(ui_builder); if flags.clip { let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); let margin = margin.min(0.5 * self.ui.spacing().item_spacing); let clip_rect = max_rect.expand2(margin); child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect())); + + if !child_ui.is_sizing_pass() { + // Better to truncate (if we can), rather than hard clipping: + child_ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + } } if flags.selected { diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 999dcfe7..f2b2dfe4 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -38,6 +38,10 @@ pub struct Column { clip: bool, resizable: Option, + + /// If set, we should acurately measure the size of this column this frame + /// so that we can correctly auto-size it. This is done as a `sizing_pass`. + auto_size_this_frame: bool, } impl Column { @@ -86,6 +90,7 @@ impl Column { width_range: Rangef::new(0.0, f32::INFINITY), resizable: None, clip: false, + auto_size_this_frame: false, } } @@ -138,6 +143,15 @@ impl Column { self } + /// If set, the column will be automatically sized based on the content this frame. + /// + /// Do not set this every frame, just on a specific action. + #[inline] + pub fn auto_size_this_frame(mut self, auto_size_this_frame: bool) -> Self { + self.auto_size_this_frame = auto_size_this_frame; + self + } + fn is_auto(&self) -> bool { match self.initial_width { InitialColumnSize::Automatic(_) => true, @@ -446,10 +460,11 @@ impl<'a> TableBuilder<'a> { let mut max_used_widths = vec![0.0; columns.len()]; let table_top = ui.cursor().top(); - ui.scope(|ui| { - if is_sizing_pass { - ui.set_sizing_pass(); - } + let mut ui_builder = egui::UiBuilder::new(); + if is_sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + ui.scope_builder(ui_builder, |ui| { let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense); let mut response: Option = None; add_header_row(TableRow { @@ -671,7 +686,7 @@ impl<'a> Table<'a> { ui, table_top, state_id, - columns, + mut columns, resizable, mut available_width, mut state, @@ -695,6 +710,15 @@ impl<'a> Table<'a> { scroll_bar_visibility, } = scroll_options; + for (i, column) in columns.iter_mut().enumerate() { + let column_resize_id = ui.id().with("resize_column").with(i); + if let Some(response) = ui.ctx().read_response(column_resize_id) { + if response.double_clicked() { + column.auto_size_this_frame = true; + } + } + } + let cursor_position = ui.cursor().min; let mut scroll_area = ScrollArea::new([false, vscroll]) @@ -718,11 +742,11 @@ impl<'a> Table<'a> { let clip_rect = ui.clip_rect(); - ui.scope(|ui| { - if is_sizing_pass { - ui.set_sizing_pass(); - } - + let mut ui_builder = egui::UiBuilder::new(); + if is_sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + ui.scope_builder(ui_builder, |ui| { let hovered_row_index_id = self.state_id.with("__table_hovered_row"); let hovered_row_index = ui.data_mut(|data| data.remove_temp::(hovered_row_index_id)); @@ -736,8 +760,7 @@ impl<'a> Table<'a> { max_used_widths: max_used_widths_ref, striped, row_index: 0, - start_y: clip_rect.top(), - end_y: clip_rect.bottom(), + y_range: clip_rect.y_range(), scroll_to_row: scroll_to_row.map(|(r, _)| r), scroll_to_y_range: &mut scroll_to_y_range, hovered_row_index, @@ -745,7 +768,7 @@ impl<'a> Table<'a> { }); if scroll_to_row.is_some() && scroll_to_y_range.is_none() { - // TableBody::row didn't find the right row, so scroll to the bottom: + // TableBody::row didn't find the correct row, so scroll to the bottom: scroll_to_y_range = Some(Rangef::new(f32::INFINITY, f32::INFINITY)); } }); @@ -811,9 +834,8 @@ impl<'a> Table<'a> { let resize_response = ui.interact(line_rect, column_resize_id, egui::Sense::click_and_drag()); - if resize_response.double_clicked() { - // Resize to the minimum of what is needed. - + if column.auto_size_this_frame { + // Auto-size: resize to what is needed. *column_width = width_range.clamp(max_used_widths[i]); } else if resize_response.dragged() { if let Some(pointer) = ui.ctx().pointer_latest_pos() { @@ -884,8 +906,7 @@ pub struct TableBody<'a> { striped: bool, row_index: usize, - start_y: f32, - end_y: f32, + y_range: Rangef, /// Look for this row to scroll to. scroll_to_row: Option, @@ -916,7 +937,7 @@ impl<'a> TableBody<'a> { } fn scroll_offset_y(&self) -> f32 { - self.start_y - self.layout.rect.top() + self.y_range.min - self.layout.rect.top() } /// Return a vector containing all column widths for this table body. @@ -1002,7 +1023,7 @@ impl<'a> TableBody<'a> { let scroll_offset_y = self .scroll_offset_y() .min(total_rows as f32 * row_height_with_spacing); - let max_height = self.end_y - self.start_y; + let max_height = self.y_range.span(); let mut min_row = 0; if scroll_offset_y > 0.0 { @@ -1074,7 +1095,7 @@ impl<'a> TableBody<'a> { let spacing = self.layout.ui.spacing().item_spacing; let mut enumerated_heights = heights.enumerate(); - let max_height = self.end_y - self.start_y; + let max_height = self.y_range.span(); let scroll_offset_y = self.scroll_offset_y() as f64; let scroll_to_y_range_offset = self.layout.cursor.y as f64; @@ -1221,7 +1242,7 @@ pub struct TableRow<'a, 'b> { } impl<'a, 'b> TableRow<'a, 'b> { - /// Add the contents of a column. + /// Add the contents of a column on this row (i.e. a cell). /// /// Returns the used space (`min_rect`) plus the [`Response`] of the whole cell. #[cfg_attr(debug_assertions, track_caller)] @@ -1229,6 +1250,10 @@ impl<'a, 'b> TableRow<'a, 'b> { let col_index = self.col_index; let clip = self.columns.get(col_index).map_or(false, |c| c.clip); + let auto_size_this_frame = self + .columns + .get(col_index) + .map_or(false, |c| c.auto_size_this_frame); let width = if let Some(width) = self.widths.get(col_index) { self.col_index += 1; @@ -1249,6 +1274,7 @@ impl<'a, 'b> TableRow<'a, 'b> { striped: self.striped, hovered: self.hovered, selected: self.selected, + sizing_pass: auto_size_this_frame || self.layout.ui.is_sizing_pass(), }; let (used_rect, response) = self.layout.add(