diff --git a/Cargo.lock b/Cargo.lock index ee70687ba77f..a5d7e947a821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,7 +1923,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecolor" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "bytemuck", "color-hex", @@ -1940,7 +1940,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f" [[package]] name = "eframe" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "ahash", "bytemuck", @@ -1979,7 +1979,7 @@ dependencies = [ [[package]] name = "egui" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "accesskit", "ahash", @@ -1996,7 +1996,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "ahash", "bytemuck", @@ -2015,7 +2015,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "accesskit_winit", "ahash", @@ -2057,7 +2057,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "ahash", "egui", @@ -2074,7 +2074,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "ahash", "bytemuck", @@ -2092,7 +2092,7 @@ dependencies = [ [[package]] name = "egui_kittest" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "dify", "egui", @@ -2161,7 +2161,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "bytemuck", "serde", @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" dependencies = [ "ab_glyph", "ahash", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66" +source = "git+https://github.com/emilk/egui.git?rev=eac7ba01fa37bad35f71bc303561761952361b7c#eac7ba01fa37bad35f71bc303561761952361b7c" [[package]] name = "equivalent" @@ -6765,8 +6765,6 @@ dependencies = [ "ahash", "egui", "egui_tiles", - "glam", - "image", "itertools 0.13.0", "nohash-hasher", "rayon", diff --git a/Cargo.toml b/Cargo.toml index aaa142bea4e0..061d2ac225bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -559,20 +559,20 @@ significant_drop_tightening = "allow" # An update of parking_lot made this trigg # As a last resport, patch with a commit to our own repository. # ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk. -ecolor = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 -eframe = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 -egui = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 -egui_extras = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 -egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 -emath = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 - -egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27 +ecolor = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 +eframe = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 +egui = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 +egui_extras = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 +egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 +egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 +emath = { git = "https://github.com/emilk/egui.git", rev = "eac7ba01fa37bad35f71bc303561761952361b7c" } # egui master 2024-12-03 # Useful while developing: # ecolor = { path = "../../egui/crates/ecolor" } # eframe = { path = "../../egui/crates/eframe" } # egui = { path = "../../egui/crates/egui" } # egui_extras = { path = "../../egui/crates/egui_extras" } +# egui_kittest = { path = "../../egui/crates/egui_kittest" } # egui-wgpu = { path = "../../egui/crates/egui-wgpu" } # emath = { path = "../../egui/crates/emath" } diff --git a/crates/viewer/re_context_menu/src/actions/mod.rs b/crates/viewer/re_context_menu/src/actions/mod.rs index f438c2e3698f..8601da12fdd5 100644 --- a/crates/viewer/re_context_menu/src/actions/mod.rs +++ b/crates/viewer/re_context_menu/src/actions/mod.rs @@ -6,3 +6,9 @@ pub(super) mod collapse_expand_all; pub(super) mod move_contents_to_new_container; pub(super) mod remove; pub(super) mod show_hide; + +#[cfg(not(target_arch = "wasm32"))] // TODO(#8264): screenshotting on web +mod screenshot_action; + +#[cfg(not(target_arch = "wasm32"))] // TODO(#8264): screenshotting on web +pub use screenshot_action::ScreenshotAction; diff --git a/crates/viewer/re_context_menu/src/actions/screenshot_action.rs b/crates/viewer/re_context_menu/src/actions/screenshot_action.rs new file mode 100644 index 000000000000..6c53168d31ff --- /dev/null +++ b/crates/viewer/re_context_menu/src/actions/screenshot_action.rs @@ -0,0 +1,78 @@ +use re_viewer_context::{ + Item, PublishedSpaceViewInfo, ScreenshotTarget, SpaceViewId, SpaceViewRectPublisher, +}; + +use crate::{ContextMenuAction, ContextMenuContext}; + +/// Space view screenshot action. +#[cfg(not(target_arch = "wasm32"))] +pub enum ScreenshotAction { + /// Screenshot the space view, and copy the results to clipboard. + CopyScreenshot, + + /// Screenshot the space view, and save the results to disk. + SaveScreenshot, +} + +impl ContextMenuAction for ScreenshotAction { + /// Do we have a context menu for this selection? + fn supports_selection(&self, ctx: &ContextMenuContext<'_>) -> bool { + // Allow if there is a single space view selected. + ctx.selection.len() == 1 + && ctx + .selection + .iter() + .all(|(item, _)| self.supports_item(ctx, item)) + } + + /// Do we have a context menu for this item? + fn supports_item(&self, ctx: &ContextMenuContext<'_>, item: &Item) -> bool { + let Item::SpaceView(space_view_id) = item else { + return false; + }; + + ctx.egui_context.memory_mut(|mem| { + mem.caches + .cache::() + .get(space_view_id) + .is_some() + }) + } + + fn label(&self, _ctx: &ContextMenuContext<'_>) -> String { + match self { + Self::CopyScreenshot => "Copy screenshot".to_owned(), + Self::SaveScreenshot => "Save screenshot…".to_owned(), + } + } + + fn process_space_view(&self, ctx: &ContextMenuContext<'_>, space_view_id: &SpaceViewId) { + let Some(space_view_info) = ctx.egui_context.memory_mut(|mem| { + mem.caches + .cache::() + .get(space_view_id) + .cloned() + }) else { + return; + }; + + let PublishedSpaceViewInfo { name, rect } = space_view_info; + + let rect = rect.shrink(1.75); // Hacky: Shrink so we don't accidentally include the border of the space-view. + + let target = match self { + Self::CopyScreenshot => ScreenshotTarget::CopyToClipboard, + Self::SaveScreenshot => ScreenshotTarget::SaveToDisk, + }; + + ctx.egui_context + .send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new( + re_viewer_context::ScreenshotInfo { + ui_rect: Some(rect), + pixels_per_point: ctx.egui_context.pixels_per_point(), + name, + target, + }, + ))); + } +} diff --git a/crates/viewer/re_context_menu/src/lib.rs b/crates/viewer/re_context_menu/src/lib.rs index 603d503b93cc..c9314c31de64 100644 --- a/crates/viewer/re_context_menu/src/lib.rs +++ b/crates/viewer/re_context_menu/src/lib.rs @@ -113,6 +113,11 @@ fn action_list( Box::new(HideAction), Box::new(RemoveAction), ], + #[cfg(not(target_arch = "wasm32"))] // TODO(#8264): screenshotting on web + vec![ + Box::new(actions::ScreenshotAction::CopyScreenshot), + Box::new(actions::ScreenshotAction::SaveScreenshot), + ], vec![ Box::new(CollapseExpandAllAction::ExpandAll), Box::new(CollapseExpandAllAction::CollapseAll), diff --git a/crates/viewer/re_data_ui/src/blob.rs b/crates/viewer/re_data_ui/src/blob.rs index 24cb3cebb354..3246da88f532 100644 --- a/crates/viewer/re_data_ui/src/blob.rs +++ b/crates/viewer/re_data_ui/src/blob.rs @@ -155,7 +155,11 @@ pub fn blob_preview_and_save_ui( file_name.push_str(file_extension); } - ctx.save_file_dialog(file_name, "Save blob".to_owned(), blob.to_vec()); + ctx.command_sender.save_file_dialog( + &file_name, + "Save blob".to_owned(), + blob.to_vec(), + ); } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/viewer/re_data_ui/src/instance_path.rs b/crates/viewer/re_data_ui/src/instance_path.rs index 887a812b1d2b..7dd2d6b9026d 100644 --- a/crates/viewer/re_data_ui/src/instance_path.rs +++ b/crates/viewer/re_data_ui/src/instance_path.rs @@ -384,7 +384,8 @@ fn image_download_button_ui( .map_or("image", |name| name.unescaped_str()) .to_owned() ); - ctx.save_file_dialog(file_name, "Save image".to_owned(), png_bytes); + ctx.command_sender + .save_file_dialog(&file_name, "Save image".to_owned(), png_bytes); } Err(err) => { re_log::error!("{err}"); diff --git a/crates/viewer/re_space_view/src/lib.rs b/crates/viewer/re_space_view/src/lib.rs index e85ee751a16a..e6620167efc8 100644 --- a/crates/viewer/re_space_view/src/lib.rs +++ b/crates/viewer/re_space_view/src/lib.rs @@ -11,7 +11,6 @@ mod instance_hash_conversions; mod outlines; mod query; mod results_ext; -mod screenshot; mod view_property_ui; pub use annotation_context_utils::{ @@ -31,7 +30,6 @@ pub use query::{ pub use results_ext::{ HybridLatestAtResults, HybridResults, HybridResultsChunkIter, RangeResultsExt, }; -pub use screenshot::ScreenshotMode; pub use view_property_ui::view_property_ui; pub mod external { diff --git a/crates/viewer/re_space_view/src/screenshot.rs b/crates/viewer/re_space_view/src/screenshot.rs deleted file mode 100644 index dbeef43b0aa0..000000000000 --- a/crates/viewer/re_space_view/src/screenshot.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[derive(PartialEq, Eq, Clone, Copy)] -#[allow(dead_code)] // Not used on the web. -pub enum ScreenshotMode { - /// The screenshot will be saved to disc and copied to the clipboard. - SaveAndCopyToClipboard, - - /// The screenshot will be copied to the clipboard. - CopyToClipboard, -} diff --git a/crates/viewer/re_space_view_spatial/src/ui.rs b/crates/viewer/re_space_view_spatial/src/ui.rs index 1f85d0d88368..ee8ae745bc09 100644 --- a/crates/viewer/re_space_view_spatial/src/ui.rs +++ b/crates/viewer/re_space_view_spatial/src/ui.rs @@ -2,15 +2,12 @@ use egui::{epaint::util::OrderedFloat, text::TextWrapping, NumExt as _, WidgetTe use re_format::format_f32; use re_math::BoundingBox; -use re_space_view::ScreenshotMode; use re_types::{ archetypes::Pinhole, blueprint::components::VisualBounds2D, components::ViewCoordinates, image::ImageKind, }; use re_ui::UiExt as _; -use re_viewer_context::{ - HoverHighlight, SelectionHighlight, SpaceViewHighlights, SpaceViewState, ViewerContext, -}; +use re_viewer_context::{HoverHighlight, SelectionHighlight, SpaceViewHighlights, SpaceViewState}; use crate::{ eye::EyeMode, @@ -341,35 +338,6 @@ pub fn paint_loading_spinners( } } -pub fn screenshot_context_menu( - _ctx: &ViewerContext<'_>, - _response: &egui::Response, -) -> Option { - #[cfg(not(target_arch = "wasm32"))] - { - if _ctx.app_options.experimental_space_view_screenshots { - let mut take_screenshot = None; - _response.context_menu(|ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - if ui.button("Save screenshot to disk").clicked() { - take_screenshot = Some(ScreenshotMode::SaveAndCopyToClipboard); - ui.close_menu(); - } else if ui.button("Copy screenshot to clipboard").clicked() { - take_screenshot = Some(ScreenshotMode::CopyToClipboard); - ui.close_menu(); - } - }); - take_screenshot - } else { - None - } - } - #[cfg(target_arch = "wasm32")] - { - None - } -} - pub fn format_vector(v: glam::Vec3) -> String { use glam::Vec3; diff --git a/crates/viewer/re_space_view_spatial/src/ui_2d.rs b/crates/viewer/re_space_view_spatial/src/ui_2d.rs index b2d76326bca3..54d3a64c89b3 100644 --- a/crates/viewer/re_space_view_spatial/src/ui_2d.rs +++ b/crates/viewer/re_space_view_spatial/src/ui_2d.rs @@ -20,10 +20,7 @@ use re_viewer_context::{ }; use re_viewport_blueprint::ViewProperty; -use super::{ - eye::Eye, - ui::{create_labels, screenshot_context_menu}, -}; +use super::{eye::Eye, ui::create_labels}; use crate::{ query_pinhole_legacy, ui::SpatialSpaceViewState, view_kind::SpatialSpaceViewKind, visualizers::collect_ui_labels, SpatialSpaceView2D, @@ -169,7 +166,7 @@ impl SpatialSpaceView2D { state.pinhole_at_origin = query_pinhole_legacy(ctx, &ctx.current_query(), query.space_origin); - let (mut response, painter) = + let (response, painter) = ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag()); // Convert ui coordinates to/from scene coordinates. @@ -216,7 +213,7 @@ impl SpatialSpaceView2D { ui.ctx().pixels_per_point(), &eye, ); - response = crate::picking_ui::picking( + crate::picking_ui::picking( ctx, &picking_context, ui, @@ -249,12 +246,6 @@ impl SpatialSpaceView2D { // ------------------------------------------------------------------------ - if let Some(mode) = screenshot_context_menu(ctx, &response) { - view_builder - .schedule_screenshot(render_ctx, query.space_view_id.gpu_readback_id(), mode) - .ok(); - } - // Draw a re_renderer driven view. // Camera & projection are configured to ingest space coordinates directly. painter.add(gpu_bridge::new_renderer_callback( diff --git a/crates/viewer/re_space_view_spatial/src/ui_3d.rs b/crates/viewer/re_space_view_spatial/src/ui_3d.rs index 18fd9c8248fd..1a82a5794846 100644 --- a/crates/viewer/re_space_view_spatial/src/ui_3d.rs +++ b/crates/viewer/re_space_view_spatial/src/ui_3d.rs @@ -24,7 +24,7 @@ use re_viewport_blueprint::ViewProperty; use crate::{ scene_bounding_boxes::SceneBoundingBoxes, space_camera_3d::SpaceCamera3D, - ui::{create_labels, screenshot_context_menu, SpatialSpaceViewState}, + ui::{create_labels, SpatialSpaceViewState}, view_kind::SpatialSpaceViewKind, visualizers::{collect_ui_labels, image_view_coordinates, CamerasVisualizer}, SpatialSpaceView3D, @@ -609,13 +609,6 @@ impl SpatialSpaceView3D { } } - // Screenshot context menu. - if let Some(mode) = screenshot_context_menu(ctx, &response) { - view_builder - .schedule_screenshot(render_ctx, query.space_view_id.gpu_readback_id(), mode) - .ok(); - } - for selected_context in ctx.selection_state().selection_space_contexts() { show_projections_from_2d_space( &mut line_builder, diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 6ac5a3997d91..21e939406064 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use itertools::Itertools as _; + use re_build_info::CrateVersion; use re_data_source::{DataSource, FileContents}; use re_entity_db::entity_db::EntityDb; @@ -1602,6 +1603,70 @@ impl App { false } + + #[cfg(not(target_arch = "wasm32"))] // TODO(#8264): screenshotting on web + fn process_screenshot_result( + &mut self, + image: &Arc, + user_data: &egui::UserData, + ) { + use re_viewer_context::ScreenshotInfo; + + if let Some(info) = &user_data + .data + .as_ref() + .and_then(|data| data.downcast_ref::()) + { + let ScreenshotInfo { + ui_rect, + pixels_per_point, + name, + target, + } = (*info).clone(); + + let rgba = if let Some(ui_rect) = ui_rect { + Arc::new(image.region(&ui_rect, Some(pixels_per_point))) + } else { + image.clone() + }; + + match target { + re_viewer_context::ScreenshotTarget::CopyToClipboard => { + #[cfg(not(target_arch = "wasm32"))] // TODO(#8264): screenshotting on web + re_viewer_context::Clipboard::with(|clipboard| { + clipboard.set_image( + [rgba.width(), rgba.height()], + bytemuck::cast_slice(rgba.as_raw()), + ); + }); + } + + re_viewer_context::ScreenshotTarget::SaveToDisk => { + use image::ImageEncoder as _; + let mut png_bytes: Vec = Vec::new(); + if let Err(err) = image::codecs::png::PngEncoder::new(&mut png_bytes) + .write_image( + rgba.as_raw(), + rgba.width() as u32, + rgba.height() as u32, + image::ExtendedColorType::Rgba8, + ) + { + re_log::error!("Failed to encode screenshot as PNG: {err}"); + } else { + let file_name = format!("{name}.png"); + self.command_sender.save_file_dialog( + &file_name, + "Save screenshot".to_owned(), + png_bytes, + ); + } + } + } + } else { + self.screenshotter.save(image); + } + } } #[cfg(target_arch = "wasm32")] @@ -1894,11 +1959,14 @@ impl eframe::App for App { self.store_hub = Some(store_hub); // Check for returned screenshot: - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(target_arch = "wasm32"))] // TODO(#8264): screenshotting on web egui_ctx.input(|i| { for event in &i.raw.events { - if let egui::Event::Screenshot { image, .. } = event { - self.screenshotter.save(image); + if let egui::Event::Screenshot { + image, user_data, .. + } = event + { + self.process_screenshot_result(image, user_data); } } }); diff --git a/crates/viewer/re_viewer/src/saving.rs b/crates/viewer/re_viewer/src/saving.rs index 0a17146796fc..142fda1997df 100644 --- a/crates/viewer/re_viewer/src/saving.rs +++ b/crates/viewer/re_viewer/src/saving.rs @@ -3,11 +3,7 @@ use re_log_types::ApplicationId; /// Convert to lowercase and replace any character that is not a fairly common /// filename character with '-' pub fn sanitize_app_id(app_id: &ApplicationId) -> String { - let output = app_id.0.to_lowercase(); - output.replace( - |c: char| !matches!(c, '0'..='9' | 'a'..='z' | '.' | '_' | '+' | '(' | ')' | '[' | ']'), - "-", - ) + re_viewer_context::santitize_file_name(&app_id.0.to_lowercase()) } /// Determine the default path for a blueprint based on its `ApplicationId` diff --git a/crates/viewer/re_viewer/src/screenshotter.rs b/crates/viewer/re_viewer/src/screenshotter.rs index cad6ed3da1fc..93d8ac88e4fe 100644 --- a/crates/viewer/re_viewer/src/screenshotter.rs +++ b/crates/viewer/re_viewer/src/screenshotter.rs @@ -57,7 +57,7 @@ impl Screenshotter { // is done and transferred to ram. // Obviously we want to send the command this command only once, so we keep counting down // to negatives until we get a call to `save` which then disables the counter. - egui_ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); + egui_ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(Default::default())); } *countdown -= 1; diff --git a/crates/viewer/re_viewer/src/ui/settings_screen.rs b/crates/viewer/re_viewer/src/ui/settings_screen.rs index 0f120d47345f..2a0aa8b88cf1 100644 --- a/crates/viewer/re_viewer/src/ui/settings_screen.rs +++ b/crates/viewer/re_viewer/src/ui/settings_screen.rs @@ -129,12 +129,10 @@ fn settings_screen_ui_impl(ui: &mut egui::Ui, app_options: &mut AppOptions, keep // Currently, the wasm target does not have any experimental features. If/when that changes, // move the conditional compilation flag to the respective checkbox code. #[cfg(not(target_arch = "wasm32"))] - { + // Currently there are no experimental features + if false { separator_with_some_space(ui); ui.strong("Experimental features"); - ui - .re_checkbox(&mut app_options.experimental_space_view_screenshots, "Space view screenshots") - .on_hover_text("Allow taking screenshots of 2D and 3D space views via their context menu. Does not contain labels."); } } diff --git a/crates/viewer/re_viewer_context/src/app_options.rs b/crates/viewer/re_viewer_context/src/app_options.rs index 6f9afe532fd9..21bbfde2c159 100644 --- a/crates/viewer/re_viewer_context/src/app_options.rs +++ b/crates/viewer/re_viewer_context/src/app_options.rs @@ -18,10 +18,6 @@ pub struct AppOptions { /// Include the "Welcome screen" application in the recordings panel? pub include_welcome_screen_button_in_recordings_panel: bool, - /// Enable the experimental feature for space view screenshots. - #[cfg(not(target_arch = "wasm32"))] - pub experimental_space_view_screenshots: bool, - /// Displays an overlay for debugging picking. pub show_picking_debug_overlay: bool, @@ -80,9 +76,6 @@ impl Default for AppOptions { include_welcome_screen_button_in_recordings_panel: true, - #[cfg(not(target_arch = "wasm32"))] - experimental_space_view_screenshots: false, - show_picking_debug_overlay: false, inspect_blueprint_timeline: false, diff --git a/crates/viewer/re_viewer_context/src/cache/caches.rs b/crates/viewer/re_viewer_context/src/cache/caches.rs index fcd45c9e0530..d35c6276cd09 100644 --- a/crates/viewer/re_viewer_context/src/cache/caches.rs +++ b/crates/viewer/re_viewer_context/src/cache/caches.rs @@ -55,6 +55,8 @@ impl Caches { } /// A cache for memoizing things in order to speed up immediate mode UI & other immediate mode style things. +/// +/// See also egus's cache system, in [`egui::cache`] (). pub trait Cache: std::any::Any + Send + Sync { /// Called once per frame to potentially flush the cache. /// diff --git a/crates/viewer/re_viewer_context/src/file_dialog.rs b/crates/viewer/re_viewer_context/src/file_dialog.rs index 55d57a03a626..9fbd18746b1b 100644 --- a/crates/viewer/re_viewer_context/src/file_dialog.rs +++ b/crates/viewer/re_viewer_context/src/file_dialog.rs @@ -1,18 +1,31 @@ -use crate::ViewerContext; +use crate::CommandSender; -impl ViewerContext<'_> { +fn is_safe_filename_char(c: char) -> bool { + c.is_alphanumeric() || matches!(c, ' ' | '-' | '_' | '.') +} + +/// Replace "dangerous" characters by a safe one. +pub fn santitize_file_name(file_name: &str) -> String { + file_name.replace(|c: char| !is_safe_filename_char(c), "-") +} + +impl CommandSender { /// Save some bytes to disk, after first showing a save dialog. #[allow(clippy::unused_self)] // Not used on Wasm - pub fn save_file_dialog(&self, file_name: String, title: String, data: Vec) { + pub fn save_file_dialog(&self, file_name: &str, title: String, data: Vec) { re_tracing::profile_function!(); + let file_name = santitize_file_name(file_name); + #[cfg(target_arch = "wasm32")] { // Web wasm_bindgen_futures::spawn_local(async move { if let Err(err) = async_save_dialog(&file_name, &title, data).await { re_log::error!("File saving failed: {err}"); - } + } else { + re_log::info!("{file_name} saved."); + }; }); } @@ -28,11 +41,10 @@ impl ViewerContext<'_> { }; if let Some(path) = path { use crate::SystemCommandSender as _; - self.command_sender - .send_system(crate::SystemCommand::FileSaver(Box::new(move || { - std::fs::write(&path, &data)?; - Ok(path) - }))); + self.send_system(crate::SystemCommand::FileSaver(Box::new(move || { + std::fs::write(&path, &data)?; + Ok(path) + }))); } } } diff --git a/crates/viewer/re_viewer_context/src/lib.rs b/crates/viewer/re_viewer_context/src/lib.rs index 59aa0a339f01..221a532e4fd4 100644 --- a/crates/viewer/re_viewer_context/src/lib.rs +++ b/crates/viewer/re_viewer_context/src/lib.rs @@ -35,58 +35,59 @@ mod viewer_context; // TODO(andreas): Move to its own crate? pub mod gpu_bridge; -pub use annotations::{ - AnnotationMap, Annotations, ResolvedAnnotationInfo, ResolvedAnnotationInfos, +pub use self::{ + annotations::{AnnotationMap, Annotations, ResolvedAnnotationInfo, ResolvedAnnotationInfos}, + app_options::AppOptions, + blueprint_helpers::{blueprint_timeline, blueprint_timepoint_for_writes}, + blueprint_id::{BlueprintId, BlueprintIdRegistry, ContainerId, SpaceViewId}, + cache::{Cache, Caches, ImageDecodeCache, ImageStatsCache, TensorStatsCache, VideoCache}, + collapsed_id::{CollapseItem, CollapseScope, CollapsedId}, + command_sender::{ + command_channel, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender, + }, + component_fallbacks::{ + ComponentFallbackError, ComponentFallbackProvider, ComponentFallbackProviderResult, + TypedComponentFallbackProvider, + }, + component_ui_registry::{ComponentUiRegistry, ComponentUiTypes, UiLayout}, + contents::{blueprint_id_to_tile_id, Contents, ContentsName}, + file_dialog::santitize_file_name, + image_info::{ColormapWithRange, ImageInfo}, + item::Item, + maybe_mut_ref::MaybeMutRef, + query_context::{ + DataQueryResult, DataResultHandle, DataResultNode, DataResultTree, QueryContext, + }, + query_range::QueryRange, + selection_history::SelectionHistory, + selection_state::{ + ApplicationSelectionState, HoverHighlight, InteractionHighlight, ItemCollection, + ItemSpaceContext, SelectionHighlight, + }, + space_view::{ + DataResult, IdentifiedViewSystem, OptionalSpaceViewEntityHighlight, OverridePath, + PerSystemDataResults, PerSystemEntities, PropertyOverrides, RecommendedSpaceView, + SmallVisualizerSet, SpaceViewClass, SpaceViewClassExt, SpaceViewClassLayoutPriority, + SpaceViewClassRegistry, SpaceViewClassRegistryError, SpaceViewEntityHighlight, + SpaceViewHighlights, SpaceViewOutlineMasks, SpaceViewSpawnHeuristics, SpaceViewState, + SpaceViewStateExt, SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, + SystemExecutionOutput, ViewContext, ViewContextCollection, ViewContextSystem, ViewQuery, + ViewStates, ViewSystemIdentifier, VisualizableFilterContext, + VisualizerAdditionalApplicabilityFilter, VisualizerCollection, VisualizerQueryInfo, + VisualizerSystem, + }, + store_context::StoreContext, + store_hub::StoreHub, + tensor::{ImageStats, TensorStats}, + time_control::{Looping, PlayState, TimeControl, TimeView}, + time_drag_value::TimeDragValue, + typed_entity_collections::{ + ApplicableEntities, IndicatedEntities, PerVisualizer, VisualizableEntities, + }, + undo::BlueprintUndoState, + utils::{auto_color_egui, auto_color_for_entity_path, level_to_rich_text}, + viewer_context::{RecordingConfig, ViewerContext}, }; -pub use app_options::AppOptions; -pub use blueprint_helpers::{blueprint_timeline, blueprint_timepoint_for_writes}; -pub use blueprint_id::{BlueprintId, BlueprintIdRegistry, ContainerId, SpaceViewId}; -pub use cache::{Cache, Caches, ImageDecodeCache, ImageStatsCache, TensorStatsCache, VideoCache}; -pub use collapsed_id::{CollapseItem, CollapseScope, CollapsedId}; -pub use command_sender::{ - command_channel, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender, -}; -pub use component_fallbacks::{ - ComponentFallbackError, ComponentFallbackProvider, ComponentFallbackProviderResult, - TypedComponentFallbackProvider, -}; -pub use component_ui_registry::{ComponentUiRegistry, ComponentUiTypes, UiLayout}; -pub use contents::{blueprint_id_to_tile_id, Contents, ContentsName}; -pub use image_info::{ColormapWithRange, ImageInfo}; -pub use item::Item; -pub use maybe_mut_ref::MaybeMutRef; -pub use query_context::{ - DataQueryResult, DataResultHandle, DataResultNode, DataResultTree, QueryContext, -}; -pub use query_range::QueryRange; -pub use selection_history::SelectionHistory; -pub use selection_state::{ - ApplicationSelectionState, HoverHighlight, InteractionHighlight, ItemCollection, - ItemSpaceContext, SelectionHighlight, -}; -pub use space_view::{ - DataResult, IdentifiedViewSystem, OptionalSpaceViewEntityHighlight, OverridePath, - PerSystemDataResults, PerSystemEntities, PropertyOverrides, RecommendedSpaceView, - SmallVisualizerSet, SpaceViewClass, SpaceViewClassExt, SpaceViewClassLayoutPriority, - SpaceViewClassRegistry, SpaceViewClassRegistryError, SpaceViewEntityHighlight, - SpaceViewHighlights, SpaceViewOutlineMasks, SpaceViewSpawnHeuristics, SpaceViewState, - SpaceViewStateExt, SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, - SystemExecutionOutput, ViewContext, ViewContextCollection, ViewContextSystem, ViewQuery, - ViewStates, ViewSystemIdentifier, VisualizableFilterContext, - VisualizerAdditionalApplicabilityFilter, VisualizerCollection, VisualizerQueryInfo, - VisualizerSystem, -}; -pub use store_context::StoreContext; -pub use store_hub::StoreHub; -pub use tensor::{ImageStats, TensorStats}; -pub use time_control::{Looping, PlayState, TimeControl, TimeView}; -pub use time_drag_value::TimeDragValue; -pub use typed_entity_collections::{ - ApplicableEntities, IndicatedEntities, PerVisualizer, VisualizableEntities, -}; -pub use undo::BlueprintUndoState; -pub use utils::{auto_color_egui, auto_color_for_entity_path, level_to_rich_text}; -pub use viewer_context::{RecordingConfig, ViewerContext}; #[cfg(not(target_arch = "wasm32"))] mod clipboard; @@ -127,3 +128,50 @@ pub fn contents_name_style(name: &ContentsName) -> re_ui::LabelStyle { ContentsName::Placeholder(_) => re_ui::LabelStyle::Unnamed, } } + +/// Info given to egui when taking a screenshot. +/// +/// Specified what we are screenshotting. +#[derive(Clone, Debug, PartialEq)] +pub struct ScreenshotInfo { + /// What portion of the UI to take a screenshot of (in ui points). + pub ui_rect: Option, + pub pixels_per_point: f32, + + /// Name of the screenshot (e.g. space view name), excluding file extension. + pub name: String, + + /// Where to put the screenshot. + pub target: ScreenshotTarget, +} + +/// Where to put the screenshot. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScreenshotTarget { + /// The screenshot will be copied to the clipboard. + CopyToClipboard, + + /// The screenshot will be saved to disk. + SaveToDisk, +} + +// ---------------------------------------------------------------------------------------- + +/// Used to publish info aboutr each space view. +/// +/// We use this for space-view screenshotting. +/// +/// Accessed with [`egui::Memory::caches`]. +pub type SpaceViewRectPublisher = egui::cache::FramePublisher; + +/// Information about a space view that is published each frame by [`SpaceViewRectPublisher`]. +#[derive(Clone, Debug)] +pub struct PublishedSpaceViewInfo { + /// Human-readable name of the space view. + pub name: String, + + /// Where on screen (in ui coords). + /// + /// NOTE: this can include a highlighted border of the space-view. + pub rect: egui::Rect, +} diff --git a/crates/viewer/re_viewport/Cargo.toml b/crates/viewer/re_viewport/Cargo.toml index fef653a6770b..f1e9570115be 100644 --- a/crates/viewer/re_viewport/Cargo.toml +++ b/crates/viewer/re_viewport/Cargo.toml @@ -40,8 +40,6 @@ re_viewport_blueprint.workspace = true ahash.workspace = true egui_tiles.workspace = true egui.workspace = true -glam.workspace = true -image = { workspace = true, default-features = false, features = ["png"] } itertools.workspace = true nohash-hasher.workspace = true rayon.workspace = true diff --git a/crates/viewer/re_viewport/src/lib.rs b/crates/viewer/re_viewport/src/lib.rs index ad9840282dd9..2e2266757fc3 100644 --- a/crates/viewer/re_viewport/src/lib.rs +++ b/crates/viewer/re_viewport/src/lib.rs @@ -6,7 +6,6 @@ #![allow(clippy::unwrap_used)] mod auto_layout; -mod screenshot; mod space_view_highlights; mod system_execution; mod viewport_ui; diff --git a/crates/viewer/re_viewport/src/screenshot.rs b/crates/viewer/re_viewport/src/screenshot.rs deleted file mode 100644 index 83f650da4452..000000000000 --- a/crates/viewer/re_viewport/src/screenshot.rs +++ /dev/null @@ -1,52 +0,0 @@ -use re_space_view::ScreenshotMode; -use re_viewport_blueprint::SpaceViewBlueprint; - -pub fn handle_pending_space_view_screenshots( - space_view: &SpaceViewBlueprint, - data: &[u8], - extent: glam::UVec2, - mode: ScreenshotMode, -) { - // Set to clipboard. - #[cfg(not(target_arch = "wasm32"))] - re_viewer_context::Clipboard::with(|clipboard| { - clipboard.set_image([extent.x as _, extent.y as _], data); - }); - if mode == ScreenshotMode::CopyToClipboard { - return; - } - - // Get next available file name. - fn is_safe_filename_char(c: char) -> bool { - c.is_alphanumeric() || matches!(c, ' ' | '-' | '_') - } - let safe_display_name = space_view - .display_name_or_default() - .as_ref() - .replace(|c: char| !is_safe_filename_char(c), ""); - let mut i = 1; - let filename = loop { - let filename = format!("Screenshot {safe_display_name} - {i}.png"); - if !std::path::Path::new(&filename).exists() { - break filename; - } - i += 1; - }; - let filename = std::path::Path::new(&filename); - - match image::save_buffer(filename, data, extent.x, extent.y, image::ColorType::Rgba8) { - Ok(_) => { - re_log::info!( - "Saved screenshot to {:?}.", - filename.canonicalize().unwrap_or(filename.to_path_buf()) - ); - } - Err(err) => { - re_log::error!( - "Failed to safe screenshot to {:?}: {}", - filename.canonicalize().unwrap_or(filename.to_path_buf()), - err - ); - } - } -} diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index 64b6c7e25edc..ff03ad6cc05a 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -6,18 +6,15 @@ use ahash::HashMap; use egui_tiles::{Behavior as _, EditAction}; use re_context_menu::{context_menu_ui_for_item, SelectionUpdateBehavior}; -use re_renderer::ScreenshotProcessor; use re_ui::{ContextExt as _, DesignTokens, Icon, UiExt as _}; use re_viewer_context::{ - blueprint_id_to_tile_id, icon_for_container_kind, Contents, Item, SpaceViewClassRegistry, - SpaceViewId, SystemExecutionOutput, ViewQuery, ViewStates, ViewerContext, + blueprint_id_to_tile_id, icon_for_container_kind, Contents, Item, PublishedSpaceViewInfo, + SpaceViewClassRegistry, SpaceViewId, SystemExecutionOutput, ViewQuery, ViewStates, + ViewerContext, }; use re_viewport_blueprint::{ViewportBlueprint, ViewportCommand}; -use crate::{ - screenshot::handle_pending_space_view_screenshots, - system_execution::{execute_systems_for_all_views, execute_systems_for_space_view}, -}; +use crate::system_execution::{execute_systems_for_all_views, execute_systems_for_space_view}; fn tree_simplification_options() -> egui_tiles::SimplificationOptions { egui_tiles::SimplificationOptions { @@ -172,22 +169,6 @@ impl ViewportUi { pub fn on_frame_start(&self, ctx: &ViewerContext<'_>) { re_tracing::profile_function!(); - // Handle pending view screenshots: - if let Some(render_ctx) = ctx.render_ctx { - for space_view in self.blueprint.space_views.values() { - #[allow(clippy::blocks_in_conditions)] - while ScreenshotProcessor::next_readback_result( - render_ctx, - space_view.id.gpu_readback_id(), - |data, extent, mode| { - handle_pending_space_view_screenshots(space_view, data, extent, mode); - }, - ) - .is_some() - {} - } - } - self.blueprint.spawn_heuristic_space_views(ctx); } @@ -543,6 +524,21 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { class.display_name(), ); }); + + ui.ctx().memory_mut(|mem| { + mem.caches + .cache::() + .set( + *view_id, + PublishedSpaceViewInfo { + name: space_view_blueprint + .display_name_or_default() + .as_ref() + .to_owned(), + rect: ui.min_rect(), + }, + ); + }); }); Default::default()