diff --git a/crates/viewer/re_time_panel/src/time_panel.rs b/crates/viewer/re_time_panel/src/time_panel.rs index ac17af8b167c..b6b1f3abda0e 100644 --- a/crates/viewer/re_time_panel/src/time_panel.rs +++ b/crates/viewer/re_time_panel/src/time_panel.rs @@ -2,7 +2,8 @@ use std::sync::Arc; use egui::emath::Rangef; use egui::{ - pos2, Color32, CursorIcon, NumExt, Painter, PointerButton, Rect, Response, Shape, Ui, Vec2, + pos2, Color32, CursorIcon, Modifiers, NumExt, Painter, PointerButton, Rect, Response, Shape, + Ui, Vec2, }; use re_context_menu::{context_menu_ui_for_item_with_context, SelectionUpdateBehavior}; @@ -15,7 +16,10 @@ use re_log_types::{ use re_types::blueprint::components::PanelState; use re_types_core::ComponentName; use re_ui::filter_widget::format_matching_text; -use re_ui::{filter_widget, list_item, ContextExt as _, DesignTokens, UiExt as _}; +use re_ui::{ + filter_widget, icon_text, icons, list_item, ContextExt as _, DesignTokens, Help, ModifiersText, + UiExt as _, +}; use re_viewer_context::{ CollapseScope, HoverHighlight, Item, ItemContext, RecordingConfig, TimeControl, TimeView, UiLayout, ViewerContext, VisitorControlFlow, @@ -1320,17 +1324,28 @@ fn paint_range_highlight( } fn help_button(ui: &mut egui::Ui) { - // TODO(andreas): Nicer help text like on views. - ui.help_hover_button().on_hover_text( - "\ - In the top row you can drag to move the time, or shift-drag to select a loop region.\n\ - \n\ - Drag main area to pan.\n\ - Zoom: Ctrl/cmd + scroll, or drag up/down with secondary mouse button.\n\ - Double-click to reset view.\n\ - \n\ - Press the space bar to play/pause.", - ); + ui.help_hover_button().on_hover_ui(|ui| { + Help::new("Timeline") + .control("Play/Pause", icon_text!("Space")) + .control( + "Move time cursor", + icon_text!(icons::LEFT_MOUSE_CLICK, "+ drag time scale"), + ) + .control( + "Select time segment", + icon_text!(icons::SHIFT, "+ drag time scale"), + ) + .control( + "Pan", + icon_text!(icons::LEFT_MOUSE_CLICK, "+ drag event canvas"), + ) + .control( + "Zoom", + icon_text!(ModifiersText(Modifiers::COMMAND, ui.ctx()), icons::SCROLL), + ) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) + .ui(ui); + }); } fn current_time_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui, time_ctrl: &mut TimeControl) { diff --git a/crates/viewer/re_ui/data/icons/lmc.png b/crates/viewer/re_ui/data/icons/lmc.png new file mode 100644 index 000000000000..fca721c768f0 Binary files /dev/null and b/crates/viewer/re_ui/data/icons/lmc.png differ diff --git a/crates/viewer/re_ui/data/icons/rmc.png b/crates/viewer/re_ui/data/icons/rmc.png new file mode 100644 index 000000000000..079e3dafd4a2 Binary files /dev/null and b/crates/viewer/re_ui/data/icons/rmc.png differ diff --git a/crates/viewer/re_ui/data/icons/scroll.png b/crates/viewer/re_ui/data/icons/scroll.png new file mode 100644 index 000000000000..269298813381 Binary files /dev/null and b/crates/viewer/re_ui/data/icons/scroll.png differ diff --git a/crates/viewer/re_ui/data/icons/shift.png b/crates/viewer/re_ui/data/icons/shift.png new file mode 100644 index 000000000000..514b6090f231 Binary files /dev/null and b/crates/viewer/re_ui/data/icons/shift.png differ 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 7fe4f32623d5..31fcd2807d4a 100644 --- a/crates/viewer/re_ui/examples/re_ui_example/main.rs +++ b/crates/viewer/re_ui/examples/re_ui_example/main.rs @@ -3,11 +3,11 @@ mod hierarchical_drag_and_drop; mod right_panel; use re_ui::filter_widget::format_matching_text; -use re_ui::notifications; use re_ui::{ - filter_widget::FilterState, list_item, CommandPalette, ContextExt as _, DesignTokens, + filter_widget::FilterState, list_item, CommandPalette, ContextExt as _, DesignTokens, Help, UICommand, UICommandSender, UiExt as _, }; +use re_ui::{icon_text, icons, notifications}; /// Sender that queues up the execution of a command. pub struct CommandSender(std::sync::mpsc::Sender); @@ -479,6 +479,15 @@ impl egui_tiles::Behavior for MyTileTreeBehavior { ui.label("Hover me for a tooltip") .on_hover_text("This is a tooltip"); + ui.label("Help").on_hover_ui(|ui| { + Help::new("Help example") + .docs_link("https://rerun.io/docs/reference/types/views/map_view") + .control("Pan", icon_text!(icons::LEFT_MOUSE_CLICK, "+ drag")) + .control("Zoom", icon_text!("Ctrl/Cmd +", icons::SCROLL)) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) + .ui(ui); + }); + ui.label( egui::RichText::new("Welcome to the ReUi example") .text_style(DesignTokens::welcome_screen_h1()), diff --git a/crates/viewer/re_ui/src/help.rs b/crates/viewer/re_ui/src/help.rs new file mode 100644 index 000000000000..89ec2bfa14ca --- /dev/null +++ b/crates/viewer/re_ui/src/help.rs @@ -0,0 +1,186 @@ +use crate::icon_text::{IconText, IconTextItem}; +use crate::{design_tokens, icons, ColorToken, DesignTokens, Scale, UiExt}; +use egui::{OpenUrl, RichText, Sense, TextBuffer, Ui, UiBuilder}; + +/// A help popup where you can show markdown text and controls as a table. +#[derive(Debug, Clone)] +pub struct Help<'a> { + title: String, + docs_link: Option, + sections: Vec>, +} + +/// A single section, separated by a [`egui::Separator`]. +#[derive(Debug, Clone)] +enum HelpSection<'a> { + Markdown(String), + Controls(Vec>), +} + +/// A single row in the controls table. +#[derive(Debug, Clone)] +pub struct ControlRow<'a> { + text: String, + items: IconText<'a>, +} + +impl<'a> ControlRow<'a> { + /// Create a new control row. + #[allow(clippy::needless_pass_by_value)] + pub fn new(text: impl ToString, items: IconText<'a>) -> Self { + Self { + text: text.to_string(), + items, + } + } +} + +impl<'a> Help<'a> { + /// Create a new help popup. + #[allow(clippy::needless_pass_by_value)] + pub fn new(title: impl ToString) -> Self { + Self { + title: title.to_string(), + docs_link: None, + sections: Vec::new(), + } + } + + /// Add a docs link, to be shown in the top right corner. + #[allow(clippy::needless_pass_by_value)] + #[inline] + pub fn docs_link(mut self, docs_link: impl ToString) -> Self { + self.docs_link = Some(docs_link.to_string()); + self + } + + /// Add a markdown section. + #[allow(clippy::needless_pass_by_value)] + #[inline] + pub fn markdown(mut self, markdown: impl ToString) -> Self { + self.sections + .push(HelpSection::Markdown(markdown.to_string())); + self + } + + /// Add a controls section. + #[inline] + pub fn controls(mut self, controls: Vec>) -> Self { + self.sections.push(HelpSection::Controls(controls)); + self + } + + /// Add a single control row to the last controls section. + #[allow(clippy::needless_pass_by_value)] + #[inline] + pub fn control(mut self, label: impl ToString, items: IconText<'a>) -> Self { + if let Some(HelpSection::Controls(controls)) = self.sections.last_mut() { + controls.push(ControlRow::new(label, items)); + } else { + self.sections + .push(HelpSection::Controls(vec![ControlRow::new(label, items)])); + } + self + } + + /// Create a new empty control section. + #[inline] + pub fn control_separator(mut self) -> Self { + self.sections.push(HelpSection::Controls(vec![])); + self + } + + fn separator(ui: &mut Ui) { + ui.scope(|ui| { + ui.style_mut() + .visuals + .widgets + .noninteractive + .bg_stroke + .color = design_tokens().color_table.gray(Scale::S400); + ui.separator(); + }); + } + + /// Show the help popup. Usually you want to show this in [`egui::Response::on_hover_ui`]. + pub fn ui(&self, ui: &mut Ui) { + egui::Sides::new().show( + ui, + |ui| { + ui.label(RichText::new(&self.title).strong().size(11.0)); + }, + |ui| { + if let Some(docs_link) = &self.docs_link { + // Since we are in rtl layout, we need to make our own link since the + // re_ui link icon would be reversed. + let response = ui + .scope_builder(UiBuilder::new().sense(Sense::click()), |ui| { + ui.spacing_mut().item_spacing.x = 2.0; + let hovered = ui.response().hovered(); + + let tint = design_tokens().color(ColorToken::gray(if hovered { + Scale::S900 + } else { + Scale::S700 + })); + + ui.label(RichText::new("Docs").color(tint).size(11.0)); + + ui.small_icon(&icons::EXTERNAL_LINK, Some(tint)); + }) + .response; + + if response.clicked() { + ui.ctx().open_url(OpenUrl::new_tab(docs_link)); + } + } + }, + ); + + for section in &self.sections { + Self::separator(ui); + match section { + HelpSection::Markdown(md) => { + ui.markdown_ui(md); + } + HelpSection::Controls(controls) => { + for row in controls { + egui::Sides::new().spacing(8.0).show( + ui, + |ui| { + ui.strong(RichText::new(&row.text).size(11.0)); + }, + |ui| { + ui.set_height(DesignTokens::small_icon_size().y); + for item in row.items.0.iter().rev() { + match item { + IconTextItem::Icon(icon) => { + ui.small_icon( + icon, + Some( + design_tokens() + .color(ColorToken::gray(Scale::S700)), + ), + ); + } + IconTextItem::Text(text) => { + ui.label( + RichText::new(text.as_str()) + .monospace() + .size(11.0) + .color( + design_tokens() + .color(ColorToken::gray(Scale::S700)), + ), + ); + } + } + } + }, + ); + } + } + } + } + } +} diff --git a/crates/viewer/re_ui/src/icon_text.rs b/crates/viewer/re_ui/src/icon_text.rs new file mode 100644 index 000000000000..859e2b0f2405 --- /dev/null +++ b/crates/viewer/re_ui/src/icon_text.rs @@ -0,0 +1,115 @@ +use crate::{icons, Icon}; +use egui::{ModifierNames, Modifiers}; +use std::borrow::Cow; + +#[derive(Debug, Clone)] +pub enum IconTextItem<'a> { + Icon(Icon), + Text(Cow<'a, str>), +} + +impl<'a> IconTextItem<'a> { + pub fn icon(icon: Icon) -> Self { + Self::Icon(icon) + } + + pub fn text(text: impl Into>) -> Self { + Self::Text(text.into()) + } +} + +/// Helper to show text with icons in a row. +/// Usually created via the [`crate::icon_text!`] macro. +#[derive(Default, Debug, Clone)] +pub struct IconText<'a>(pub Vec>); + +impl<'a> IconText<'a> { + /// Create a new, empty `IconText`. + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Add an icon to the row. + pub fn icon(&mut self, icon: Icon) { + self.0.push(IconTextItem::Icon(icon)); + } + + /// Add text to the row. + pub fn text(&mut self, text: impl Into>) { + self.0.push(IconTextItem::Text(text.into())); + } + + /// Add an item to the row. + pub fn add(&mut self, item: impl Into>) { + self.0.push(item.into()); + } +} + +impl<'a> From for IconTextItem<'a> { + fn from(icon: Icon) -> Self { + IconTextItem::Icon(icon) + } +} + +impl<'a> From<&'a str> for IconTextItem<'a> { + fn from(text: &'a str) -> Self { + IconTextItem::Text(text.into()) + } +} + +impl<'a> From for IconTextItem<'a> { + fn from(text: String) -> Self { + IconTextItem::Text(text.into()) + } +} + +/// Create an [`IconText`] with the given items. +#[macro_export] +macro_rules! icon_text { + ($($item:expr),* $(,)?) => {{ + let mut icon_text = $crate::IconText::new(); + $(icon_text.add($item);)* + icon_text + }}; +} + +/// Helper to add [`egui::Modifiers`] as text with icons. +/// Will automatically show Cmd/Ctrl based on the OS. +pub struct ModifiersText<'a>(pub Modifiers, pub &'a egui::Context); + +impl<'a> From> for IconTextItem<'static> { + fn from(value: ModifiersText<'a>) -> Self { + let ModifiersText(modifiers, ctx) = value; + + let is_mac = matches!( + ctx.os(), + egui::os::OperatingSystem::Mac | egui::os::OperatingSystem::IOS + ); + + let mut names = ModifierNames::NAMES; + names.concat = " + "; + let text = names.format(&modifiers, is_mac); + + // Only shift has an icon for now + if text == "Shift" { + IconTextItem::Icon(icons::SHIFT) + } else { + IconTextItem::text(text) + } + } +} + +/// Helper to show mouse buttons as text/icons. +pub struct MouseButtonText(pub egui::PointerButton); + +impl From for IconTextItem<'static> { + fn from(value: MouseButtonText) -> Self { + match value.0 { + egui::PointerButton::Primary => IconTextItem::icon(icons::LEFT_MOUSE_CLICK), + egui::PointerButton::Secondary => IconTextItem::icon(icons::RIGHT_MOUSE_CLICK), + egui::PointerButton::Middle => IconTextItem::text("middle mouse button"), + egui::PointerButton::Extra1 => IconTextItem::text("extra 1 mouse button"), + egui::PointerButton::Extra2 => IconTextItem::text("extra 2 mouse button"), + } + } +} diff --git a/crates/viewer/re_ui/src/icons.rs b/crates/viewer/re_ui/src/icons.rs index f012e7f98a9b..fd7e0183e3a0 100644 --- a/crates/viewer/re_ui/src/icons.rs +++ b/crates/viewer/re_ui/src/icons.rs @@ -138,3 +138,9 @@ pub const DND_MOVE: Icon = icon_from_path!("../data/icons/dnd_move.png"); pub const BREADCRUMBS_SEPARATOR: Icon = icon_from_path!("../data/icons/breadcrumbs_separator.png"); pub const SEARCH: Icon = icon_from_path!("../data/icons/search.png"); + +/// Shortcut icons +pub const LEFT_MOUSE_CLICK: Icon = icon_from_path!("../data/icons/lmc.png"); +pub const RIGHT_MOUSE_CLICK: Icon = icon_from_path!("../data/icons/rmc.png"); +pub const SCROLL: Icon = icon_from_path!("../data/icons/scroll.png"); +pub const SHIFT: Icon = icon_from_path!("../data/icons/shift.png"); diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index 96cb28147f4f..4a602c51bdad 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -7,6 +7,8 @@ mod context_ext; mod design_tokens; pub mod drag_and_drop; pub mod filter_widget; +mod help; +mod icon_text; pub mod icons; pub mod list_item; mod markdown_utils; @@ -26,6 +28,8 @@ pub use self::{ command_palette::CommandPalette, context_ext::ContextExt, design_tokens::DesignTokens, + help::*, + icon_text::*, icons::Icon, markdown_utils::*, section_collapsing_header::SectionCollapsingHeader, diff --git a/crates/viewer/re_view_bar_chart/src/view_class.rs b/crates/viewer/re_view_bar_chart/src/view_class.rs index cb18863be2fc..95a9384d6c43 100644 --- a/crates/viewer/re_view_bar_chart/src/view_class.rs +++ b/crates/viewer/re_view_bar_chart/src/view_class.rs @@ -4,11 +4,8 @@ use re_types::blueprint::archetypes::PlotLegend; use re_types::blueprint::components::{Corner2D, Visible}; use re_types::View; use re_types::{datatypes::TensorBuffer, ViewClassIdentifier}; -use re_ui::{list_item, ModifiersMarkdown, MouseButtonMarkdown}; -use re_view::controls::{ - ASPECT_SCROLL_MODIFIER, HORIZONTAL_SCROLL_MODIFIER, SELECTION_RECT_ZOOM_BUTTON, - ZOOM_SCROLL_MODIFIER, -}; +use re_ui::{icon_text, icons, list_item, Help, ModifiersText, MouseButtonText}; +use re_view::controls::{ASPECT_SCROLL_MODIFIER, SELECTION_RECT_ZOOM_BUTTON, ZOOM_SCROLL_MODIFIER}; use re_view::{controls, suggest_view_for_each_entity, view_property_ui}; use re_viewer_context::{ IdentifiedViewSystem as _, IndicatedEntities, MaybeVisualizableEntities, PerVisualizer, @@ -41,24 +38,31 @@ impl ViewClass for BarChartView { Box::<()>::default() } - fn help_markdown(&self, egui_ctx: &egui::Context) -> String { - format!( - "# Bar chart view - -Display a 1D tensor as a bar chart. - -## Navigation controls - -- Pan by dragging, or scroll (+{horizontal_scroll_modifier} for horizontal). -- Zoom with pinch gesture or scroll + {zoom_scroll_modifier}. -- Scroll + {aspect_scroll_modifier} to zoom only the temporal axis while holding the y-range fixed. -- Drag with the {selection_rect_zoom_button} to zoom in/out using a selection. -- Double-click to reset the view.", - horizontal_scroll_modifier = ModifiersMarkdown(HORIZONTAL_SCROLL_MODIFIER, egui_ctx), - zoom_scroll_modifier = ModifiersMarkdown(ZOOM_SCROLL_MODIFIER, egui_ctx), - aspect_scroll_modifier = ModifiersMarkdown(ASPECT_SCROLL_MODIFIER, egui_ctx), - selection_rect_zoom_button = MouseButtonMarkdown(SELECTION_RECT_ZOOM_BUTTON), - ) + fn help(&self, egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Bar chart view") + .docs_link("https://rerun.io/docs/reference/types/views/bar_chart_view") + .control("Pan", icon_text!(icons::LEFT_MOUSE_CLICK, "+ drag")) + .control( + "Zoom", + icon_text!( + ModifiersText(ZOOM_SCROLL_MODIFIER, egui_ctx), + "+", + icons::SCROLL + ), + ) + .control( + "Zoom only x-axis", + icon_text!( + ModifiersText(ASPECT_SCROLL_MODIFIER, egui_ctx), + "+", + icons::SCROLL + ), + ) + .control( + "Zoom to selection", + icon_text!(MouseButtonText(SELECTION_RECT_ZOOM_BUTTON), "+ drag"), + ) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) } fn on_register( diff --git a/crates/viewer/re_view_dataframe/src/view_class.rs b/crates/viewer/re_view_dataframe/src/view_class.rs index 7cd7643f7ea0..f9496e7b24c8 100644 --- a/crates/viewer/re_view_dataframe/src/view_class.rs +++ b/crates/viewer/re_view_dataframe/src/view_class.rs @@ -4,7 +4,7 @@ use re_chunk_store::{ColumnDescriptor, SparseFillStrategy}; use re_dataframe::QueryEngine; use re_log_types::EntityPath; use re_types_core::ViewClassIdentifier; -use re_ui::UiExt; +use re_ui::{Help, UiExt}; use re_viewer_context::{ Item, SystemExecutionOutput, ViewClass, ViewClassRegistryError, ViewId, ViewQuery, ViewState, ViewStateExt, ViewSystemExecutionError, ViewerContext, @@ -50,26 +50,17 @@ impl ViewClass for DataframeView { &re_ui::icons::VIEW_DATAFRAME } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "# Dataframe view + fn help(&self, _egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Dataframe view") + .docs_link("https://rerun.io/docs/reference/types/views/dataframe_view") + .markdown( + "This view displays entity content in a tabular form. -This view displays the content of the entities it contains in tabular form. - -## View types - -The Dataframe view operates in two modes: the _latest-at_ mode and the _time range_ mode. You can -select the mode in the selection panel. - -In the _latest-at_ mode, the view displays the latest data for the timeline and time set in the time -panel. A row is shown for each entity instance. - -In the _time range_ mode, the view displays all the data logged within the time range set for each -view entity. In this mode, each row corresponds to an entity and time pair. Rows are further split -if multiple `rr.log()` calls were made for the same entity/time. Static data is also displayed. - -Note that the default visible time range depends on the selected mode. In particular, the time range -mode sets the default time range to _everything_. You can override this in the selection panel." - .to_owned() +Configure in the selection panel: + - Handling of empty cells + - Column visibility + - Row filtering by time range", + ) } fn on_register( diff --git a/crates/viewer/re_view_graph/src/view.rs b/crates/viewer/re_view_graph/src/view.rs index 5d6a85aa2ece..d2aac8c500f0 100644 --- a/crates/viewer/re_view_graph/src/view.rs +++ b/crates/viewer/re_view_graph/src/view.rs @@ -9,7 +9,7 @@ use re_types::{ }, ViewClassIdentifier, }; -use re_ui::{self, ModifiersMarkdown, MouseButtonMarkdown, UiExt as _}; +use re_ui::{self, icon_text, icons, Help, ModifiersText, MouseButtonText, UiExt as _}; use re_view::{ controls::{DRAG_PAN2D_BUTTON, ZOOM_SCROLL_MODIFIER}, view_property_ui, @@ -46,19 +46,18 @@ impl ViewClass for GraphView { &re_ui::icons::VIEW_GRAPH } - fn help_markdown(&self, egui_ctx: &egui::Context) -> String { - format!( - r"# Graph View - -Display a graph of nodes and edges. - -## Navigation controls -- Pinch gesture or {zoom_scroll_modifier} + scroll to zoom. -- Click and drag with the {drag_pan2d_button} to pan. -- Double-click to reset the view.", - zoom_scroll_modifier = ModifiersMarkdown(ZOOM_SCROLL_MODIFIER, egui_ctx), - drag_pan2d_button = MouseButtonMarkdown(DRAG_PAN2D_BUTTON), - ) + fn help(&self, egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Graph view") + .docs_link("https://rerun.io/docs/reference/types/views/graph_view") + .control( + "Pan", + icon_text!(MouseButtonText(DRAG_PAN2D_BUTTON), "+ drag"), + ) + .control( + "Zoom", + icon_text!(ModifiersText(ZOOM_SCROLL_MODIFIER, egui_ctx), icons::SCROLL), + ) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) } /// Register all systems (contexts & parts) that the view needs. diff --git a/crates/viewer/re_view_map/src/map_view.rs b/crates/viewer/re_view_map/src/map_view.rs index a07938ac841e..abc2af7fa977 100644 --- a/crates/viewer/re_view_map/src/map_view.rs +++ b/crates/viewer/re_view_map/src/map_view.rs @@ -1,4 +1,4 @@ -use egui::{Context, NumExt as _, Rect, Response}; +use egui::{Context, Modifiers, NumExt as _, Rect, Response}; use re_view::AnnotationSceneContext; use walkers::{HttpTiles, Map, MapMemory, Tiles}; @@ -14,7 +14,7 @@ use re_types::{ }, View, ViewClassIdentifier, }; -use re_ui::list_item; +use re_ui::{icon_text, icons, list_item, Help, ModifiersText}; use re_viewer_context::{ gpu_bridge, IdentifiedViewSystem as _, Item, SystemExecutionOutput, UiLayout, ViewClass, ViewClassLayoutPriority, ViewClassRegistryError, ViewHighlights, ViewId, ViewQuery, @@ -102,17 +102,19 @@ impl ViewClass for MapView { &re_ui::icons::VIEW_MAP } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "# Map view - -Displays geospatial primitives on a map. - -## Navigation controls - -- Pan by dragging. -- Zoom with pinch gesture. -- Double-click to reset the view." - .to_owned() + fn help(&self, egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Map view") + .docs_link("https://rerun.io/docs/reference/types/views/map_view") + .control("Pan", icon_text!(icons::LEFT_MOUSE_CLICK, "+ drag")) + .control( + "Zoom", + icon_text!( + ModifiersText(Modifiers::COMMAND, egui_ctx), + "+", + icons::SCROLL + ), + ) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) } fn on_register( diff --git a/crates/viewer/re_view_spatial/src/ui_2d.rs b/crates/viewer/re_view_spatial/src/ui_2d.rs index 3afcda0fa952..3e264384c4af 100644 --- a/crates/viewer/re_view_spatial/src/ui_2d.rs +++ b/crates/viewer/re_view_spatial/src/ui_2d.rs @@ -8,7 +8,7 @@ use re_types::blueprint::{ archetypes::{Background, NearClipPlane, VisualBounds2D}, components as blueprint_components, }; -use re_ui::{ContextExt as _, ModifiersMarkdown, MouseButtonMarkdown}; +use re_ui::{icon_text, icons, ContextExt as _, Help, ModifiersText, MouseButtonText}; use re_view::controls::{DRAG_PAN2D_BUTTON, ZOOM_SCROLL_MODIFIER}; use re_viewer_context::{ gpu_bridge, ItemContext, ViewQuery, ViewSystemExecutionError, ViewerContext, @@ -113,20 +113,22 @@ fn scale_rect(rect: Rect, factor: Vec2) -> Rect { ) } -pub fn help_markdown(egui_ctx: &egui::Context) -> String { - format!( - "# 2D View - -Display 2D content in the reference frame defined by the space origin. - -## Navigation controls -- Pinch gesture or {zoom_scroll_modifier} + scroll to zoom. -- Click and drag with the {drag_pan2d_button} to pan. -- Double-click to reset the view.", - zoom_scroll_modifier = ModifiersMarkdown(ZOOM_SCROLL_MODIFIER, egui_ctx), - drag_pan2d_button = MouseButtonMarkdown(DRAG_PAN2D_BUTTON), - ) - .to_owned() +pub fn help(egui_ctx: &egui::Context) -> Help<'static> { + Help::new("2D view") + .docs_link("https://rerun.io/docs/reference/types/views/spatial2d_view") + .control( + "Pan", + icon_text!(MouseButtonText(DRAG_PAN2D_BUTTON), "+ drag"), + ) + .control( + "Zoom", + icon_text!( + ModifiersText(ZOOM_SCROLL_MODIFIER, egui_ctx), + "+", + icons::SCROLL + ), + ) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) } /// Create the outer 2D view, which consists of a scrollable region diff --git a/crates/viewer/re_view_spatial/src/ui_3d.rs b/crates/viewer/re_view_spatial/src/ui_3d.rs index a5e1b6d8321a..b3bd3a355bb9 100644 --- a/crates/viewer/re_view_spatial/src/ui_3d.rs +++ b/crates/viewer/re_view_spatial/src/ui_3d.rs @@ -16,10 +16,10 @@ use re_types::{ components::ViewCoordinates, view_coordinates::SignedAxis3, }; -use re_ui::{ContextExt, ModifiersMarkdown, MouseButtonMarkdown}; +use re_ui::{icon_text, icons, ContextExt, Help, ModifiersText, MouseButtonText}; use re_view::controls::{ - RuntimeModifiers, DRAG_PAN3D_BUTTON, ROLL_MOUSE, ROLL_MOUSE_ALT, ROLL_MOUSE_MODIFIER, - ROTATE3D_BUTTON, SPEED_UP_3D_MODIFIER, TRACKED_OBJECT_RESTORE_KEY, + RuntimeModifiers, DRAG_PAN3D_BUTTON, ROLL_MOUSE_ALT, ROLL_MOUSE_MODIFIER, ROTATE3D_BUTTON, + SPEED_UP_3D_MODIFIER, TRACKED_OBJECT_RESTORE_KEY, }; use re_viewer_context::{ gpu_bridge, Item, ItemContext, ViewQuery, ViewSystemExecutionError, ViewerContext, @@ -386,36 +386,43 @@ fn find_camera(space_cameras: &[SpaceCamera3D], needle: &EntityPath) -> Option String { - // TODO(#6876): this line was removed to from the help text, because the corresponding feature no - // longer works. To be restored when it works again (or deleted forever). - /* - Reset the view again with {TRACKED_OBJECT_RESTORE_KEY}.*/ - - format!( - "# 3D view - -Display 3D content in the reference frame defined by the space origin. - -## Navigation controls - -- Click and drag the {rotate3d_button} to rotate. -- Click and drag with the {drag_pan3d_button} to pan. -- Drag with the {roll_mouse} (or the {roll_mouse_alt} + holding {roll_mouse_modifier}) to roll the view. -- Scroll or pinch to zoom. -- While hovering the 3D view, navigate with the `WASD` and `QE` keys. -- {slow_down} slows down, {speed_up_3d_modifier} speeds up. -- Double-click an object to focus the view on it. -- Double-click on an empty space to reset the view.", - rotate3d_button = MouseButtonMarkdown(ROTATE3D_BUTTON), - drag_pan3d_button = MouseButtonMarkdown(DRAG_PAN3D_BUTTON), - roll_mouse = MouseButtonMarkdown(ROLL_MOUSE), - roll_mouse_alt = MouseButtonMarkdown(ROLL_MOUSE_ALT), - roll_mouse_modifier = ModifiersMarkdown(ROLL_MOUSE_MODIFIER, egui_ctx), - slow_down = ModifiersMarkdown(RuntimeModifiers::slow_down(&egui_ctx.os()), egui_ctx), - speed_up_3d_modifier = ModifiersMarkdown(SPEED_UP_3D_MODIFIER, egui_ctx), - // TODO(#6876): see above - /*TRACKED_OBJECT_RESTORE_KEY = KeyMarkdown(TRACKED_OBJECT_RESTORE_KEY),*/ - ) +pub fn help(egui_ctx: &egui::Context) -> Help<'static> { + Help::new("3D view") + .docs_link("https://rerun.io/docs/reference/types/views/spatial3d_view") + .control( + "Pan", + icon_text!(MouseButtonText(DRAG_PAN3D_BUTTON), "+ drag"), + ) + .control("Zoom", icon_text!(icons::SCROLL)) + .control( + "Rotate", + icon_text!(MouseButtonText(ROTATE3D_BUTTON), "+ drag"), + ) + .control( + "Roll", + icon_text!( + MouseButtonText(ROLL_MOUSE_ALT), + "+", + ModifiersText(ROLL_MOUSE_MODIFIER, egui_ctx) + ), + ) + .control("Navigate", icon_text!("WASD / QE")) + .control( + "Slow down / speed up", + icon_text!( + ModifiersText(RuntimeModifiers::slow_down(&egui_ctx.os()), egui_ctx), + "/", + ModifiersText(SPEED_UP_3D_MODIFIER, egui_ctx) + ), + ) + .control( + "Focus", + icon_text!("double", icons::LEFT_MOUSE_CLICK, "object"), + ) + .control( + "Reset view", + icon_text!("double", icons::LEFT_MOUSE_CLICK, "background"), + ) } impl SpatialView3D { diff --git a/crates/viewer/re_view_spatial/src/view_2d.rs b/crates/viewer/re_view_spatial/src/view_2d.rs index 3a33f48b884f..6fe8f4dd26d0 100644 --- a/crates/viewer/re_view_spatial/src/view_2d.rs +++ b/crates/viewer/re_view_spatial/src/view_2d.rs @@ -9,7 +9,7 @@ use re_types::{ blueprint::archetypes::{Background, NearClipPlane, VisualBounds2D}, ComponentName, ViewClassIdentifier, }; -use re_ui::UiExt as _; +use re_ui::{Help, UiExt as _}; use re_view::view_property_ui; use re_viewer_context::{ RecommendedView, ViewClass, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, @@ -58,8 +58,8 @@ impl ViewClass for SpatialView2D { &re_ui::icons::VIEW_2D } - fn help_markdown(&self, egui_ctx: &egui::Context) -> String { - super::ui_2d::help_markdown(egui_ctx) + fn help(&self, egui_ctx: &egui::Context) -> Help<'_> { + super::ui_2d::help(egui_ctx) } fn on_register( diff --git a/crates/viewer/re_view_spatial/src/view_3d.rs b/crates/viewer/re_view_spatial/src/view_3d.rs index 919012d003af..53af7eccff00 100644 --- a/crates/viewer/re_view_spatial/src/view_3d.rs +++ b/crates/viewer/re_view_spatial/src/view_3d.rs @@ -9,7 +9,7 @@ use re_types::{ blueprint::archetypes::Background, components::ViewCoordinates, Component, View, ViewClassIdentifier, }; -use re_ui::{list_item, UiExt as _}; +use re_ui::{list_item, Help, UiExt as _}; use re_view::view_property_ui; use re_viewer_context::{ IdentifiedViewSystem, IndicatedEntities, MaybeVisualizableEntities, PerVisualizer, @@ -60,8 +60,8 @@ impl ViewClass for SpatialView3D { &re_ui::icons::VIEW_3D } - fn help_markdown(&self, egui_ctx: &egui::Context) -> String { - super::ui_3d::help_markdown(egui_ctx) + fn help(&self, egui_ctx: &egui::Context) -> Help<'_> { + super::ui_3d::help(egui_ctx) } fn new_state(&self) -> Box { diff --git a/crates/viewer/re_view_tensor/src/view_class.rs b/crates/viewer/re_view_tensor/src/view_class.rs index 037420102362..73c616a1a765 100644 --- a/crates/viewer/re_view_tensor/src/view_class.rs +++ b/crates/viewer/re_view_tensor/src/view_class.rs @@ -12,7 +12,7 @@ use re_types::{ datatypes::TensorData, View, ViewClassIdentifier, }; -use re_ui::{list_item, UiExt as _}; +use re_ui::{list_item, Help, UiExt as _}; use re_view::{suggest_view_for_each_entity, view_property_ui}; use re_viewer_context::{ gpu_bridge, ColormapWithRange, IdentifiedViewSystem as _, IndicatedEntities, @@ -64,13 +64,14 @@ impl ViewClass for TensorView { &re_ui::icons::VIEW_TENSOR } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "# Tensor view + fn help(&self, _egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Tensor view") + .docs_link("https://rerun.io/docs/reference/types/views/tensor_view") + .markdown( + "An N-dimensional tensor displayed as a 2D slice with a custom colormap. -Display an N-dimensional tensor as an arbitrary 2D slice with custom colormap. - -Note: select the view to configure which dimensions are shown." - .to_owned() +Set the displayed dimensions in a selection panel.", + ) } fn on_register( diff --git a/crates/viewer/re_view_text_document/src/view_class.rs b/crates/viewer/re_view_text_document/src/view_class.rs index dfd76653bce3..c745c015cbf6 100644 --- a/crates/viewer/re_view_text_document/src/view_class.rs +++ b/crates/viewer/re_view_text_document/src/view_class.rs @@ -3,6 +3,7 @@ use egui::Label; use egui::Sense; use re_types::View; use re_types::ViewClassIdentifier; +use re_ui::Help; use re_ui::UiExt as _; use re_view::suggest_view_for_each_entity; @@ -60,11 +61,10 @@ impl ViewClass for TextDocumentView { &re_ui::icons::VIEW_TEXT } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "# Text document view - -Displays text from a text component, as raw text or markdown." - .to_owned() + fn help(&self, _egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Text document view") + .docs_link("https://rerun.io/docs/reference/types/views/text_document_view") + .markdown("Supports raw text and markdown.") } fn on_register( diff --git a/crates/viewer/re_view_text_log/src/view_class.rs b/crates/viewer/re_view_text_log/src/view_class.rs index 7e9aba2fc536..3bf7b5a474a0 100644 --- a/crates/viewer/re_view_text_log/src/view_class.rs +++ b/crates/viewer/re_view_text_log/src/view_class.rs @@ -4,7 +4,7 @@ use re_data_ui::item_ui; use re_log_types::{EntityPath, Timeline}; use re_types::View; use re_types::{components::TextLogLevel, ViewClassIdentifier}; -use re_ui::UiExt as _; +use re_ui::{Help, UiExt as _}; use re_viewer_context::{ level_to_rich_text, IdentifiedViewSystem as _, ViewClass, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewStateExt, ViewSystemExecutionError, @@ -55,13 +55,14 @@ impl ViewClass for TextView { &re_ui::icons::VIEW_LOG } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "# Text log view + fn help(&self, _egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Text log view") + .docs_link("https://rerun.io/docs/reference/types/views/text_log") + .markdown( + "TextLog entries over time. -Shows `TextLog` entries over time. - -Note: select the View for filtering options." - .to_owned() +Filter message types and toggle column visibility in a selection panel.", + ) } fn on_register( diff --git a/crates/viewer/re_view_time_series/src/view_class.rs b/crates/viewer/re_view_time_series/src/view_class.rs index 9b3b5d6bf2c2..fa8d5cc09a9d 100644 --- a/crates/viewer/re_view_time_series/src/view_class.rs +++ b/crates/viewer/re_view_time_series/src/view_class.rs @@ -9,10 +9,10 @@ use re_types::blueprint::archetypes::{PlotLegend, ScalarAxis}; use re_types::blueprint::components::{Corner2D, LockRangeDuringZoom, Visible}; use re_types::components::AggregationPolicy; use re_types::{components::Range1D, datatypes::TimeRange, View, ViewClassIdentifier}; -use re_ui::{list_item, ModifiersMarkdown, MouseButtonMarkdown, UiExt as _}; +use re_ui::{icon_text, icons, list_item, Help, ModifiersText, MouseButtonText, UiExt as _}; use re_view::controls::{ - ASPECT_SCROLL_MODIFIER, HORIZONTAL_SCROLL_MODIFIER, MOVE_TIME_CURSOR_BUTTON, - SELECTION_RECT_ZOOM_BUTTON, ZOOM_SCROLL_MODIFIER, + ASPECT_SCROLL_MODIFIER, MOVE_TIME_CURSOR_BUTTON, SELECTION_RECT_ZOOM_BUTTON, + ZOOM_SCROLL_MODIFIER, }; use re_view::{controls, view_property_ui}; use re_viewer_context::{ @@ -99,32 +99,49 @@ impl ViewClass for TimeSeriesView { &re_ui::icons::VIEW_TIMESERIES } - fn help_markdown(&self, egui_ctx: &egui::Context) -> String { - format!( - "# Time series view - -Display time series data in a plot. - -## Navigation controls - -- Pan by dragging, or scroll (+{horizontal_scroll_modifier} for horizontal). -- Zoom with pinch gesture or scroll + {zoom_scroll_modifier}. -- Scroll + {aspect_scroll_modifier} to zoom only the temporal axis while holding the y-range fixed. -- Drag with the {selection_rect_zoom_button} to zoom in/out using a selection. -- Click the {move_time_cursor_button} to move the time cursor. -- Double-click to reset the view. - -## Legend interactions - -- Click on a series in the legend to show/hide it. -- {alt_modifier}-Click on a series to show/hide all other series.", - horizontal_scroll_modifier = ModifiersMarkdown(HORIZONTAL_SCROLL_MODIFIER, egui_ctx), - zoom_scroll_modifier = ModifiersMarkdown(ZOOM_SCROLL_MODIFIER, egui_ctx), - aspect_scroll_modifier = ModifiersMarkdown(ASPECT_SCROLL_MODIFIER, egui_ctx), - selection_rect_zoom_button = MouseButtonMarkdown(SELECTION_RECT_ZOOM_BUTTON), - move_time_cursor_button = MouseButtonMarkdown(MOVE_TIME_CURSOR_BUTTON), - alt_modifier = ModifiersMarkdown(egui::Modifiers::ALT, egui_ctx), - ) + fn help(&self, egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Time series view") + .docs_link("https://rerun.io/docs/reference/types/views/time_series_view") + .control("Pan", icon_text!(icons::LEFT_MOUSE_CLICK, "+ drag")) + .control( + "Zoom", + icon_text!( + ModifiersText(ZOOM_SCROLL_MODIFIER, egui_ctx), + "+", + icons::SCROLL + ), + ) + .control( + "Zoom only x-axis", + icon_text!( + ModifiersText(ASPECT_SCROLL_MODIFIER, egui_ctx), + "+", + icons::SCROLL + ), + ) + .control( + "Zoom to selection", + icon_text!(MouseButtonText(SELECTION_RECT_ZOOM_BUTTON), "+ drag"), + ) + .control( + "Move time cursor", + icon_text!(MouseButtonText(MOVE_TIME_CURSOR_BUTTON)), + ) + .control("Reset view", icon_text!("double", icons::LEFT_MOUSE_CLICK)) + .control_separator() + .control( + "Hide/show series", + icon_text!(icons::LEFT_MOUSE_CLICK, "legend"), + ) + .control( + "Hide/show other series", + icon_text!( + ModifiersText(egui::Modifiers::ALT, egui_ctx), + "+", + icons::LEFT_MOUSE_CLICK, + "legend" + ), + ) } fn on_register( diff --git a/crates/viewer/re_viewer_context/src/view/view_class.rs b/crates/viewer/re_viewer_context/src/view/view_class.rs index 1f9f2ab61f4b..904df56ebdb6 100644 --- a/crates/viewer/re_viewer_context/src/view/view_class.rs +++ b/crates/viewer/re_viewer_context/src/view/view_class.rs @@ -62,8 +62,7 @@ pub trait ViewClass: Send + Sync { &re_ui::icons::VIEW_GENERIC } - /// Help text describing how to interact with this view in the ui. - fn help_markdown(&self, egui_ctx: &egui::Context) -> String; + fn help(&self, egui_ctx: &egui::Context) -> re_ui::Help<'_>; /// Called once upon registration of the class /// diff --git a/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs b/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs index eb5020bca90a..5a99752432d5 100644 --- a/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs +++ b/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs @@ -1,5 +1,5 @@ use re_types::ViewClassIdentifier; -use re_ui::UiExt; +use re_ui::{Help, UiExt}; use crate::{ SystemExecutionOutput, ViewClass, ViewClassRegistryError, ViewQuery, ViewSpawnHeuristics, @@ -23,8 +23,8 @@ impl ViewClass for ViewClassPlaceholder { &re_ui::icons::VIEW_UNKNOWN } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "Placeholder view for unknown view class".to_owned() + fn help(&self, _egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Placeholder view").markdown("Placeholder view for unknown view class") } fn on_register( diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index a7c77a2e84fc..9040e8f8c024 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -572,9 +572,8 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { ); }); - let help_markdown = view_class.help_markdown(self.ctx.egui_ctx); ui.help_hover_button().on_hover_ui(|ui| { - ui.markdown_ui(&help_markdown); + view_class.help(ui.ctx()).ui(ui); }); } diff --git a/examples/rust/custom_view/src/color_coordinates_view.rs b/examples/rust/custom_view/src/color_coordinates_view.rs index 631401888f33..9d34bc6236d6 100644 --- a/examples/rust/custom_view/src/color_coordinates_view.rs +++ b/examples/rust/custom_view/src/color_coordinates_view.rs @@ -1,3 +1,5 @@ +use crate::color_coordinates_visualizer_system::{ColorWithInstance, InstanceColorSystem}; +use re_viewer::external::re_ui::Help; use re_viewer::external::{ egui, re_data_ui::{item_ui, DataUi}, @@ -13,8 +15,6 @@ use re_viewer::external::{ }, }; -use crate::color_coordinates_visualizer_system::{ColorWithInstance, InstanceColorSystem}; - /// The different modes for displaying color coordinates in the custom view. #[derive(Default, Debug, PartialEq, Clone, Copy)] enum ColorCoordinatesMode { @@ -80,8 +80,9 @@ impl ViewClass for ColorCoordinatesView { &re_ui::icons::VIEW_GENERIC } - fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { - "A demo view that shows colors as coordinates on a 2D plane.".to_owned() + fn help(&self, _egui_ctx: &egui::Context) -> Help<'_> { + Help::new("Color coordinates view") + .markdown("A demo view that shows colors as coordinates on a 2D plane.") } /// Register all systems (contexts & parts) that the view needs.