From 3306465cd1f7a176c6d8897aef14aeb08a1a3b28 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 9 Sep 2025 18:41:18 +0200 Subject: [PATCH 1/5] Add `ItemButtons` for consistent list item button handling --- .../re_blueprint_tree/src/blueprint_tree.rs | 26 ++--- crates/viewer/re_data_ui/src/item_ui.rs | 2 - .../src/recording_panel_ui.rs | 11 +- .../re_selection_panel/src/selection_panel.rs | 4 - .../re_selection_panel/src/visualizer_ui.rs | 8 +- .../viewer/re_ui/src/list_item/item_button.rs | 12 ++ .../re_ui/src/list_item/item_buttons.rs | 107 ++++++++++++++++++ .../re_ui/src/list_item/label_content.rs | 73 ++++-------- crates/viewer/re_ui/src/list_item/mod.rs | 2 + .../re_ui/src/section_collapsing_header.rs | 28 ++--- 10 files changed, 166 insertions(+), 107 deletions(-) create mode 100644 crates/viewer/re_ui/src/list_item/item_buttons.rs diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index c98378302382..079b445b0b68 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -299,15 +299,12 @@ impl BlueprintTree { .label_style(contents_name_style(&container_data.name)) .with_icon(icon_for_container_kind(&container_data.kind)) .with_buttons(|ui| { - let vis_response = visibility_button_ui(ui, parent_visible, &mut visible); + visibility_button_ui(ui, parent_visible, &mut visible); - let remove_response = remove_button_ui(ui, "Remove container"); - if remove_response.clicked() { + if remove_button_ui(ui, "Remove container").clicked() { viewport_blueprint.mark_user_interaction(ctx); viewport_blueprint.remove_contents(content); } - - remove_response | vis_response }); // Globally unique id - should only be one of these in view at one time. @@ -390,15 +387,12 @@ impl BlueprintTree { .with_icon(class.icon()) .subdued(!view_visible) .with_buttons(|ui| { - let vis_response = visibility_button_ui(ui, container_visible, &mut visible); + visibility_button_ui(ui, container_visible, &mut visible); - let response = remove_button_ui(ui, "Remove view from the viewport"); - if response.clicked() { + if remove_button_ui(ui, "Remove view from the viewport").clicked() { viewport_blueprint.mark_user_interaction(ctx); viewport_blueprint.remove_contents(Contents::View(view_data.id)); } - - response | vis_response }); // Globally unique id - should only be one of these in view at one time. @@ -517,22 +511,20 @@ impl BlueprintTree { .subdued(!view_visible || !data_result_data.visible) .with_buttons(|ui: &mut egui::Ui| { let mut visible_after = data_result_data.visible; - let vis_response = - visibility_button_ui(ui, view_visible, &mut visible_after); + visibility_button_ui(ui, view_visible, &mut visible_after); if visible_after != data_result_data.visible { data_result_data.update_visibility(ctx, visible_after); } - let response = remove_button_ui( + if remove_button_ui( ui, "Remove this entity and all its children from the view", - ); - if response.clicked() { + ) + .clicked() + { data_result_data .remove_data_result_from_view(ctx, viewport_blueprint); } - - response | vis_response }) } } diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 535a86b4da7e..177b60d99c63 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -740,7 +740,6 @@ pub fn entity_db_button_ui( store_id.clone().into(), )); } - resp }); } @@ -812,7 +811,6 @@ pub fn table_id_button_ui( table_id.clone().into(), )); } - resp }); } diff --git a/crates/viewer/re_recording_panel/src/recording_panel_ui.rs b/crates/viewer/re_recording_panel/src/recording_panel_ui.rs index 8554b8ed8b40..74995d97cb55 100644 --- a/crates/viewer/re_recording_panel/src/recording_panel_ui.rs +++ b/crates/viewer/re_recording_panel/src/recording_panel_ui.rs @@ -221,7 +221,7 @@ fn server_section_ui( let content = list_item::LabelContent::header(origin.host.to_string()) .always_show_buttons(true) .with_buttons(|ui| { - Box::new(ItemMenuButton::new(&icons::MORE, "Actions", move |ui| { + ItemMenuButton::new(&icons::MORE, "Actions", move |ui| { if icons::RESET .as_button_with_label(ui.tokens(), "Refresh") .ui(ui) @@ -243,8 +243,8 @@ fn server_section_ui( { servers.send_command(Command::RemoveServer(origin.clone())); } - })) - .ui(ui) + }) + .ui(ui); }); let item_response = ui @@ -358,8 +358,6 @@ fn dataset_entry_ui( )); } } - - resp }); } @@ -498,7 +496,6 @@ fn app_id_section_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui, local_app_id: & ctx.command_sender() .send_system(SystemCommand::CloseApp(app_id.clone())); } - resp }); } @@ -565,8 +562,6 @@ fn receiver_ui( if resp.clicked() { ctx.connected_receivers.remove(receiver); } - - resp }); if show_hierarchal { diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index fe6c1c163fff..849c4f75518c 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -997,8 +997,6 @@ fn show_list_item_for_container_child( if response.clicked() { remove_contents = true; } - - response }), ) } @@ -1023,8 +1021,6 @@ fn show_list_item_for_container_child( if response.clicked() { remove_contents = true; } - - response }), ) } diff --git a/crates/viewer/re_selection_panel/src/visualizer_ui.rs b/crates/viewer/re_selection_panel/src/visualizer_ui.rs index 713c001e885a..099af74e339d 100644 --- a/crates/viewer/re_selection_panel/src/visualizer_ui.rs +++ b/crates/viewer/re_selection_panel/src/visualizer_ui.rs @@ -129,7 +129,9 @@ pub fn visualizer_ui_impl( ), ) .min_desired_width(150.0) - .with_buttons(|ui| remove_visualizer_button(ui, visualizer_id)) + .with_buttons(|ui| { + remove_visualizer_button(ui, visualizer_id); + }) .always_show_buttons(true), ); visualizer_components(ctx, ui, data_result, visualizer); @@ -138,7 +140,9 @@ pub fn visualizer_ui_impl( list_item::LabelContent::new(format!("{visualizer_id} (unknown visualizer)")) .weak(true) .min_desired_width(150.0) - .with_buttons(|ui| remove_visualizer_button(ui, visualizer_id)) + .with_buttons(|ui| { + remove_visualizer_button(ui, visualizer_id); + }) .always_show_buttons(true), ); } diff --git a/crates/viewer/re_ui/src/list_item/item_button.rs b/crates/viewer/re_ui/src/list_item/item_button.rs index e20ac742e082..216415b031bc 100644 --- a/crates/viewer/re_ui/src/list_item/item_button.rs +++ b/crates/viewer/re_ui/src/list_item/item_button.rs @@ -95,6 +95,12 @@ impl super::ItemButton for ItemMenuButton<'_> { } } +impl egui::Widget for ItemMenuButton<'_> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + super::ItemButton::ui(Box::new(self), ui) + } +} + // ------------------------------------------------------------------------------------------------- /// An [`super::ItemButton`] that acts as an action button. @@ -178,3 +184,9 @@ impl super::ItemButton for ItemActionButton<'_> { .inner } } + +impl egui::Widget for ItemActionButton<'_> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + super::ItemButton::ui(Box::new(self), ui) + } +} diff --git a/crates/viewer/re_ui/src/list_item/item_buttons.rs b/crates/viewer/re_ui/src/list_item/item_buttons.rs new file mode 100644 index 000000000000..7f76986c2d2a --- /dev/null +++ b/crates/viewer/re_ui/src/list_item/item_buttons.rs @@ -0,0 +1,107 @@ +use crate::UiExt; +use crate::list_item::ContentContext; +use egui::Widget; + +#[derive(Default)] +pub struct ItemButtons<'a>(Vec>); + +impl Clone for ItemButtons<'_> { + fn clone(&self) -> Self { + Self::default() + } +} + +impl std::fmt::Debug for ItemButtons<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Buttons").field(&self.0.len()).finish() + } +} + +impl<'a> ItemButtons<'a> { + pub fn add(&mut self, button: impl Widget + 'a) { + self.0.push(Box::new(move |ui: &mut egui::Ui| { + button.ui(ui); + })); + } + + pub fn add_buttons(&mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) { + self.0.push(Box::new(buttons)); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn should_show_buttons(context: &ContentContext) -> bool { + // We can't use `.hovered()` or the buttons disappear just as the user clicks, + // so we use `contains_pointer` instead. That also means we need to check + // that we aren't dragging anything. + // By showing the buttons when selected, we allow users to find them on touch screens. + (context.list_item.interactive + && context + .response + .ctx + .rect_contains_pointer(context.response.layer_id, context.bg_rect) + && !egui::DragAndDrop::has_any_payload(&context.response.ctx)) + || context.list_item.selected + } + + pub fn show_and_shrink_rect( + self, + ui: &mut egui::Ui, + context: &ContentContext, + always_show: bool, + rect: &mut egui::Rect, + ) { + if self.0.is_empty() || !(Self::should_show_buttons(context) || always_show) { + return; + } + + let mut ui = ui.new_child( + egui::UiBuilder::new() + .max_rect(*rect) + .layout(egui::Layout::right_to_left(egui::Align::Center)), + ); + + let tokens = ui.tokens(); + if context.list_item.selected { + // Icons and text get different colors when they are on a selected background: + let visuals = ui.visuals_mut(); + + visuals.widgets.noninteractive.weak_bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.active.weak_bg_fill = tokens.surface_on_primary_hovered; + visuals.widgets.hovered.weak_bg_fill = tokens.surface_on_primary_hovered; + + visuals.widgets.noninteractive.fg_stroke.color = tokens.icon_color_on_primary; + visuals.widgets.inactive.fg_stroke.color = tokens.icon_color_on_primary; + visuals.widgets.active.fg_stroke.color = tokens.icon_color_on_primary_hovered; + visuals.widgets.hovered.fg_stroke.color = tokens.icon_color_on_primary_hovered; + } + + for button in self.0 { + button(&mut ui); + } + + let used_rect = ui.min_rect(); + rect.max.x -= used_rect.width() + tokens.text_to_icon_padding(); + } +} + +pub trait ListItemContentButtonsExt<'a> +where + Self: Sized, +{ + fn buttons(&self) -> &ItemButtons<'a>; + fn buttons_mut(&mut self) -> &mut ItemButtons<'a>; + + fn button(mut self, button: impl Widget + 'a) -> Self { + self.buttons_mut().add(button); + self + } + + fn buttons_fn(mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) -> Self { + self.buttons_mut().add_buttons(buttons); + self + } +} diff --git a/crates/viewer/re_ui/src/list_item/label_content.rs b/crates/viewer/re_ui/src/list_item/label_content.rs index b6ef0b3f6c85..82243fcea187 100644 --- a/crates/viewer/re_ui/src/list_item/label_content.rs +++ b/crates/viewer/re_ui/src/list_item/label_content.rs @@ -1,7 +1,10 @@ use egui::{Align, Align2, NumExt as _, RichText, Ui, text::TextWrapping}; use std::sync::Arc; -use super::{ContentContext, DesiredWidth, ListItemContent, ListVisuals}; +use super::{ + ContentContext, DesiredWidth, ListItemContent, ListItemContentButtonsExt, ListVisuals, +}; +use crate::list_item::item_buttons::ItemButtons; use crate::{DesignTokens, Icon, LabelStyle, UiExt as _}; /// [`ListItemContent`] that displays a simple label with optional icon and buttons. @@ -17,7 +20,7 @@ pub struct LabelContent<'a> { label_style: LabelStyle, icon_fn: Option>, - buttons_fn: Option egui::Response + 'a>>, + buttons: ItemButtons<'a>, always_show_buttons: bool, text_wrap_mode: Option, @@ -36,7 +39,7 @@ impl<'a> LabelContent<'a> { label_style: Default::default(), icon_fn: None, - buttons_fn: None, + buttons: ItemButtons::default(), always_show_buttons: false, text_wrap_mode: None, @@ -153,11 +156,8 @@ impl<'a> LabelContent<'a> { // TODO(#6191): This should reconciled this with the `ItemButton` abstraction by using something // like `Vec>` instead of a generic closure. #[inline] - pub fn with_buttons( - mut self, - buttons: impl FnOnce(&mut egui::Ui) -> egui::Response + 'a, - ) -> Self { - self.buttons_fn = Some(Box::new(buttons)); + pub fn with_buttons(mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) -> Self { + self.buttons.add_buttons(buttons); self } @@ -194,7 +194,7 @@ impl ListItemContent for LabelContent<'_> { strong, label_style, icon_fn, - buttons_fn, + buttons, always_show_buttons, text_wrap_mode: _, min_desired_width: _, @@ -233,52 +233,9 @@ impl ListItemContent for LabelContent<'_> { icon_fn(ui, icon_rect, visuals); } - // We can't use `.hovered()` or the buttons disappear just as the user clicks, - // so we use `contains_pointer` instead. That also means we need to check - // that we aren't dragging anything. - // By showing the buttons when selected, we allow users to find them on touch screens. - let should_show_buttons = (context.list_item.interactive - && ui.rect_contains_pointer(context.bg_rect) - && !egui::DragAndDrop::has_any_payload(ui.ctx())) - || context.list_item.selected - || always_show_buttons; - let button_response = if should_show_buttons { - if let Some(buttons) = buttons_fn { - let mut ui = ui.new_child( - egui::UiBuilder::new() - .max_rect(text_rect) - .layout(egui::Layout::right_to_left(egui::Align::Center)), - ); - - if context.list_item.selected { - // Icons and text get different colors when they are on a selected background: - let visuals = ui.visuals_mut(); - - visuals.widgets.noninteractive.weak_bg_fill = egui::Color32::TRANSPARENT; - visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; - visuals.widgets.active.weak_bg_fill = tokens.surface_on_primary_hovered; - visuals.widgets.hovered.weak_bg_fill = tokens.surface_on_primary_hovered; - - visuals.widgets.noninteractive.fg_stroke.color = tokens.icon_color_on_primary; - visuals.widgets.inactive.fg_stroke.color = tokens.icon_color_on_primary; - visuals.widgets.active.fg_stroke.color = tokens.icon_color_on_primary_hovered; - visuals.widgets.hovered.fg_stroke.color = tokens.icon_color_on_primary_hovered; - } - - Some(buttons(&mut ui)) - } else { - None - } - } else { - None - }; + buttons.show_and_shrink_rect(ui, context, always_show_buttons, &mut text_rect); // Draw text - - if let Some(button_response) = &button_response { - text_rect.max.x -= button_response.rect.width() + tokens.text_to_icon_padding(); - } - let mut layout_job = Arc::unwrap_or_clone(text.into_layout_job( ui.style(), egui::FontSelection::Default, @@ -348,3 +305,13 @@ impl ListItemContent for LabelContent<'_> { } } } + +impl<'a> ListItemContentButtonsExt<'a> for LabelContent<'a> { + fn buttons(&self) -> &ItemButtons<'a> { + &self.buttons + } + + fn buttons_mut(&mut self) -> &mut ItemButtons<'a> { + &mut self.buttons + } +} diff --git a/crates/viewer/re_ui/src/list_item/mod.rs b/crates/viewer/re_ui/src/list_item/mod.rs index f0c7ffc8d620..948b54f4ae35 100644 --- a/crates/viewer/re_ui/src/list_item/mod.rs +++ b/crates/viewer/re_ui/src/list_item/mod.rs @@ -6,6 +6,7 @@ mod button_content; mod custom_content; mod debug_content; mod item_button; +mod item_buttons; mod label_content; #[allow(clippy::module_inception)] mod list_item; @@ -16,6 +17,7 @@ pub use button_content::*; pub use custom_content::*; pub use debug_content::*; pub use item_button::*; +pub use item_buttons::*; pub use label_content::*; pub use list_item::*; pub use property_content::*; diff --git a/crates/viewer/re_ui/src/section_collapsing_header.rs b/crates/viewer/re_ui/src/section_collapsing_header.rs index ed2f5dfe3774..afed1437fe61 100644 --- a/crates/viewer/re_ui/src/section_collapsing_header.rs +++ b/crates/viewer/re_ui/src/section_collapsing_header.rs @@ -1,3 +1,4 @@ +use crate::list_item::{ItemButtons, ListItemContentButtonsExt}; use crate::{UiExt as _, list_item}; /// A collapsible section header, with support for optional help tooltip and button. @@ -7,7 +8,7 @@ use crate::{UiExt as _, list_item}; pub struct SectionCollapsingHeader<'a> { label: egui::WidgetText, default_open: bool, - button: Option>, + buttons: ItemButtons<'a>, help: Option>, } @@ -19,7 +20,7 @@ impl<'a> SectionCollapsingHeader<'a> { Self { label: label.into(), default_open: true, - button: None, + buttons: ItemButtons::default(), help: None, } } @@ -35,8 +36,8 @@ impl<'a> SectionCollapsingHeader<'a> { /// Set the button to be shown in the header. #[inline] - pub fn button(mut self, button: impl list_item::ItemButton + 'a) -> Self { - self.button = Some(Box::new(button)); + pub fn button(mut self, button: impl egui::Widget + 'a) -> Self { + self.buttons.add(button); self } @@ -78,29 +79,14 @@ impl<'a> SectionCollapsingHeader<'a> { let Self { label, default_open, - button, + buttons, help, } = self; let id = ui.make_persistent_id(label.text()); let mut content = list_item::LabelContent::new(label); - if button.is_some() || help.is_some() { - content = content - .with_buttons(|ui| { - let button_response = button.map(|button| button.ui(ui)); - let help_response = help.map(|help| ui.help_button(help)); - - match (button_response, help_response) { - (Some(button_response), Some(help_response)) => { - button_response | help_response - } - (Some(response), None) | (None, Some(response)) => response, - (None, None) => unreachable!("at least one of button or help is set"), - } - }) - .always_show_buttons(true); - } + *content.buttons_mut() = buttons; let resp = list_item::ListItem::new() .interactive(true) From d8e79e352d4430ad50f4d87bdae1a35af04d3ea3 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 10 Sep 2025 15:32:03 +0200 Subject: [PATCH 2/5] PropertyContent buttons --- .../re_blueprint_tree/src/blueprint_tree.rs | 1 + crates/viewer/re_data_ui/src/item_ui.rs | 1 + .../src/recording_panel_ui.rs | 4 +- .../re_selection_panel/src/defaults_ui.rs | 3 +- .../re_selection_panel/src/selection_panel.rs | 1 + .../re_selection_panel/src/visualizer_ui.rs | 38 ++--- .../examples/re_ui_example/right_panel.rs | 26 ++-- .../re_ui/src/list_item/item_buttons.rs | 106 +++++++++++--- .../re_ui/src/list_item/label_content.rs | 35 +---- .../re_ui/src/list_item/property_content.rs | 130 +++--------------- crates/viewer/re_ui/tests/list_item_tests.rs | 21 +-- crates/viewer/re_ui/tests/modal_tests.rs | 4 +- crates/viewer/re_view/src/view_property_ui.rs | 4 +- .../re_view_dataframe/src/view_query/ui.rs | 7 +- 14 files changed, 175 insertions(+), 206 deletions(-) diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index 079b445b0b68..66128c57e4b2 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -8,6 +8,7 @@ use re_data_ui::item_ui::guess_instance_path_icon; use re_entity_db::InstancePath; use re_log_types::{ApplicationId, EntityPath}; use re_ui::filter_widget::format_matching_text; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{ ContextExt as _, DesignTokens, UiExt as _, drag_and_drop::DropTarget, filter_widget, list_item, }; diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 177b60d99c63..4823c510e0b5 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -9,6 +9,7 @@ use re_types::{ archetypes::RecordingInfo, components::{Name, Timestamp}, }; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{SyntaxHighlighting as _, UiExt as _, icons, list_item}; use re_viewer_context::{ HoverHighlight, Item, SystemCommand, SystemCommandSender as _, UiLayout, ViewId, ViewerContext, diff --git a/crates/viewer/re_recording_panel/src/recording_panel_ui.rs b/crates/viewer/re_recording_panel/src/recording_panel_ui.rs index 74995d97cb55..7f9c64596931 100644 --- a/crates/viewer/re_recording_panel/src/recording_panel_ui.rs +++ b/crates/viewer/re_recording_panel/src/recording_panel_ui.rs @@ -7,7 +7,7 @@ use re_data_ui::item_ui::{entity_db_button_ui, table_id_button_ui}; use re_log_types::TableId; use re_redap_browser::{Command, EXAMPLES_ORIGIN, LOCAL_ORIGIN, RedapServers}; use re_smart_channel::SmartChannelSource; -use re_ui::list_item::{ItemButton as _, ItemMenuButton, LabelContent}; +use re_ui::list_item::{ItemButton as _, ItemMenuButton, LabelContent, ListItemContentButtonsExt}; use re_ui::{UiExt as _, UiLayout, icons, list_item}; use re_viewer_context::{ DisplayMode, Item, RecordingOrTable, SystemCommand, SystemCommandSender as _, ViewerContext, @@ -219,7 +219,7 @@ fn server_section_ui( } = server_data; let content = list_item::LabelContent::header(origin.host.to_string()) - .always_show_buttons(true) + .with_always_show_buttons(true) .with_buttons(|ui| { ItemMenuButton::new(&icons::MORE, "Actions", move |ui| { if icons::RESET diff --git a/crates/viewer/re_selection_panel/src/defaults_ui.rs b/crates/viewer/re_selection_panel/src/defaults_ui.rs index e993bc888bdc..84a75cf73c18 100644 --- a/crates/viewer/re_selection_panel/src/defaults_ui.rs +++ b/crates/viewer/re_selection_panel/src/defaults_ui.rs @@ -9,6 +9,7 @@ use re_data_ui::{DataUi as _, archetype_label_list_item_ui}; use re_log_types::EntityPath; use re_types_core::ComponentDescriptor; use re_types_core::reflection::ComponentDescriptorExt as _; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{SyntaxHighlighting as _, UiExt as _, list_item::LabelContent}; use re_viewer_context::{ ComponentUiTypes, QueryContext, SystemCommand, SystemCommandSender as _, UiLayout, ViewContext, @@ -146,7 +147,7 @@ fn active_default_ui( let response = ui.list_item_flat_noninteractive( re_ui::list_item::PropertyContent::new(component_descr.archetype_field_name()) .min_desired_width(150.0) - .action_button(&re_ui::icons::CLOSE, "Clear blueprint component", || { + .with_action_button(&re_ui::icons::CLOSE, "Clear blueprint component", || { ctx.clear_blueprint_component( view.defaults_path.clone(), component_descr.clone(), diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index 849c4f75518c..1136cd7aef9b 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -9,6 +9,7 @@ use re_data_ui::{ use re_entity_db::{EntityPath, InstancePath}; use re_log_types::{ComponentPath, EntityPathFilter, EntityPathSubs, ResolvedEntityPathFilter}; use re_types::ComponentDescriptor; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{ SyntaxHighlighting as _, UiExt as _, icons, list_item::{self, PropertyContent}, diff --git a/crates/viewer/re_selection_panel/src/visualizer_ui.rs b/crates/viewer/re_selection_panel/src/visualizer_ui.rs index 099af74e339d..00c1ac3dceca 100644 --- a/crates/viewer/re_selection_panel/src/visualizer_ui.rs +++ b/crates/viewer/re_selection_panel/src/visualizer_ui.rs @@ -8,6 +8,7 @@ use re_log_types::{ComponentPath, EntityPath}; use re_types::blueprint::archetypes::VisualizerOverrides; use re_types::{ComponentDescriptor, reflection::ComponentDescriptorExt as _}; use re_types_core::external::arrow::array::ArrayRef; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{UiExt as _, design_tokens_of_visuals, list_item}; use re_view::latest_at_with_blueprint_resolved_data; use re_viewer_context::{ @@ -132,7 +133,7 @@ pub fn visualizer_ui_impl( .with_buttons(|ui| { remove_visualizer_button(ui, visualizer_id); }) - .always_show_buttons(true), + .with_always_show_buttons(true), ); visualizer_components(ctx, ui, data_result, visualizer); } else { @@ -143,7 +144,7 @@ pub fn visualizer_ui_impl( .with_buttons(|ui| { remove_visualizer_button(ui, visualizer_id); }) - .always_show_buttons(true), + .with_always_show_buttons(true), ); } } @@ -410,22 +411,21 @@ fn visualizer_components( ) .value_fn(value_fn) .show_only_when_collapsed(false) - .menu_button( - &re_ui::icons::MORE, - "More options", - |ui: &mut egui::Ui| { - menu_more( - ctx, - ui, - component_descr.clone(), - override_path, - &raw_override.clone().map(|(_, raw_override)| raw_override), - raw_default.clone().map(|(_, raw_override)| raw_override), - raw_fallback.clone(), - raw_current_value.clone(), - ); - }, - ), + .with_menu_button(&re_ui::icons::MORE, "More options", |ui: &mut egui::Ui| { + menu_more( + ctx, + ui, + component_descr.clone(), + override_path, + &raw_override.clone().map(|(_, raw_override)| raw_override), + raw_default.clone().map(|(_, raw_override)| raw_override), + raw_fallback.clone(), + raw_current_value.clone(), + ); + }) + // TODO(emilk/egui#7531): Ideally we would hide the button unless hovered, but this + // currently breaks the menu. + .with_always_show_buttons(true), add_children, ) .item_response; @@ -464,7 +464,7 @@ fn editable_blueprint_component_list_item( allow_multiline, ); }) - .action_button(&re_ui::icons::CLOSE, "Clear blueprint component", || { + .with_action_button(&re_ui::icons::CLOSE, "Clear blueprint component", || { query_ctx .viewer_ctx() .clear_blueprint_component(blueprint_path, component_descr.clone()); diff --git a/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs b/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs index a8d930fd4df3..3151b292bd10 100644 --- a/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs +++ b/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs @@ -1,8 +1,8 @@ use egui::Ui; -use re_ui::{UiExt as _, list_item}; - use crate::{drag_and_drop, hierarchical_drag_and_drop}; +use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::{UiExt as _, list_item}; pub struct RightPanel { show_hierarchical_demo: bool, @@ -195,8 +195,8 @@ impl RightPanel { ui.list_item().show_hierarchical( ui, list_item::LabelContent::new("LabelContent with buttons").with_buttons(|ui| { - ui.small_icon_button(&re_ui::icons::ADD, "Add") - | ui.small_icon_button(&re_ui::icons::REMOVE, "Remove") + ui.small_icon_button(&re_ui::icons::ADD, "Add"); + ui.small_icon_button(&re_ui::icons::REMOVE, "Remove"); }), ); @@ -204,10 +204,10 @@ impl RightPanel { ui, list_item::LabelContent::new("LabelContent with buttons (always shown)") .with_buttons(|ui| { - ui.small_icon_button(&re_ui::icons::ADD, "Add") - | ui.small_icon_button(&re_ui::icons::REMOVE, "Remove") + ui.small_icon_button(&re_ui::icons::ADD, "Add"); + ui.small_icon_button(&re_ui::icons::REMOVE, "Remove"); }) - .always_show_buttons(true), + .with_always_show_buttons(true), ); }, ); @@ -248,20 +248,22 @@ impl RightPanel { ui, list_item::PropertyContent::new("Color") .with_icon(&re_ui::icons::VIEW_TEXT) - .action_button(&re_ui::icons::ADD, "Add", || { + .with_action_button(&re_ui::icons::ADD, "Add", || { re_log::warn!("Add button clicked"); }) - .value_color(&self.color), + .value_color(&self.color) + .with_always_show_buttons(true), ); ui.list_item().show_hierarchical( ui, list_item::PropertyContent::new("Color (editable)") .with_icon(&re_ui::icons::VIEW_TEXT) - .action_button(&re_ui::icons::ADD, "Add", || { + .with_action_button(&re_ui::icons::ADD, "Add", || { re_log::warn!("Add button clicked"); }) - .value_color_mut(&mut self.color), + .value_color_mut(&mut self.color) + .with_always_show_buttons(true), ); }); }, @@ -296,7 +298,7 @@ impl RightPanel { let mut content = list_item::PropertyContent::new("Use action button"); if self.use_action_button { - content = content.action_button(&re_ui::icons::ADD, "Add", || { + content = content.with_action_button(&re_ui::icons::ADD, "Add", || { re_log::warn!("Add button clicked"); }); } diff --git a/crates/viewer/re_ui/src/list_item/item_buttons.rs b/crates/viewer/re_ui/src/list_item/item_buttons.rs index 7f76986c2d2a..53e0a7b51737 100644 --- a/crates/viewer/re_ui/src/list_item/item_buttons.rs +++ b/crates/viewer/re_ui/src/list_item/item_buttons.rs @@ -3,7 +3,10 @@ use crate::list_item::ContentContext; use egui::Widget; #[derive(Default)] -pub struct ItemButtons<'a>(Vec>); +pub struct ItemButtons<'a> { + buttons: Vec>, + always_show_buttons: bool, +} impl Clone for ItemButtons<'_> { fn clone(&self) -> Self { @@ -13,47 +16,46 @@ impl Clone for ItemButtons<'_> { impl std::fmt::Debug for ItemButtons<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Buttons").field(&self.0.len()).finish() + f.debug_tuple("Buttons").field(&self.buttons.len()).finish() } } impl<'a> ItemButtons<'a> { pub fn add(&mut self, button: impl Widget + 'a) { - self.0.push(Box::new(move |ui: &mut egui::Ui| { + self.buttons.push(Box::new(move |ui: &mut egui::Ui| { button.ui(ui); })); } pub fn add_buttons(&mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) { - self.0.push(Box::new(buttons)); + self.buttons.push(Box::new(buttons)); } pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.buttons.is_empty() } - pub fn should_show_buttons(context: &ContentContext) -> bool { + fn should_show_buttons(&self, context: &ContentContext) -> bool { // We can't use `.hovered()` or the buttons disappear just as the user clicks, // so we use `contains_pointer` instead. That also means we need to check // that we aren't dragging anything. // By showing the buttons when selected, we allow users to find them on touch screens. - (context.list_item.interactive - && context - .response - .ctx - .rect_contains_pointer(context.response.layer_id, context.bg_rect) + (context + .response + .ctx + .rect_contains_pointer(context.response.layer_id, context.bg_rect) && !egui::DragAndDrop::has_any_payload(&context.response.ctx)) || context.list_item.selected + || self.always_show_buttons } pub fn show_and_shrink_rect( self, ui: &mut egui::Ui, context: &ContentContext, - always_show: bool, rect: &mut egui::Rect, ) { - if self.0.is_empty() || !(Self::should_show_buttons(context) || always_show) { + if self.buttons.is_empty() || !self.should_show_buttons(context) { return; } @@ -79,7 +81,7 @@ impl<'a> ItemButtons<'a> { visuals.widgets.hovered.fg_stroke.color = tokens.icon_color_on_primary_hovered; } - for button in self.0 { + for button in self.buttons { button(&mut ui); } @@ -95,13 +97,85 @@ where fn buttons(&self) -> &ItemButtons<'a>; fn buttons_mut(&mut self) -> &mut ItemButtons<'a>; - fn button(mut self, button: impl Widget + 'a) -> Self { + /// Add a single widget. + /// + /// It will be shown on the right side of the list item. + /// By default, buttons are only shown on hover or when selected, use + /// [`Self::with_always_show_buttons`] to change that. + /// + /// Usually you want to add an [`crate::list_item::ItemMenuButton`] or + /// [`crate::list_item::ItemActionButton`]. + /// + /// Notes: + /// - If buttons are used, the item will allocate the full available width of the parent. If the + /// enclosing UI adapts to the childrens width, it will unnecessarily grow. If buttons aren't + /// used, the item will only allocate the width needed for the text and icons if any. + /// - A right to left layout is used, so the right-most button must be added first. + fn with_button(mut self, button: impl Widget + 'a) -> Self { self.buttons_mut().add(button); self } - fn buttons_fn(mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) -> Self { + fn with_buttons(mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) -> Self { self.buttons_mut().add_buttons(buttons); self } + + /// Always show the buttons. + /// + /// By default, buttons are only shown when the item is hovered or selected. By setting this to + /// `true`, the buttons are always shown. + fn with_always_show_buttons(mut self, always_show: bool) -> Self { + self.buttons_mut().always_show_buttons = always_show; + self + } + + /// Helper to add an [`super::ItemActionButton`] to the right of the item. + /// + /// The `alt_text` will be used for accessibility (e.g. read by screen readers), + /// and is also how we can query the button in tests. + /// + /// See [`Self::with_button`] for more information. + #[inline] + fn with_action_button( + self, + icon: &'static crate::icons::Icon, + alt_text: impl Into, + on_click: impl FnOnce() + 'a, + ) -> Self { + self.with_action_button_enabled(icon, alt_text, true, on_click) + } + + /// Helper to add an enabled/disabled [`super::ItemActionButton`] to the right of the item. + /// + /// The `alt_text` will be used for accessibility (e.g. read by screen readers), + /// and is also how we can query the button in tests. + /// + /// See [`Self::with_button`] for more information. + #[inline] + fn with_action_button_enabled( + self, + icon: &'static crate::icons::Icon, + alt_text: impl Into, + enabled: bool, + on_click: impl FnOnce() + 'a, + ) -> Self { + self.with_button(super::ItemActionButton::new(icon, alt_text, on_click).enabled(enabled)) + } + + /// Helper to add a [`super::ItemMenuButton`] to the right of the item. + /// + /// The `alt_text` will be used for accessibility (e.g. read by screen readers), + /// and is also how we can query the button in tests. + /// + /// See [`Self::with_button`] for more information. + #[inline] + fn with_menu_button( + self, + icon: &'static crate::icons::Icon, + alt_text: impl Into, + add_contents: impl FnOnce(&mut egui::Ui) + 'a, + ) -> Self { + self.with_button(super::ItemMenuButton::new(icon, alt_text, add_contents)) + } } diff --git a/crates/viewer/re_ui/src/list_item/label_content.rs b/crates/viewer/re_ui/src/list_item/label_content.rs index 82243fcea187..ef1828394c66 100644 --- a/crates/viewer/re_ui/src/list_item/label_content.rs +++ b/crates/viewer/re_ui/src/list_item/label_content.rs @@ -21,7 +21,6 @@ pub struct LabelContent<'a> { label_style: LabelStyle, icon_fn: Option>, buttons: ItemButtons<'a>, - always_show_buttons: bool, text_wrap_mode: Option, min_desired_width: Option, @@ -40,7 +39,6 @@ impl<'a> LabelContent<'a> { label_style: Default::default(), icon_fn: None, buttons: ItemButtons::default(), - always_show_buttons: false, text_wrap_mode: None, min_desired_width: None, @@ -141,36 +139,6 @@ impl<'a> LabelContent<'a> { self } - /// Provide a closure to display on-hover buttons on the right of the item. - /// - /// Buttons also show when the item is selected, in order to support clicking them on touch - /// screens. The buttons can be set to be always shown with [`Self::always_show_buttons`]. - /// - /// If there are multiple buttons, the response returned should be the union of both buttons. - /// - /// Notes: - /// - If buttons are used, the item will allocate the full available width of the parent. If the - /// enclosing UI adapts to the childrens width, it will unnecessarily grow. If buttons aren't - /// used, the item will only allocate the width needed for the text and icons if any. - /// - A right to left layout is used, so the right-most button must be added first. - // TODO(#6191): This should reconciled this with the `ItemButton` abstraction by using something - // like `Vec>` instead of a generic closure. - #[inline] - pub fn with_buttons(mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) -> Self { - self.buttons.add_buttons(buttons); - self - } - - /// Always show the buttons. - /// - /// By default, buttons are only shown when the item is hovered or selected. By setting this to - /// `true`, the buttons are always shown. - #[inline] - pub fn always_show_buttons(mut self, always_show_buttons: bool) -> Self { - self.always_show_buttons = always_show_buttons; - self - } - fn get_text_wrap_mode(&self, ui: &egui::Ui) -> egui::TextWrapMode { if let Some(text_wrap_mode) = self.text_wrap_mode { text_wrap_mode @@ -195,7 +163,6 @@ impl ListItemContent for LabelContent<'_> { label_style, icon_fn, buttons, - always_show_buttons, text_wrap_mode: _, min_desired_width: _, } = *self; @@ -233,7 +200,7 @@ impl ListItemContent for LabelContent<'_> { icon_fn(ui, icon_rect, visuals); } - buttons.show_and_shrink_rect(ui, context, always_show_buttons, &mut text_rect); + buttons.show_and_shrink_rect(ui, context, &mut text_rect); // Draw text let mut layout_job = Arc::unwrap_or_clone(text.into_layout_job( diff --git a/crates/viewer/re_ui/src/list_item/property_content.rs b/crates/viewer/re_ui/src/list_item/property_content.rs index 92b8bc6d16e3..76769f4389af 100644 --- a/crates/viewer/re_ui/src/list_item/property_content.rs +++ b/crates/viewer/re_ui/src/list_item/property_content.rs @@ -4,7 +4,10 @@ use egui::{Align, Align2, NumExt as _, Ui, text::TextWrapping}; use crate::{Icon, UiExt as _}; -use super::{ContentContext, DesiredWidth, LayoutInfoStack, ListItemContent, ListVisuals}; +use super::{ + ContentContext, DesiredWidth, ItemButtons, LabelContent, LayoutInfoStack, ListItemContent, + ListItemContentButtonsExt, ListVisuals, +}; /// Closure to draw an icon left of the label. type IconFn<'a> = dyn FnOnce(&mut egui::Ui, egui::Rect, ListVisuals) + 'a; @@ -24,7 +27,7 @@ pub struct PropertyContent<'a> { show_only_when_collapsed: bool, value_fn: Option>>, //TODO(ab): in the future, that should be a `Vec`, with some auto expanding mini-toolbar - button: Option>, + buttons: ItemButtons<'a>, /**/ //TODO(ab): icon styling? link icon right of label? clickable label? } @@ -40,7 +43,7 @@ impl<'a> PropertyContent<'a> { icon_fn: None, show_only_when_collapsed: true, value_fn: None, - button: None, + buttons: ItemButtons::default(), } } @@ -72,71 +75,6 @@ impl<'a> PropertyContent<'a> { self } - /// Add a right-aligned [`super::ItemButton`]. - /// - /// Note: for aesthetics, space is always reserved for the action button. - // TODO(#6191): accept multiple calls for this function for multiple actions. - #[inline] - pub fn button(mut self, button: impl super::ItemButton + 'a) -> Self { - // TODO(#6191): support multiple action buttons - assert!( - self.button.is_none(), - "Only one action button is supported right now" - ); - - self.button = Some(Box::new(button)); - self - } - - /// Helper to add an [`super::ItemActionButton`] to the right of the item. - /// - /// The `alt_text` will be used for accessibility (e.g. read by screen readers), - /// and is also how we can query the button in tests. - /// - /// See [`Self::button`] for more information. - #[inline] - pub fn action_button( - self, - icon: &'static crate::icons::Icon, - alt_text: impl Into, - on_click: impl FnOnce() + 'a, - ) -> Self { - self.action_button_with_enabled(icon, alt_text, true, on_click) - } - - /// Helper to add an enabled/disabled [`super::ItemActionButton`] to the right of the item. - /// - /// The `alt_text` will be used for accessibility (e.g. read by screen readers), - /// and is also how we can query the button in tests. - /// - /// See [`Self::button`] for more information. - #[inline] - pub fn action_button_with_enabled( - self, - icon: &'static crate::icons::Icon, - alt_text: impl Into, - enabled: bool, - on_click: impl FnOnce() + 'a, - ) -> Self { - self.button(super::ItemActionButton::new(icon, alt_text, on_click).enabled(enabled)) - } - - /// Helper to add a [`super::ItemMenuButton`] to the right of the item. - /// - /// The `alt_text` will be used for accessibility (e.g. read by screen readers), - /// and is also how we can query the button in tests. - /// - /// See [`Self::button`] for more information. - #[inline] - pub fn menu_button( - self, - icon: &'static crate::icons::Icon, - alt_text: impl Into, - add_contents: impl FnOnce(&mut egui::Ui) + 'a, - ) -> Self { - self.button(super::ItemMenuButton::new(icon, alt_text, add_contents)) - } - /// Display value only for leaf or collapsed items. /// /// When enabled, the value for this item is not displayed for uncollapsed hierarchical items. @@ -241,7 +179,7 @@ impl ListItemContent for PropertyContent<'_> { icon_fn, show_only_when_collapsed, value_fn, - button, + buttons, } = *self; let tokens = ui.tokens(); @@ -283,25 +221,19 @@ impl ListItemContent for PropertyContent<'_> { // Based on egui::ImageButton::ui() let action_button_dimension = tokens.small_icon_size.x + 2.0 * ui.spacing().button_padding.x; - let reserve_action_button_space = - button.is_some() || context.layout_info.reserve_action_button_space; - let action_button_extra = if reserve_action_button_space { - action_button_dimension + tokens.text_to_icon_padding() - } else { - 0.0 - }; let label_rect = egui::Rect::from_x_y_ranges( (content_left_x + icon_extra)..=(mid_point_x - Self::COLUMN_SPACING / 2.0), context.rect.y_range(), ); - let value_rect = egui::Rect::from_x_y_ranges( - (mid_point_x + Self::COLUMN_SPACING / 2.0) - ..=(context.rect.right() - action_button_extra), + let mut value_rect = egui::Rect::from_x_y_ranges( + (mid_point_x + Self::COLUMN_SPACING / 2.0)..=context.rect.right(), context.rect.y_range(), ); + buttons.show_and_shrink_rect(ui, context, &mut value_rect); + let visuals = context.visuals; // Draw icon @@ -329,9 +261,6 @@ impl ListItemContent for PropertyContent<'_> { context .layout_info .register_desired_left_column_width(ui.ctx(), desired_width); - context - .layout_info - .reserve_action_button_space(ui.ctx(), button.is_some()); let galley = if desired_galley.size().x <= label_rect.width() { desired_galley @@ -398,24 +327,6 @@ impl ListItemContent for PropertyContent<'_> { child_ui.min_rect().right() - context.layout_info.left_x, ); } - - // Draw action button - if let Some(button) = button { - let action_button_rect = egui::Rect::from_center_size( - context.rect.right_center() - egui::vec2(action_button_dimension / 2.0, 0.0), - egui::Vec2::splat(action_button_dimension), - ); - - // the right to left layout is used to mimic LabelContent's buttons behavior and get a - // better alignment - let mut child_ui = ui.new_child( - egui::UiBuilder::new() - .max_rect(action_button_rect) - .layout(egui::Layout::right_to_left(egui::Align::Center)), - ); - - button.ui(&mut child_ui); - } } fn desired_width(&self, ui: &Ui) -> DesiredWidth { @@ -427,18 +338,19 @@ impl ListItemContent for PropertyContent<'_> { } else if let Some(max_width) = layout_info.property_content_max_width { let mut desired_width = max_width + layout_info.left_x - ui.max_rect().left(); - // TODO(ab): ideally there wouldn't be as much code duplication with `Self::ui` - let action_button_dimension = - tokens.small_icon_size.x + 2.0 * ui.spacing().button_padding.x; - let reserve_action_button_space = - self.button.is_some() || layout_info.reserve_action_button_space; - if reserve_action_button_space { - desired_width += action_button_dimension + tokens.text_to_icon_padding(); - } - DesiredWidth::AtLeast(desired_width.ceil()) } else { DesiredWidth::AtLeast(self.min_desired_width) } } } + +impl<'a> ListItemContentButtonsExt<'a> for PropertyContent<'a> { + fn buttons(&self) -> &ItemButtons<'a> { + &self.buttons + } + + fn buttons_mut(&mut self) -> &mut ItemButtons<'a> { + &mut self.buttons + } +} diff --git a/crates/viewer/re_ui/tests/list_item_tests.rs b/crates/viewer/re_ui/tests/list_item_tests.rs index f32ecb8a2bb0..2fcf3eeef067 100644 --- a/crates/viewer/re_ui/tests/list_item_tests.rs +++ b/crates/viewer/re_ui/tests/list_item_tests.rs @@ -1,5 +1,6 @@ use egui::Vec2; use egui_kittest::SnapshotOptions; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{UiExt as _, icons, list_item}; #[test] @@ -112,8 +113,8 @@ pub fn test_list_items_should_match_snapshot() { ui.list_item().show_hierarchical( ui, list_item::LabelContent::new("LabelContent with buttons").with_buttons(|ui| { - ui.small_icon_button(&icons::ADD, "Add") - | ui.small_icon_button(&icons::REMOVE, "Remove") + ui.small_icon_button(&icons::ADD, "Add"); + ui.small_icon_button(&icons::REMOVE, "Remove"); }), ); @@ -121,10 +122,10 @@ pub fn test_list_items_should_match_snapshot() { ui, list_item::LabelContent::new("LabelContent with buttons (always shown)") .with_buttons(|ui| { - ui.small_icon_button(&icons::ADD, "Add") - | ui.small_icon_button(&icons::REMOVE, "Remove") + ui.small_icon_button(&icons::ADD, "Add"); + ui.small_icon_button(&icons::REMOVE, "Remove"); }) - .always_show_buttons(true), + .with_always_show_buttons(true), ); }, ); @@ -165,16 +166,18 @@ pub fn test_list_items_should_match_snapshot() { ui, list_item::PropertyContent::new("Color") .with_icon(&icons::VIEW_TEXT) - .action_button(&icons::ADD, "Add", || {}) - .value_color(&color), + .with_action_button(&icons::ADD, "Add", || {}) + .value_color(&color) + .with_always_show_buttons(true), ); ui.list_item().show_hierarchical( ui, list_item::PropertyContent::new("Color (editable)") .with_icon(&icons::VIEW_TEXT) - .action_button(&icons::ADD, "Add", || {}) - .value_color_mut(&mut color), + .with_action_button(&icons::ADD, "Add", || {}) + .value_color_mut(&mut color) + .with_always_show_buttons(true), ); }); }, diff --git a/crates/viewer/re_ui/tests/modal_tests.rs b/crates/viewer/re_ui/tests/modal_tests.rs index d5cf74bc06b6..377eef751e84 100644 --- a/crates/viewer/re_ui/tests/modal_tests.rs +++ b/crates/viewer/re_ui/tests/modal_tests.rs @@ -1,5 +1,6 @@ use egui::Vec2; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::modal::{ModalHandler, ModalWrapper}; use re_ui::{UiExt as _, list_item}; @@ -36,7 +37,8 @@ pub fn test_modal_list_item_should_match_snapshot() { ui.list_item_flat_noninteractive( list_item::PropertyContent::new("Property content") .value_color(&egui::Color32::RED.to_array()) - .action_button(&re_ui::icons::EDIT, "Edit", || {}), + .with_action_button(&re_ui::icons::EDIT, "Edit", || {}) + .with_always_show_buttons(true), ); }); }, diff --git a/crates/viewer/re_view/src/view_property_ui.rs b/crates/viewer/re_view/src/view_property_ui.rs index 4c21c11f71fd..877afe2eba38 100644 --- a/crates/viewer/re_view/src/view_property_ui.rs +++ b/crates/viewer/re_view/src/view_property_ui.rs @@ -1,5 +1,6 @@ use re_types::ComponentDescriptor; use re_types_core::{Archetype, ArchetypeReflectionMarker, reflection::ArchetypeFieldReflection}; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{UiExt as _, list_item}; use re_viewer_context::{ ComponentFallbackProvider, ComponentUiTypes, QueryContext, ViewContext, ViewerContext, @@ -160,9 +161,10 @@ pub fn view_property_component_ui_custom( let component_descr = field.component_descriptor(property.archetype_name); let singleline_list_item_content = list_item::PropertyContent::new(display_name) - .menu_button(&re_ui::icons::MORE, "More options", |ui| { + .with_menu_button(&re_ui::icons::MORE, "More options", |ui| { menu_more(ctx.viewer_ctx(), ui, property, &component_descr); }) + .with_always_show_buttons(true) .value_fn(move |ui, _| singleline_ui(ui)); let list_item_response = if let Some(multiline_ui) = multiline_ui { diff --git a/crates/viewer/re_view_dataframe/src/view_query/ui.rs b/crates/viewer/re_view_dataframe/src/view_query/ui.rs index a2e74152e88e..4dcfe0b67f4a 100644 --- a/crates/viewer/re_view_dataframe/src/view_query/ui.rs +++ b/crates/viewer/re_view_dataframe/src/view_query/ui.rs @@ -7,6 +7,7 @@ use re_log_types::{ }; use re_sorbet::ColumnSelector; use re_types::blueprint::components; +use re_ui::list_item::ListItemContentButtonsExt; use re_ui::{TimeDragValue, UiExt as _, list_item}; use re_viewer_context::{ViewId, ViewSystemExecutionError, ViewerContext}; use std::collections::{BTreeSet, HashSet}; @@ -66,7 +67,7 @@ impl Query { ui.list_item_flat_noninteractive( list_item::PropertyContent::new("Start") - .action_button_with_enabled( + .with_action_button_enabled( &re_ui::icons::RESET, "Reset", start != TimeInt::MIN, @@ -74,6 +75,7 @@ impl Query { reset_start = true; }, ) + .with_always_show_buttons(true) .value_fn(|ui, _| { if let Some((time_drag_value, timeline_type)) = &time_drag_value_and_type { let response = time_boundary_ui( @@ -106,7 +108,7 @@ impl Query { ui.list_item_flat_noninteractive( list_item::PropertyContent::new("End") - .action_button_with_enabled( + .with_action_button_enabled( &re_ui::icons::RESET, "Reset", end != TimeInt::MAX, @@ -114,6 +116,7 @@ impl Query { reset_to = true; }, ) + .with_always_show_buttons(true) .value_fn(|ui, _| { if let Some((time_drag_value, timeline_type)) = &time_drag_value_and_type { let response = time_boundary_ui( From 33247fa9a113c1aca3958ee5283b935a18319a70 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 10 Sep 2025 15:51:39 +0200 Subject: [PATCH 3/5] Add help button helpers --- .../re_blueprint_tree/src/blueprint_tree.rs | 2 +- crates/viewer/re_data_ui/src/item_ui.rs | 2 +- .../src/recording_panel_ui.rs | 2 +- .../re_selection_panel/src/defaults_ui.rs | 6 +-- .../re_selection_panel/src/selection_panel.rs | 8 +-- .../src/visible_time_range_ui.rs | 3 +- .../re_selection_panel/src/visualizer_ui.rs | 6 +-- .../re_ui/examples/re_ui_example/main.rs | 3 +- .../examples/re_ui_example/right_panel.rs | 2 +- .../re_ui/src/list_item/item_buttons.rs | 49 ++++++++++++++++-- .../re_ui/src/list_item/property_content.rs | 9 +--- crates/viewer/re_ui/src/list_item/scope.rs | 5 -- .../re_ui/src/section_collapsing_header.rs | 51 ++++--------------- crates/viewer/re_ui/tests/list_item_tests.rs | 2 +- crates/viewer/re_ui/tests/modal_tests.rs | 2 +- crates/viewer/re_view/src/view_property_ui.rs | 2 +- .../re_view_dataframe/src/view_query/ui.rs | 2 +- 17 files changed, 80 insertions(+), 76 deletions(-) diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index 66128c57e4b2..b1d93e9e5c7d 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -8,7 +8,7 @@ use re_data_ui::item_ui::guess_instance_path_icon; use re_entity_db::InstancePath; use re_log_types::{ApplicationId, EntityPath}; use re_ui::filter_widget::format_matching_text; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{ ContextExt as _, DesignTokens, UiExt as _, drag_and_drop::DropTarget, filter_widget, list_item, }; diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 4823c510e0b5..4f9241e9d750 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -9,7 +9,7 @@ use re_types::{ archetypes::RecordingInfo, components::{Name, Timestamp}, }; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{SyntaxHighlighting as _, UiExt as _, icons, list_item}; use re_viewer_context::{ HoverHighlight, Item, SystemCommand, SystemCommandSender as _, UiLayout, ViewId, ViewerContext, diff --git a/crates/viewer/re_recording_panel/src/recording_panel_ui.rs b/crates/viewer/re_recording_panel/src/recording_panel_ui.rs index 7f9c64596931..e0757dbdcdd0 100644 --- a/crates/viewer/re_recording_panel/src/recording_panel_ui.rs +++ b/crates/viewer/re_recording_panel/src/recording_panel_ui.rs @@ -7,7 +7,7 @@ use re_data_ui::item_ui::{entity_db_button_ui, table_id_button_ui}; use re_log_types::TableId; use re_redap_browser::{Command, EXAMPLES_ORIGIN, LOCAL_ORIGIN, RedapServers}; use re_smart_channel::SmartChannelSource; -use re_ui::list_item::{ItemButton as _, ItemMenuButton, LabelContent, ListItemContentButtonsExt}; +use re_ui::list_item::{ItemMenuButton, LabelContent, ListItemContentButtonsExt as _}; use re_ui::{UiExt as _, UiLayout, icons, list_item}; use re_viewer_context::{ DisplayMode, Item, RecordingOrTable, SystemCommand, SystemCommandSender as _, ViewerContext, diff --git a/crates/viewer/re_selection_panel/src/defaults_ui.rs b/crates/viewer/re_selection_panel/src/defaults_ui.rs index 84a75cf73c18..a0932a4aa517 100644 --- a/crates/viewer/re_selection_panel/src/defaults_ui.rs +++ b/crates/viewer/re_selection_panel/src/defaults_ui.rs @@ -9,7 +9,7 @@ use re_data_ui::{DataUi as _, archetype_label_list_item_ui}; use re_log_types::EntityPath; use re_types_core::ComponentDescriptor; use re_types_core::reflection::ComponentDescriptorExt as _; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{SyntaxHighlighting as _, UiExt as _, list_item::LabelContent}; use re_viewer_context::{ ComponentUiTypes, QueryContext, SystemCommand, SystemCommandSender as _, UiLayout, ViewContext, @@ -91,8 +91,8 @@ Click on the `+` button to add a new default value."; active_default_ui(ctx, ui, &active_defaults, view, query, db); }; ui.section_collapsing_header("Component defaults") - .button(add_button) - .help_markdown(markdown) + .with_button(add_button) + .with_help_markdown(markdown) .show(ui, body); } diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index 1136cd7aef9b..6dac2572d69c 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -9,7 +9,7 @@ use re_data_ui::{ use re_entity_db::{EntityPath, InstancePath}; use re_log_types::{ComponentPath, EntityPathFilter, EntityPathSubs, ResolvedEntityPathFilter}; use re_types::ComponentDescriptor; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{ SyntaxHighlighting as _, UiExt as _, icons, list_item::{self, PropertyContent}, @@ -428,7 +428,7 @@ The last rule matching `/world/house` is `+ /world/**`, so it is included. if let Some(view) = viewport.view(view_id) { ui.section_collapsing_header("Entity path filter") - .button( + .with_button( list_item::ItemActionButton::new( &re_ui::icons::EDIT, "Add new entity…", @@ -438,7 +438,7 @@ The last rule matching `/world/house` is `+ /world/**`, so it is included. ) .hover_text("Modify the entity query using the editor"), ) - .help_markdown(markdown) + .with_help_markdown(markdown) .show(ui, |ui| { // TODO(#6075): Because `list_item_scope` changes it. Temporary until everything is `ListItem`. ui.spacing_mut().item_spacing.y = ui.ctx().style().spacing.item_spacing.y; @@ -709,7 +709,7 @@ fn container_children( }; ui.section_collapsing_header("Contents") - .button( + .with_button( list_item::ItemActionButton::new(&re_ui::icons::ADD, "Add to container", || { show_add_view_or_container_modal(*container_id); }) diff --git a/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs b/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs index 90d29b96fdda..348fb3e22e00 100644 --- a/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs +++ b/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs @@ -7,6 +7,7 @@ use re_types::{ blueprint::{archetypes as blueprint_archetypes, components::VisibleTimeRange}, datatypes::{TimeInt, TimeRange, TimeRangeBoundary}, }; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{TimeDragValue, UiExt as _}; use re_viewer_context::{QueryRange, ViewClass, ViewState, ViewerContext}; use re_viewport_blueprint::{ViewBlueprint, entity_path_for_view_property}; @@ -173,7 +174,7 @@ Notes: let collapsing_response = ui .section_collapsing_header("Visible time range") .default_open(true) - .help_markdown(markdown) + .with_help_markdown(markdown) .show(ui, |ui| { ui.horizontal(|ui| { ui.re_radio_value(has_individual_time_range, false, "Default") diff --git a/crates/viewer/re_selection_panel/src/visualizer_ui.rs b/crates/viewer/re_selection_panel/src/visualizer_ui.rs index 00c1ac3dceca..cbbb60a1c86a 100644 --- a/crates/viewer/re_selection_panel/src/visualizer_ui.rs +++ b/crates/viewer/re_selection_panel/src/visualizer_ui.rs @@ -8,7 +8,7 @@ use re_log_types::{ComponentPath, EntityPath}; use re_types::blueprint::archetypes::VisualizerOverrides; use re_types::{ComponentDescriptor, reflection::ComponentDescriptorExt as _}; use re_types_core::external::arrow::array::ArrayRef; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{UiExt as _, design_tokens_of_visuals, list_item}; use re_view::latest_at_with_blueprint_resolved_data; use re_viewer_context::{ @@ -75,8 +75,8 @@ in the blueprint or in the UI by selecting the view. specific to the visualizer and the current view type."; ui.section_collapsing_header("Visualizers") - .button(button) - .help_markdown(markdown) + .with_button(button) + .with_help_markdown(markdown) .show(ui, |ui| { visualizer_ui_impl(ctx, ui, &data_result, &active_visualizers, &all_visualizers); }); diff --git a/crates/viewer/re_ui/examples/re_ui_example/main.rs b/crates/viewer/re_ui/examples/re_ui_example/main.rs index 6776cb4b4097..50ea271af458 100644 --- a/crates/viewer/re_ui/examples/re_ui_example/main.rs +++ b/crates/viewer/re_ui/examples/re_ui_example/main.rs @@ -3,6 +3,7 @@ mod hierarchical_drag_and_drop; mod right_panel; use egui::{Modifiers, os}; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{ CommandPalette, CommandPaletteAction, CommandPaletteUrl, ContextExt as _, DesignTokens, Help, IconText, UICommand, UICommandSender, UiExt as _, @@ -250,7 +251,7 @@ impl eframe::App for ExampleApp { // --- ui.section_collapsing_header("Data") - .button(list_item::ItemMenuButton::new( + .with_button(list_item::ItemMenuButton::new( &re_ui::icons::ADD, "Add", |ui| { diff --git a/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs b/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs index 3151b292bd10..c67c949709a8 100644 --- a/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs +++ b/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs @@ -1,7 +1,7 @@ use egui::Ui; use crate::{drag_and_drop, hierarchical_drag_and_drop}; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{UiExt as _, list_item}; pub struct RightPanel { diff --git a/crates/viewer/re_ui/src/list_item/item_buttons.rs b/crates/viewer/re_ui/src/list_item/item_buttons.rs index 53e0a7b51737..834907f34f98 100644 --- a/crates/viewer/re_ui/src/list_item/item_buttons.rs +++ b/crates/viewer/re_ui/src/list_item/item_buttons.rs @@ -1,10 +1,12 @@ -use crate::UiExt; +use crate::UiExt as _; use crate::list_item::ContentContext; use egui::Widget; +type ButtonFn<'a> = Box; + #[derive(Default)] pub struct ItemButtons<'a> { - buttons: Vec>, + buttons: Vec>, always_show_buttons: bool, } @@ -35,7 +37,7 @@ impl<'a> ItemButtons<'a> { self.buttons.is_empty() } - fn should_show_buttons(&self, context: &ContentContext) -> bool { + fn should_show_buttons(&self, context: &ContentContext<'_>) -> bool { // We can't use `.hovered()` or the buttons disappear just as the user clicks, // so we use `contains_pointer` instead. That also means we need to check // that we aren't dragging anything. @@ -52,7 +54,7 @@ impl<'a> ItemButtons<'a> { pub fn show_and_shrink_rect( self, ui: &mut egui::Ui, - context: &ContentContext, + context: &ContentContext<'_>, rect: &mut egui::Rect, ) { if self.buttons.is_empty() || !self.should_show_buttons(context) { @@ -111,11 +113,27 @@ where /// enclosing UI adapts to the childrens width, it will unnecessarily grow. If buttons aren't /// used, the item will only allocate the width needed for the text and icons if any. /// - A right to left layout is used, so the right-most button must be added first. + #[inline] fn with_button(mut self, button: impl Widget + 'a) -> Self { self.buttons_mut().add(button); self } + /// Add some content in the button area. + /// + /// It will be shown on the right side of the list item. + /// By default, buttons are only shown on hover or when selected, use + /// [`Self::with_always_show_buttons`] to change that. + /// + /// Usually you want to add [`crate::list_item::ItemMenuButton`]s or + /// [`crate::list_item::ItemActionButton`]s. + /// + /// Notes: + /// - If buttons are used, the item will allocate the full available width of the parent. If the + /// enclosing UI adapts to the childrens width, it will unnecessarily grow. If buttons aren't + /// used, the item will only allocate the width needed for the text and icons if any. + /// - A right to left layout is used, so the right-most button must be added first. + #[inline] fn with_buttons(mut self, buttons: impl FnOnce(&mut egui::Ui) + 'a) -> Self { self.buttons_mut().add_buttons(buttons); self @@ -125,6 +143,7 @@ where /// /// By default, buttons are only shown when the item is hovered or selected. By setting this to /// `true`, the buttons are always shown. + #[inline] fn with_always_show_buttons(mut self, always_show: bool) -> Self { self.buttons_mut().always_show_buttons = always_show; self @@ -178,4 +197,26 @@ where ) -> Self { self.with_button(super::ItemMenuButton::new(icon, alt_text, add_contents)) } + + /// Set the help text tooltip to be shown in the header. + #[inline] + fn with_help_text(self, help: impl Into + 'a) -> Self { + self.with_help_ui(|ui| { + ui.label(help); + }) + } + + /// Set the help markdown tooltip to be shown in the header. + #[inline] + fn with_help_markdown(self, help: &'a str) -> Self { + self.with_help_ui(|ui| { + ui.markdown_ui(help); + }) + } + + /// Set the help UI closure to be shown in the header. + #[inline] + fn with_help_ui(self, help: impl FnOnce(&mut egui::Ui) + 'a) -> Self { + self.with_button(|ui: &mut egui::Ui| ui.help_button(help)) + } } diff --git a/crates/viewer/re_ui/src/list_item/property_content.rs b/crates/viewer/re_ui/src/list_item/property_content.rs index 76769f4389af..8d15aecaa84d 100644 --- a/crates/viewer/re_ui/src/list_item/property_content.rs +++ b/crates/viewer/re_ui/src/list_item/property_content.rs @@ -5,7 +5,7 @@ use egui::{Align, Align2, NumExt as _, Ui, text::TextWrapping}; use crate::{Icon, UiExt as _}; use super::{ - ContentContext, DesiredWidth, ItemButtons, LabelContent, LayoutInfoStack, ListItemContent, + ContentContext, DesiredWidth, ItemButtons, LayoutInfoStack, ListItemContent, ListItemContentButtonsExt, ListVisuals, }; @@ -218,10 +218,6 @@ impl ListItemContent for PropertyContent<'_> { 0.0 }; - // Based on egui::ImageButton::ui() - let action_button_dimension = - tokens.small_icon_size.x + 2.0 * ui.spacing().button_padding.x; - let label_rect = egui::Rect::from_x_y_ranges( (content_left_x + icon_extra)..=(mid_point_x - Self::COLUMN_SPACING / 2.0), context.rect.y_range(), @@ -331,12 +327,11 @@ impl ListItemContent for PropertyContent<'_> { fn desired_width(&self, ui: &Ui) -> DesiredWidth { let layout_info = LayoutInfoStack::top(ui.ctx()); - let tokens = ui.tokens(); if crate::is_in_resizable_panel(ui) { DesiredWidth::AtLeast(self.min_desired_width) } else if let Some(max_width) = layout_info.property_content_max_width { - let mut desired_width = max_width + layout_info.left_x - ui.max_rect().left(); + let desired_width = max_width + layout_info.left_x - ui.max_rect().left(); DesiredWidth::AtLeast(desired_width.ceil()) } else { diff --git a/crates/viewer/re_ui/src/list_item/scope.rs b/crates/viewer/re_ui/src/list_item/scope.rs index 8a59b386edb5..24ef2ad44807 100644 --- a/crates/viewer/re_ui/src/list_item/scope.rs +++ b/crates/viewer/re_ui/src/list_item/scope.rs @@ -124,9 +124,6 @@ pub struct LayoutInfo { /// value. pub(crate) left_column_width: Option, - /// If true, right-aligned space should be reserved for the action button, even if not used. - pub(crate) reserve_action_button_space: bool, - /// Scope id, used to retrieve the corresponding [`LayoutStatistics`]. scope_id: egui::Id, @@ -141,7 +138,6 @@ impl Default for LayoutInfo { Self { left_x: 0.0, left_column_width: None, - reserve_action_button_space: true, scope_id: egui::Id::NULL, property_content_max_width: None, } @@ -284,7 +280,6 @@ pub fn list_item_scope( let state = LayoutInfo { left_x: ui.max_rect().left(), left_column_width, - reserve_action_button_space: layout_stats.is_action_button_used, scope_id, property_content_max_width: layout_stats.property_content_max_width, }; diff --git a/crates/viewer/re_ui/src/section_collapsing_header.rs b/crates/viewer/re_ui/src/section_collapsing_header.rs index afed1437fe61..ae6af6f51be6 100644 --- a/crates/viewer/re_ui/src/section_collapsing_header.rs +++ b/crates/viewer/re_ui/src/section_collapsing_header.rs @@ -9,10 +9,9 @@ pub struct SectionCollapsingHeader<'a> { label: egui::WidgetText, default_open: bool, buttons: ItemButtons<'a>, - help: Option>, } -impl<'a> SectionCollapsingHeader<'a> { +impl SectionCollapsingHeader<'_> { /// Create a new [`Self`]. /// /// See also [`crate::UiExt::section_collapsing_header`] @@ -21,7 +20,6 @@ impl<'a> SectionCollapsingHeader<'a> { label: label.into(), default_open: true, buttons: ItemButtons::default(), - help: None, } } @@ -34,42 +32,6 @@ impl<'a> SectionCollapsingHeader<'a> { self } - /// Set the button to be shown in the header. - #[inline] - pub fn button(mut self, button: impl egui::Widget + 'a) -> Self { - self.buttons.add(button); - self - } - - /// Set the help text tooltip to be shown in the header. - //TODO(#6191): the help button should be just another `impl ItemButton`. - #[inline] - pub fn help_text(mut self, help: impl Into) -> Self { - let help = help.into(); - self.help = Some(Box::new(move |ui| { - ui.label(help); - })); - self - } - - /// Set the help markdown tooltip to be shown in the header. - //TODO(#6191): the help button should be just another `impl ItemButton`. - #[inline] - pub fn help_markdown(mut self, help: &'a str) -> Self { - self.help = Some(Box::new(move |ui| { - ui.markdown_ui(help); - })); - self - } - - /// Set the help UI closure to be shown in the header. - //TODO(#6191): the help button should be just another `impl ItemButton`. - #[inline] - pub fn help_ui(mut self, help: impl FnOnce(&mut egui::Ui) + 'a) -> Self { - self.help = Some(Box::new(help)); - self - } - /// Display the header. pub fn show( self, @@ -80,7 +42,6 @@ impl<'a> SectionCollapsingHeader<'a> { label, default_open, buttons, - help, } = self; let id = ui.make_persistent_id(label.text()); @@ -115,3 +76,13 @@ impl<'a> SectionCollapsingHeader<'a> { } } } + +impl<'a> ListItemContentButtonsExt<'a> for SectionCollapsingHeader<'a> { + fn buttons(&self) -> &ItemButtons<'a> { + &self.buttons + } + + fn buttons_mut(&mut self) -> &mut ItemButtons<'a> { + &mut self.buttons + } +} diff --git a/crates/viewer/re_ui/tests/list_item_tests.rs b/crates/viewer/re_ui/tests/list_item_tests.rs index 2fcf3eeef067..ce0149bb19e2 100644 --- a/crates/viewer/re_ui/tests/list_item_tests.rs +++ b/crates/viewer/re_ui/tests/list_item_tests.rs @@ -1,6 +1,6 @@ use egui::Vec2; use egui_kittest::SnapshotOptions; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{UiExt as _, icons, list_item}; #[test] diff --git a/crates/viewer/re_ui/tests/modal_tests.rs b/crates/viewer/re_ui/tests/modal_tests.rs index 377eef751e84..83572798581b 100644 --- a/crates/viewer/re_ui/tests/modal_tests.rs +++ b/crates/viewer/re_ui/tests/modal_tests.rs @@ -1,6 +1,6 @@ use egui::Vec2; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::modal::{ModalHandler, ModalWrapper}; use re_ui::{UiExt as _, list_item}; diff --git a/crates/viewer/re_view/src/view_property_ui.rs b/crates/viewer/re_view/src/view_property_ui.rs index 877afe2eba38..6afe618ec135 100644 --- a/crates/viewer/re_view/src/view_property_ui.rs +++ b/crates/viewer/re_view/src/view_property_ui.rs @@ -1,6 +1,6 @@ use re_types::ComponentDescriptor; use re_types_core::{Archetype, ArchetypeReflectionMarker, reflection::ArchetypeFieldReflection}; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{UiExt as _, list_item}; use re_viewer_context::{ ComponentFallbackProvider, ComponentUiTypes, QueryContext, ViewContext, ViewerContext, diff --git a/crates/viewer/re_view_dataframe/src/view_query/ui.rs b/crates/viewer/re_view_dataframe/src/view_query/ui.rs index 4dcfe0b67f4a..f47cbc2f2e2d 100644 --- a/crates/viewer/re_view_dataframe/src/view_query/ui.rs +++ b/crates/viewer/re_view_dataframe/src/view_query/ui.rs @@ -7,7 +7,7 @@ use re_log_types::{ }; use re_sorbet::ColumnSelector; use re_types::blueprint::components; -use re_ui::list_item::ListItemContentButtonsExt; +use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{TimeDragValue, UiExt as _, list_item}; use re_viewer_context::{ViewId, ViewSystemExecutionError, ViewerContext}; use std::collections::{BTreeSet, HashSet}; From 0e28fb58db54773e34175d413267b2d00a74d1a4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 10 Sep 2025 16:10:14 +0200 Subject: [PATCH 4/5] Implement for custom content --- .../re_blueprint_tree/src/blueprint_tree.rs | 2 +- crates/viewer/re_ui/src/filter_widget.rs | 3 +- .../re_ui/src/list_item/custom_content.rs | 124 +++--------------- 3 files changed, 21 insertions(+), 108 deletions(-) diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index b1d93e9e5c7d..b14addc5538e 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -102,7 +102,7 @@ impl BlueprintTree { ); } }) - .menu_button( + .with_menu_button( &re_ui::icons::MORE, "Open menu with more options", |ui| { diff --git a/crates/viewer/re_ui/src/filter_widget.rs b/crates/viewer/re_ui/src/filter_widget.rs index 78f031cd5a04..6f59e5d4078f 100644 --- a/crates/viewer/re_ui/src/filter_widget.rs +++ b/crates/viewer/re_ui/src/filter_widget.rs @@ -4,6 +4,7 @@ use egui::{Color32, NumExt as _, Widget as _}; use itertools::Itertools as _; use smallvec::SmallVec; +use crate::list_item::ListItemContentButtonsExt; use crate::{UiExt as _, icons, list_item}; /// State for the filter widget when it is toggled on. @@ -169,7 +170,7 @@ impl FilterState { } }) .with_content_width(text_width) - .action_button( + .with_action_button( if is_searching { &icons::CLOSE } else { diff --git a/crates/viewer/re_ui/src/list_item/custom_content.rs b/crates/viewer/re_ui/src/list_item/custom_content.rs index 1df5aa91c9e4..52ec3360dedc 100644 --- a/crates/viewer/re_ui/src/list_item/custom_content.rs +++ b/crates/viewer/re_ui/src/list_item/custom_content.rs @@ -1,7 +1,9 @@ use egui::{NumExt as _, Ui}; use crate::UiExt as _; -use crate::list_item::{ContentContext, DesiredWidth, ListItemContent}; +use crate::list_item::{ + ContentContext, DesiredWidth, ItemButtons, ListItemContent, ListItemContentButtonsExt, +}; /// Control how the [`CustomContent`] advertises its width. #[derive(Debug, Clone, Copy)] @@ -26,8 +28,7 @@ pub struct CustomContent<'a> { ui: Box) + 'a>, desired_width: CustomContentDesiredWidth, - //TODO(ab): in the future, that should be a `Vec`, with some auto expanding mini-toolbar - button: Option>, + buttons: ItemButtons<'a>, } impl<'a> CustomContent<'a> { @@ -40,7 +41,7 @@ impl<'a> CustomContent<'a> { Self { ui: Box::new(ui), desired_width: Default::default(), - button: None, + buttons: ItemButtons::default(), } } @@ -58,71 +59,6 @@ impl<'a> CustomContent<'a> { self.desired_width = CustomContentDesiredWidth::ContentWidth(desired_content_width); self } - - /// Add a right-aligned [`super::ItemButton`]. - /// - /// Note: for aesthetics, space is always reserved for the action button. - // TODO(#6191): accept multiple calls for this function for multiple actions. - #[inline] - pub fn button(mut self, button: impl super::ItemButton + 'a) -> Self { - // TODO(#6191): support multiple action buttons - assert!( - self.button.is_none(), - "Only one action button is supported right now" - ); - - self.button = Some(Box::new(button)); - self - } - - /// Helper to add an [`super::ItemActionButton`] to the right of the item. - /// - /// The `alt_text` will be used for accessibility (e.g. read by screen readers), - /// and is also how we can query the button in tests. - /// - /// See [`Self::button`] for more information. - #[inline] - pub fn action_button( - self, - icon: &'static crate::icons::Icon, - alt_text: impl Into, - on_click: impl FnOnce() + 'a, - ) -> Self { - self.action_button_with_enabled(icon, alt_text, true, on_click) - } - - /// Helper to add an enabled/disabled [`super::ItemActionButton`] to the right of the item. - /// - /// The `alt_text` will be used for accessibility (e.g. read by screen readers), - /// and is also how we can query the button in tests. - /// - /// See [`Self::button`] for more information. - #[inline] - pub fn action_button_with_enabled( - self, - icon: &'static crate::icons::Icon, - alt_text: impl Into, - enabled: bool, - on_click: impl FnOnce() + 'a, - ) -> Self { - self.button(super::ItemActionButton::new(icon, alt_text, on_click).enabled(enabled)) - } - - /// Helper to add a [`super::ItemMenuButton`] to the right of the item. - /// - /// The `alt_text` will be used for accessibility (e.g. read by screen readers), - /// and is also how we can query the button in tests. - /// - /// See [`Self::button`] for more information. - #[inline] - pub fn menu_button( - self, - icon: &'static crate::icons::Icon, - alt_text: impl Into, - add_contents: impl FnOnce(&mut egui::Ui) + 'a, - ) -> Self { - self.button(super::ItemMenuButton::new(icon, alt_text, add_contents)) - } } impl ListItemContent for CustomContent<'_> { @@ -130,22 +66,11 @@ impl ListItemContent for CustomContent<'_> { let Self { ui: content_ui, desired_width: _, - button, + buttons, } = *self; - let tokens = ui.tokens(); - let button_dimension = tokens.small_icon_size.x + 2.0 * ui.spacing().button_padding.x; - - let content_width = if button.is_some() { - (context.rect.width() - button_dimension - tokens.text_to_icon_padding()).at_least(0.0) - } else { - context.rect.width() - }; - - let content_rect = egui::Rect::from_min_size( - context.rect.min, - egui::vec2(content_width, context.rect.height()), - ); + let mut content_rect = context.rect; + buttons.show_and_shrink_rect(ui, context, &mut content_rect); ui.scope_builder( egui::UiBuilder::new() @@ -160,37 +85,24 @@ impl ListItemContent for CustomContent<'_> { content_ui(ui, context); }, ); - - if let Some(button) = button { - let action_button_rect = egui::Rect::from_center_size( - context.rect.right_center() - egui::vec2(button_dimension / 2.0, 0.0), - egui::Vec2::splat(button_dimension), - ); - - // the right to left layout is used to mimic LabelContent's buttons behavior and get a - // better alignment - let mut child_ui = ui.new_child( - egui::UiBuilder::new() - .max_rect(action_button_rect) - .layout(egui::Layout::right_to_left(egui::Align::Center)), - ); - - button.ui(&mut child_ui); - } } fn desired_width(&self, ui: &Ui) -> DesiredWidth { match self.desired_width { CustomContentDesiredWidth::DesiredWidth(desired_width) => desired_width, CustomContentDesiredWidth::ContentWidth(mut content_width) => { - if self.button.is_some() { - let tokens = ui.tokens(); - content_width += tokens.small_icon_size.x - + 2.0 * ui.spacing().button_padding.x - + tokens.text_to_icon_padding(); - } DesiredWidth::AtLeast(content_width) } } } } + +impl<'a> ListItemContentButtonsExt<'a> for CustomContent<'a> { + fn buttons(&self) -> &ItemButtons<'a> { + &self.buttons + } + + fn buttons_mut(&mut self) -> &mut ItemButtons<'a> { + &mut self.buttons + } +} From 306e096d5a5025f0ad715d34a2ab02975cc2194a Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 11 Sep 2025 09:21:04 +0200 Subject: [PATCH 5/5] Try to fix buttons overflow behaviour (doesn't work yet) --- .../re_blueprint_tree/src/blueprint_tree.rs | 15 +-- .../examples/re_ui_example/right_panel.rs | 5 +- crates/viewer/re_ui/src/filter_widget.rs | 3 +- .../re_ui/src/list_item/custom_content.rs | 30 ++--- .../re_ui/src/list_item/item_buttons.rs | 113 +++++++++++------- .../re_ui/src/list_item/label_content.rs | 50 ++++---- .../re_ui/src/list_item/property_content.rs | 55 ++++----- .../viewer/re_ui/tests/filter_widget_test.rs | 11 +- crates/viewer/re_ui/tests/list_item_tests.rs | 3 +- .../re_ui/tests/snapshots/filter_widget.png | 4 +- 10 files changed, 159 insertions(+), 130 deletions(-) diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index b14addc5538e..693d11ca094d 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -102,15 +102,12 @@ impl BlueprintTree { ); } }) - .with_menu_button( - &re_ui::icons::MORE, - "Open menu with more options", - |ui| { - add_new_view_or_container_menu_button(ctx, viewport_blueprint, ui); - set_blueprint_to_default_menu_buttons(ctx, ui); - set_blueprint_to_auto_menu_button(ctx, ui); - }, - ), + .with_menu_button(&re_ui::icons::MORE, "Open menu with more options", |ui| { + add_new_view_or_container_menu_button(ctx, viewport_blueprint, ui); + set_blueprint_to_default_menu_buttons(ctx, ui); + set_blueprint_to_auto_menu_button(ctx, ui); + }) + .with_always_show_buttons(true), ); }); }); diff --git a/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs b/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs index c67c949709a8..54ff0901728a 100644 --- a/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs +++ b/crates/viewer/re_ui/examples/re_ui_example/right_panel.rs @@ -344,9 +344,10 @@ impl RightPanel { "CustomContent with an action button", ); }) - .action_button(&re_ui::icons::ADD, "Add", || { + .with_action_button(&re_ui::icons::ADD, "Add", || { re_log::warn!("Add button clicked"); - }), + }) + .with_always_show_buttons(true), ); }, ); diff --git a/crates/viewer/re_ui/src/filter_widget.rs b/crates/viewer/re_ui/src/filter_widget.rs index 6f59e5d4078f..88e1178c02f2 100644 --- a/crates/viewer/re_ui/src/filter_widget.rs +++ b/crates/viewer/re_ui/src/filter_widget.rs @@ -184,7 +184,8 @@ impl FilterState { || { toggle_search_clicked = true; }, - ), + ) + .with_always_show_buttons(true), ); }); diff --git a/crates/viewer/re_ui/src/list_item/custom_content.rs b/crates/viewer/re_ui/src/list_item/custom_content.rs index 52ec3360dedc..24223f69b72a 100644 --- a/crates/viewer/re_ui/src/list_item/custom_content.rs +++ b/crates/viewer/re_ui/src/list_item/custom_content.rs @@ -41,7 +41,7 @@ impl<'a> CustomContent<'a> { Self { ui: Box::new(ui), desired_width: Default::default(), - buttons: ItemButtons::default(), + buttons: ItemButtons::default().with_extend_on_overflow(true), } } @@ -70,21 +70,21 @@ impl ListItemContent for CustomContent<'_> { } = *self; let mut content_rect = context.rect; - buttons.show_and_shrink_rect(ui, context, &mut content_rect); + let buttons_rect = buttons.show(ui, context, content_rect, |ui| { + // When selected we override the text color so e.g. syntax highlighted code + // doesn't become unreadable + if context.visuals.selected { + ui.visuals_mut().override_text_color = Some(context.visuals.text_color()); + } + content_ui(ui, context); + }); - ui.scope_builder( - egui::UiBuilder::new() - .max_rect(content_rect) - .layout(egui::Layout::left_to_right(egui::Align::Center)), - |ui| { - // When selected we override the text color so e.g. syntax highlighted code - // doesn't become unreadable - if context.visuals.selected { - ui.visuals_mut().override_text_color = Some(context.visuals.text_color()); - } - content_ui(ui, context); - }, - ); + // context.layout_info.register_max_item_width( + // ui.ctx(), + // response.response.rect.width() + // + ui.tokens().text_to_icon_padding() + // + buttons_rect.width(), + // ) } fn desired_width(&self, ui: &Ui) -> DesiredWidth { diff --git a/crates/viewer/re_ui/src/list_item/item_buttons.rs b/crates/viewer/re_ui/src/list_item/item_buttons.rs index 834907f34f98..8a725641d065 100644 --- a/crates/viewer/re_ui/src/list_item/item_buttons.rs +++ b/crates/viewer/re_ui/src/list_item/item_buttons.rs @@ -8,6 +8,7 @@ type ButtonFn<'a> = Box; pub struct ItemButtons<'a> { buttons: Vec>, always_show_buttons: bool, + extend_on_overflow: bool, } impl Clone for ItemButtons<'_> { @@ -51,44 +52,57 @@ impl<'a> ItemButtons<'a> { || self.always_show_buttons } - pub fn show_and_shrink_rect( + pub fn show( self, ui: &mut egui::Ui, context: &ContentContext<'_>, - rect: &mut egui::Rect, - ) { - if self.buttons.is_empty() || !self.should_show_buttons(context) { - return; - } - + rect: egui::Rect, + content: impl FnOnce(&mut egui::Ui), + ) -> egui::Rect { let mut ui = ui.new_child( egui::UiBuilder::new() - .max_rect(*rect) - .layout(egui::Layout::right_to_left(egui::Align::Center)), + .max_rect(rect) + .layout(egui::Layout::left_to_right(egui::Align::Center)), ); - let tokens = ui.tokens(); - if context.list_item.selected { - // Icons and text get different colors when they are on a selected background: - let visuals = ui.visuals_mut(); - - visuals.widgets.noninteractive.weak_bg_fill = egui::Color32::TRANSPARENT; - visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; - visuals.widgets.active.weak_bg_fill = tokens.surface_on_primary_hovered; - visuals.widgets.hovered.weak_bg_fill = tokens.surface_on_primary_hovered; - - visuals.widgets.noninteractive.fg_stroke.color = tokens.icon_color_on_primary; - visuals.widgets.inactive.fg_stroke.color = tokens.icon_color_on_primary; - visuals.widgets.active.fg_stroke.color = tokens.icon_color_on_primary_hovered; - visuals.widgets.hovered.fg_stroke.color = tokens.icon_color_on_primary_hovered; - } - - for button in self.buttons { - button(&mut ui); - } - - let used_rect = ui.min_rect(); - rect.max.x -= used_rect.width() + tokens.text_to_icon_padding(); + let mut sides = egui::Sides::new() + .spacing(ui.tokens().text_to_icon_padding()) + .height(rect.height()); + if self.extend_on_overflow { + sides = sides.extend(); + } else { + sides = sides.shrink_left(); + }; + + // TODO: Properly handle empty case: + // if self.buttons.is_empty() || !self.should_show_buttons(context) { + // return egui::Rect::ZERO; + // } + + sides.show(&mut ui, content, |ui| { + let tokens = ui.tokens(); + if context.list_item.selected { + // Icons and text get different colors when they are on a selected background: + let visuals = ui.visuals_mut(); + + visuals.widgets.noninteractive.weak_bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.active.weak_bg_fill = tokens.surface_on_primary_hovered; + visuals.widgets.hovered.weak_bg_fill = tokens.surface_on_primary_hovered; + + visuals.widgets.noninteractive.fg_stroke.color = tokens.icon_color_on_primary; + visuals.widgets.inactive.fg_stroke.color = tokens.icon_color_on_primary; + visuals.widgets.active.fg_stroke.color = tokens.icon_color_on_primary_hovered; + visuals.widgets.hovered.fg_stroke.color = tokens.icon_color_on_primary_hovered; + } + + for button in self.buttons { + button(ui); + } + }); + + ui.advance_cursor_after_rect(ui.min_rect()); + ui.min_rect() } } @@ -99,6 +113,23 @@ where fn buttons(&self) -> &ItemButtons<'a>; fn buttons_mut(&mut self) -> &mut ItemButtons<'a>; + /// Always show the buttons. + /// + /// By default, buttons are only shown when the item is hovered or selected. By setting this to + /// `true`, the buttons are always shown. + #[inline] + fn with_always_show_buttons(mut self, always_show: bool) -> Self { + self.buttons_mut().always_show_buttons = always_show; + self + } + + /// Allocate more space than available if needed. + #[inline] + fn with_extend_on_overflow(mut self, extend_on_overflow: bool) -> Self { + self.buttons_mut().extend_on_overflow = extend_on_overflow; + self + } + /// Add a single widget. /// /// It will be shown on the right side of the list item. @@ -139,16 +170,6 @@ where self } - /// Always show the buttons. - /// - /// By default, buttons are only shown when the item is hovered or selected. By setting this to - /// `true`, the buttons are always shown. - #[inline] - fn with_always_show_buttons(mut self, always_show: bool) -> Self { - self.buttons_mut().always_show_buttons = always_show; - self - } - /// Helper to add an [`super::ItemActionButton`] to the right of the item. /// /// The `alt_text` will be used for accessibility (e.g. read by screen readers), @@ -220,3 +241,13 @@ where self.with_button(|ui: &mut egui::Ui| ui.help_button(help)) } } + +impl<'a> ListItemContentButtonsExt<'a> for ItemButtons<'a> { + fn buttons(&self) -> &ItemButtons<'a> { + self + } + + fn buttons_mut(&mut self) -> &mut ItemButtons<'a> { + self + } +} diff --git a/crates/viewer/re_ui/src/list_item/label_content.rs b/crates/viewer/re_ui/src/list_item/label_content.rs index ef1828394c66..4866d0253bf6 100644 --- a/crates/viewer/re_ui/src/list_item/label_content.rs +++ b/crates/viewer/re_ui/src/list_item/label_content.rs @@ -1,4 +1,4 @@ -use egui::{Align, Align2, NumExt as _, RichText, Ui, text::TextWrapping}; +use egui::{Align, Align2, NumExt as _, RichText, Ui, Widget, text::TextWrapping}; use std::sync::Arc; use super::{ @@ -200,33 +200,35 @@ impl ListItemContent for LabelContent<'_> { icon_fn(ui, icon_rect, visuals); } - buttons.show_and_shrink_rect(ui, context, &mut text_rect); + buttons.show(ui, context, text_rect, |ui| { + egui::Label::new(text).selectable(false).ui(ui); + }); - // Draw text - let mut layout_job = Arc::unwrap_or_clone(text.into_layout_job( - ui.style(), - egui::FontSelection::Default, - Align::LEFT, - )); - layout_job.wrap = TextWrapping::from_wrap_mode_and_width(text_wrap_mode, text_rect.width()); + // // Draw text + // let mut layout_job = Arc::unwrap_or_clone(text.into_layout_job( + // ui.style(), + // egui::FontSelection::Default, + // Align::LEFT, + // )); + // layout_job.wrap = TextWrapping::from_wrap_mode_and_width(text_wrap_mode, text_rect.width()); - let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + // let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); // this happens here to avoid cloning the text - context.response.widget_info(|| { - egui::WidgetInfo::selected( - egui::WidgetType::SelectableLabel, - ui.is_enabled(), - context.list_item.selected, - galley.text(), - ) - }); - - let text_pos = Align2::LEFT_CENTER - .align_size_within_rect(galley.size(), text_rect) - .min; - - ui.painter().galley(text_pos, galley, text_color); + // context.response.widget_info(|| { + // egui::WidgetInfo::selected( + // egui::WidgetType::SelectableLabel, + // ui.is_enabled(), + // context.list_item.selected, + // galley.text(), + // ) + // }); + // + // let text_pos = Align2::LEFT_CENTER + // .align_size_within_rect(galley.size(), text_rect) + // .min; + // + // ui.painter().galley(text_pos, galley, text_color); } fn desired_width(&self, ui: &Ui) -> DesiredWidth { diff --git a/crates/viewer/re_ui/src/list_item/property_content.rs b/crates/viewer/re_ui/src/list_item/property_content.rs index 8d15aecaa84d..35ffd60fb356 100644 --- a/crates/viewer/re_ui/src/list_item/property_content.rs +++ b/crates/viewer/re_ui/src/list_item/property_content.rs @@ -43,7 +43,7 @@ impl<'a> PropertyContent<'a> { icon_fn: None, show_only_when_collapsed: true, value_fn: None, - buttons: ItemButtons::default(), + buttons: ItemButtons::default().with_extend_on_overflow(true), } } @@ -223,13 +223,11 @@ impl ListItemContent for PropertyContent<'_> { context.rect.y_range(), ); - let mut value_rect = egui::Rect::from_x_y_ranges( + let value_rect = egui::Rect::from_x_y_ranges( (mid_point_x + Self::COLUMN_SPACING / 2.0)..=context.rect.right(), context.rect.y_range(), ); - buttons.show_and_shrink_rect(ui, context, &mut value_rect); - let visuals = context.visuals; // Draw icon @@ -295,34 +293,29 @@ impl ListItemContent for PropertyContent<'_> { } else { true }; - if let Some(value_fn) = value_fn - && should_show_value - { - let mut child_ui = ui.new_child( - egui::UiBuilder::new() - .max_rect(value_rect) - .layout(egui::Layout::left_to_right(egui::Align::Center)), - ); - // This sets the default text color for e.g. ui.label, but syntax highlighted - // text won't be overridden - child_ui - .visuals_mut() - .widgets - .noninteractive - .fg_stroke - .color = visuals_for_value.text_color(); - // When selected we override the text color so e.g. syntax highlighted code - // doesn't become unreadable - if context.visuals.selected { - child_ui.visuals_mut().override_text_color = Some(visuals_for_value.text_color()); - } - value_fn(&mut child_ui, visuals_for_value); - context.layout_info.register_property_content_max_width( - child_ui.ctx(), - child_ui.min_rect().right() - context.layout_info.left_x, - ); - } + let rect = buttons.show(ui, context, value_rect, |ui| { + if let Some(value_fn) = value_fn + && should_show_value + { + // This sets the default text color for e.g. ui.label, but syntax highlighted + // text won't be overridden + ui.visuals_mut().widgets.noninteractive.fg_stroke.color = + visuals_for_value.text_color(); + // When selected we override the text color so e.g. syntax highlighted code + // doesn't become unreadable + if context.visuals.selected { + ui.visuals_mut().override_text_color = Some(visuals_for_value.text_color()); + } + value_fn(ui, visuals_for_value); + + // TODO: + } + }); + context.layout_info.register_property_content_max_width( + ui.ctx(), + rect.right() - context.layout_info.left_x, + ); } fn desired_width(&self, ui: &Ui) -> DesiredWidth { diff --git a/crates/viewer/re_ui/tests/filter_widget_test.rs b/crates/viewer/re_ui/tests/filter_widget_test.rs index e1d6e0bfff3e..11d450a7a6c3 100644 --- a/crates/viewer/re_ui/tests/filter_widget_test.rs +++ b/crates/viewer/re_ui/tests/filter_widget_test.rs @@ -1,6 +1,7 @@ use egui::Vec2; use re_ui::filter_widget::FilterState; +use re_ui::list_item::list_item_scope; #[test] pub fn test_filter_widget() { @@ -10,10 +11,12 @@ pub fn test_filter_widget() { FilterState::default().section_title_ui(ui, egui::RichText::new("Small").strong()); - FilterState::default().section_title_ui( - ui, - egui::RichText::new("Expanding available width").strong(), - ); + list_item_scope(ui, "expanding", |ui| { + FilterState::default().section_title_ui( + ui, + egui::RichText::new("Expanding available width").strong(), + ); + }); ui.set_width(600.0); ui.set_max_width(600.0); diff --git a/crates/viewer/re_ui/tests/list_item_tests.rs b/crates/viewer/re_ui/tests/list_item_tests.rs index ce0149bb19e2..d73801d01ab9 100644 --- a/crates/viewer/re_ui/tests/list_item_tests.rs +++ b/crates/viewer/re_ui/tests/list_item_tests.rs @@ -215,7 +215,8 @@ pub fn test_list_items_should_match_snapshot() { "CustomContent with an action button", ); }) - .action_button(&icons::ADD, "Add", || {}), + .with_action_button(&icons::ADD, "Add", || {}) + .with_always_show_buttons(true), ); }, ); diff --git a/crates/viewer/re_ui/tests/snapshots/filter_widget.png b/crates/viewer/re_ui/tests/snapshots/filter_widget.png index ee490ffbe83d..6f25e7cba663 100644 --- a/crates/viewer/re_ui/tests/snapshots/filter_widget.png +++ b/crates/viewer/re_ui/tests/snapshots/filter_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d6ab8cf3bf12341821892f5a23235fdfc13d9f96a8e077bac8e6d1ef51b4aef -size 9812 +oid sha256:ccaeb1194f7078aa1be271796d29dae3ee9490403e23b8c32cae3e9458d9bab1 +size 9656