Skip to content

Commit

Permalink
Allow attaching custom user data to a screenshot command (#5416)
Browse files Browse the repository at this point in the history
This lets users trigger a screenshot from anywhere, and then when they
get back the results they have some context about what part of their
code triggered the screenshot.
  • Loading branch information
emilk authored Dec 3, 2024
1 parent 6a1131f commit a9c76ba
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 23 deletions.
3 changes: 2 additions & 1 deletion crates/eframe/src/native/glow_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,13 +661,14 @@ impl<'app> GlowWinitRunning<'app> {
{
for action in viewport.actions_requested.drain() {
match action {
ActionRequested::Screenshot => {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.into(),
});
}
Expand Down
45 changes: 32 additions & 13 deletions crates/eframe/src/native/wgpu_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,10 +643,16 @@ impl<'app> WgpuWinitRunning<'app> {

let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);

let screenshot_requested = viewport
.actions_requested
.take(&ActionRequested::Screenshot)
.is_some();
let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone());
false
} else {
true
}
});
let screenshot_requested = !screenshot_commands.is_empty();
let (vsync_secs, screenshot) = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
Expand All @@ -655,19 +661,32 @@ impl<'app> WgpuWinitRunning<'app> {
&textures_delta,
screenshot_requested,
);
if let Some(screenshot) = screenshot {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
image: screenshot.into(),
});
match (screenshot_requested, screenshot) {
(false, None) => {}
(true, Some(screenshot)) => {
let screenshot = Arc::new(screenshot);
for user_data in screenshot_commands {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.clone(),
});
}
}
(true, None) => {
log::error!("Bug in egui_wgpu: screenshot requested, but no screenshot was taken");
}
(false, Some(_)) => {
log::warn!("Bug in egui_wgpu: Got screenshot without requesting it");
}
}

for action in viewport.actions_requested.drain() {
match action {
ActionRequested::Screenshot => {
ActionRequested::Screenshot { .. } => {
// already handled above
}
ActionRequested::Cut => {
Expand Down
6 changes: 3 additions & 3 deletions crates/egui-winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,7 +1301,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
// ---------------------------------------------------------------------------
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum ActionRequested {
Screenshot,
Screenshot(egui::UserData),
Cut,
Copy,
Paste,
Expand Down Expand Up @@ -1516,8 +1516,8 @@ fn process_viewport_command(
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::Screenshot => {
actions_requested.insert(ActionRequested::Screenshot);
ViewportCommand::Screenshot(user_data) => {
actions_requested.insert(ActionRequested::Screenshot(user_data));
}
ViewportCommand::RequestCut => {
actions_requested.insert(ActionRequested::Cut);
Expand Down
4 changes: 4 additions & 0 deletions crates/egui/src/data/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ pub enum Event {
/// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`].
Screenshot {
viewport_id: crate::ViewportId,

/// Whatever was passed to [`crate::ViewportCommand::Screenshot`].
user_data: crate::UserData,

image: std::sync::Arc<ColorImage>,
},
}
Expand Down
2 changes: 2 additions & 0 deletions crates/egui/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
pub mod input;
mod key;
pub mod output;
mod user_data;

pub use key::Key;
pub use user_data::UserData;
74 changes: 74 additions & 0 deletions crates/egui/src/data/user_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::{any::Any, sync::Arc};

/// A wrapper around `dyn Any`, used for passing custom user data
/// to [`crate::ViewportCommand::Screenshot`].
#[derive(Clone, Debug, Default)]
pub struct UserData {
/// A user value given to the screenshot command,
/// that will be returned in [`crate::Event::Screenshot`].
pub data: Option<Arc<dyn Any + Send + Sync>>,
}

impl UserData {
/// You can also use [`Self::default`].
pub fn new(user_info: impl Any + Send + Sync) -> Self {
Self {
data: Some(Arc::new(user_info)),
}
}
}

impl PartialEq for UserData {
fn eq(&self, other: &Self) -> bool {
match (&self.data, &other.data) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
}
}
}

impl Eq for UserData {}

impl std::hash::Hash for UserData {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.as_ref().map(Arc::as_ptr).hash(state);
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for UserData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_none() // can't serialize an `Any`
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UserData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct UserDataVisitor;

impl<'de> serde::de::Visitor<'de> for UserDataVisitor {
type Value = UserData;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a None value")
}

fn visit_none<E>(self) -> Result<UserData, E>
where
E: serde::de::Error,
{
Ok(UserData::default())
}
}

deserializer.deserialize_option(UserDataVisitor)
}
}
2 changes: 1 addition & 1 deletion crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ pub use self::{
output::{
self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo,
},
Key,
Key, UserData,
},
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
Expand Down
6 changes: 4 additions & 2 deletions crates/egui/src/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,8 +1058,8 @@ pub enum ViewportCommand {

/// Take a screenshot.
///
/// The results are returned in `crate::Event::Screenshot`.
Screenshot,
/// The results are returned in [`crate::Event::Screenshot`].
Screenshot(crate::UserData),

/// Request cut of the current selection
///
Expand Down Expand Up @@ -1100,6 +1100,8 @@ impl ViewportCommand {
}
}

// ----------------------------------------------------------------------------

/// Describes a viewport, i.e. a native window.
///
/// This is returned by [`crate::Context::run`] on each frame, and should be applied
Expand Down
10 changes: 7 additions & 3 deletions examples/screenshot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl eframe::App for MyApp {

if ui.button("save to 'top_left.png'").clicked() {
self.save_to_file = true;
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(Default::default()));
}

ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
Expand All @@ -58,9 +58,13 @@ impl eframe::App for MyApp {
} else {
ctx.set_theme(egui::Theme::Light);
};
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
ctx.send_viewport_cmd(
egui::ViewportCommand::Screenshot(Default::default()),
);
} else if ui.button("take screenshot!").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
ctx.send_viewport_cmd(
egui::ViewportCommand::Screenshot(Default::default()),
);
}
});
});
Expand Down

0 comments on commit a9c76ba

Please sign in to comment.