Skip to content

Commit

Permalink
Introduce a welcome screen when no recording is loaded (#2982)
Browse files Browse the repository at this point in the history
### What

This adds a MVP Welcome Screen that shows up instead of the current
"Loading" screen in certain circumstances.

In general terms:
- The Welcome Screen is drawn in the Viewport's place, ie. with the
panels visible.
- The Welcome Screen is never displayed when a recording is available.
- The current implementation of the Welcome Screen is very basic—mostly
links to webpages.
- The current "loading screen" still exists and is "sometimes"
displayed.

The tricky thing is when to show the Welcome Screen vs. the legacy
loading screen. This PR includes an heuristic (implemented in
`App::handle_default_blueprint()`). The main determinant is the nature
of the receiver (file, tcp, web socket, etc.) which we use as a proxy
for the workflow in which the viewer is being used. The TCP receiver is
a bit tricky, as it's used by both the Python SDK (we don't want the
Welcome Screen to distractingly flash at spawn, before data arrives) and
when running manually (`$ rerun`). To address this, this PR introduces a
new `--skip-welcome-screen` CLI option, used by the Python SDK. This
situation is still not entirely perfect though: #3018.

Implementation details:
- Technically, the Welcome Screen is triggered when the app ID is set to
`StoreHub::welcome_screen_app_id()`. A corresponding blank blueprint is
created at startup (by `StoreHub`) to make the UI happy.
`App::handle_default_blueprint()` basically sets that app ID to trigger
the Welcome Screen.
- Likewise, an empty recording is _always_ set as active whenever the
app ID is set but no recording is available. This empty recording isn't
in the list of available recordings
(`ViewerContext::alternate_recordings`). This make the UI happy.
- The status string and source originally displayed in the legacy
loading screen are _also_ displayed on the Welcome Screen in some
circumstances (i.e for "infinite" data sources, tcp, ws, etc.)


Expect these follow-up PRs:
- display the status string in the menu bar instead (as per Mårten
designs)
- address #2229 
- address #3018 
- fix UI of streams with empty recording
- add `external_link` icon wherever we have external links

Fixes #2513 

<img width="1456" alt="image"
src="https://github.com/rerun-io/rerun/assets/49431240/d35a3f02-571d-4fc0-ae7f-67ce8b9d416d">

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested [demo.rerun.io](https://demo.rerun.io/pr/2982) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/2982)
- [Docs
preview](https://rerun.io/preview/pr%3Aantoine%2Fwelcome-screen-v0/docs)
- [Examples
preview](https://rerun.io/preview/pr%3Aantoine%2Fwelcome-screen-v0/examples)

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
abey79 and emilk authored Aug 17, 2023
1 parent 2c0d2f3 commit ce6829b
Show file tree
Hide file tree
Showing 16 changed files with 570 additions and 104 deletions.
Binary file added crates/re_ui/data/icons/external_link.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use re_ui::{toasts, CommandPalette, UICommand, UICommandSender};
use re_ui::{toasts, CommandPalette, ReUi, UICommand, UICommandSender};

/// Sender that queues up the execution of a command.
pub struct CommandSender(std::sync::mpsc::Sender<UICommand>);
Expand Down Expand Up @@ -493,6 +493,11 @@ impl egui_tiles::Behavior<Tab> for MyTileTreeBehavior {
ui.label("Hover me for a tooltip")
.on_hover_text("This is a tooltip");

ui.label(
egui::RichText::new("Welcome to the ReUi example")
.text_style(ReUi::welcome_screen_h1()),
);

Default::default()
}

Expand Down
17 changes: 17 additions & 0 deletions crates/re_ui/src/design_tokens.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)] // fixed json file

use crate::ReUi;
use egui::Color32;

/// The look and feel of the UI.
Expand Down Expand Up @@ -79,6 +80,22 @@ fn apply_design_tokens(ctx: &egui::Context) -> DesignTokens {
// egui_style.spacing.interact_size.y = font_size;
}

// fonts used in the welcome screen
// TODO(ab): font sizes should come from design tokens
egui_style
.text_styles
.insert(ReUi::welcome_screen_h1(), egui::FontId::proportional(42.0));
egui_style
.text_styles
.insert(ReUi::welcome_screen_h2(), egui::FontId::proportional(24.0)); //TODO(ab): thin variant
egui_style
.text_styles
.insert(ReUi::welcome_screen_h3(), egui::FontId::proportional(18.0));
egui_style.text_styles.insert(
ReUi::welcome_screen_body(),
egui::FontId::proportional(14.0),
);

let panel_bg_color = get_aliased_color(&json, "{Alias.Color.Surface.Default.value}");
// let floating_color = get_aliased_color(&json, "{Alias.Color.Surface.Floating.value}");
let floating_color = Color32::from_gray(38); // TODO(emilk): change the content of the design_tokens.json origin instead
Expand Down
24 changes: 24 additions & 0 deletions crates/re_ui/src/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ pub const RESET: Icon = Icon::new("reset", include_bytes!("../data/icons/reset.p

pub const CLOSE: Icon = Icon::new("close", include_bytes!("../data/icons/close.png"));

/// Used for HTTP URLs that leads out of the app.
///
/// Remember to also use `.on_hover_cursor(egui::CursorIcon::PointingHand)`
/// and `.on_hover_text(url)`.
pub const EXTERNAL_LINK: Icon = Icon::new(
"external_link",
include_bytes!("../data/icons/external_link.png"),
);

pub const SPACE_VIEW_TEXT: Icon = Icon::new(
"spaceview_text",
include_bytes!("../data/icons/spaceview_text.png"),
Expand Down Expand Up @@ -97,3 +106,18 @@ pub const SPACE_VIEW_UNKNOWN: Icon = Icon::new(
);

pub const CONTAINER: Icon = Icon::new("container", include_bytes!("../data/icons/container.png"));

pub const WELCOME_SCREEN_CONFIGURE: Icon = Icon::new(
"welcome_screen_configure",
include_bytes!("../data/images/welcome_screen_configure.png"),
);

pub const WELCOME_SCREEN_LIVE_DATA: Icon = Icon::new(
"welcome_screen_live_data",
include_bytes!("../data/images/welcome_screen_live_data.png"),
);

pub const WELCOME_SCREEN_RECORDED_DATA: Icon = Icon::new(
"welcome_screen_recorded_data",
include_bytes!("../data/images/welcome_screen_recorded_data.png"),
);
21 changes: 21 additions & 0 deletions crates/re_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ impl ReUi {
}
}

/// Welcome screen big title
#[inline]
pub fn welcome_screen_h1() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-h1".into())
}

#[inline]
pub fn welcome_screen_h2() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-h2".into())
}

#[inline]
pub fn welcome_screen_h3() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-h3".into())
}

