From 13dcb4666d167c7597818affc1e6f3ecaca4fb19 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 8 Nov 2022 15:47:00 +0100 Subject: [PATCH] Make the space views bigger and less cluttered (#269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move view3d settings to Selection View * Move some tensor settings to the selection panel * Start the tensor slice in the middle * Update egui * Put Space View maximize/back/options/help buttons on top of 2d/3d views * UI tweaks * Code cleanup * Nicer panels * Tweak egui_dock style and margin * Update egui * update egui * captialize Co-authored-by: Andreas Reich * Better tooltip Co-authored-by: Andreas Reich * cargo fmt * Update egui * Add ellipsis to "Load…" menu item * update cargo.lock Co-authored-by: Andreas Reich --- Cargo.lock | 16 +- Cargo.toml | 12 +- crates/re_viewer/src/app.rs | 6 +- crates/re_viewer/src/design_tokens.rs | 13 +- crates/re_viewer/src/misc/viewer_context.rs | 16 ++ crates/re_viewer/src/ui/selection_panel.rs | 16 +- crates/re_viewer/src/ui/space_view.rs | 157 +++++++++++------- crates/re_viewer/src/ui/view_2d/mod.rs | 2 +- crates/re_viewer/src/ui/view_2d/ui.rs | 11 +- crates/re_viewer/src/ui/view_3d/mod.rs | 2 +- .../re_viewer/src/ui/view_3d/space_camera.rs | 1 + crates/re_viewer/src/ui/view_3d/ui.rs | 151 ++++++++--------- crates/re_viewer/src/ui/view_tensor/ui.rs | 57 ++++--- crates/re_viewer/src/ui/viewport.rs | 120 ++++++++++--- 14 files changed, 374 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64574f803e93..a7a0a61b9b74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "eframe" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "bytemuck", "directories-next", @@ -1047,7 +1047,7 @@ dependencies = [ [[package]] name = "egui" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "ahash 0.8.1", "epaint", @@ -1069,7 +1069,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "bytemuck", "egui", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "arboard", "egui", @@ -1108,7 +1108,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "egui", "tracing", @@ -1117,7 +1117,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "bytemuck", "egui", @@ -1139,7 +1139,7 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "emath" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "bytemuck", "serde", @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.19.0" -source = "git+https://github.com/emilk/egui?rev=8c76b8caff32e47e4419338e6e0cdf52353cbf2c#8c76b8caff32e47e4419338e6e0cdf52353cbf2c" +source = "git+https://github.com/emilk/egui?rev=51ff32797da027125e9ce2b9903251b61d1bb11b#51ff32797da027125e9ce2b9903251b61d1bb11b" dependencies = [ "ab_glyph", "ahash 0.8.1", diff --git a/Cargo.toml b/Cargo.toml index 512d4038e0ed..2b00fb0ae6d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,12 @@ opt-level = 2 debug = true [patch.crates-io] -# 2022-11-07 - update puffin -eframe = { git = "https://github.com/emilk/egui", rev = "8c76b8caff32e47e4419338e6e0cdf52353cbf2c" } -egui = { git = "https://github.com/emilk/egui", rev = "8c76b8caff32e47e4419338e6e0cdf52353cbf2c" } -egui_extras = { git = "https://github.com/emilk/egui", rev = "8c76b8caff32e47e4419338e6e0cdf52353cbf2c" } -egui_glow = { git = "https://github.com/emilk/egui", rev = "8c76b8caff32e47e4419338e6e0cdf52353cbf2c" } -egui-wgpu = { git = "https://github.com/emilk/egui", rev = "8c76b8caff32e47e4419338e6e0cdf52353cbf2c" } +# 2022-11-08 - improve panels +eframe = { git = "https://github.com/emilk/egui", rev = "51ff32797da027125e9ce2b9903251b61d1bb11b" } +egui = { git = "https://github.com/emilk/egui", rev = "51ff32797da027125e9ce2b9903251b61d1bb11b" } +egui_extras = { git = "https://github.com/emilk/egui", rev = "51ff32797da027125e9ce2b9903251b61d1bb11b" } +egui_glow = { git = "https://github.com/emilk/egui", rev = "51ff32797da027125e9ce2b9903251b61d1bb11b" } +egui-wgpu = { git = "https://github.com/emilk/egui", rev = "51ff32797da027125e9ce2b9903251b61d1bb11b" } # eframe = { path = "../../egui/crates/eframe" } # egui = { path = "../../egui/crates/egui" } diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index d6de55fcc0f3..09c69d1d535f 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -506,7 +506,7 @@ impl AppState { let central_panel_frame = egui::Frame { fill: egui_ctx.style().visuals.window_fill(), - inner_margin: egui::style::Margin::symmetric(4.0, 0.0), + inner_margin: egui::style::Margin::same(0.0), ..Default::default() }; @@ -551,11 +551,9 @@ fn top_panel(egui_ctx: &egui::Context, frame: &mut eframe::Frame, app: &mut App) crate::profile_function!(); let panel_frame = { - let style = egui_ctx.style(); egui::Frame { inner_margin: egui::style::Margin::symmetric(8.0, 2.0), fill: app.design_tokens.top_bar_color, - stroke: style.visuals.window_stroke(), ..Default::default() } }; @@ -776,7 +774,7 @@ fn file_menu(ui: &mut egui::Ui, app: &mut App, frame: &mut eframe::Frame) { #[cfg(not(target_arch = "wasm32"))] if ui - .button("Load") + .button("Load…") .on_hover_text("Load a Rerun Data File (.rrd)") .clicked() { diff --git a/crates/re_viewer/src/design_tokens.rs b/crates/re_viewer/src/design_tokens.rs index dc143734e499..1d4f48d82020 100644 --- a/crates/re_viewer/src/design_tokens.rs +++ b/crates/re_viewer/src/design_tokens.rs @@ -13,7 +13,18 @@ impl DesignTokens { egui::Frame { fill: egui_ctx.style().visuals.window_fill(), inner_margin: egui::style::Margin::same(4.0), - stroke: egui_ctx.style().visuals.window_stroke(), + ..Default::default() + } + } + + #[allow(clippy::unused_self)] + pub fn hovering_frame(&self, style: &egui::Style) -> egui::Frame { + egui::Frame { + inner_margin: egui::style::Margin::same(2.0), + outer_margin: egui::style::Margin::same(4.0), + rounding: 4.0.into(), + fill: style.visuals.window_fill(), + stroke: style.visuals.window_stroke(), ..Default::default() } } diff --git a/crates/re_viewer/src/misc/viewer_context.rs b/crates/re_viewer/src/misc/viewer_context.rs index 7426a968b9a9..bd560f9c13e5 100644 --- a/crates/re_viewer/src/misc/viewer_context.rs +++ b/crates/re_viewer/src/misc/viewer_context.rs @@ -3,6 +3,8 @@ use macaw::Ray3; use re_data_store::{log_db::LogDb, InstanceId, ObjTypePath}; use re_log_types::{DataPath, MsgId, ObjPath, TimeInt, Timeline}; +use crate::ui::SpaceViewId; + /// Common things needed by many parts of the viewer. pub(crate) struct ViewerContext<'a> { /// Global options for the whole viewer. @@ -118,6 +120,20 @@ impl<'a> ViewerContext<'a> { response } + pub fn space_view_button_to( + &mut self, + ui: &mut egui::Ui, + text: impl Into, + space_view_id: SpaceViewId, + ) -> egui::Response { + let is_selected = self.rec_cfg.selection == Selection::SpaceView(space_view_id); + let response = ui.selectable_label(is_selected, text); + if response.clicked() { + self.rec_cfg.selection = Selection::SpaceView(space_view_id); + } + response + } + pub fn time_button( &mut self, ui: &mut egui::Ui, diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_viewer/src/ui/selection_panel.rs index 2b0f8ca1963e..9132cb22ffd1 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_viewer/src/ui/selection_panel.rs @@ -198,5 +198,19 @@ fn ui_space_view(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui, space_view: &mu ui.end_row(); }); - // TODO(emilk): view settings + ui.separator(); + + match space_view.selected_category { + super::space_view::ViewCategory::ThreeD => { + ui.label("3D view."); + super::view_3d::show_settings_ui(ctx, ui, &mut space_view.view_state.state_3d); + } + super::space_view::ViewCategory::Tensor => { + if let Some(state_tensor) = &mut space_view.view_state.state_tensor { + ui.label("Tensor view."); + state_tensor.ui(ui); + } + } + super::space_view::ViewCategory::TwoD | super::space_view::ViewCategory::Text => {} + } } diff --git a/crates/re_viewer/src/ui/space_view.rs b/crates/re_viewer/src/ui/space_view.rs index 66b5ef109c75..319c92211968 100644 --- a/crates/re_viewer/src/ui/space_view.rs +++ b/crates/re_viewer/src/ui/space_view.rs @@ -8,7 +8,7 @@ use super::{view_2d, view_3d, view_tensor, view_text, Scene}; // ---------------------------------------------------------------------------- #[derive(Copy, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -enum ViewCategory { +pub(crate) enum ViewCategory { TwoD, #[default] ThreeD, @@ -26,7 +26,7 @@ pub(crate) struct SpaceView { pub view_state: ViewState, /// In case we are a mix of 2d/3d/tensor/text, we show what? - selected_category: ViewCategory, + pub selected_category: ViewCategory, pub obj_tree_properties: ObjectTreeProperties, } @@ -55,7 +55,7 @@ impl SpaceView { // NOTE: mutable because the glow-based 3D view `take()`s the scene because reasons. // TODO(cmc): remove this while removing glow. scene: &mut Scene, - ) -> egui::Response { + ) { crate::profile_function!(); let has_2d = !scene.two_d.is_empty() && scene.tensor.is_empty(); @@ -71,14 +71,28 @@ impl SpaceView { .iter() .filter_map(|cat| *cat) .collect::>(); + // Extra headroom required for the hovering controls at the top of the space view. + let extra_headroom = { + let frame = ctx.design_tokens.hovering_frame(ui.style()); + frame.total_margin().sum().y + + ui.spacing().interact_size.y + + ui.spacing().item_spacing.y + }; match categories.len() { - 0 => ui.label("(empty)"), + 0 => { + ui.centered_and_justified(|ui| { + ui.label("(empty)"); + }); + } 1 => { + self.selected_category = categories[0]; if has_2d { + _ = extra_headroom; // ignored - we just overlay on top of the 2D view. self.view_state - .ui_2d(ctx, ui, &self.space_path, &scene.two_d) + .ui_2d(ctx, ui, &self.space_path, &scene.two_d); } else if has_3d { + _ = extra_headroom; // ignored - we just overlay on top of the 2D view. self.view_state.ui_3d( ctx, ui, @@ -86,59 +100,59 @@ impl SpaceView { spaces_info, space_info, &mut scene.three_d, - ) + ); } else if has_tensor { - self.view_state.ui_tensor(ui, &scene.tensor) + ui.add_space(extra_headroom); + self.view_state.ui_tensor(ui, &scene.tensor); } else { assert!(has_text); - self.view_state.ui_text(ctx, ui, &scene.text) + ui.add_space(extra_headroom); + self.view_state.ui_text(ctx, ui, &scene.text); } } _ => { // Show tabs to let user select which category to view - ui.vertical(|ui| { - if !categories.contains(&self.selected_category) { - self.selected_category = categories[0]; + ui.add_space(extra_headroom); + if !categories.contains(&self.selected_category) { + self.selected_category = categories[0]; + } + + ui.horizontal(|ui| { + for category in categories { + let text = match category { + ViewCategory::TwoD => "2D", + ViewCategory::ThreeD => "3D", + ViewCategory::Tensor => "Tensor", + ViewCategory::Text => "Text", + }; + ui.selectable_value(&mut self.selected_category, category, text); + // TODO(emilk): make it look like tabs } + }); + ui.separator(); - ui.horizontal(|ui| { - for category in categories { - let text = match category { - ViewCategory::TwoD => "2D", - ViewCategory::ThreeD => "3D", - ViewCategory::Tensor => "Tensor", - ViewCategory::Text => "Text", - }; - ui.selectable_value(&mut self.selected_category, category, text); - // TODO(emilk): make it look like tabs - } - }); - ui.separator(); - - match self.selected_category { - ViewCategory::Text => { - self.view_state.ui_text(ctx, ui, &scene.text); - } - ViewCategory::Tensor => { - self.view_state.ui_tensor(ui, &scene.tensor); - } - ViewCategory::TwoD => { - self.view_state - .ui_2d(ctx, ui, &self.space_path, &scene.two_d); - } - ViewCategory::ThreeD => { - self.view_state.ui_3d( - ctx, - ui, - &self.space_path, - spaces_info, - space_info, - &mut scene.three_d, - ); - } + match self.selected_category { + ViewCategory::Text => { + self.view_state.ui_text(ctx, ui, &scene.text); } - }) - .response + ViewCategory::Tensor => { + self.view_state.ui_tensor(ui, &scene.tensor); + } + ViewCategory::TwoD => { + self.view_state + .ui_2d(ctx, ui, &self.space_path, &scene.two_d); + } + ViewCategory::ThreeD => { + self.view_state.ui_3d( + ctx, + ui, + &self.space_path, + spaces_info, + space_info, + &mut scene.three_d, + ); + } + } } } } @@ -146,6 +160,25 @@ impl SpaceView { // ---------------------------------------------------------------------------- +/// Show help-text on top of space +fn show_help_button_overlay( + ui: &mut egui::Ui, + rect: egui::Rect, + ctx: &mut ViewerContext<'_>, + help_text: &str, +) { + { + let mut ui = ui.child_ui(rect, egui::Layout::right_to_left(egui::Align::TOP)); + ctx.design_tokens + .hovering_frame(ui.style()) + .show(&mut ui, |ui| { + crate::misc::help_hover_button(ui).on_hover_text(help_text); + }); + } +} + +// ---------------------------------------------------------------------------- + /// Camera position and similar. #[derive(Default, serde::Deserialize, serde::Serialize)] pub(crate) struct ViewState { @@ -163,7 +196,15 @@ impl ViewState { space: &ObjPath, scene: &view_2d::Scene2D, ) -> egui::Response { - view_2d::view_2d(ctx, ui, &mut self.state_2d, Some(space), scene) + let response = ui + .scope(|ui| { + view_2d::view_2d(ctx, ui, &mut self.state_2d, Some(space), scene); + }) + .response; + + show_help_button_overlay(ui, response.rect, ctx, view_2d::HELP_TEXT); + + response } fn ui_3d( @@ -179,16 +220,14 @@ impl ViewState { let state = &mut self.state_3d; let space_cameras = &space_cameras(spaces_info, space_info); let coordinates = space_info.coordinates; - let space_specs = view_3d::SpaceSpecs::from_view_coordinates(coordinates); - view_3d::view_3d( - ctx, - ui, - state, - Some(space), - &space_specs, - scene, - space_cameras, - ); + state.space_specs = view_3d::SpaceSpecs::from_view_coordinates(coordinates); + let response = ui + .scope(|ui| { + view_3d::view_3d(ctx, ui, state, Some(space), scene, space_cameras); + }) + .response; + + show_help_button_overlay(ui, response.rect, ctx, super::view_3d::HELP_TEXT); }) .response } diff --git a/crates/re_viewer/src/ui/view_2d/mod.rs b/crates/re_viewer/src/ui/view_2d/mod.rs index 4e76fd15f06f..05f4178e4f14 100644 --- a/crates/re_viewer/src/ui/view_2d/mod.rs +++ b/crates/re_viewer/src/ui/view_2d/mod.rs @@ -2,7 +2,7 @@ mod scene; pub use self::scene::{Box2D, Image, LineSegments2D, ObjectPaintProperties, Point2D, Scene2D}; mod ui; -pub(crate) use self::ui::{view_2d, View2DState}; +pub(crate) use self::ui::{view_2d, View2DState, HELP_TEXT}; mod class_description_ui; pub(crate) use self::class_description_ui::view_class_description_map; diff --git a/crates/re_viewer/src/ui/view_2d/ui.rs b/crates/re_viewer/src/ui/view_2d/ui.rs index 6607904b0f67..7640e8ce13dd 100644 --- a/crates/re_viewer/src/ui/view_2d/ui.rs +++ b/crates/re_viewer/src/ui/view_2d/ui.rs @@ -214,6 +214,10 @@ impl View2DState { } } +pub const HELP_TEXT: &str = "Ctrl-scroll to zoom (⌘-scroll or Mac).\n\ + Drag to pan.\n\ + Double-click to reset the view."; + /// Create the outer 2D view, which consists of a scrollable region pub(crate) fn view_2d( ctx: &mut ViewerContext<'_>, @@ -224,13 +228,6 @@ pub(crate) fn view_2d( ) -> egui::Response { crate::profile_function!(); - // TODO(emilk): make help button hover over the 2D view (https://github.com/emilk/egui/issues/980). - crate::misc::help_hover_button(ui).on_hover_text( - "Ctrl-scroll to zoom (⌘-scroll or Mac).\n\ - Drag to pan.\n\ - Double-click to reset the view.", - ); - // Save off the available_size since this is used for some of the layout updates later let available_size = ui.available_size(); diff --git a/crates/re_viewer/src/ui/view_3d/mod.rs b/crates/re_viewer/src/ui/view_3d/mod.rs index 57312c847d61..7b278d4e6e52 100644 --- a/crates/re_viewer/src/ui/view_3d/mod.rs +++ b/crates/re_viewer/src/ui/view_3d/mod.rs @@ -13,7 +13,7 @@ pub use self::scene::{ }; mod ui; -pub(crate) use self::ui::{view_3d, SpaceSpecs, View3DState}; +pub(crate) use self::ui::{show_settings_ui, view_3d, SpaceSpecs, View3DState, HELP_TEXT}; #[cfg(feature = "glow")] mod glow_rendering; diff --git a/crates/re_viewer/src/ui/view_3d/space_camera.rs b/crates/re_viewer/src/ui/view_3d/space_camera.rs index 0689ee06a2e7..56414774de54 100644 --- a/crates/re_viewer/src/ui/view_3d/space_camera.rs +++ b/crates/re_viewer/src/ui/view_3d/space_camera.rs @@ -5,6 +5,7 @@ use re_data_store::ObjPath; use re_log_types::{IndexHash, ViewCoordinates}; /// A logged camera that connects spaces. +#[derive(Clone)] pub struct SpaceCamera { /// Path to the object which has the rigid [Self::world_from_camera`] transforms. pub camera_obj_path: ObjPath, diff --git a/crates/re_viewer/src/ui/view_3d/ui.rs b/crates/re_viewer/src/ui/view_3d/ui.rs index 70d48d0102bd..d3f7a5da986b 100644 --- a/crates/re_viewer/src/ui/view_3d/ui.rs +++ b/crates/re_viewer/src/ui/view_3d/ui.rs @@ -41,6 +41,12 @@ pub(crate) struct View3DState { show_axes: bool, last_eye_interact_time: f64, + + /// Filled in at the start of each frame + #[serde(skip)] + pub(crate) space_specs: SpaceSpecs, + #[serde(skip)] + space_camera: Vec, } impl Default for View3DState { @@ -54,6 +60,8 @@ impl Default for View3DState { spin: false, show_axes: false, last_eye_interact_time: f64::NEG_INFINITY, + space_specs: Default::default(), + space_camera: Default::default(), } } } @@ -64,14 +72,13 @@ impl View3DState { ctx: &mut ViewerContext<'_>, tracking_camera: Option, response: &egui::Response, - space_specs: &SpaceSpecs, ) -> &mut OrbitEye { if response.double_clicked() { - // Reset camera + // Reset eye if tracking_camera.is_some() { ctx.rec_cfg.selection = Selection::None; } - self.interpolate_to_orbit_eye(default_eye(&self.scene_bbox, space_specs)); + self.interpolate_to_orbit_eye(default_eye(&self.scene_bbox, &self.space_specs)); } if let Some(tracking_camera) = tracking_camera { @@ -89,7 +96,7 @@ impl View3DState { let orbit_camera = self .orbit_eye - .get_or_insert_with(|| default_eye(&self.scene_bbox, space_specs)); + .get_or_insert_with(|| default_eye(&self.scene_bbox, &self.space_specs)); if self.spin { orbit_camera.rotate(egui::vec2( @@ -175,82 +182,70 @@ impl EyeInterpolation { } } -fn show_settings_ui( +pub(crate) fn show_settings_ui( ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui, state: &mut View3DState, - space_specs: &SpaceSpecs, ) { - ui.horizontal(|ui| { - { - let up_response = if let Some(up) = space_specs.up { - if up == Vec3::X { - ui.label("Up: +X") - } else if up == -Vec3::X { - ui.label("Up: -X") - } else if up == Vec3::Y { - ui.label("Up: +Y") - } else if up == -Vec3::Y { - ui.label("Up: -Y") - } else if up == Vec3::Z { - ui.label("Up: +Z") - } else if up == -Vec3::Z { - ui.label("Up: -Z") - } else if up != Vec3::ZERO { - ui.label(format!("Up: [{:.3} {:.3} {:.3}]", up.x, up.y, up.z)) - } else { - ui.label("Up: —") - } + { + let up_response = if let Some(up) = state.space_specs.up { + if up == Vec3::X { + ui.label("Up: +X") + } else if up == -Vec3::X { + ui.label("Up: -X") + } else if up == Vec3::Y { + ui.label("Up: +Y") + } else if up == -Vec3::Y { + ui.label("Up: -Y") + } else if up == Vec3::Z { + ui.label("Up: +Z") + } else if up == -Vec3::Z { + ui.label("Up: -Z") + } else if up != Vec3::ZERO { + ui.label(format!("Up: [{:.3} {:.3} {:.3}]", up.x, up.y, up.z)) } else { ui.label("Up: —") - }; - - up_response.on_hover_ui(|ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("Set with "); - ui.code("rerun.log_view_coordinates"); - ui.label("."); - }); + } + } else { + ui.label("Up: —") + }; + + up_response.on_hover_ui(|ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Set with "); + ui.code("rerun.log_view_coordinates"); + ui.label("."); }); - } + }); + } - if ui - .button("Reset view") - .on_hover_text("You can also double-click the 3D view") - .clicked() - { - state.orbit_eye = Some(default_eye(&state.scene_bbox, space_specs)); - state.eye_interpolation = None; - // TODO(emilk): reset tracking camera too - } + if ui + .button("Reset virtual camera") + .on_hover_text( + "Resets camera position & orientation.\nYou can also double-click the 3D view", + ) + .clicked() + { + state.orbit_eye = Some(default_eye(&state.scene_bbox, &state.space_specs)); + state.eye_interpolation = None; + // TODO(emilk): reset tracking camera too + } + + ui.checkbox(&mut state.spin, "Spin virtual camera") + .on_hover_text("Spin view"); + ui.checkbox(&mut state.show_axes, "Show origin axes") + .on_hover_text("Show X-Y-Z axes"); - // TODO(emilk): only show if there is a camera om scene. - ui.toggle_value(&mut ctx.options.show_camera_mesh_in_3d, "📷") - .on_hover_text("Show camera mesh"); - - ui.toggle_value(&mut state.spin, "Spin") - .on_hover_text("Spin view"); - ui.toggle_value(&mut state.show_axes, "Axes") - .on_hover_text("Show X-Y-Z axes"); - - crate::misc::help_hover_button(ui).on_hover_text( - "Drag to rotate.\n\ - Drag with secondary mouse button to pan.\n\ - Drag with middle mouse button to roll the view.\n\ - Scroll to zoom.\n\ - \n\ - While hovering the 3D view, navigate with WSAD and QE.\n\ - CTRL slows down, SHIFT speeds up.\n\ - \n\ - Click on a object to focus the view on it.\n\ - \n\ - Double-click anywhere to reset the view.", + if !state.space_camera.is_empty() { + ui.checkbox( + &mut ctx.options.show_camera_mesh_in_3d, + "Show camera meshes", ); - }); + } } -#[derive(Default)] +#[derive(Clone, Default)] pub(crate) struct SpaceSpecs { up: Option, right: Option, @@ -314,27 +309,35 @@ fn click_object( // ---------------------------------------------------------------------------- +pub(crate) const HELP_TEXT: &str = "Drag to rotate.\n\ + Drag with secondary mouse button to pan.\n\ + Drag with middle mouse button to roll the view.\n\ + Scroll to zoom.\n\ + \n\ + While hovering the 3D view, navigate with WSAD and QE.\n\ + CTRL slows down, SHIFT speeds up.\n\ + \n\ + Click on a object to focus the view on it.\n\ + \n\ + Double-click anywhere to reset the view."; + pub(crate) fn view_3d( ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui, state: &mut View3DState, space: Option<&ObjPath>, - space_specs: &SpaceSpecs, scene: &mut Scene3D, space_cameras: &[SpaceCamera], ) -> egui::Response { crate::profile_function!(); state.scene_bbox = state.scene_bbox.union(scene.calc_bbox()); - - // TODO(emilk): show settings on top of 3D view. - // Requires some egui work to handle interaction of overlapping widgets. - show_settings_ui(ctx, ui, state, space_specs); + state.space_camera = space_cameras.to_vec(); let (rect, response) = ui.allocate_at_least(ui.available_size(), egui::Sense::click_and_drag()); let tracking_camera = tracking_camera(ctx, space_cameras); - let orbit_eye = state.update_eye(ctx, tracking_camera, &response, space_specs); + let orbit_eye = state.update_eye(ctx, tracking_camera, &response); let did_interact_wth_eye = orbit_eye.interact(&response); let orbit_eye = *orbit_eye; diff --git a/crates/re_viewer/src/ui/view_tensor/ui.rs b/crates/re_viewer/src/ui/view_tensor/ui.rs index db57911e7f48..8165849ec953 100644 --- a/crates/re_viewer/src/ui/view_tensor/ui.rs +++ b/crates/re_viewer/src/ui/view_tensor/ui.rs @@ -23,6 +23,19 @@ pub struct ViewTensorState { /// Scaling, filtering, aspect ratio, etc for the rendered texture. texture_settings: TextureSettings, + + // last viewed tensor, copied each frame + #[serde(skip)] + #[serde(default = "empty_tensor")] + tensor: Tensor, +} + +fn empty_tensor() -> Tensor { + Tensor { + shape: vec![TensorDimension::unnamed(0)], + dtype: TensorDataType::U8, + data: re_log_types::TensorDataStore::Dense(vec![].into()), + } } impl ViewTensorState { @@ -32,22 +45,30 @@ impl ViewTensorState { dimension_mapping: DimensionMapping::create(tensor), color_mapping: ColorMapping::default(), texture_settings: TextureSettings::default(), + tensor: tensor.clone(), } } + + pub(crate) fn ui(&mut self, ui: &mut egui::Ui) { + ui.collapsing("Dimension Mapping", |ui| { + ui.label(format!("shape: {:?}", self.tensor.shape)); + ui.label(format!("dtype: {:?}", self.tensor.dtype)); + ui.add_space(12.0); + + dimension_mapping_ui(ui, &mut self.dimension_mapping, &self.tensor.shape); + }); + + self.texture_settings.show(ui); + + color_mapping_ui(ui, &mut self.color_mapping); + } } pub(crate) fn view_tensor(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Tensor) { crate::profile_function!(); - ui.collapsing("Dimension Mapping", |ui| { - ui.label(format!("shape: {:?}", tensor.shape)); - ui.label(format!("dtype: {:?}", tensor.dtype)); - ui.add_space(12.0); - - dimension_mapping_ui(ui, &mut state.dimension_mapping, &tensor.shape); - }); + state.tensor = tensor.clone(); - state.texture_settings.show(ui); selectors_ui(ui, state, tensor); let tensor_shape = &tensor.shape; @@ -55,8 +76,6 @@ pub(crate) fn view_tensor(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor match tensor.dtype { TensorDataType::U8 => match re_tensor_ops::as_ndarray::(tensor) { Ok(tensor) => { - color_mapping_ui(ui, &mut state.color_mapping); - let color_from_value = |value: u8| { state .color_mapping @@ -74,11 +93,7 @@ pub(crate) fn view_tensor(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor TensorDataType::U16 => match re_tensor_ops::as_ndarray::(tensor) { Ok(tensor) => { let (tensor_min, tensor_max) = tensor_range_u16(&tensor); - - ui.group(|ui| { - ui.monospace(format!("Data range: [{tensor_min} - {tensor_max}]")); - color_mapping_ui(ui, &mut state.color_mapping); - }); + ui.monospace(format!("Data range: [{tensor_min} - {tensor_max}]")); let color_from_value = |value: u16| { state.color_mapping.color_from_normalized(egui::remap( @@ -99,11 +114,7 @@ pub(crate) fn view_tensor(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor TensorDataType::F32 => match re_tensor_ops::as_ndarray::(tensor) { Ok(tensor) => { let (tensor_min, tensor_max) = tensor_range_f32(&tensor); - - ui.group(|ui| { - ui.monospace(format!("Data range: [{tensor_min} - {tensor_max}]")); - color_mapping_ui(ui, &mut state.color_mapping); - }); + ui.monospace(format!("Data range: [{tensor_min} - {tensor_max}]")); let color_from_value = |value: f32| { state.color_mapping.color_from_normalized(egui::remap( @@ -358,7 +369,6 @@ fn slice_ui( ) { crate::profile_function!(); - ui.monospace(format!("Slice shape: {:?}", slice.shape())); let ndims = slice.ndim(); if let Ok(slice) = slice.into_dimensionality::() { let dimension_labels = { @@ -495,7 +505,10 @@ fn selectors_ui(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Tensor) for &dim_idx in &state.dimension_mapping.selectors { let dim = &tensor.shape[dim_idx]; if dim.size > 1 { - let selector_value = state.selector_values.entry(dim_idx).or_default(); + let selector_value = state + .selector_values + .entry(dim_idx) + .or_insert_with(|| dim.size / 2); // start in the middle ui.add( egui::Slider::new(selector_value, 0..=dim.size - 1) diff --git a/crates/re_viewer/src/ui/viewport.rs b/crates/re_viewer/src/ui/viewport.rs index 2dc4f968554f..c0d33c85c39e 100644 --- a/crates/re_viewer/src/ui/viewport.rs +++ b/crates/re_viewer/src/ui/viewport.rs @@ -141,9 +141,10 @@ impl ViewportBlueprint { .show_header(ui, |ui| { ui.label("🗖"); // icon indicating this is a space-view - let is_selected = ctx.rec_cfg.selection == Selection::SpaceView(*space_view_id); - if ui.selectable_label(is_selected, &space_view.name).clicked() { - ctx.rec_cfg.selection = Selection::SpaceView(*space_view_id); + if ctx + .space_view_button_to(ui, space_view.name.clone(), *space_view_id) + .clicked() + { if let Some(tree) = self.trees.get_mut(&self.visible) { focus_tab(tree, space_view_id); } @@ -206,6 +207,7 @@ impl ViewportBlueprint { ui: &mut egui::Ui, ctx: &mut ViewerContext<'_>, spaces_info: &SpacesInfo, + selection_panel_expanded: &mut bool, ) { // Lazily create a layout tree based on which SpaceViews are currently visible: let tree = self.trees.entry(self.visible.clone()).or_insert_with(|| { @@ -222,16 +224,26 @@ impl ViewportBlueprint { .get_mut(&space_view_id) .expect("Should have been populated beforehand"); - ui.strong(&space_view.name); + let response = ui + .scope(|ui| space_view_ui(ctx, ui, spaces_info, space_view)) + .response; - space_view_ui(ctx, ui, spaces_info, space_view); + let frame = ctx.design_tokens.hovering_frame(ui.style()); + hovering_panel(ui, frame, response.rect, |ui| { + space_view_options_link(ctx, selection_panel_expanded, space_view_id, ui, "⛭"); + }); } else if let Some(space_view_id) = self.maximized { let space_view = self .space_views .get_mut(&space_view_id) .expect("Should have been populated beforehand"); - ui.horizontal(|ui| { + let response = ui + .scope(|ui| space_view_ui(ctx, ui, spaces_info, space_view)) + .response; + + let frame = ctx.design_tokens.hovering_frame(ui.style()); + hovering_panel(ui, frame, response.rect, |ui| { if ui .button("⬅") .on_hover_text("Restore - show all spaces") @@ -239,21 +251,21 @@ impl ViewportBlueprint { { self.maximized = None; } - ui.strong(&space_view.name); + space_view_options_link(ctx, selection_panel_expanded, space_view_id, ui, "⛭"); }); - - space_view_ui(ctx, ui, spaces_info, space_view); } else { let mut dock_style = egui_dock::Style::from_egui(ui.style().as_ref()); dock_style.separator_width = 2.0; dock_style.show_close_buttons = false; dock_style.tab_include_scrollarea = false; + dock_style.expand_tabs = true; let mut tab_viewer = TabViewer { ctx, spaces_info, space_views: &mut self.space_views, maximized: &mut self.maximized, + selection_panel_expanded, }; egui_dock::DockArea::new(tree) @@ -380,6 +392,7 @@ struct TabViewer<'a, 'b> { spaces_info: &'a SpacesInfo, space_views: &'a mut HashMap, maximized: &'a mut Option, + selection_panel_expanded: &'a mut bool, } impl<'a, 'b> egui_dock::TabViewer for TabViewer<'a, 'b> { @@ -388,17 +401,33 @@ impl<'a, 'b> egui_dock::TabViewer for TabViewer<'a, 'b> { fn ui(&mut self, ui: &mut egui::Ui, space_view_id: &mut Self::Tab) { crate::profile_function!(); - ui.horizontal_top(|ui| { - if ui.button("🗖").on_hover_text("Maximize space").clicked() { + let space_view = self + .space_views + .get_mut(space_view_id) + .expect("Should have been populated beforehand"); + + let response = ui + .scope(|ui| space_view_ui(self.ctx, ui, self.spaces_info, space_view)) + .response; + + // Show buttons for maximize and space view options: + let frame = self.ctx.design_tokens.hovering_frame(ui.style()); + hovering_panel(ui, frame, response.rect, |ui| { + if ui + .button("🗖") + .on_hover_text("Maximize Space View") + .clicked() + { *self.maximized = Some(*space_view_id); } - let space_view = self - .space_views - .get_mut(space_view_id) - .expect("Should have been populated beforehand"); - - space_view_ui(self.ctx, ui, self.spaces_info, space_view); + space_view_options_link( + self.ctx, + self.selection_panel_expanded, + *space_view_id, + ui, + "⛭", + ); }); } @@ -409,6 +438,46 @@ impl<'a, 'b> egui_dock::TabViewer for TabViewer<'a, 'b> { .expect("Should have been populated beforehand"); space_view.name.clone().into() } + + fn inner_margin(&self) -> egui::style::Margin { + egui::style::Margin::same(0.0) + } +} + +fn space_view_options_link( + ctx: &mut ViewerContext<'_>, + selection_panel_expanded: &mut bool, + space_view_id: SpaceViewId, + ui: &mut egui::Ui, + text: &str, +) { + let is_selected = + ctx.rec_cfg.selection == Selection::SpaceView(space_view_id) && *selection_panel_expanded; + if ui + .selectable_label(is_selected, text) + .on_hover_text("Space View options") + .clicked() + { + if is_selected { + ctx.rec_cfg.selection = Selection::None; + *selection_panel_expanded = false; + } else { + ctx.rec_cfg.selection = Selection::SpaceView(space_view_id); + *selection_panel_expanded = true; + } + } +} + +fn hovering_panel( + ui: &mut egui::Ui, + frame: egui::Frame, + rect: egui::Rect, + add_contents: impl FnOnce(&mut egui::Ui), +) { + let mut ui = ui.child_ui(rect, egui::Layout::top_down(egui::Align::LEFT)); + ui.horizontal(|ui| { + frame.show(ui, add_contents); + }); } fn space_view_ui( @@ -416,12 +485,14 @@ fn space_view_ui( ui: &mut egui::Ui, spaces_info: &SpacesInfo, space_view: &mut SpaceView, -) -> egui::Response { +) { let Some(space_info) = spaces_info.spaces.get(&space_view.space_path) else { - return unknown_space_label(ui, &space_view.space_path); + unknown_space_label(ui, &space_view.space_path); + return }; let Some(time_query) = ctx.rec_cfg.time_ctrl.time_query() else { - return invalid_space_label(ui, &space_view.space_path); + invalid_space_label(ui, &space_view.space_path); + return }; crate::profile_function!(); @@ -444,7 +515,7 @@ fn space_view_ui( scene.tensor.load_objects(ctx, obj_tree_props, &query); } - space_view.scene_ui(ctx, ui, spaces_info, space_info, &mut scene) + space_view.scene_ui(ctx, ui, spaces_info, space_info, &mut scene); } fn unknown_space_label(ui: &mut egui::Ui, space_path: &ObjPath) -> egui::Response { @@ -503,7 +574,12 @@ impl Blueprint { egui::CentralPanel::default() .frame(viewport_frame) .show_inside(ui, |ui| { - self.viewport.viewport_ui(ui, ctx, &spaces_info); + self.viewport.viewport_ui( + ui, + ctx, + &spaces_info, + &mut self.selection_panel_expanded, + ); }); }