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 image ### 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.)), + ); +}