#[inline]
pub fn welcome_screen_body() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-body".into())
}

/// Margin on all sides of views.
pub fn view_padding() -> f32 {
12.0
Expand Down
132 changes: 96 additions & 36 deletions crates/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use web_time::Instant;
use re_data_store::store_db::StoreDb;
use re_log_types::{LogMsg, StoreKind};
use re_renderer::WgpuResourcePoolStatistics;
use re_smart_channel::Receiver;
use re_smart_channel::{Receiver, SmartChannelSource};
use re_ui::{toasts, UICommand, UICommandSender};
use re_viewer_context::{
command_channel, AppOptions, CommandReceiver, CommandSender, ComponentUiRegistry,
Expand Down Expand Up @@ -47,6 +47,8 @@ pub struct StartupOptions {
/// Set the screen resolution in logical points.
#[cfg(not(target_arch = "wasm32"))]
pub resolution_in_points: Option<[f32; 2]>,

pub skip_welcome_screen: bool,
}

impl Default for StartupOptions {
Expand All @@ -60,6 +62,8 @@ impl Default for StartupOptions {

#[cfg(not(target_arch = "wasm32"))]
resolution_in_points: None,

skip_welcome_screen: false,
}
}
}
Expand Down Expand Up @@ -199,7 +203,7 @@ impl App {
rx,
state,
background_tasks: Default::default(),
store_hub: Some(StoreHub::default()),
store_hub: Some(StoreHub::new()),
toasts: toasts::Toasts::new(),
memory_panel: Default::default(),
memory_panel_open: false,
Expand Down Expand Up @@ -564,43 +568,56 @@ impl App {
self.style_panel_ui(egui_ctx, ui);

if let Some(store_view) = store_context {
// TODO(jleibs): We don't necessarily want to show the wait
// screen just because we're missing a recording. If we've
// loaded a blueprint, we can still show the empty layouts or
// static data, but we need to jump through some hoops to
// handle a missing `RecordingConfig` in this case.
if let Some(store_db) = store_view.recording {
// TODO(andreas): store the re_renderer somewhere else.
let egui_renderer = {
let render_state = frame.wgpu_render_state().unwrap();
&mut render_state.renderer.write()
};
if let Some(render_ctx) = egui_renderer
.callback_resources
.get_mut::<re_renderer::RenderContext>()
{
render_ctx.begin_frame();

self.state.show(
app_blueprint,
ui,
render_ctx,
store_db,
store_view,
&self.re_ui,
&self.component_ui_registry,
&self.space_view_class_registry,
&self.rx,
&self.command_sender,
);

render_ctx.before_submit();
}
static EMPTY_STORE_DB: once_cell::sync::Lazy<StoreDb> =
once_cell::sync::Lazy::new(|| {
StoreDb::new(re_log_types::StoreId::from_string(
StoreKind::Recording,
"<EMPTY>".to_owned(),
))
});

// We want the regular UI as soon as a blueprint is available (or, rather, an
// app ID is set). If no recording is available, we use a default, empty one.
// Note that EMPTY_STORE_DB is *not* part of the list of available recordings
// (StoreContext::alternate_recordings), which means that it's not displayed in
// the recordings UI.
let store_db = if let Some(store_db) = store_view.recording {
store_db
} else {
crate::ui::wait_screen_ui(ui, &self.rx);
&EMPTY_STORE_DB
};

// TODO(andreas): store the re_renderer somewhere else.
let egui_renderer = {
let render_state = frame.wgpu_render_state().unwrap();
&mut render_state.renderer.write()
};
if let Some(render_ctx) = egui_renderer
.callback_resources
.get_mut::<re_renderer::RenderContext>()
{
render_ctx.begin_frame();

self.state.show(
app_blueprint,
ui,
render_ctx,
store_db,
store_view,
&self.re_ui,
&self.component_ui_registry,
&self.space_view_class_registry,
&self.rx,
&self.command_sender,
);

render_ctx.before_submit();
}
} else {
crate::ui::wait_screen_ui(ui, &self.rx);
// This is part of the loading vs. welcome screen UI logic. The loading screen
// is displayed when no app ID is set. This is e.g. the initial state for the
// web demos.
crate::ui::loading_ui(ui, &self.rx);
}
});
}
Expand Down Expand Up @@ -824,6 +841,45 @@ impl App {
}
}
}

