From d80ef25f84bee6f6ca5a3970b3d07eef33fe2811 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Wed, 11 Sep 2024 17:26:37 +0200 Subject: [PATCH] Proper UI for collapsing/uncollapsing instances --- Cargo.lock | 2 +- .../viewer/re_space_view_dataframe/Cargo.toml | 2 +- .../src/dataframe_ui.rs | 110 +++++++++++++++--- .../src/display_record_batch.rs | 5 +- .../src/expanded_rows.rs | 16 ++- crates/viewer/re_ui/data/icons/collapse.png | Bin 0 -> 232 bytes crates/viewer/re_ui/data/icons/expand.png | Bin 0 -> 277 bytes crates/viewer/re_ui/src/icons.rs | 3 + 8 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 crates/viewer/re_ui/data/icons/collapse.png create mode 100644 crates/viewer/re_ui/data/icons/expand.png diff --git a/Cargo.lock b/Cargo.lock index 8f499981fb6f..6b0fe8a44169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1947,7 +1947,7 @@ dependencies = [ [[package]] name = "egui_table" version = "0.28.1" -source = "git+https://github.com/rerun-io/egui_table.git?rev=36887f386bdd5a324c0ae06f1cae5d73524c9b77#36887f386bdd5a324c0ae06f1cae5d73524c9b77" +source = "git+https://github.com/rerun-io/egui_table.git?rev=a3efe0bddfdb7c684cdaa478b5c0e979d238ea98#a3efe0bddfdb7c684cdaa478b5c0e979d238ea98" dependencies = [ "egui", "serde", diff --git a/crates/viewer/re_space_view_dataframe/Cargo.toml b/crates/viewer/re_space_view_dataframe/Cargo.toml index 537bfc7c96e1..6b2dc2a71be7 100644 --- a/crates/viewer/re_space_view_dataframe/Cargo.toml +++ b/crates/viewer/re_space_view_dataframe/Cargo.toml @@ -36,7 +36,7 @@ re_viewport_blueprint.workspace = true anyhow.workspace = true egui_extras.workspace = true -egui_table = { git = "https://github.com/rerun-io/egui_table.git", rev = "36887f386bdd5a324c0ae06f1cae5d73524c9b77" } # PR #14 as of 2024-09-11 +egui_table = { git = "https://github.com/rerun-io/egui_table.git", rev = "a3efe0bddfdb7c684cdaa478b5c0e979d238ea98" } # PR #14 as of 2024-09-11 egui.workspace = true itertools.workspace = true thiserror.workspace = true diff --git a/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs b/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs index 0719875de9a8..6a36c006c7c0 100644 --- a/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs +++ b/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs @@ -258,6 +258,7 @@ impl<'a> egui_table::TableDelegate for DataframeTableDelegate<'a> { } }; + //TODO: this is getting wild, refactor in some functions let cell_ui = |ui: &mut egui::Ui| { if let Some(BatchRef { batch_idx, row_idx }) = display_data.batch_ref_from_row.get(&cell.row_nr).copied() @@ -311,26 +312,47 @@ impl<'a> egui_table::TableDelegate for DataframeTableDelegate<'a> { let mut sub_cell_ui = ui.new_child(egui::UiBuilder::new().max_rect(sub_cell_rect)); - if instance_index == None && instance_count > 1 { - if sub_cell_ui - .button(format!("{instance_count} instances")) - .clicked() - { + if instance_index.is_none() && instance_count > 1 { + let cell_clicked = cell_with_hover_button_ui( + &mut sub_cell_ui, + Some(&re_ui::icons::EXPAND), + |ui| { + ui.label(format!("{instance_count} instances")); + }, + ); + + if cell_clicked { if instance_count == row_expansion { - self.expanded_rows.expend_row(cell.row_nr, 0); + self.expanded_rows.expand_row(cell.row_nr, 0); } else { - self.expanded_rows.expend_row(cell.row_nr, instance_count) + self.expanded_rows.expand_row(cell.row_nr, instance_count); } } } else { - column.data_ui( - self.ctx, + let has_collapse_button = if let Some(instance_index) = instance_index { + instance_index < instance_count + } else { + false + }; + + let cell_clicked = cell_with_hover_button_ui( &mut sub_cell_ui, - row_id, - &latest_at_query, - row_idx, - instance_index, + has_collapse_button.then_some(&re_ui::icons::COLLAPSE), + |ui| { + column.data_ui( + self.ctx, + ui, + row_id, + &latest_at_query, + row_idx, + instance_index, + ); + }, ); + + if cell_clicked { + self.expanded_rows.expand_row(cell.row_nr, 0); + } } } } else { @@ -346,9 +368,8 @@ impl<'a> egui_table::TableDelegate for DataframeTableDelegate<'a> { .show(ui, cell_ui); } - fn row_top_offset(&self, ctx: &egui::Context, table_id_salt: egui::Id, row_nr: u64) -> f32 { - self.expanded_rows - .row_top_offset(ctx, table_id_salt, row_nr) + fn row_top_offset(&self, ctx: &egui::Context, table_id: egui::Id, row_nr: u64) -> f32 { + self.expanded_rows.row_top_offset(ctx, table_id, row_nr) } fn default_row_height(&self) -> f32 { @@ -453,3 +474,60 @@ fn error_ui(ui: &mut egui::Ui, error: impl AsRef) { ui.error_label(error); re_log::warn_once!("{error}"); } + +/// Draw some cell content with an optional, right-aligned, on-hover button. +/// +/// If no icon is provided, no button is shown. Returns true if the button was shown and the cell +/// was clicked. +// TODO(ab, emilk): ideally, egui::Sides should work for that, but it doesn't yet support the +// symmetric behaviour (left variable width, right fixed width). +fn cell_with_hover_button_ui( + ui: &mut egui::Ui, + icon: Option<&'static re_ui::Icon>, + cell_content: impl FnOnce(&mut egui::Ui), +) -> bool { + let Some(icon) = icon else { + cell_content(ui); + return false; + }; + + let (is_hovering_cell, is_clicked) = ui.input(|i| { + ( + i.pointer + .interact_pos() + .is_some_and(|pos| ui.max_rect().contains(pos)), + i.pointer.button_clicked(egui::PointerButton::Primary), + ) + }); + + if is_hovering_cell { + let mut content_rect = ui.max_rect(); + content_rect.max.x = (content_rect.max.x + - re_ui::DesignTokens::small_icon_size().x + - re_ui::DesignTokens::text_to_icon_padding()) + .at_least(content_rect.min.x); + + let button_rect = egui::Rect::from_x_y_ranges( + (content_rect.max.x + re_ui::DesignTokens::text_to_icon_padding()) + ..=ui.max_rect().max.x, + ui.max_rect().y_range(), + ); + + let mut content_ui = ui.new_child(egui::UiBuilder::new().max_rect(content_rect)); + cell_content(&mut content_ui); + + let mut button_ui = ui.new_child(egui::UiBuilder::new().max_rect(button_rect)); + button_ui.visuals_mut().widgets.hovered.weak_bg_fill = egui::Color32::TRANSPARENT; + button_ui.visuals_mut().widgets.active.weak_bg_fill = egui::Color32::TRANSPARENT; + button_ui.add(egui::Button::image( + icon.as_image() + .fit_to_exact_size(re_ui::DesignTokens::small_icon_size()) + .tint(button_ui.visuals().widgets.hovered.text_color()), + )); + + is_clicked + } else { + cell_content(ui); + false + } +} diff --git a/crates/viewer/re_space_view_dataframe/src/display_record_batch.rs b/crates/viewer/re_space_view_dataframe/src/display_record_batch.rs index 1a0c8cf04f0c..cb2d5e55a46f 100644 --- a/crates/viewer/re_space_view_dataframe/src/display_record_batch.rs +++ b/crates/viewer/re_space_view_dataframe/src/display_record_batch.rs @@ -123,7 +123,10 @@ impl ComponentData { ) { let data = match self { Self::Null => { - ui.label("null"); + // don't repeat the null value when expanding instances + if instance_index.is_none() { + ui.label("null"); + } return; } Self::ListArray(list_array) => list_array diff --git a/crates/viewer/re_space_view_dataframe/src/expanded_rows.rs b/crates/viewer/re_space_view_dataframe/src/expanded_rows.rs index 7e7c9baac321..633c3c673de3 100644 --- a/crates/viewer/re_space_view_dataframe/src/expanded_rows.rs +++ b/crates/viewer/re_space_view_dataframe/src/expanded_rows.rs @@ -75,24 +75,28 @@ impl<'a> ExpandedRows<'a> { pub(crate) fn row_top_offset( &self, ctx: &egui::Context, - id_salt: egui::Id, + table_id: egui::Id, row_nr: u64, ) -> f32 { self.cache .expanded_rows .range(0..row_nr) .map(|(expanded_row_nr, expanded)| { - let how_expanded = ctx.animate_bool(id_salt.with(expanded_row_nr), *expanded > 0); + let how_expanded = ctx.animate_bool(table_id.with(expanded_row_nr), *expanded > 0); how_expanded * *expanded as f32 * self.row_height }) .sum::() + row_nr as f32 * self.row_height } - pub(crate) fn expend_row(&mut self, row_nr: u64, additional_row_space: u64) { - self.cache - .expanded_rows - .insert(row_nr, additional_row_space); + pub(crate) fn expand_row(&mut self, row_nr: u64, additional_row_space: u64) { + if additional_row_space == 0 { + self.cache.expanded_rows.remove(&row_nr); + } else { + self.cache + .expanded_rows + .insert(row_nr, additional_row_space); + } } /// Return by how much row space this row is expended. diff --git a/crates/viewer/re_ui/data/icons/collapse.png b/crates/viewer/re_ui/data/icons/collapse.png new file mode 100644 index 0000000000000000000000000000000000000000..f0983e45834a647cc276691d9fd20407c439029d GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|zoCO|{#S9E$svykh8Km+7D9BhG zmF{Fa=?fHkC4GIEn56>6e&uWrc7&>dg z#6_Gw&K^gYJFPz+-}}L_@rS~K-U;99_-iT+e(< a!?yIzS&u)Z?u&sAVeoYIb6Mw<&;$Th?p9*} literal 0 HcmV?d00001 diff --git a/crates/viewer/re_ui/data/icons/expand.png b/crates/viewer/re_ui/data/icons/expand.png new file mode 100644 index 0000000000000000000000000000000000000000..3d9ae99ae7c52669f94385b7d55fa133827fce4a GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|zoCO|{#S9E$svykh8Km+7D9BhG zx0U3CNe#(uL*448*`D%JIH_SlcsAk z{?$AF&FhXj)8rf2W_V#i;fpiIZJ(#@Y88-EuAMNC>5%H1w&3DJ$6h`-cyMR5t(Xqn*o VPOJ9)b)c&mJYD@<);T3K0RYR_Y^wkO literal 0 HcmV?d00001 diff --git a/crates/viewer/re_ui/src/icons.rs b/crates/viewer/re_ui/src/icons.rs index 7a03285b816b..da9849aed9bb 100644 --- a/crates/viewer/re_ui/src/icons.rs +++ b/crates/viewer/re_ui/src/icons.rs @@ -57,6 +57,9 @@ pub const LEFT_PANEL_TOGGLE: Icon = icon_from_path!("../data/icons/left_panel_to pub const MINIMIZE: Icon = icon_from_path!("../data/icons/minimize.png"); pub const MAXIMIZE: Icon = icon_from_path!("../data/icons/maximize.png"); +pub const COLLAPSE: Icon = icon_from_path!("../data/icons/collapse.png"); +pub const EXPAND: Icon = icon_from_path!("../data/icons/expand.png"); + pub const VISIBLE: Icon = icon_from_path!("../data/icons/visible.png"); pub const INVISIBLE: Icon = icon_from_path!("../data/icons/invisible.png");