From 1643db8118fabe901435e9f8d952aa083837503d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <1665677+jprochazk@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:57:18 +0100 Subject: [PATCH] Merge example page into welcome screen (#5329) ### What - Part of https://github.com/rerun-io/rerun/issues/4961 - Closes https://github.com/rerun-io/rerun/issues/3244 Changes: - Updated design of thumbnails on example page to match https://rerun.io/examples - Updated some spacing on welcome screen to better match design from #3244 - Remove loading spinners on example page thumbnails - Remove tabs on welcome screen + remove the examples welcome screen card + the whole mechanism of switching between the welcome screen and example page - Move example page contents below welcome screen - If example page is not visible, indicate their presence with a floating `See examples` button, which may be clicked to scroll the examples page into view I have not done anything to change the loading behavior, or added any extra effects as the example page loads in. There are also a few things in #4961 not done in this PR, such as updating the copy or merging the quick start examples. ![image](https://github.com/rerun-io/rerun/assets/1665677/173cbfd6-4f5d-43d8-8b91-e01aa0519376) ### 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 the web demo (if applicable): * Using newly built examples: [app.rerun.io](https://app.rerun.io/pr/5329/index.html) * Using examples from latest `main` build: [app.rerun.io](https://app.rerun.io/pr/5329/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [app.rerun.io](https://app.rerun.io/pr/5329/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG * [x] If applicable, add a new check to the [release checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)! - [PR Build Summary](https://build.rerun.io/pr/5329) - [Docs preview](https://rerun.io/preview/b23e7aa1a53bc82682b6327c95b56fd600c09a99/docs) - [Examples preview](https://rerun.io/preview/b23e7aa1a53bc82682b6327c95b56fd600c09a99/examples) - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) --- crates/re_ui/data/icons/arrow_down.png | Bin 0 -> 426 bytes crates/re_ui/src/design_tokens.rs | 4 + crates/re_ui/src/icons.rs | 2 + crates/re_ui/src/lib.rs | 5 + .../{example_page.rs => example_section.rs} | 306 ++++++++++-------- crates/re_viewer/src/ui/welcome_screen/mod.rs | 93 ++---- .../{welcome_page.rs => welcome_section.rs} | 124 +++---- 7 files changed, 233 insertions(+), 301 deletions(-) create mode 100644 crates/re_ui/data/icons/arrow_down.png rename crates/re_viewer/src/ui/welcome_screen/{example_page.rs => example_section.rs} (63%) rename crates/re_viewer/src/ui/welcome_screen/{welcome_page.rs => welcome_section.rs} (78%) diff --git a/crates/re_ui/data/icons/arrow_down.png b/crates/re_ui/data/icons/arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..07ad0708cb2ceab96f5f2de485ab9b0fb76b49a3 GIT binary patch literal 426 zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s0wiy+h|2>hg=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWO>_%)lU~3c`$@K`I}B zg6t)pzOL*qm}EtHWPCK|ZUqWedb&7R4jZa;OPX=k6cOh(sH zH=a9xj|yKkFxYj)SkU$Xv-M@(eF=uVA5<@kbn7N`Rn-UvZ<+Ge9 wnO2EggK*UoS)c|DxD6$lxv9k^iMa(>^jJVFX+FPx8&D5}r>mdKI;Vst0CmcO3jhEB literal 0 HcmV?d00001 diff --git a/crates/re_ui/src/design_tokens.rs b/crates/re_ui/src/design_tokens.rs index d30e7831a6eb..5a81e0fe1982 100644 --- a/crates/re_ui/src/design_tokens.rs +++ b/crates/re_ui/src/design_tokens.rs @@ -91,6 +91,10 @@ fn apply_design_tokens(ctx: &egui::Context) -> DesignTokens { egui_style .text_styles .insert(ReUi::welcome_screen_h3(), egui::FontId::proportional(15.0)); + egui_style.text_styles.insert( + ReUi::welcome_screen_example_title(), + egui::FontId::proportional(16.0), + ); egui_style.text_styles.insert( ReUi::welcome_screen_body(), egui::FontId::proportional(13.0), diff --git a/crates/re_ui/src/icons.rs b/crates/re_ui/src/icons.rs index 1069f05aa77a..6bf1776014f2 100644 --- a/crates/re_ui/src/icons.rs +++ b/crates/re_ui/src/icons.rs @@ -35,6 +35,8 @@ pub const ARROW_RIGHT: Icon = Icon::new( "arrow_right", include_bytes!("../data/icons/arrow_right.png"), ); +pub const ARROW_DOWN: Icon = + Icon::new("arrow_down", include_bytes!("../data/icons/arrow_down.png")); pub const LOOP: Icon = Icon::new("loop", include_bytes!("../data/icons/loop.png")); pub const RIGHT_PANEL_TOGGLE: Icon = Icon::new( diff --git a/crates/re_ui/src/lib.rs b/crates/re_ui/src/lib.rs index 3283015697ad..1611fb742b4a 100644 --- a/crates/re_ui/src/lib.rs +++ b/crates/re_ui/src/lib.rs @@ -119,6 +119,11 @@ impl ReUi { egui::TextStyle::Name("welcome-screen-h3".into()) } + #[inline] + pub fn welcome_screen_example_title() -> egui::TextStyle { + egui::TextStyle::Name("welcome-screen-example-title".into()) + } + #[inline] pub fn welcome_screen_body() -> egui::TextStyle { egui::TextStyle::Name("welcome-screen-body".into()) diff --git a/crates/re_viewer/src/ui/welcome_screen/example_page.rs b/crates/re_viewer/src/ui/welcome_screen/example_section.rs similarity index 63% rename from crates/re_viewer/src/ui/welcome_screen/example_page.rs rename to crates/re_viewer/src/ui/welcome_screen/example_section.rs index 89498fd7efa0..6668d41dfb43 100644 --- a/crates/re_viewer/src/ui/welcome_screen/example_page.rs +++ b/crates/re_viewer/src/ui/welcome_screen/example_section.rs @@ -1,13 +1,13 @@ +use egui::vec2; +use egui::Color32; use egui::{NumExt as _, Ui}; use ehttp::{fetch, Request}; use poll_promise::Promise; -use re_log_types::LogMsg; -use re_smart_channel::ReceiveSet; +use re_ui::icons::ARROW_DOWN; +use re_ui::ReUi; use re_viewer_context::SystemCommandSender; -use super::WelcomeScreenResponse; - #[derive(Debug, serde::Deserialize)] struct ExampleThumbnail { url: String, @@ -23,7 +23,6 @@ struct ExampleDesc { /// human readable version of the example name title: String, - description: String, tags: Vec, rrd_url: String, @@ -36,8 +35,7 @@ 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 THUMBNAIL_TO_DESCRIPTION_VSPACE: f32 = 8.0; const ROW_VSPACE: f32 = 32.0; const THUMBNAIL_RADIUS: f32 = 4.0; @@ -172,7 +170,7 @@ fn load_file_size(egui_ctx: &egui::Context, url: String) -> Promise> promise } -pub(super) struct ExamplePage { +pub(super) struct ExampleSection { id: egui::Id, manifest_url: String, examples: Option, @@ -211,17 +209,17 @@ fn default_manifest_url() -> String { } } -impl Default for ExamplePage { +impl Default for ExampleSection { fn default() -> Self { Self { - id: egui::Id::new("example_page"), + id: egui::Id::new("example_section"), manifest_url: default_manifest_url(), examples: None, } } } -impl ExamplePage { +impl ExampleSection { pub fn set_manifest_url(&mut self, egui_ctx: &egui::Context, url: String) { if self.manifest_url != url { self.manifest_url = url.clone(); @@ -233,29 +231,28 @@ impl ExamplePage { &mut self, ui: &mut egui::Ui, re_ui: &re_ui::ReUi, - rx: &re_smart_channel::ReceiveSet, command_sender: &re_viewer_context::CommandSender, - ) -> WelcomeScreenResponse { + ) { let examples = self .examples .get_or_insert_with(|| load_manifest(ui.ctx(), self.manifest_url.clone())); let Some(examples) = examples.ready_mut() else { ui.spinner(); - return WelcomeScreenResponse::default(); + return; }; let examples = match examples { Ok(examples) => examples, Err(err) => { ui.label(re_ui.error_text(format!("Failed to load examples: {err}"))); - return WelcomeScreenResponse::default(); + return; } }; if examples.is_empty() { ui.label("No examples found."); - return WelcomeScreenResponse::default(); + return; } // vertical spacing isn't homogeneous so it's handled manually @@ -269,97 +266,90 @@ impl ExamplePage { .floor() .at_most(MAX_COLUMN_WIDTH); - // this space is added on the left so that the grid is centered - let centering_hspace = (ui.available_width() - - column_count as f32 * column_width - - (column_count - 1) as f32 * grid_spacing.x) - .max(0.0) - / 2.0; + // cursor is currently at the top of the section, + // so we use it to check for visibility of the whole section. + let example_section_rect = ui.cursor(); + let examples_visible = ui.is_rect_visible(ui.cursor().translate(vec2(0.0, 16.0))); - ui.horizontal(|ui| { - ui.add_space(centering_hspace); - - ui.vertical(|ui| { - ui.horizontal_wrapped(|ui| { + let title_response = ui + .horizontal(|ui| { + ui.vertical_centered(|ui| { ui.add(egui::Label::new( - egui::RichText::new("Examples.") + egui::RichText::new("Examples") .strong() .line_height(Some(32.0)) .text_style(re_ui::ReUi::welcome_screen_h1()), - )); + )) + }) + .inner + }) + .inner; + ui.end_row(); - ui.add(egui::Label::new( - egui::RichText::new("Explore what you can build.") - .line_height(Some(32.0)) - .text_style(re_ui::ReUi::welcome_screen_h1()), - )); - }); + ui.horizontal(|ui| { + // this space is added on the left so that the grid is centered + let centering_hspace = (ui.available_width() + - column_count as f32 * column_width + - (column_count - 1) as f32 * grid_spacing.x) + .max(0.0) + / 2.0; + ui.add_space(centering_hspace); + ui.vertical(|ui| { ui.add_space(TITLE_TO_GRID_VSPACE); - egui::Grid::new("example_page_grid") + egui::Grid::new("example_section_grid") .spacing(grid_spacing) .min_col_width(column_width) .max_col_width(column_width) .show(ui, |ui| { - 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, - 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(); - }); + for example_layouts in examples.chunks_mut(column_count) { + 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, + &example.desc, + size, + example.hovered(ui, self.id), + ); + }); + } + + ui.end_row(); + + for example in &mut *example_layouts { + ui.vertical(|ui| { + example_title(ui, example); + }); + } + + 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(); + } }); for example in examples { @@ -379,35 +369,81 @@ impl ExamplePage { }); }); - WelcomeScreenResponse::default() - } -} + if !examples_visible { + let screen_rect = ui.ctx().screen_rect(); + let indicator_rect = example_section_rect + .with_min_y(screen_rect.bottom() - 125.0) + .with_max_y(screen_rect.bottom()); + + let mut ui = ui.child_ui( + indicator_rect, + egui::Layout::centered_and_justified(egui::Direction::LeftToRight), + ); -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 + ui.vertical_centered(|ui| { + ui.add_space(16.0); + + ui.scope(|ui| { + ui.spacing_mut().button_padding = vec2(16.0, 8.0); + let response = ui.add( + egui::Button::image_and_text( + ARROW_DOWN + .as_image() + .tint(egui::Color32::BLACK) + .fit_to_exact_size(ReUi::small_icon_size()), + egui::RichText::new("See examples").color(egui::Color32::BLACK), + ) + .rounding(16.0) + .fill(egui::Color32::from_gray(0xfa)), + ); + if response.clicked() { + title_response.scroll_to_me(Some(egui::Align::Min)); + } + }) + }); } - }) + } } fn example_thumbnail( ui: &mut Ui, - rx: &ReceiveSet, example: &ExampleDesc, - size: egui::Vec2, + thumbnail_size: egui::Vec2, hovered: bool, ) { - let rounding = egui::Rounding::same(THUMBNAIL_RADIUS); + const ASPECT_RATIO: f32 = 16.0 / 6.75; // same as `rerun.io/examples` + const PADDING_PCT: f32 = 0.07; // 7% + + let rounding = egui::Rounding { + nw: THUMBNAIL_RADIUS, + ne: THUMBNAIL_RADIUS, + sw: 0.0, + se: 0.0, + }; + + let clip_width = thumbnail_size.x; + let clip_height = thumbnail_size.x / ASPECT_RATIO; + let padding = thumbnail_size.x * PADDING_PCT; - let response = ui.add( - egui::Image::new(&example.thumbnail.url) - .rounding(rounding) - .fit_to_exact_size(size), + let clip_top_left = ui.cursor().left_top(); + let bottom_right = clip_top_left + vec2(clip_width, clip_height); + let clip_rect = egui::Rect::from_min_max(clip_top_left, bottom_right); + + let thumbnail_top_left = clip_top_left + vec2(padding, 0.0); + let thumbnail_rect = egui::Rect::from_min_max( + thumbnail_top_left, + thumbnail_top_left + thumbnail_size - vec2(padding * 2.0, padding * 2.0 / ASPECT_RATIO), ); + // manually clip the rect and paint the image + let orig_clip_rect = ui.clip_rect(); + ui.set_clip_rect(orig_clip_rect.intersect(clip_rect)); + egui::Image::new(&example.thumbnail.url) + .rounding(rounding) + .paint_at(ui, thumbnail_rect); + ui.advance_cursor_after_rect(clip_rect.expand2(vec2(0.0, THUMBNAIL_TO_DESCRIPTION_VSPACE))); + ui.set_clip_rect(orig_clip_rect); + // TODO(ab): use design tokens let border_color = if hovered { ui.visuals_mut().widgets.hovered.fg_stroke.color @@ -415,36 +451,27 @@ fn example_thumbnail( egui::Color32::from_gray(44) }; - ui.painter() - .rect_stroke(response.rect, rounding, (1.0, border_color)); - - // Show spinner overlay while loading the example: - if is_loading(rx, example) { - ui.painter().rect_filled( - response.rect, - rounding, - egui::Color32::BLACK.gamma_multiply(0.75), - ); - - let spinner_size = response.rect.size().min_elem().at_most(72.0); - let spinner_rect = - egui::Rect::from_center_size(response.rect.center(), egui::Vec2::splat(spinner_size)); - ui.allocate_ui_at_rect(spinner_rect, |ui| { - ui.add(egui::Spinner::new().size(spinner_size)); - }); - } + // paint border + ui.painter().rect_stroke( + clip_rect.intersect(thumbnail_rect), + rounding, + (1.0, border_color), + ); + ui.painter().line_segment( + [clip_rect.left_bottom(), clip_rect.right_bottom()], + (1.0, border_color), + ); } -fn example_description(ui: &mut Ui, example: &ExampleDescLayout, hovered: bool) { - let desc = &example.desc; - - let title = egui::RichText::new(desc.title.clone()) +fn example_title(ui: &mut Ui, example: &ExampleDescLayout) { + let title = egui::RichText::new(example.desc.title.clone()) .strong() .line_height(Some(22.0)) - .text_style(re_ui::ReUi::welcome_screen_body()); + .color(Color32::from_rgb(178, 178, 187)) + .text_style(re_ui::ReUi::welcome_screen_example_title()); ui.horizontal(|ui| { - ui.label(title); + ui.add(egui::Label::new(title).wrap(true)); if let Some(Some(size)) = example.rrd_byte_size_promise.ready().cloned() { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -453,14 +480,7 @@ fn example_description(ui: &mut Ui, example: &ExampleDescLayout, hovered: bool) } }); - ui.add_space(4.0); - - let mut desc_text = egui::RichText::new(desc.description.clone()).line_height(Some(19.0)); - if hovered { - desc_text = desc_text.strong(); - } - - ui.add(egui::Label::new(desc_text).wrap(true)); + ui.add_space(1.0); } fn example_tags(ui: &mut Ui, example: &ExampleDesc) { diff --git a/crates/re_viewer/src/ui/welcome_screen/mod.rs b/crates/re_viewer/src/ui/welcome_screen/mod.rs index cd0d24307c2d..5dee8b9bee93 100644 --- a/crates/re_viewer/src/ui/welcome_screen/mod.rs +++ b/crates/re_viewer/src/ui/welcome_screen/mod.rs @@ -1,40 +1,17 @@ -mod example_page; -mod welcome_page; - -use std::hash::Hash; +mod example_section; +mod welcome_section; use egui::Widget; -use welcome_page::welcome_page_ui; +use example_section::ExampleSection; +use welcome_section::welcome_section_ui; use re_log_types::LogMsg; use re_smart_channel::ReceiveSet; use re_ui::ReUi; -#[derive(Debug, Default, PartialEq, Hash)] -enum WelcomeScreenPage { - #[default] - Welcome, - Examples, -} - -pub struct WelcomeScreen { - current_page: WelcomeScreenPage, - example_page: example_page::ExamplePage, -} - #[derive(Default)] -#[must_use] -pub(super) struct WelcomeScreenResponse { - pub go_to_example_page: bool, -} - -impl Default for WelcomeScreen { - fn default() -> Self { - Self { - current_page: WelcomeScreenPage::Welcome, - example_page: example_page::ExamplePage::default(), - } - } +pub struct WelcomeScreen { + example_page: ExampleSection, } impl WelcomeScreen { @@ -50,59 +27,29 @@ impl WelcomeScreen { 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()); - let response: WelcomeScreenResponse = egui::ScrollArea::vertical() - .id_source(("welcome_screen_page", &self.current_page)) + egui::ScrollArea::vertical() + .id_source("welcome_screen_page") .auto_shrink([false, false]) .show(ui, |ui| { - let margin = egui::Margin { - left: 40.0, - right: 40.0, - top: 16.0, - bottom: 8.0, - }; egui::Frame { - inner_margin: margin, + inner_margin: egui::Margin { + left: 40.0, + right: 40.0, + top: 32.0, + bottom: 8.0, + }, ..Default::default() } - .show(ui, |ui| match self.current_page { - WelcomeScreenPage::Welcome => welcome_page_ui(ui, rx, command_sender), - WelcomeScreenPage::Examples => { - self.example_page.ui(ui, re_ui, rx, command_sender) - } - }) - .inner - }) - .inner; - - if response.go_to_example_page { - self.current_page = WelcomeScreenPage::Examples; - } + .show(ui, |ui| { + welcome_section_ui(ui, rx, command_sender); + ui.add_space(80.0); + self.example_page.ui(ui, re_ui, command_sender); + }); + }); } } diff --git a/crates/re_viewer/src/ui/welcome_screen/welcome_page.rs b/crates/re_viewer/src/ui/welcome_screen/welcome_section.rs similarity index 78% rename from crates/re_viewer/src/ui/welcome_screen/welcome_page.rs rename to crates/re_viewer/src/ui/welcome_screen/welcome_section.rs index a29ef88408cd..81b0dcf48ed7 100644 --- a/crates/re_viewer/src/ui/welcome_screen/welcome_page.rs +++ b/crates/re_viewer/src/ui/welcome_screen/welcome_section.rs @@ -1,4 +1,4 @@ -use super::{large_text_button, url_large_text_button, WelcomeScreenResponse}; +use super::{large_text_button, url_large_text_button}; use egui::{NumExt, Ui}; use re_entity_db::EntityDb; use re_log_types::{ @@ -6,6 +6,7 @@ use re_log_types::{ }; use re_smart_channel::ReceiveSet; use re_ui::UICommandSender; +use re_viewer_context::CommandSender; use re_viewer_context::{SystemCommand, SystemCommandSender}; use std::collections::HashMap; @@ -36,42 +37,37 @@ const RUST_CONNECT_CODE_EXAMPLE: &str = const RUST_SPAWN_CODE_EXAMPLE: &str = include_str!("../../../data/quick_start_guides/quick_start_spawn.rs"); -/// Show the welcome page. -/// -/// Return `true` if the user wants to switch to the example page. -pub(super) fn welcome_page_ui( +/// Show the welcome section. +pub(super) fn welcome_section_ui( ui: &mut egui::Ui, rx: &ReceiveSet, command_sender: &re_viewer_context::CommandSender, -) -> WelcomeScreenResponse { +) { ui.vertical(|ui| { let accepts_connections = rx.accepts_tcp_connections(); - onboarding_content_ui(ui, command_sender, accepts_connections) - }) - .inner + onboarding_content_ui(ui, command_sender, accepts_connections); + }); } -struct WelcomePagePanel<'a> { +struct Panel { title: &'static str, body: &'static str, image: &'static re_ui::Icon, - add_buttons: Box bool + 'a>, // returns true if example must be shown + add_buttons: PanelButtonsCallback, } -fn onboarding_content_ui( - ui: &mut Ui, - command_sender: &re_viewer_context::CommandSender, - accepts_connections: bool, -) -> WelcomeScreenResponse { +type PanelButtonsCallback = Box; + +fn onboarding_content_ui(ui: &mut Ui, command_sender: &CommandSender, accepts_connections: bool) { // The panel data is stored in this ad hoc structure such that it can easily be iterated over // in chunks, to make the layout grid code simpler. let panels = [ - WelcomePagePanel { + Panel { title: "Connect to live data", body: "Use the Rerun SDK to stream data from your code to the Rerun Viewer. \ Visualize synchronized data from multiple processes, locally or over a network.", image: &re_ui::icons::WELCOME_SCREEN_LIVE_DATA, - add_buttons: Box::new(|ui: &mut egui::Ui| { + add_buttons: Box::new(move |ui, command_sender| { if large_text_button(ui, "C++").clicked() { let (markdown, code) = if accepts_connections { (CPP_CONNECT_MARKDOWN, CPP_CONNECT_CODE_EXAMPLE) @@ -129,42 +125,27 @@ fn onboarding_content_ui( "rust_quick_start", ); } - - false }), }, - WelcomePagePanel { + Panel { title: "Load recorded data", body: "Open and visualize recorded data from previous Rerun sessions (.rrd) as well as \ data in other formats like .gltf and .jpg. Files can be local or remote.", image: &re_ui::icons::WELCOME_SCREEN_RECORDED_DATA, - add_buttons: Box::new(|ui: &mut egui::Ui| { + add_buttons: Box::new(|ui, command_sender| { if large_text_button(ui, "Open fileā€¦").clicked() { command_sender.send_ui(re_ui::UICommand::Open); } - - false }), }, - WelcomePagePanel { + Panel { title: "Build your views", body: "Add and rearrange views. Configure what data is shown and how. Design \ interactively in the viewer or (coming soon) directly from code in the SDK.", image: &re_ui::icons::WELCOME_SCREEN_CONFIGURE, - add_buttons: Box::new(|ui: &mut egui::Ui| { + add_buttons: Box::new(|ui, _| { url_large_text_button(ui, "Learn about Views", SPACE_VIEWS_HELP); - - false - }), - }, - WelcomePagePanel { - title: "Start with an example", - body: "Load pre-built examples to explore what you can build with Rerun. Each example \ - comes with easy to run code so you can see how it's done.", - image: &re_ui::icons::WELCOME_SCREEN_EXAMPLES, - add_buttons: Box::new(|ui: &mut egui::Ui| { - large_text_button(ui, "View Examples").clicked() }), }, ]; @@ -172,9 +153,6 @@ fn onboarding_content_ui( // Shrink images if needed so user can see all the content buttons let max_image_height = ui.available_height() - 300.0; - let centering_vspace = (ui.available_height() - 650.0) / 2.0; - ui.add_space(centering_vspace.at_least(0.0)); - let panel_count = panels.len(); const MAX_COLUMN_WIDTH: f32 = 255.0; @@ -182,21 +160,28 @@ fn onboarding_content_ui( let grid_spacing = egui::vec2(12.0, 16.0); - let mut column_count = (((ui.available_width() + grid_spacing.x) + let column_count = (((ui.available_width() + grid_spacing.x) / (MIN_COLUMN_WIDTH + grid_spacing.x)) .floor() as usize) .clamp(1, panels.len()); - // we either display 4, 2, or a single column, because 3 columns is ugly with 4 panels. - if column_count == 3 { - column_count = 2; - } - let column_width = ((ui.available_width() + grid_spacing.x) / column_count as f32 - grid_spacing.x) .floor() .at_most(MAX_COLUMN_WIDTH); + ui.horizontal(|ui| { + ui.vertical_centered(|ui| { + ui.add(egui::Label::new( + egui::RichText::new("Welcome") + .strong() + .line_height(Some(32.0)) + .text_style(re_ui::ReUi::welcome_screen_h1()), + )) + }); + }); + ui.end_row(); + ui.horizontal(|ui| { // this space is added on the left so that the grid is centered let centering_hspace = (ui.available_width() @@ -206,31 +191,14 @@ fn onboarding_content_ui( ui.add_space(centering_hspace.at_least(0.0)); ui.vertical(|ui| { - ui.horizontal_wrapped(|ui| { - ui.add(egui::Label::new( - egui::RichText::new("Welcome.") - .strong() - .line_height(Some(32.0)) - .text_style(re_ui::ReUi::welcome_screen_h1()), - )); - - ui.add(egui::Label::new( - egui::RichText::new("Visualize multimodal data.") - .line_height(Some(32.0)) - .text_style(re_ui::ReUi::welcome_screen_h1()), - )); - }); - ui.add_space(32.0); - let grid = egui::Grid::new("welcome_screen_grid") + let grid = egui::Grid::new("welcome_section_grid") .spacing(grid_spacing) .min_col_width(column_width) .max_col_width(column_width); grid.show(ui, |ui| { - let mut show_example = false; - for panels in panels.chunks(column_count) { if column_count == panel_count { for panel in panels { @@ -257,32 +225,18 @@ fn onboarding_content_ui( .text_style(re_ui::ReUi::welcome_screen_h3()), ); ui.label(egui::RichText::new(panel.body).line_height(Some(19.0))); - }); - } - - ui.end_row(); - - for panel in panels { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 4.0; - if (panel.add_buttons)(ui) { - show_example = true; - } + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 4.0; + (panel.add_buttons)(ui, command_sender); + }); }); } ui.end_row(); } - - WelcomeScreenResponse { - go_to_example_page: show_example, - } - }) - .inner - }) - .inner - }) - .inner + }); + }); + }); } fn image_banner(ui: &mut egui::Ui, icon: &re_ui::Icon, column_width: f32, max_image_height: f32) {