/// This function will create an empty blueprint whenever the welcome screen should be
/// displayed.
///
/// The welcome screen can be displayed only when a blueprint is available (and no recording is
/// loaded). This function implements the heuristic which determines when the welcome screen
/// should show up.
fn handle_default_blueprint(&mut self, store_hub: &mut StoreHub) {
if store_hub.current_recording().is_some()
|| store_hub.selected_application_id().is_some()
|| self.startup_options.skip_welcome_screen
{
return;
}

// Here, we use the type of Receiver as a proxy for which kind of workflow the viewer is
// being used in.
let welcome = match self.rx.source() {
// These source are typically "finite". We want the loading screen so long as data is
// coming in.
SmartChannelSource::Files { .. } | SmartChannelSource::RrdHttpStream { .. } => {
!self.rx.is_connected()
}
// The workflows associated with these sources typically do not require showing the
// welcome screen until after some recording have been loaded and then closed.
SmartChannelSource::RrdWebEventListener
| SmartChannelSource::Sdk
| SmartChannelSource::WsClient { .. } => false,
// This might be the trickiest case. When running the bare executable, we want to show
// the welcome screen (default, "new user" workflow). There are other case using Tcp
// where it's not the case, including Python/C++ SDKs and possibly other, advanced used,
// scenarios. In this cases, `--skip-welcome-screen` should be used.
SmartChannelSource::TcpServer { .. } => true,
};

if welcome {
store_hub.set_app_id(StoreHub::welcome_screen_app_id());
}
}
}

impl eframe::App for App {
Expand Down Expand Up @@ -925,6 +981,10 @@ impl eframe::App for App {

file_saver_progress_ui(egui_ctx, &mut self.background_tasks); // toasts for background file saver

// Heuristic to set the app_id to the welcome screen blueprint.
// Must be called before `read_context` below.
self.handle_default_blueprint(&mut store_hub);

let store_context = store_hub.read_context();

let app_blueprint = AppBlueprint::new(store_context.as_ref(), egui_ctx);
Expand Down
9 changes: 8 additions & 1 deletion crates/re_viewer/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,17 @@ impl AppState {
..Default::default()
};

let show_welcome =
store_context.blueprint.app_id() == Some(&StoreHub::welcome_screen_app_id());

egui::CentralPanel::default()
.frame(viewport_frame)
.show_inside(ui, |ui| {
viewport.viewport_ui(ui, &mut ctx);
if show_welcome {
crate::ui::welcome_ui(re_ui, ui, rx, command_sender);
} else {
viewport.viewport_ui(ui, &mut ctx);
}
});

// If the viewport was user-edited, then disable auto space views
Expand Down
Loading

0 comments on commit ce6829b

Please sign in to comment.