Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved help view #8947

Merged
merged 20 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions crates/viewer/re_time_panel/src/time_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Binary file added crates/viewer/re_ui/data/icons/lmc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added crates/viewer/re_ui/data/icons/rmc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added crates/viewer/re_ui/data/icons/scroll.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added crates/viewer/re_ui/data/icons/shift.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions crates/viewer/re_ui/examples/re_ui_example/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UICommand>);
Expand Down Expand Up @@ -479,6 +479,15 @@ impl egui_tiles::Behavior<Tab> 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()),
Expand Down
186 changes: 186 additions & 0 deletions crates/viewer/re_ui/src/help.rs
Original file line number Diff line number Diff line change
@@ -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> {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
title: String,
docs_link: Option<String>,
sections: Vec<HelpSection<'a>>,
}

/// A single section, separated by a [`egui::Separator`].
#[derive(Debug, Clone)]
enum HelpSection<'a> {
Markdown(String),
Controls(Vec<ControlRow<'a>>),
}

/// 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<ControlRow<'a>>) -> 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)),
),
);
}
}
}
},
);
}
}
}
}
}
}
115 changes: 115 additions & 0 deletions crates/viewer/re_ui/src/icon_text.rs
Original file line number Diff line number Diff line change
@@ -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<Cow<'a, str>>) -> 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<IconTextItem<'a>>);

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<Cow<'a, str>>) {
self.0.push(IconTextItem::Text(text.into()));
}

/// Add an item to the row.
pub fn add(&mut self, item: impl Into<IconTextItem<'a>>) {
self.0.push(item.into());
}
}

impl<'a> From<Icon> 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<String> 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<ModifiersText<'a>> 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)
Comment on lines +93 to +95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should open an issue about making this consistent with the Rerun menu and command palette

} else {
IconTextItem::text(text)
}
}
}

/// Helper to show mouse buttons as text/icons.
pub struct MouseButtonText(pub egui::PointerButton);

impl From<MouseButtonText> 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"),
}
}
}
Loading
Loading