Skip to content

Commit

Permalink
Added offscreen rendering support for wgpu & tiny-skia exposed with t…
Browse files Browse the repository at this point in the history
…he window::screenshot command.
  • Loading branch information
bungoboingo committed May 11, 2023
1 parent cf43423 commit 8a9b1c4
Show file tree
Hide file tree
Showing 16 changed files with 893 additions and 24 deletions.
11 changes: 11 additions & 0 deletions examples/screenshot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "screenshot"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
publish = false

[dependencies]
iced = { path = "../..", features = ["debug", "image", "advanced"], default-features = false }
image = { version = "0.24.6", features = ["png"]}
env_logger = "0.10.0"
305 changes: 305 additions & 0 deletions examples/screenshot/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
use iced::alignment::{Horizontal, Vertical};
use iced::keyboard::KeyCode;
use iced::theme::{Button, Container};
use iced::widget::runtime::{CropError, Screenshot};
use iced::widget::{
button, column as col, container, image as iced_image, row, text,
text_input,
};
use iced::{
event, executor, keyboard, subscription, Alignment, Application, Command,
ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription,
Theme,
};
use image as img;
use image::ColorType;

fn main() -> iced::Result {
env_logger::builder().format_timestamp(None).init();

Example::run(iced::Settings::default())
}

struct Example {
screenshot: Option<Screenshot>,
saved_png_path: Option<Result<String, PngError>>,
png_saving: bool,
crop_error: Option<CropError>,
x_input_value: u32,
y_input_value: u32,
width_input_value: u32,
height_input_value: u32,
}

#[derive(Clone, Debug)]
enum Message {
Crop,
Screenshot,
ScreenshotData(Screenshot),
Png,
PngSaved(Result<String, PngError>),
XInputChanged(String),
YInputChanged(String),
WidthInputChanged(String),
HeightInputChanged(String),
}

impl Application for Example {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();

fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
(
Example {
screenshot: None,
saved_png_path: None,
png_saving: false,
crop_error: None,
x_input_value: 0,
y_input_value: 0,
width_input_value: 0,
height_input_value: 0,
},
Command::none(),
)
}

fn title(&self) -> String {
"Screenshot".to_string()
}

fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::Screenshot => {
return iced::window::screenshot(Message::ScreenshotData);
}
Message::ScreenshotData(screenshot) => {
self.screenshot = Some(screenshot);
}
Message::Png => {
if let Some(screenshot) = &self.screenshot {
return Command::perform(
save_to_png(screenshot.clone()),
Message::PngSaved,
);
}
self.png_saving = true;
}
Message::PngSaved(res) => {
self.png_saving = false;
self.saved_png_path = Some(res);
}
Message::XInputChanged(new) => {
if let Ok(value) = new.parse::<u32>() {
self.x_input_value = value;
}
}
Message::YInputChanged(new) => {
if let Ok(value) = new.parse::<u32>() {
self.y_input_value = value;
}
}
Message::WidthInputChanged(new) => {
if let Ok(value) = new.parse::<u32>() {
self.width_input_value = value;
}
}
Message::HeightInputChanged(new) => {
if let Ok(value) = new.parse::<u32>() {
self.height_input_value = value;
}
}
Message::Crop => {
if let Some(screenshot) = &self.screenshot {
let cropped = screenshot.crop(Rectangle::<u32> {
x: self.x_input_value,
y: self.y_input_value,
width: self.width_input_value,
height: self.height_input_value,
});

match cropped {
Ok(screenshot) => {
self.screenshot = Some(screenshot);
self.crop_error = None;
}
Err(crop_error) => {
self.crop_error = Some(crop_error);
}
}
}
}
}

Command::none()
}

fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
let image: Element<Message> = if let Some(screenshot) = &self.screenshot
{
iced_image(iced_image::Handle::from_pixels(
screenshot.size.width,
screenshot.size.height,
screenshot.bytes.clone(),
))
.content_fit(ContentFit::ScaleDown)
.width(Length::Fill)
.height(Length::Fill)
.into()
} else {
text("Press the button to take a screenshot!").into()
};

let image = container(image)
.padding(10)
.style(Container::Custom(Box::new(ScreenshotDisplayContainer)))
.width(Length::FillPortion(2))
.height(Length::Fill)
.center_x()
.center_y();

let crop_origin_controls = row![
text("X:").vertical_alignment(Vertical::Center).width(14),
text_input("0", &format!("{}", self.x_input_value),)
.on_input(Message::XInputChanged)
.width(40),
text("Y:").vertical_alignment(Vertical::Center).width(14),
text_input("0", &format!("{}", self.y_input_value),)
.on_input(Message::YInputChanged)
.width(40),
]
.spacing(10)
.align_items(Alignment::Center);

let crop_dimension_controls = row![
text("W:").vertical_alignment(Vertical::Center).width(14),
text_input("0", &format!("{}", self.width_input_value),)
.on_input(Message::WidthInputChanged)
.width(40),
text("H:").vertical_alignment(Vertical::Center).width(14),
text_input("0", &format!("{}", self.height_input_value),)
.on_input(Message::HeightInputChanged)
.width(40),
]
.spacing(10)
.align_items(Alignment::Center);

