From cb197a268085bcae5a1a7c1c550d53f97aaddeae Mon Sep 17 00:00:00 2001
From: Antoine Beyeler <49431240+abey79@users.noreply.github.com>
Date: Wed, 6 Sep 2023 17:34:05 +0200
Subject: [PATCH] Add Examples page to the Welcome Screen (#3191)
### What
Add Example page to the Welcome Screen.
Fixes #3096
### TODO
- [x] fix layout issues
- [x] display tags
- [x] have dedicated, short copy for the description: #3201
### Not included in this PR
- **WARNING**: here, we bake in a manifest with hard-coded links to RRDs
that were generated within this PR. This will lead to issue down the
line, when the RRD format changes.
- https://github.com/rerun-io/rerun/issues/3212
- #3213
- download updated manifest
- #3190
- load thumbnail from the web
- emilk/egui#3291
- provide feedback while downloading a RRD
- https://github.com/rerun-io/rerun/issues/3192
### 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/3191) (if
applicable)
- [PR Build Summary](https://build.rerun.io/pr/3191)
- [Docs
preview](https://rerun.io/preview/3be107e4cc6aa6758a3f22c27a79233b33f2ea6b/docs)
- [Examples
preview](https://rerun.io/preview/3be107e4cc6aa6758a3f22c27a79233b33f2ea6b/examples)
- [Recent benchmark results](https://ref.rerun.io/dev/bench/)
- [Wasm size tracking](https://ref.rerun.io/dev/sizes/)
---------
Co-authored-by: Emil Ernerfeldt
---
Cargo.lock | 29 +-
Cargo.toml | 25 +-
crates/re_ui/src/design_tokens.rs | 8 +-
crates/re_ui/src/lib.rs | 12 +
crates/re_viewer/Cargo.toml | 2 +
crates/re_viewer/data/examples_manifest.json | 128 +++++++
crates/re_viewer/src/app_state.rs | 6 +-
crates/re_viewer/src/ui/mod.rs | 7 +-
crates/re_viewer/src/ui/wait_screen_ui.rs | 356 ------------------
.../src/ui/welcome_screen/example_page.rs | 349 +++++++++++++++++
crates/re_viewer/src/ui/welcome_screen/mod.rs | 231 ++++++++++++
.../src/ui/welcome_screen/welcome_page.rs | 181 +++++++++
12 files changed, 948 insertions(+), 386 deletions(-)
create mode 100644 crates/re_viewer/data/examples_manifest.json
delete mode 100644 crates/re_viewer/src/ui/wait_screen_ui.rs
create mode 100644 crates/re_viewer/src/ui/welcome_screen/example_page.rs
create mode 100644 crates/re_viewer/src/ui/welcome_screen/mod.rs
create mode 100644 crates/re_viewer/src/ui/welcome_screen/welcome_page.rs
diff --git a/Cargo.lock b/Cargo.lock
index a2ea151e9142..2ff9c8a1b9bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1366,7 +1366,7 @@ checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
[[package]]
name = "ecolor"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"bytemuck",
"serde",
@@ -1375,7 +1375,7 @@ dependencies = [
[[package]]
name = "eframe"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"bytemuck",
"cocoa",
@@ -1388,6 +1388,7 @@ dependencies = [
"js-sys",
"log",
"objc",
+ "parking_lot 0.12.1",
"percent-encoding",
"pollster",
"puffin",
@@ -1407,13 +1408,14 @@ dependencies = [
[[package]]
name = "egui"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"accesskit",
"ahash 0.8.3",
"epaint",
"log",
"nohash-hasher",
+ "puffin",
"ron",
"serde",
]
@@ -1421,7 +1423,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"bytemuck",
"epaint",
@@ -1436,16 +1438,16 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"arboard",
"egui",
- "instant",
"log",
"puffin",
"raw-window-handle",
"serde",
"smithay-clipboard",
+ "web-time",
"webbrowser",
"winit",
]
@@ -1453,17 +1455,20 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"egui",
+ "ehttp",
+ "image",
"log",
+ "puffin",
"serde",
]
[[package]]
name = "egui_glow"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"bytemuck",
"egui",
@@ -1479,7 +1484,7 @@ dependencies = [
[[package]]
name = "egui_plot"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"egui",
]
@@ -1521,7 +1526,7 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "emath"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"bytemuck",
"serde",
@@ -1602,7 +1607,7 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.22.0"
-source = "git+https://github.com/emilk/egui?rev=70bfc7e09f1b1f794a77064b62a4932f9e60ef15#70bfc7e09f1b1f794a77064b62a4932f9e60ef15"
+source = "git+https://github.com/emilk/egui?rev=2338a854f932658ad0548aa4f00874b3427970c1#2338a854f932658ad0548aa4f00874b3427970c1"
dependencies = [
"ab_glyph",
"ahash 0.8.3",
@@ -4695,6 +4700,7 @@ dependencies = [
"eframe",
"egui",
"egui-wgpu",
+ "egui_extras",
"egui_plot",
"image",
"itertools 0.11.0",
@@ -4733,6 +4739,7 @@ dependencies = [
"rfd",
"ron",
"serde",
+ "serde_json",
"time",
"wasm-bindgen-futures",
"web-sys",
diff --git a/Cargo.toml b/Cargo.toml
index d4f9cc624d29..f9077ba05728 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -86,8 +86,13 @@ eframe = { version = "0.22.0", default-features = false, features = [
"x11",
] }
egui = { version = "0.22.0", features = ["extra_debug_asserts", "log"] }
-egui_extras = { version = "0.22.0", features = ["log"] }
-egui_plot = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
+egui_extras = { version = "0.22.0", features = [
+ "log",
+ "image",
+ "http",
+ "puffin",
+] }
+egui_plot = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
egui_tiles = { version = "0.2" }
egui-wgpu = "0.22.0"
ehttp = { version = "0.3" }
@@ -160,14 +165,14 @@ debug = true
# ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk.
# Temporary patch until next egui release
-ecolor = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-eframe = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-egui-wgpu = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-egui-winit = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-egui = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-egui_extras = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-emath = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
-epaint = { git = "https://github.com/emilk/egui", rev = "70bfc7e09f1b1f794a77064b62a4932f9e60ef15" }
+ecolor = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+eframe = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+egui-wgpu = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+egui-winit = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+egui = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+egui_extras = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+emath = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
+epaint = { git = "https://github.com/emilk/egui", rev = "2338a854f932658ad0548aa4f00874b3427970c1" }
# Temporary patch until next egui_tiles release
egui_tiles = { git = "https://github.com/rerun-io/egui_tiles", rev = "f835c4df1cc260a58122a8d37c7c3738902b9643" }
diff --git a/crates/re_ui/src/design_tokens.rs b/crates/re_ui/src/design_tokens.rs
index e74e5bf04f06..199cfdd919e8 100644
--- a/crates/re_ui/src/design_tokens.rs
+++ b/crates/re_ui/src/design_tokens.rs
@@ -84,16 +84,16 @@ fn apply_design_tokens(ctx: &egui::Context) -> DesignTokens {
// TODO(ab): font sizes should come from design tokens
egui_style
.text_styles
- .insert(ReUi::welcome_screen_h1(), egui::FontId::proportional(42.0));
+ .insert(ReUi::welcome_screen_h1(), egui::FontId::proportional(28.0));
egui_style
.text_styles
- .insert(ReUi::welcome_screen_h2(), egui::FontId::proportional(24.0)); //TODO(ab): thin variant
+ .insert(ReUi::welcome_screen_h2(), egui::FontId::proportional(24.0));
egui_style
.text_styles
- .insert(ReUi::welcome_screen_h3(), egui::FontId::proportional(18.0));
+ .insert(ReUi::welcome_screen_h3(), egui::FontId::proportional(15.0));
egui_style.text_styles.insert(
ReUi::welcome_screen_body(),
- egui::FontId::proportional(14.0),
+ egui::FontId::proportional(13.0),
);
let panel_bg_color = get_aliased_color(&json, "{Alias.Color.Surface.Default.value}");
diff --git a/crates/re_ui/src/lib.rs b/crates/re_ui/src/lib.rs
index 1a49cad32d92..3b4ffeb85e2e 100644
--- a/crates/re_ui/src/lib.rs
+++ b/crates/re_ui/src/lib.rs
@@ -68,6 +68,8 @@ pub struct ReUi {
impl ReUi {
/// Create [`ReUi`] and apply style to the given egui context.
pub fn load_and_apply(egui_ctx: &egui::Context) -> Self {
+ egui_extras::loaders::install(egui_ctx);
+
Self {
egui_ctx: egui_ctx.clone(),
design_tokens: DesignTokens::load_and_apply(egui_ctx),
@@ -110,6 +112,16 @@ impl ReUi {
egui::TextStyle::Name("welcome-screen-body".into())
}
+ pub fn welcome_screen_tab_bar_style(ui: &mut egui::Ui) {
+ ui.spacing_mut().item_spacing.x = 16.0;
+ ui.visuals_mut().selection.bg_fill = egui::Color32::TRANSPARENT;
+ ui.visuals_mut().selection.stroke = ui.visuals().widgets.active.fg_stroke;
+ ui.visuals_mut().widgets.hovered.weak_bg_fill = egui::Color32::TRANSPARENT;
+ ui.visuals_mut().widgets.hovered.fg_stroke = ui.visuals().widgets.active.fg_stroke;
+ ui.visuals_mut().widgets.active.weak_bg_fill = egui::Color32::TRANSPARENT;
+ ui.visuals_mut().widgets.inactive.fg_stroke = ui.visuals().widgets.noninteractive.fg_stroke;
+ }
+
/// Margin on all sides of views.
pub fn view_padding() -> f32 {
12.0
diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml
index b2e842cce116..1fc1602d3c90 100644
--- a/crates/re_viewer/Cargo.toml
+++ b/crates/re_viewer/Cargo.toml
@@ -84,6 +84,7 @@ eframe = { workspace = true, default-features = false, features = [
"puffin",
"wgpu",
] }
+egui_extras.workspace = true
egui_plot.workspace = true
egui-wgpu.workspace = true
egui.workspace = true
@@ -94,6 +95,7 @@ poll-promise = { version = "0.3", features = ["web"] }
rfd.workspace = true
ron = "0.8.0"
serde = { version = "1", features = ["derive"] }
+serde_json = "1"
time = { workspace = true, features = ["formatting"] }
web-time.workspace = true
wgpu.workspace = true
diff --git a/crates/re_viewer/data/examples_manifest.json b/crates/re_viewer/data/examples_manifest.json
new file mode 100644
index 000000000000..79e4d619a927
--- /dev/null
+++ b/crates/re_viewer/data/examples_manifest.json
@@ -0,0 +1,128 @@
+[
+ {
+ "name": "arkit_scenes",
+ "title": "ARKit Scenes",
+ "description": "Visualize the ARKitScenes dataset, which contains color+depth images, the reconstructed mesh and labeled bounding boxes.",
+ "tags": [
+ "2D",
+ "3D",
+ "depth",
+ "mesh",
+ "object-detection",
+ "pinhole-camera"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/arkit_scenes/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/arkit_scenes/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/8b90a80c72b27fad289806b7e5dff0c9ac97e87c_arkit_scenes_480w.png",
+ "width": 480,
+ "height": 243
+ }
+ },
+ {
+ "name": "structure_from_motion",
+ "title": "Structure from Motion",
+ "description": "Visualize a sparse reconstruction by COLMAP, a general-purpose Structure-from-Motion and Multi-View Stereo pipeline.",
+ "tags": [
+ "2D",
+ "3D",
+ "colmap",
+ "pinhole-camera",
+ "time-series"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/structure_from_motion/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/structure_from_motion/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/033edff752f86bcdc9a81f7877e0b4411ff4e6c5_structure_from_motion_480w.png",
+ "width": 480,
+ "height": 275
+ }
+ },
+ {
+ "name": "dicom_mri",
+ "title": "Dicom MRI",
+ "description": "Example using a DICOM MRI scan. This demonstrates the flexible tensor slicing capabilities of the Rerun viewer.",
+ "tags": [
+ "tensor",
+ "mri",
+ "dicom"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/dicom_mri/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/dicom_mri/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/b8b25dd01e892e6daf5177e6fc05ff5feb19ee8d_dicom_mri_480w.png",
+ "width": 480,
+ "height": 285
+ }
+ },
+ {
+ "name": "human_pose_tracking",
+ "title": "Human Pose Tracking",
+ "description": "Use the MediaPipe Pose solution to detect and track a human pose in video.",
+ "tags": [
+ "mediapipe",
+ "keypoint-detection",
+ "2D",
+ "3D"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/human_pose_tracking/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/human_pose_tracking/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/277b9c72da1d0d0ae9d221f7552dede9c4d5b2fa_human_pose_tracking_480w.png",
+ "width": 480,
+ "height": 272
+ }
+ },
+ {
+ "name": "plots",
+ "title": "Plots",
+ "description": "Demonstration of various plots and charts supported by Rerun.",
+ "tags": [
+ "2d",
+ "plots",
+ "api-example"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/plots/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/plots/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/ca0c72df93d70c79b0e640fb4b7c33cdc0bfe5f4_plots_480w.png",
+ "width": 480,
+ "height": 271
+ }
+ },
+ {
+ "name": "detect_and_track_objects",
+ "title": "Detect and Track Objects",
+ "description": "Visualize object detection and segmentation using the Huggingface `transformers` library.",
+ "tags": [
+ "2D",
+ "huggingface",
+ "object-detection",
+ "object-tracking",
+ "opencv"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/detect_and_track_objects/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/detect_and_track_objects/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/efb301d64eef6f25e8f6ae29294bd003c0cda3a7_detect_and_track_objects_480w.png",
+ "width": 480,
+ "height": 279
+ }
+ },
+ {
+ "name": "dna",
+ "title": "Helix",
+ "description": "Simple example of logging point and line primitives to draw a 3D helix.",
+ "tags": [
+ "3d",
+ "api-example"
+ ],
+ "demo_url": "https://demo.rerun.io/commit/5be001c/examples/dna/",
+ "rrd_url": "https://demo.rerun.io/commit/5be001c/examples/dna/data.rrd",
+ "thumbnail": {
+ "url": "https://static.rerun.io/ea7a9ab2f716bd37d1bbc1eabf3f55e4f526660e_helix_480w.png",
+ "width": 480,
+ "height": 285
+ }
+ }
+]
diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs
index 48b88fcb4005..9bd473483879 100644
--- a/crates/re_viewer/src/app_state.rs
+++ b/crates/re_viewer/src/app_state.rs
@@ -30,6 +30,9 @@ pub struct AppState {
selection_panel: crate::selection_panel::SelectionPanel,
time_panel: re_time_panel::TimePanel,
+ #[serde(skip)]
+ welcome_screen: crate::ui::WelcomeScreen,
+
// TODO(jleibs): This is sort of a weird place to put this but makes more
// sense than the blueprint
#[serde(skip)]
@@ -90,6 +93,7 @@ impl AppState {
recording_configs,
selection_panel,
time_panel,
+ welcome_screen,
viewport_state,
} = self;
@@ -189,7 +193,7 @@ impl AppState {
.frame(viewport_frame)
.show_inside(ui, |ui| {
if show_welcome {
- crate::ui::welcome_ui(re_ui, ui, rx, command_sender);
+ welcome_screen.ui(re_ui, ui, rx, command_sender);
} else {
viewport.viewport_ui(ui, &mut ctx);
}
diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs
index ffe86a211c93..f907e0206ab4 100644
--- a/crates/re_viewer/src/ui/mod.rs
+++ b/crates/re_viewer/src/ui/mod.rs
@@ -4,7 +4,7 @@ mod recordings_panel;
mod rerun_menu;
mod selection_history_ui;
mod top_panel;
-mod wait_screen_ui;
+mod welcome_screen;
pub(crate) mod memory_panel;
pub(crate) mod selection_panel;
@@ -14,7 +14,6 @@ pub use recordings_panel::recordings_panel_ui;
// ----
pub(crate) use {
- self::mobile_warning_ui::mobile_warning_ui,
- self::top_panel::top_panel,
- self::wait_screen_ui::{loading_ui, welcome_ui},
+ self::mobile_warning_ui::mobile_warning_ui, self::top_panel::top_panel,
+ self::welcome_screen::loading_ui, self::welcome_screen::WelcomeScreen,
};
diff --git a/crates/re_viewer/src/ui/wait_screen_ui.rs b/crates/re_viewer/src/ui/wait_screen_ui.rs
deleted file mode 100644
index 4eb0373b6ec5..000000000000
--- a/crates/re_viewer/src/ui/wait_screen_ui.rs
+++ /dev/null
@@ -1,356 +0,0 @@
-use egui::{Ui, Widget};
-
-use re_log_types::LogMsg;
-use re_smart_channel::{ReceiveSet, SmartChannelSource};
-use re_ui::ReUi;
-
-const MIN_COLUMN_WIDTH: f32 = 250.0;
-const MAX_COLUMN_WIDTH: f32 = 400.0;
-
-//const CPP_QUICKSTART: &str = "https://www.rerun.io/docs/getting-started/cpp";
-const PYTHON_QUICKSTART: &str = "https://www.rerun.io/docs/getting-started/python";
-const RUST_QUICKSTART: &str = "https://www.rerun.io/docs/getting-started/rust";
-const SPACE_VIEWS_HELP: &str = "https://www.rerun.io/docs/getting-started/viewer-walkthrough";
-
-/// Welcome screen shown in place of the viewport when no data is loaded.
-pub fn welcome_ui(
- re_ui: &re_ui::ReUi,
- ui: &mut egui::Ui,
- rx: &ReceiveSet,
- command_sender: &re_viewer_context::CommandSender,
-) {
- egui::ScrollArea::both()
- .id_source("welcome screen")
- .auto_shrink([false, false])
- .show(ui, |ui| {
- welcome_ui_impl(re_ui, ui, rx, command_sender);
- });
-}
-
-/// Full-screen UI shown while in loading state.
-pub fn loading_ui(ui: &mut egui::Ui, rx: &ReceiveSet) {
- let status_strings = status_strings(rx);
- if status_strings.is_empty() {
- return;
- }
-
- ui.centered_and_justified(|ui| {
- for status_string in status_strings {
- let style = ui.style();
- let mut layout_job = egui::text::LayoutJob::default();
- layout_job.append(
- status_string.status,
- 0.0,
- egui::TextFormat::simple(
- egui::TextStyle::Heading.resolve(style),
- style.visuals.strong_text_color(),
- ),
- );
- layout_job.append(
- &format!("\n\n{}", status_string.source),
- 0.0,
- egui::TextFormat::simple(
- egui::TextStyle::Body.resolve(style),
- style.visuals.text_color(),
- ),
- );
- layout_job.halign = egui::Align::Center;
- ui.label(layout_job);
- }
- });
-}
-
-fn welcome_ui_impl(
- re_ui: &re_ui::ReUi,
- ui: &mut egui::Ui,
- rx: &ReceiveSet,
- command_sender: &re_viewer_context::CommandSender,
-) {
- let mut margin = egui::Margin::same(40.0);
- margin.bottom = 0.0;
- egui::Frame {
- inner_margin: margin,
- ..Default::default()
- }
- .show(ui, |ui| {
- ui.vertical(|ui| {
- ui.add(
- egui::Label::new(
- egui::RichText::new("Welcome")
- .strong()
- .text_style(re_ui::ReUi::welcome_screen_h1()),
- )
- .wrap(false),
- );
-
- ui.add(
- egui::Label::new(
- egui::RichText::new("Visualize multimodal data")
- .text_style(re_ui::ReUi::welcome_screen_h2()),
- )
- .wrap(false),
- );
-
- ui.add_space(20.0);
-
- onboarding_content_ui(re_ui, ui, command_sender);
-
- for status_strings in status_strings(rx) {
- if status_strings.long_term {
- ui.add_space(55.0);
- ui.vertical_centered(|ui| {
- ui.label(status_strings.status);
- ui.label(
- egui::RichText::new(status_strings.source)
- .color(ui.visuals().weak_text_color()),
- );
- });
- }
- }
- });
- });
-}
-
-fn onboarding_content_ui(
- re_ui: &ReUi,
- ui: &mut Ui,
- command_sender: &re_viewer_context::CommandSender,
-) {
- let column_spacing = 15.0;
- let stability_adjustment = 1.0; // minimize jitter with sizing and scroll bars
- let column_width = ((ui.available_width() - 2. * column_spacing) / 3.0 - stability_adjustment)
- .clamp(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
-
- let grid = egui::Grid::new("welcome_screen_grid")
- .spacing(egui::Vec2::splat(column_spacing))
- .min_col_width(column_width)
- .max_col_width(column_width);
-
- grid.show(ui, |ui| {
- image_banner(
- re_ui,
- ui,
- &re_ui::icons::WELCOME_SCREEN_LIVE_DATA,
- column_width,
- );
- image_banner(
- re_ui,
- ui,
- &re_ui::icons::WELCOME_SCREEN_RECORDED_DATA,
- column_width,
- );
- image_banner(
- re_ui,
- ui,
- &re_ui::icons::WELCOME_SCREEN_CONFIGURE,
- column_width,
- );
-
- ui.end_row();
-
- ui.vertical(|ui| {
- ui.label(
- egui::RichText::new("Connect to live data")
- .strong()
- .text_style(re_ui::ReUi::welcome_screen_h3()),
- );
- ui.label(
- egui::RichText::new(
- "Use the Rerun SDK to stream data from your code to the Rerun Viewer. \
- synchronized data from multiple processes, locally or over a network.",
- )
- .text_style(re_ui::ReUi::welcome_screen_body()),
- );
- });
-
- ui.vertical(|ui| {
- ui.label(
- egui::RichText::new("Load recorded data")
- .strong()
- .text_style(re_ui::ReUi::welcome_screen_h3()),
- );
- ui.label(
- egui::RichText::new(
- "Open and visualize recorded data from previous Rerun sessions (.rrd) as well \
- as data in formats like .gltf and .jpg.",
- )
- .text_style(re_ui::ReUi::welcome_screen_body()),
- );
- });
-
- ui.vertical(|ui| {
- ui.label(
- egui::RichText::new("Configure your views")
- .strong()
- .text_style(re_ui::ReUi::welcome_screen_h3()),
- );
- ui.label(
- egui::RichText::new(
- "Add and rearrange views, and configure what data is shown and how. Configure \
- interactively in the viewer or (coming soon) directly from code in the SDK.",
- )
- .text_style(re_ui::ReUi::welcome_screen_body()),
- );
- });
-
- ui.end_row();
-
- ui.horizontal(|ui| {
- button_centered_label(ui, "Quick start…");
- // TODO(ab): activate when C++ is ready!
- // url_large_text_button(re_ui, ui, "C++", CPP_QUICKSTART);
- url_large_text_button(re_ui, ui, "Python", PYTHON_QUICKSTART);
- url_large_text_button(re_ui, ui, "Rust", RUST_QUICKSTART);
- });
-
- {
- use re_ui::UICommandSender as _;
- ui.horizontal(|ui| {
- if large_text_button(ui, "Open file…").clicked() {
- command_sender.send_ui(re_ui::UICommand::Open);
- }
- button_centered_label(ui, "Or drop a file anywhere!");
- });
- }
-
- #[cfg(target_arch = "wasm32")]
- ui.horizontal(|ui| {
- button_centered_label(ui, "Drop a file anywhere!");
- });
-
- ui.horizontal(|ui| {
- url_large_text_button(re_ui, ui, "Learn about Views", SPACE_VIEWS_HELP);
- });
-
- ui.end_row();
- });
-}
-
-fn button_centered_label(ui: &mut egui::Ui, label: impl Into) {
- ui.vertical(|ui| {
- ui.add_space(9.0);
- ui.label(label);
- });
-}
-
-fn set_large_button_style(ui: &mut egui::Ui) {
- ui.style_mut().spacing.button_padding = egui::vec2(12.0, 9.0);
- let visuals = ui.visuals_mut();
- visuals.widgets.hovered.expansion = 0.0;
- visuals.widgets.active.expansion = 0.0;
- visuals.widgets.open.expansion = 0.0;
-
- visuals.widgets.inactive.rounding = egui::Rounding::same(8.);
- visuals.widgets.hovered.rounding = egui::Rounding::same(8.);
- visuals.widgets.active.rounding = egui::Rounding::same(8.);
-
- visuals.widgets.inactive.weak_bg_fill = visuals.widgets.inactive.bg_fill;
-}
-
-fn url_large_text_button(
- re_ui: &re_ui::ReUi,
- ui: &mut egui::Ui,
- text: impl Into,
- url: &str,
-) {
- ui.scope(|ui| {
- set_large_button_style(ui);
-
- let image = re_ui.icon_image(&re_ui::icons::EXTERNAL_LINK);
- let texture_id = image.texture_id(ui.ctx());
-
- if egui::Button::image_and_text(texture_id, ReUi::small_icon_size(), text)
- .ui(ui)
- .on_hover_cursor(egui::CursorIcon::PointingHand)
- .on_hover_text(url)
- .clicked()
- {
- ui.ctx().output_mut(|o| {
- o.open_url = Some(egui::output::OpenUrl {
- url: url.to_owned(),
- new_tab: true,
- });
- });
- }
- });
-}
-
-#[allow(dead_code)] // TODO(ab): remove if/when wasm uses one of these buttons
-fn large_text_button(ui: &mut egui::Ui, text: impl Into) -> egui::Response {
- ui.scope(|ui| {
- set_large_button_style(ui);
- ui.button(text)
- })
- .inner
-}
-
-fn image_banner(re_ui: &re_ui::ReUi, ui: &mut egui::Ui, image: &re_ui::Icon, column_width: f32) {
- let image = re_ui.icon_image(image);
- let texture_id = image.texture_id(ui.ctx());
- let height = column_width * image.size()[1] as f32 / image.size()[0] as f32;
- ui.add(
- egui::Image::new(texture_id, egui::vec2(column_width, height))
- .rounding(egui::Rounding::same(8.)),
- );
-}
-
-/// Describes the current state of the Rerun viewer.
-struct StatusString {
- /// General status string (e.g. "Ready", "Loading…", etc.).
- status: &'static str,
-
- /// Source string (e.g. listening IP, file path, etc.).
- source: String,
-
- /// Whether or not the status is valid once data loading is completed, i.e. if data may still
- /// be received later.
- long_term: bool,
-}
-
-impl StatusString {
- fn new(status: &'static str, source: String, long_term: bool) -> Self {
- Self {
- status,
- source,
- long_term,
- }
- }
-}
-
-/// Returns the status strings to be displayed by the loading and welcome screen.
-fn status_strings(rx: &ReceiveSet) -> Vec {
- rx.sources()
- .into_iter()
- .map(|s| status_string(&s))
- .collect()
-}
-
-fn status_string(source: &SmartChannelSource) -> StatusString {
- match source {
- re_smart_channel::SmartChannelSource::File(path) => {
- StatusString::new("Loading…", path.display().to_string(), false)
- }
- re_smart_channel::SmartChannelSource::RrdHttpStream { url } => {
- StatusString::new("Loading…", url.clone(), false)
- }
- re_smart_channel::SmartChannelSource::RrdWebEventListener => {
- StatusString::new("Ready", "Waiting for logging data…".to_owned(), true)
- }
- re_smart_channel::SmartChannelSource::Sdk => StatusString::new(
- "Ready",
- "Waiting for logging data from SDK".to_owned(),
- true,
- ),
- re_smart_channel::SmartChannelSource::WsClient { ws_server_url } => {
- // TODO(emilk): it would be even better to know whether or not we are connected, or are attempting to connect
- StatusString::new(
- "Ready",
- format!("Waiting for data from {ws_server_url}"),
- true,
- )
- }
- re_smart_channel::SmartChannelSource::TcpServer { port } => {
- StatusString::new("Ready", format!("Listening on port {port}"), true)
- }
- }
-}
diff --git a/crates/re_viewer/src/ui/welcome_screen/example_page.rs b/crates/re_viewer/src/ui/welcome_screen/example_page.rs
new file mode 100644
index 000000000000..8bcd4ed2f599
--- /dev/null
+++ b/crates/re_viewer/src/ui/welcome_screen/example_page.rs
@@ -0,0 +1,349 @@
+use egui::load::TexturePoll;
+use egui::{NumExt, TextureOptions, Ui};
+use re_log_types::LogMsg;
+use re_smart_channel::ReceiveSet;
+use re_viewer_context::SystemCommandSender;
+
+#[derive(Debug, serde::Deserialize)]
+struct ExampleThumbnail {
+ url: String,
+ width: u32,
+ height: u32,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct ExampleDesc {
+ /// snake_case version of the example name
+ name: String,
+
+ /// human readable version of the example name
+ title: String,
+
+ description: String,
+ tags: Vec,
+
+ #[allow(unused)]
+ demo_url: String,
+
+ rrd_url: String,
+ thumbnail: ExampleThumbnail,
+}
+
+// TODO(#3190): we should attempt to update the manifest based on the online version
+fn load_example_manifest() -> Vec {
+ serde_json::from_str(include_str!("../../../data/examples_manifest.json"))
+ .expect("Failed to parse data/examples_manifest.json")
+}
+
+// TODO(ab): use design tokens
+const MARGINS: f32 = 40.0;
+const MIN_COLUMN_WIDTH: f32 = 250.0;
+const MAX_COLUMN_WIDTH: f32 = 340.0;
+const MAX_COLUMN_COUNT: usize = 3;
+const COLUMN_HSPACE: f32 = 24.0;
+const TITLE_TO_GRID_VSPACE: f32 = 32.0;
+const THUMBNAIL_TO_DESCRIPTION_VSPACE: f32 = 10.0;
+const DESCRIPTION_TO_TAGS_VSPACE: f32 = 10.0;
+const ROW_VSPACE: f32 = 32.0;
+const THUMBNAIL_RADIUS: f32 = 4.0;
+
+/// Structure to track both an example description and its layout in the grid.
+///
+/// For layout purposes, each example spans multiple cells in the grid. This structure is used to
+/// track the rectangle that spans the block of cells used for the corresponding example, so hover/
+/// click can be detected.
+#[derive(Debug)]
+struct ExampleDescLayout {
+ desc: ExampleDesc,
+ rect: egui::Rect,
+}
+
+impl ExampleDescLayout {
+ /// Saves the top left corner of the hover/click area for this example.
+ fn set_top_left(&mut self, pos: egui::Pos2) {
+ self.rect.min = pos;
+ }
+
+ /// Saves the bottom right corner of the hover/click area for this example.
+ fn set_bottom_right(&mut self, pos: egui::Pos2) {
+ self.rect.max = pos;
+ }
+
+ fn clicked(&self, ui: &egui::Ui, id: egui::Id) -> bool {
+ ui.interact(self.rect, id.with(&self.desc.name), egui::Sense::click())
+ .clicked()
+ }
+
+ fn hovered(&self, ui: &egui::Ui, id: egui::Id) -> bool {
+ ui.interact(self.rect, id.with(&self.desc.name), egui::Sense::hover())
+ .hovered()
+ }
+}
+
+#[derive(Debug)]
+pub(super) struct ExamplePage {
+ id: egui::Id,
+ examples: Vec,
+}
+
+impl ExamplePage {
+ pub(crate) fn new() -> Self {
+ Self {
+ examples: load_example_manifest()
+ .into_iter()
+ .map(|e| ExampleDescLayout {
+ desc: e,
+ rect: egui::Rect::NOTHING,
+ })
+ .collect(),
+ id: egui::Id::new("example_page"),
+ }
+ }
+
+ pub(super) fn ui(
+ &mut self,
+ ui: &mut egui::Ui,
+ rx: &re_smart_channel::ReceiveSet,
+ command_sender: &re_viewer_context::CommandSender,
+ ) {
+ let mut margin = egui::Margin::same(MARGINS);
+ margin.bottom = MARGINS - ROW_VSPACE;
+ egui::Frame {
+ inner_margin: margin,
+ ..Default::default()
+ }
+ .show(ui, |ui| {
+ // vertical spacing isn't homogeneous so it's handled manually
+ let grid_spacing = egui::vec2(COLUMN_HSPACE, 0.0);
+ let column_count = (((ui.available_width() + grid_spacing.x)
+ / (MIN_COLUMN_WIDTH + grid_spacing.x))
+ .floor() as usize)
+ .clamp(1, MAX_COLUMN_COUNT);
+ let column_width = ((ui.available_width() + grid_spacing.x) / column_count as f32
+ - grid_spacing.x)
+ .floor()
+ .at_most(MAX_COLUMN_WIDTH);
+
+ // this space is added on the left so that the grid is centered
+ let centering_space = (ui.available_width()
+ - column_count as f32 * column_width
+ - (column_count - 1) as f32 * grid_spacing.x)
+ .max(0.0)
+ / 2.0;
+
+ ui.horizontal(|ui| {
+ ui.add_space(centering_space);
+
+ ui.vertical(|ui| {
+ ui.horizontal_wrapped(|ui| {
+ ui.add(egui::Label::new(
+ egui::RichText::new("Examples.")
+ .strong()
+ .line_height(Some(32.0))
+ .text_style(re_ui::ReUi::welcome_screen_h1()),
+ ));
+
+ ui.add(egui::Label::new(
+ egui::RichText::new("Learn from the community.")
+ .line_height(Some(32.0))
+ .text_style(re_ui::ReUi::welcome_screen_h1()),
+ ));
+ });
+
+ ui.add_space(TITLE_TO_GRID_VSPACE);
+
+ egui::Grid::new("example_page_grid")
+ .spacing(grid_spacing)
+ .min_col_width(column_width)
+ .max_col_width(column_width)
+ .show(ui, |ui| {
+ self.examples
+ .chunks_mut(column_count)
+ .for_each(|example_layouts| {
+ for example in &mut *example_layouts {
+ // this is the beginning of the first cell for this example
+ example.set_top_left(ui.cursor().min);
+
+ let thumbnail = &example.desc.thumbnail;
+ let width = thumbnail.width as f32;
+ let height = thumbnail.height as f32;
+ ui.vertical(|ui| {
+ let size = egui::vec2(
+ column_width,
+ height * column_width / width,
+ );
+
+ example_thumbnail(
+ ui,
+ rx,
+ &example.desc,
+ size,
+ example.hovered(ui, self.id),
+ );
+
+ ui.add_space(THUMBNAIL_TO_DESCRIPTION_VSPACE);
+ });
+ }
+
+ ui.end_row();
+
+ for example in &mut *example_layouts {
+ ui.vertical(|ui| {
+ example_description(
+ ui,
+ &example.desc,
+ example.hovered(ui, self.id),
+ );
+
+ ui.add_space(DESCRIPTION_TO_TAGS_VSPACE);
+ });
+ }
+
+ ui.end_row();
+
+ for example in &mut *example_layouts {
+ ui.vertical(|ui| {
+ example_tags(ui, &example.desc);
+
+ // this is the end of the last cell for this example
+ example.set_bottom_right(egui::pos2(
+ ui.cursor().min.x + column_width,
+ ui.cursor().min.y,
+ ));
+
+ ui.add_space(ROW_VSPACE);
+ });
+ }
+
+ ui.end_row();
+ });
+ });
+
+ self.examples.iter().for_each(|example| {
+ if example.clicked(ui, self.id) {
+ let data_source = re_data_source::DataSource::RrdHttpUrl(
+ example.desc.rrd_url.clone(),
+ );
+ command_sender.send_system(
+ re_viewer_context::SystemCommand::LoadDataSource(data_source),
+ );
+ }
+ });
+ });
+ });
+ });
+ }
+}
+
+fn is_loading(rx: &ReceiveSet, example: &ExampleDesc) -> bool {
+ rx.sources().iter().any(|s| {
+ if let re_smart_channel::SmartChannelSource::RrdHttpStream { url } = s.as_ref() {
+ url == &example.rrd_url
+ } else {
+ false
+ }
+ })
+}
+
+fn example_thumbnail(
+ ui: &mut Ui,
+ rx: &ReceiveSet,
+ example: &ExampleDesc,
+ size: egui::Vec2,
+ hovered: bool,
+) {
+ let rounding = egui::Rounding::same(THUMBNAIL_RADIUS);
+
+ let resp = match ui.ctx().try_load_texture(
+ example.thumbnail.url.as_str(),
+ TextureOptions::LINEAR,
+ egui::SizeHint::from(size),
+ ) {
+ Ok(TexturePoll::Ready { texture }) => {
+ ui.add(egui::Image::new(texture.id, size).rounding(rounding))
+ }
+ Ok(TexturePoll::Pending { .. }) => {
+ ui.allocate_ui_at_rect(egui::Rect::from_min_size(ui.cursor().min, size), |ui| {
+ // add some space before the spinner
+ ui.add_space(4.0);
+ ui.horizontal(|ui| {
+ ui.add_space(4.0);
+ ui.spinner()
+ .on_hover_text(format!("Loading thumbnail for {} example…", example.title));
+ });
+
+ // Eat all available space so the spinner container has the same size as the
+ // thumbnail itself.
+ ui.allocate_exact_size(ui.max_rect().max - ui.cursor().min, egui::Sense::hover());
+ })
+ .response
+ }
+
+ Err(err) => ui.colored_label(ui.visuals().error_fg_color, err.to_string()),
+ };
+
+ // TODO(ab): use design tokens
+ let border_color = if hovered {
+ ui.visuals_mut().widgets.hovered.fg_stroke.color
+ } else {
+ egui::Color32::from_gray(44)
+ };
+
+ ui.painter()
+ .rect_stroke(resp.rect, rounding, (1.0, border_color));
+
+ // spinner overlay
+ if is_loading(rx, example) {
+ ui.painter().rect_filled(
+ resp.rect,
+ rounding,
+ egui::Color32::BLACK.gamma_multiply(0.75),
+ );
+
+ let spinner_size = resp.rect.size().min_elem().at_most(72.0);
+ let spinner_rect =
+ egui::Rect::from_center_size(resp.rect.center(), egui::Vec2::splat(spinner_size));
+ ui.allocate_ui_at_rect(spinner_rect, |ui| {
+ ui.add(egui::Spinner::new().size(spinner_size));
+ });
+ }
+}
+
+fn example_description(ui: &mut Ui, example: &ExampleDesc, hovered: bool) {
+ ui.label(
+ egui::RichText::new(example.title.clone())
+ .strong()
+ .line_height(Some(22.0))
+ .text_style(re_ui::ReUi::welcome_screen_body()),
+ );
+
+ ui.add_space(4.0);
+
+ let mut desc_text = egui::RichText::new(example.description.clone()).line_height(Some(19.0));
+ if hovered {
+ desc_text = desc_text.strong();
+ }
+
+ ui.add(egui::Label::new(desc_text).wrap(true));
+}
+
+fn example_tags(ui: &mut Ui, example: &ExampleDesc) {
+ // TODO(ab): use design tokens
+ ui.horizontal_wrapped(|ui| {
+ ui.style_mut().spacing.button_padding = egui::vec2(4.0, 2.0);
+ ui.style_mut().spacing.item_spacing = egui::vec2(4.0, 4.0);
+ for tag in &example.tags {
+ ui.add(
+ egui::Button::new(tag)
+ .sense(egui::Sense::hover())
+ .rounding(6.0)
+ .fill(egui::Color32::from_rgb(26, 29, 30))
+ .stroke(egui::Stroke::new(
+ 1.0,
+ egui::Color32::WHITE.gamma_multiply(0.086),
+ ))
+ .wrap(false),
+ );
+ }
+ });
+}
diff --git a/crates/re_viewer/src/ui/welcome_screen/mod.rs b/crates/re_viewer/src/ui/welcome_screen/mod.rs
new file mode 100644
index 000000000000..f6a361cba205
--- /dev/null
+++ b/crates/re_viewer/src/ui/welcome_screen/mod.rs
@@ -0,0 +1,231 @@
+mod example_page;
+mod welcome_page;
+
+use egui::Widget;
+use re_log_types::LogMsg;
+use re_smart_channel::{ReceiveSet, SmartChannelSource};
+use re_ui::ReUi;
+use std::hash::Hash;
+use welcome_page::welcome_page_ui;
+
+#[derive(Debug, Default, PartialEq, Hash)]
+enum WelcomeScreenPage {
+ #[default]
+ Welcome,
+ Examples,
+}
+
+#[derive(Debug)]
+pub struct WelcomeScreen {
+ current_page: WelcomeScreenPage,
+
+ example_page: example_page::ExamplePage,
+}
+
+impl Default for WelcomeScreen {
+ fn default() -> Self {
+ Self {
+ current_page: WelcomeScreenPage::Welcome,
+ example_page: example_page::ExamplePage::new(),
+ }
+ }
+}
+
+impl WelcomeScreen {
+ /// Welcome screen shown in place of the viewport when no data is loaded.
+ pub fn ui(
+ &mut self,
+ re_ui: &re_ui::ReUi,
+ ui: &mut egui::Ui,
+ rx: &ReceiveSet,
+ command_sender: &re_viewer_context::CommandSender,
+ ) {
+ // tab bar
+ egui::Frame {
+ inner_margin: egui::Margin::symmetric(12.0, 8.0),
+ ..Default::default()
+ }
+ .show(ui, |ui| {
+ ui.horizontal(|ui| {
+ ReUi::welcome_screen_tab_bar_style(ui);
+
+ ui.selectable_value(
+ &mut self.current_page,
+ WelcomeScreenPage::Welcome,
+ "Welcome",
+ );
+ ui.selectable_value(
+ &mut self.current_page,
+ WelcomeScreenPage::Examples,
+ "Examples",
+ );
+ });
+ });
+
+ // This is needed otherwise `example_page_ui` bleeds by a few pixels over the timeline panel
+ // TODO(ab): figure out why that happens
+ ui.set_clip_rect(ui.available_rect_before_wrap());
+
+ egui::ScrollArea::new([
+ matches!(self.current_page, WelcomeScreenPage::Welcome),
+ true,
+ ])
+ .id_source(("welcome_screen_page", &self.current_page))
+ .auto_shrink([false, false])
+ .show(ui, |ui| match self.current_page {
+ WelcomeScreenPage::Welcome => welcome_page_ui(re_ui, ui, rx, command_sender),
+ WelcomeScreenPage::Examples => self.example_page.ui(ui, rx, command_sender),
+ });
+ }
+}
+
+/// Full-screen UI shown while in loading state.
+pub fn loading_ui(ui: &mut egui::Ui, rx: &ReceiveSet) {
+ let status_strings = status_strings(rx);
+ if status_strings.is_empty() {
+ return;
+ }
+
+ ui.centered_and_justified(|ui| {
+ for status_string in status_strings {
+ let style = ui.style();
+ let mut layout_job = egui::text::LayoutJob::default();
+ layout_job.append(
+ status_string.status,
+ 0.0,
+ egui::TextFormat::simple(
+ egui::TextStyle::Heading.resolve(style),
+ style.visuals.strong_text_color(),
+ ),
+ );
+ layout_job.append(
+ &format!("\n\n{}", status_string.source),
+ 0.0,
+ egui::TextFormat::simple(
+ egui::TextStyle::Body.resolve(style),
+ style.visuals.text_color(),
+ ),
+ );
+ layout_job.halign = egui::Align::Center;
+ ui.label(layout_job);
+ }
+ });
+}
+
+fn button_centered_label(ui: &mut egui::Ui, label: impl Into) {
+ ui.vertical(|ui| {
+ ui.add_space(9.0);
+ ui.label(label);
+ });
+}
+
+fn set_large_button_style(ui: &mut egui::Ui) {
+ ui.style_mut().spacing.button_padding = egui::vec2(12.0, 9.0);
+ let visuals = ui.visuals_mut();
+ visuals.widgets.hovered.expansion = 0.0;
+ visuals.widgets.active.expansion = 0.0;
+ visuals.widgets.open.expansion = 0.0;
+
+ visuals.widgets.inactive.rounding = egui::Rounding::same(8.);
+ visuals.widgets.hovered.rounding = egui::Rounding::same(8.);
+ visuals.widgets.active.rounding = egui::Rounding::same(8.);
+
+ visuals.widgets.inactive.weak_bg_fill = visuals.widgets.inactive.bg_fill;
+}
+
+fn url_large_text_button(
+ re_ui: &re_ui::ReUi,
+ ui: &mut egui::Ui,
+ text: impl Into,
+ url: &str,
+) {
+ ui.scope(|ui| {
+ set_large_button_style(ui);
+
+ let image = re_ui.icon_image(&re_ui::icons::EXTERNAL_LINK);
+ let texture_id = image.texture_id(ui.ctx());
+
+ if egui::Button::image_and_text(texture_id, ReUi::small_icon_size(), text)
+ .ui(ui)
+ .on_hover_cursor(egui::CursorIcon::PointingHand)
+ .on_hover_text(url)
+ .clicked()
+ {
+ ui.ctx().output_mut(|o| {
+ o.open_url = Some(egui::output::OpenUrl {
+ url: url.to_owned(),
+ new_tab: true,
+ });
+ });
+ }
+ });
+}
+
+fn large_text_button(ui: &mut egui::Ui, text: impl Into) -> egui::Response {
+ ui.scope(|ui| {
+ set_large_button_style(ui);
+ ui.button(text)
+ })
+ .inner
+}
+
+/// Describes the current state of the Rerun viewer.
+struct StatusString {
+ /// General status string (e.g. "Ready", "Loading…", etc.).
+ status: &'static str,
+
+ /// Source string (e.g. listening IP, file path, etc.).
+ source: String,
+
+ /// Whether or not the status is valid once data loading is completed, i.e. if data may still
+ /// be received later.
+ long_term: bool,
+}
+
+impl StatusString {
+ fn new(status: &'static str, source: String, long_term: bool) -> Self {
+ Self {
+ status,
+ source,
+ long_term,
+ }
+ }
+}
+
+/// Returns the status strings to be displayed by the loading and welcome screen.
+fn status_strings(rx: &ReceiveSet) -> Vec {
+ rx.sources()
+ .into_iter()
+ .map(|s| status_string(&s))
+ .collect()
+}
+
+fn status_string(source: &SmartChannelSource) -> StatusString {
+ match source {
+ re_smart_channel::SmartChannelSource::File(path) => {
+ StatusString::new("Loading…", path.display().to_string(), false)
+ }
+ re_smart_channel::SmartChannelSource::RrdHttpStream { url } => {
+ StatusString::new("Loading…", url.clone(), false)
+ }
+ re_smart_channel::SmartChannelSource::RrdWebEventListener => {
+ StatusString::new("Ready", "Waiting for logging data…".to_owned(), true)
+ }
+ re_smart_channel::SmartChannelSource::Sdk => StatusString::new(
+ "Ready",
+ "Waiting for logging data from SDK".to_owned(),
+ true,
+ ),
+ re_smart_channel::SmartChannelSource::WsClient { ws_server_url } => {
+ // TODO(emilk): it would be even better to know whether or not we are connected, or are attempting to connect
+ StatusString::new(
+ "Ready",
+ format!("Waiting for data from {ws_server_url}"),
+ true,
+ )
+ }
+ re_smart_channel::SmartChannelSource::TcpServer { port } => {
+ StatusString::new("Ready", format!("Listening on port {port}"), true)
+ }
+ }
+}
diff --git a/crates/re_viewer/src/ui/welcome_screen/welcome_page.rs b/crates/re_viewer/src/ui/welcome_screen/welcome_page.rs
new file mode 100644
index 000000000000..35cae6a1aafe
--- /dev/null
+++ b/crates/re_viewer/src/ui/welcome_screen/welcome_page.rs
@@ -0,0 +1,181 @@
+use super::{button_centered_label, large_text_button, status_strings, url_large_text_button};
+use egui::Ui;
+use re_log_types::LogMsg;
+use re_smart_channel::ReceiveSet;
+use re_ui::{ReUi, UICommandSender};
+
+const MIN_COLUMN_WIDTH: f32 = 250.0;
+const MAX_COLUMN_WIDTH: f32 = 400.0;
+
+//const CPP_QUICKSTART: &str = "https://www.rerun.io/docs/getting-started/cpp";
+const PYTHON_QUICKSTART: &str = "https://www.rerun.io/docs/getting-started/python";
+const RUST_QUICKSTART: &str = "https://www.rerun.io/docs/getting-started/rust";
+const SPACE_VIEWS_HELP: &str = "https://www.rerun.io/docs/getting-started/viewer-walkthrough";
+
+pub(super) fn welcome_page_ui(
+ re_ui: &re_ui::ReUi,
+ ui: &mut egui::Ui,
+ rx: &ReceiveSet,
+ command_sender: &re_viewer_context::CommandSender,
+) {
+ let mut margin = egui::Margin::same(40.0);
+ margin.bottom = 0.0;
+ egui::Frame {
+ inner_margin: margin,
+ ..Default::default()
+ }
+ .show(ui, |ui| {
+ ui.vertical(|ui| {
+ ui.add(
+ egui::Label::new(
+ egui::RichText::new("Welcome")
+ .strong()
+ .text_style(re_ui::ReUi::welcome_screen_h1()),
+ )
+ .wrap(false),
+ );
+
+ ui.add(
+ egui::Label::new(
+ egui::RichText::new("Visualize multimodal data")
+ .text_style(re_ui::ReUi::welcome_screen_h2()),
+ )
+ .wrap(false),
+ );
+
+ ui.add_space(20.0);
+
+ onboarding_content_ui(re_ui, ui, command_sender);
+
+ for status_strings in status_strings(rx) {
+ if status_strings.long_term {
+ ui.add_space(55.0);
+ ui.vertical_centered(|ui| {
+ ui.label(status_strings.status);
+ ui.label(
+ egui::RichText::new(status_strings.source)
+ .color(ui.visuals().weak_text_color()),
+ );
+ });
+ }
+ }
+ });
+ });
+}
+
+fn onboarding_content_ui(
+ re_ui: &ReUi,
+ ui: &mut Ui,
+ command_sender: &re_viewer_context::CommandSender,
+) {
+ let column_spacing = 15.0;
+ let stability_adjustment = 1.0; // minimize jitter with sizing and scroll bars
+ let column_width = ((ui.available_width() - 2. * column_spacing) / 3.0 - stability_adjustment)
+ .clamp(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
+
+ let grid = egui::Grid::new("welcome_screen_grid")
+ .spacing(egui::Vec2::splat(column_spacing))
+ .min_col_width(column_width)
+ .max_col_width(column_width);
+
+ grid.show(ui, |ui| {
+ image_banner(
+ re_ui,
+ ui,
+ &re_ui::icons::WELCOME_SCREEN_LIVE_DATA,
+ column_width,
+ );
+ image_banner(
+ re_ui,
+ ui,
+ &re_ui::icons::WELCOME_SCREEN_RECORDED_DATA,
+ column_width,
+ );
+ image_banner(
+ re_ui,
+ ui,
+ &re_ui::icons::WELCOME_SCREEN_CONFIGURE,
+ column_width,
+ );
+
+ ui.end_row();
+
+ ui.vertical(|ui| {
+ ui.label(
+ egui::RichText::new("Connect to live data")
+ .strong()
+ .text_style(re_ui::ReUi::welcome_screen_h3()),
+ );
+ ui.label(
+ egui::RichText::new(
+ "Use the Rerun SDK to stream data from your code to the Rerun Viewer. \
+ synchronized data from multiple processes, locally or over a network.",
+ )
+ .text_style(re_ui::ReUi::welcome_screen_body()),
+ );
+ });
+
+ ui.vertical(|ui| {
+ ui.label(
+ egui::RichText::new("Load recorded data")
+ .strong()
+ .text_style(re_ui::ReUi::welcome_screen_h3()),
+ );
+ ui.label(
+ egui::RichText::new(
+ "Open and visualize recorded data from previous Rerun sessions (.rrd) as well \
+ as data in formats like .gltf and .jpg.",
+ )
+ .text_style(re_ui::ReUi::welcome_screen_body()),
+ );
+ });
+
+ ui.vertical(|ui| {
+ ui.label(
+ egui::RichText::new("Configure your views")
+ .strong()
+ .text_style(re_ui::ReUi::welcome_screen_h3()),
+ );
+ ui.label(
+ egui::RichText::new(
+ "Add and rearrange views, and configure what data is shown and how. Configure \
+ interactively in the viewer or (coming soon) directly from code in the SDK.",
+ )
+ .text_style(re_ui::ReUi::welcome_screen_body()),
+ );
+ });
+
+ ui.end_row();
+
+ ui.horizontal(|ui| {
+ button_centered_label(ui, "Quick start…");
+ // TODO(ab): activate when C++ is ready!
+ // url_large_text_button(re_ui, ui, "C++", CPP_QUICKSTART);
+ url_large_text_button(re_ui, ui, "Python", PYTHON_QUICKSTART);
+ url_large_text_button(re_ui, ui, "Rust", RUST_QUICKSTART);
+ });
+
+ ui.horizontal(|ui| {
+ if large_text_button(ui, "Open file…").clicked() {
+ command_sender.send_ui(re_ui::UICommand::Open);
+ }
+ button_centered_label(ui, "Or drop a file anywhere!");
+ });
+
+ ui.horizontal(|ui| {
+ url_large_text_button(re_ui, ui, "Learn about Views", SPACE_VIEWS_HELP);
+ });
+
+ ui.end_row();
+ });
+}
+
+fn image_banner(re_ui: &re_ui::ReUi, ui: &mut egui::Ui, image: &re_ui::Icon, column_width: f32) {
+ let image = re_ui.icon_image(image);
+ let texture_id = image.texture_id(ui.ctx());
+ let height = column_width * image.size()[1] as f32 / image.size()[0] as f32;
+ ui.add(
+ egui::Image::new(texture_id, egui::vec2(column_width, height))
+ .rounding(egui::Rounding::same(8.)),
+ );
+}