Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to screenshot the application #2293

Merged
merged 14 commits into from
Jun 1, 2023
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion crates/re_data_ui/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,6 @@ fn save_image(tensor: &re_components::Tensor, dynamic_image: &image::DynamicImag
.save_file()
{
match dynamic_image.save(&path) {
// TODO(emilk): show a popup instead of logging result
Ok(()) => {
re_log::info!("Image saved to {path:?}");
}
Expand Down
2 changes: 1 addition & 1 deletion crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ impl ExampleApp {
};
let top_bar_style = self
.re_ui
.top_bar_style(native_pixels_per_point, fullscreen);
.top_bar_style(native_pixels_per_point, fullscreen, false);

egui::TopBottomPanel::top("top_bar")
.frame(self.re_ui.top_panel_frame())
Expand Down
13 changes: 13 additions & 0 deletions crates/re_ui/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub enum Command {
PlaybackStepBack,
PlaybackStepForward,
PlaybackRestart,

// Dev-tools:
#[cfg(not(target_arch = "wasm32"))]
ScreenshotWholeApp,
}

impl Command {
Expand Down Expand Up @@ -128,6 +132,12 @@ impl Command {
"Move the time marker to the next point in time with any data",
),
Command::PlaybackRestart => ("Restart", "Restart from beginning of timeline"),

#[cfg(not(target_arch = "wasm32"))]
Command::ScreenshotWholeApp => (
"Screenshot",
"Copy screenshot of the whole app to clipboard",
),
}
}

Expand Down Expand Up @@ -189,6 +199,9 @@ impl Command {
Command::PlaybackStepBack => Some(key(Key::ArrowLeft)),
Command::PlaybackStepForward => Some(key(Key::ArrowRight)),
Command::PlaybackRestart => Some(cmd(Key::ArrowLeft)),

#[cfg(not(target_arch = "wasm32"))]
Command::ScreenshotWholeApp => None,
}
}

Expand Down
3 changes: 2 additions & 1 deletion crates/re_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl ReUi {
&self,
native_pixels_per_point: Option<f32>,
fullscreen: bool,
style_like_web: bool,
) -> TopBarStyle {
let gui_zoom = if let Some(native_pixels_per_point) = native_pixels_per_point {
native_pixels_per_point / self.egui_ctx.pixels_per_point()
Expand All @@ -245,7 +246,7 @@ impl ReUi {

// On Mac, we share the same space as the native red/yellow/green close/minimize/maximize buttons.
// This means we need to make room for them.
let make_room_for_window_buttons = {
let make_room_for_window_buttons = !style_like_web && {
#[cfg(target_os = "macos")]
{
crate::FULLSIZE_CONTENT && !fullscreen
Expand Down
1 change: 1 addition & 0 deletions crates/re_viewer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ahash.workspace = true
anyhow.workspace = true
arrow2.workspace = true
arrow2_convert.workspace = true
bytemuck.workspace = true
cfg-if.workspace = true
eframe = { workspace = true, default-features = false, features = [
"default_fonts",
Expand Down
60 changes: 56 additions & 4 deletions crates/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,27 @@ enum TimeControlCommand {
// ----------------------------------------------------------------------------

/// Settings set once at startup (e.g. via command-line options) and not serialized.
#[derive(Clone, Copy, Default)]
#[derive(Clone)]
pub struct StartupOptions {
pub memory_limit: re_memory::MemoryLimit,

pub persist_state: bool,

/// Take a screenshot of the app and quit.
/// We use this to generate screenshots of our exmples.
#[cfg(not(target_arch = "wasm32"))]
pub screenshot_to_path_then_quite: Option<std::path::PathBuf>,
emilk marked this conversation as resolved.
Show resolved Hide resolved
}

impl Default for StartupOptions {
fn default() -> Self {
Self {
memory_limit: re_memory::MemoryLimit::default(),
persist_state: true,
#[cfg(not(target_arch = "wasm32"))]
screenshot_to_path_then_quite: None,
}
}
}

// ----------------------------------------------------------------------------
Expand All @@ -60,6 +77,7 @@ pub struct App {
startup_options: StartupOptions,
ram_limit_warner: re_memory::RamLimitWarner,
re_ui: re_ui::ReUi,
screenshotter: crate::screenshotter::Screenshotter,

/// Listens to the local text log stream
text_log_rx: std::sync::mpsc::Receiver<re_log::LogMsg>,
Expand Down Expand Up @@ -146,11 +164,21 @@ impl App {
);
}

#[allow(unused_mut, clippy::needless_update)] // false positive on web
let mut screenshotter = crate::screenshotter::Screenshotter::default();

#[cfg(not(target_arch = "wasm32"))]
if let Some(screenshot_path) = startup_options.screenshot_to_path_then_quite.clone() {
screenshotter.screenshot_to_path_then_quit(screenshot_path);
}

Self {
build_info,
startup_options,
ram_limit_warner: re_memory::RamLimitWarner::warn_at_fraction_of_max(0.75),
re_ui,
screenshotter,

text_log_rx,
component_ui_registry: re_data_ui::create_component_ui_registry(),
rx,
Expand Down Expand Up @@ -389,6 +417,11 @@ impl App {
Command::PlaybackRestart => {
self.run_time_control_command(TimeControlCommand::Restart);
}

#[cfg(not(target_arch = "wasm32"))]
Command::ScreenshotWholeApp => {
self.screenshotter.request_screenshot();
}
}
}

Expand Down Expand Up @@ -507,13 +540,19 @@ impl eframe::App for App {
fn update(&mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) {
let frame_start = Instant::now();

self.screenshotter.update(egui_ctx, frame);

if self.startup_options.memory_limit.limit.is_none() {
// we only warn about high memory usage if the user hasn't specified a limit
self.ram_limit_warner.update();
}

#[cfg(not(target_arch = "wasm32"))]
{
if self.screenshotter.is_screenshotting() {
emilk marked this conversation as resolved.
Show resolved Hide resolved
// Set a standard screenshot resolution (for our docs and examples).
frame.set_window_size(egui::Vec2::new(800.0, 600.0));
emilk marked this conversation as resolved.
Show resolved Hide resolved
egui_ctx.set_pixels_per_point(2.0);
} else {
// Ensure zoom factor is sane and in 10% steps at all times before applying it.
{
let mut zoom_factor = self.state.app_options.zoom_factor;
Expand Down Expand Up @@ -679,7 +718,10 @@ impl eframe::App for App {
}

self.handle_dropping_files(egui_ctx);
self.toasts.show(egui_ctx);

if !self.screenshotter.is_screenshotting() {
self.toasts.show(egui_ctx);
}

if let Some(cmd) = self.cmd_palette.show(egui_ctx) {
self.pending_commands.push(cmd);
Expand All @@ -702,6 +744,13 @@ impl eframe::App for App {
re_log::warn_once!("Blueprint unexpectedly missing from store.");
}
}

#[cfg(not(target_arch = "wasm32"))]
fn post_rendering(&mut self, _window_size: [u32; 2], frame: &eframe::Frame) {
if let Some(screenshot) = frame.screenshot() {
self.screenshotter.save(&screenshot);
}
}
}

fn paint_background_fill(ui: &mut egui::Ui) {
Expand Down Expand Up @@ -1237,7 +1286,10 @@ fn top_panel(
frame.info().window_info.fullscreen
}
};
let top_bar_style = app.re_ui.top_bar_style(native_pixels_per_point, fullscreen);
let style_like_web = app.screenshotter.is_screenshotting();
let top_bar_style =
app.re_ui
.top_bar_style(native_pixels_per_point, fullscreen, style_like_web);

egui::TopBottomPanel::top("top_bar")
.frame(app.re_ui.top_panel_frame())
Expand Down
1 change: 1 addition & 0 deletions crates/re_viewer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod env_vars;
#[cfg(not(target_arch = "wasm32"))]
mod profiler;
mod remote_viewer_app;
mod screenshotter;
mod ui;
mod viewer_analytics;

Expand Down
2 changes: 1 addition & 1 deletion crates/re_viewer/src/remote_viewer_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl RemoteViewerApp {
let app = crate::App::from_receiver(
self.build_info,
&self.app_env,
self.startup_options,
self.startup_options.clone(),
self.re_ui.clone(),
storage,
rx,
Expand Down
82 changes: 82 additions & 0 deletions crates/re_viewer/src/screenshotter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! Screenshotting not implemented on web yet because we
//! haven't implemented "copy image to clipboard" there.

/// Helper for screenshotting the entire app
#[derive(Default)]
pub struct Screenshotter {
#[cfg(not(target_arch = "wasm32"))]
countdown: Option<usize>,

#[cfg(not(target_arch = "wasm32"))]
target_path: Option<std::path::PathBuf>,
}

#[cfg(not(target_arch = "wasm32"))]
impl Screenshotter {
/// Used for generating screenshots in dev builds.
pub fn screenshot_to_path_then_quit(&mut self, path: std::path::PathBuf) {
self.request_screenshot();
self.target_path = Some(path);
Comment on lines +27 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's likely fine in practice, but this feels somewhat racy when it comes to repeated invocations. Not that repeated invocations are expected or needed, but maybe request_screenshot should warn if self.countdown.is_some().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some clarifying comments and an assert, but I don't want to over-engineer this too much

}

/// Call once per frame
pub fn update(&mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) {
if let Some(countdown) = &mut self.countdown {
if *countdown == 0 {
frame.request_screenshot();
} else {
*countdown -= 1;
}

egui_ctx.request_repaint(); // Make sure we keep counting down
}
}

/// If true, temporarily re-style the UI to make it suitable for capture!
///
/// We do the re-styling to create consistent screenshots across platforms.
/// In particular, we style the UI to look like the web viewer.
pub fn is_screenshotting(&self) -> bool {
self.countdown.is_some()
}

pub fn request_screenshot(&mut self) {
// give app time to set window size, and then wait for animations to finish etc:
self.countdown = Some(10);
}

pub fn save(&mut self, image: &egui::ColorImage) {
self.countdown = None;
if let Some(path) = self.target_path.take() {
let w = image.width() as _;
let h = image.height() as _;
let image =
image::RgbaImage::from_raw(w, h, bytemuck::pod_collect_to_vec(&image.pixels))
.expect("Failed to create image");
match image.save(&path) {
Ok(()) => {
re_log::info!("Screenshot saved to {path:?}");
std::process::exit(0); // Close nicely
emilk marked this conversation as resolved.
Show resolved Hide resolved
}
Err(err) => {
panic!("Failed saving screenshot to {path:?}: {err}");
}
}
} else {
re_viewer_context::Clipboard::with(|cb| {
cb.set_image(image.size, bytemuck::cast_slice(&image.pixels));
});
}
}
}

#[cfg(target_arch = "wasm32")]
impl Screenshotter {
#[allow(clippy::unused_self)]
pub fn update(&mut self, _egui_ctx: &egui::Context, _frame: &mut eframe::Frame) {}

#[allow(clippy::unused_self)]
pub fn is_screenshotting(&self) -> bool {
false
}
}
14 changes: 10 additions & 4 deletions crates/rerun/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ struct Args {
#[command(subcommand)]
commands: Option<Commands>,

/// What bind address IP to use.
#[clap(long, default_value = "0.0.0.0")]
bind: String,

/// Set a maximum input latency, e.g. "200ms" or "10s".
///
/// If we go over this, we start dropping packets.
Expand Down Expand Up @@ -86,6 +90,11 @@ struct Args {
#[clap(long)]
save: Option<String>,

/// Take a screenshot of the app and quit.
/// We use this to generate screenshots of our exmples.
#[clap(long)]
screenshot_to: Option<std::path::PathBuf>,

/// Exit with a non-zero exit code if any warning or error is logged. Useful for tests.
#[clap(long)]
strict: bool,
Expand Down Expand Up @@ -115,10 +124,6 @@ struct Args {
#[clap(long)]
web_viewer: bool,

/// What bind address IP to use.
#[clap(long, default_value = "0.0.0.0")]
bind: String,

/// What port do we listen to for hosting the web viewer over HTTP.
/// A port of 0 will pick a random port.
#[cfg(feature = "web_viewer")]
Expand Down Expand Up @@ -308,6 +313,7 @@ async fn run_impl(
.unwrap_or_else(|err| panic!("Bad --memory-limit: {err}"))
}),
persist_state: args.persist_state,
screenshot_to_path_then_quite: args.screenshot_to.clone(),
};

// Where do we get the data from?
Expand Down
2 changes: 1 addition & 1 deletion examples/rust/extend_viewer_ui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Start pruning the data once we reach this much memory allocated
limit: Some(12_000_000_000),
},
persist_state: true,
..Default::default()
};

// This is used for analytics, if the `analytics` feature is on in `Cargo.toml`
Expand Down