let mut crop_controls =
col![crop_origin_controls, crop_dimension_controls]
.spacing(10)
.align_items(Alignment::Center);

if let Some(crop_error) = &self.crop_error {
crop_controls = crop_controls
.push(text(format!("Crop error! \n{}", crop_error)));
}

let png_button = if !self.png_saving {
button("Save to png.")
.style(Button::Secondary)
.padding([10, 20, 10, 20])
.on_press(Message::Png)
} else {
button("Saving..")
.style(Button::Secondary)
.padding([10, 20, 10, 20])
};

let mut controls = col![
button("Screenshot!")
.padding([10, 20, 10, 20])
.on_press(Message::Screenshot),
button("Crop")
.style(Button::Destructive)
.padding([10, 20, 10, 20])
.on_press(Message::Crop),
crop_controls,
png_button,
]
.spacing(40)
.align_items(Alignment::Center);

if let Some(png_result) = &self.saved_png_path {
let msg = match png_result {
Ok(path) => format!("Png saved as: {:?}!", path),
Err(msg) => {
format!("Png could not be saved due to:\n{:?}", msg)
}
};

controls = controls.push(text(msg));
}

let side_content = container(controls)
.align_x(Horizontal::Center)
.width(Length::FillPortion(1))
.height(Length::Fill)
.center_y()
.center_x();

let content = row![side_content, image]
.width(Length::Fill)
.height(Length::Fill)
.align_items(Alignment::Center);

container(content)
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}

fn subscription(&self) -> Subscription<Self::Message> {
subscription::events_with(|event, status| {
if let event::Status::Captured = status {
return None;
}

if let Event::Keyboard(keyboard::Event::KeyPressed {
key_code: KeyCode::F5,
..
}) = event
{
Some(Message::Screenshot)
} else {
None
}
})
}
}

struct ScreenshotDisplayContainer;

impl container::StyleSheet for ScreenshotDisplayContainer {
type Style = Theme;

fn appearance(&self, style: &Self::Style) -> container::Appearance {
container::Appearance {
text_color: None,
background: None,
border_radius: 5.0,
border_width: 4.0,
border_color: style.palette().primary,
}
}
}

async fn save_to_png(screenshot: Screenshot) -> Result<String, PngError> {
let path = "screenshot.png".to_string();
img::save_buffer(
&path,
&screenshot.bytes,
screenshot.size.width,
screenshot.size.height,
ColorType::Rgba8,
)
.map(|_| path)
.map_err(|err| PngError(format!("{:?}", err)))
}

#[derive(Clone, Debug)]
struct PngError(String);
15 changes: 14 additions & 1 deletion graphics/src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ pub trait Compositor: Sized {
background_color: Color,
overlay: &[T],
) -> Result<(), SurfaceError>;

/// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of
/// the texture ordered as `RGBA` in the sRGB color space.
///
/// [`Renderer`]: Self::Renderer;
fn screenshot<T: AsRef<str>>(
&mut self,
renderer: &mut Self::Renderer,
surface: &mut Self::Surface,
viewport: &Viewport,
background_color: Color,
overlay: &[T],
) -> Vec<u8>;
}

/// Result of an unsuccessful call to [`Compositor::present`].
Expand All @@ -82,7 +95,7 @@ pub enum SurfaceError {
OutOfMemory,
}

/// Contains informations about the graphics (e.g. graphics adapter, graphics backend).
/// Contains information about the graphics (e.g. graphics adapter, graphics backend).
#[derive(Debug)]
pub struct Information {
/// Contains the graphics adapter.
Expand Down
30 changes: 30 additions & 0 deletions renderer/src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,36 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
}
})
}

fn screenshot<T: AsRef<str>>(
&mut self,
renderer: &mut Self::Renderer,
surface: &mut Self::Surface,
viewport: &Viewport,
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
renderer.with_primitives(|backend, primitives| match (self, backend, surface) {
(Self::TinySkia(_compositor), crate::Backend::TinySkia(backend), Surface::TinySkia(surface)) => {
iced_tiny_skia::window::compositor::screenshot(surface, backend, primitives, viewport, background_color, overlay)
},
#[cfg(feature = "wgpu")]
(Self::Wgpu(compositor), crate::Backend::Wgpu(backend), Surface::Wgpu(_)) => {
iced_wgpu::window::compositor::screenshot(
compositor,
backend,
primitives,
viewport,
background_color,
overlay,
)
},
#[allow(unreachable_patterns)]
_ => panic!(
"The provided renderer or backend are not compatible with the compositor."
),
})
}
}

enum Candidate {
Expand Down
2 changes: 2 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ mod debug;
#[cfg(not(feature = "debug"))]
#[path = "debug/null.rs"]
mod debug;
mod screenshot;

pub use iced_core as core;
pub use iced_futures as futures;
Expand All @@ -68,4 +69,5 @@ pub use command::Command;
pub use debug::Debug;
pub use font::Font;
pub use program::Program;
pub use screenshot::{CropError, Screenshot};
pub use user_interface::UserInterface;
Loading

0 comments on commit 8a9b1c4

Please sign in to comment.