Added offscreen rendering support for wgpu & tiny-skia exposed with the window::screenshot command.
This commit is contained in:
parent
c15f1b5f65
commit
233196eb14
16 changed files with 893 additions and 24 deletions
11
examples/screenshot/Cargo.toml
Normal file
11
examples/screenshot/Cargo.toml
Normal file
|
|
@ -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"] }
|
||||
image = { version = "0.24.6", features = ["png"]}
|
||||
env_logger = "0.10.0"
|
||||
305
examples/screenshot/src/main.rs
Normal file
305
examples/screenshot/src/main.rs
Normal file
|
|
@ -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);
|
||||
|
|
@ -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`].
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
80
runtime/src/screenshot.rs
Normal file
80
runtime/src/screenshot.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use iced_core::{Rectangle, Size};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
/// Data of a screenshot, captured with `window::screenshot()`.
|
||||
///
|
||||
/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space.
|
||||
#[derive(Clone)]
|
||||
pub struct Screenshot {
|
||||
/// The bytes of the [`Screenshot`].
|
||||
pub bytes: Vec<u8>,
|
||||
/// The size of the [`Screenshot`].
|
||||
pub size: Size<u32>,
|
||||
}
|
||||
|
||||
impl Debug for Screenshot {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Screenshot: {{ \n bytes: {}\n size: {:?} }}",
|
||||
self.bytes.len(),
|
||||
self.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
/// Creates a new [`Screenshot`].
|
||||
pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self {
|
||||
Self { bytes, size }
|
||||
}
|
||||
|
||||
/// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the
|
||||
/// top-left corner of the [`Screenshot`].
|
||||
pub fn crop(&self, region: Rectangle<u32>) -> Result<Self, CropError> {
|
||||
if region.width == 0 || region.height == 0 {
|
||||
return Err(CropError::Zero);
|
||||
}
|
||||
|
||||
if region.x + region.width > self.size.width
|
||||
|| region.y + region.height > self.size.height
|
||||
{
|
||||
return Err(CropError::OutOfBounds);
|
||||
}
|
||||
|
||||
// Image is always RGBA8 = 4 bytes per pixel
|
||||
const PIXEL_SIZE: usize = 4;
|
||||
|
||||
let bytes_per_row = self.size.width as usize * PIXEL_SIZE;
|
||||
let row_range = region.y as usize..(region.y + region.height) as usize;
|
||||
let column_range = region.x as usize * PIXEL_SIZE
|
||||
..(region.x + region.width) as usize * PIXEL_SIZE;
|
||||
|
||||
let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold(
|
||||
vec![],
|
||||
|mut acc, (row, bytes)| {
|
||||
if row_range.contains(&row) {
|
||||
acc.extend(&bytes[column_range.clone()]);
|
||||
}
|
||||
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
bytes: chopped,
|
||||
size: Size::new(region.width, region.height),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
/// Errors that can occur when cropping a [`Screenshot`].
|
||||
pub enum CropError {
|
||||
#[error("The cropped region is out of bounds.")]
|
||||
/// The cropped region's size is out of bounds.
|
||||
OutOfBounds,
|
||||
#[error("The cropped region is not visible.")]
|
||||
/// The cropped region's size is zero.
|
||||
Zero,
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ use crate::command::{self, Command};
|
|||
use crate::core::time::Instant;
|
||||
use crate::core::window::{Event, Icon, Level, Mode, UserAttention};
|
||||
use crate::futures::subscription::{self, Subscription};
|
||||
use crate::screenshot::Screenshot;
|
||||
|
||||
/// Subscribes to the frames of the window of the running application.
|
||||
///
|
||||
|
|
@ -115,3 +116,10 @@ pub fn fetch_id<Message>(
|
|||
pub fn change_icon<Message>(icon: Icon) -> Command<Message> {
|
||||
Command::single(command::Action::Window(Action::ChangeIcon(icon)))
|
||||
}
|
||||
|
||||
/// Captures a [`Screenshot`] from the window.
|
||||
pub fn screenshot<Message>(
|
||||
f: impl FnOnce(Screenshot) -> Message + Send + 'static,
|
||||
) -> Command<Message> {
|
||||
Command::single(command::Action::Window(Action::Screenshot(Box::new(f))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::core::window::{Icon, Level, Mode, UserAttention};
|
||||
use crate::futures::MaybeSend;
|
||||
|
||||
use crate::screenshot::Screenshot;
|
||||
use std::fmt;
|
||||
|
||||
/// An operation to be performed on some window.
|
||||
|
|
@ -89,6 +90,8 @@ pub enum Action<T> {
|
|||
/// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That
|
||||
/// said, it's usually in the same ballpark as on Windows.
|
||||
ChangeIcon(Icon),
|
||||
/// Screenshot the viewport of the window.
|
||||
Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>),
|
||||
}
|
||||
|
||||
impl<T> Action<T> {
|
||||
|
|
@ -118,6 +121,11 @@ impl<T> Action<T> {
|
|||
Self::ChangeLevel(level) => Action::ChangeLevel(level),
|
||||
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))),
|
||||
Self::ChangeIcon(icon) => Action::ChangeIcon(icon),
|
||||
Self::Screenshot(tag) => {
|
||||
Action::Screenshot(Box::new(move |screenshot| {
|
||||
f(tag(screenshot))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,6 +163,7 @@ impl<T> fmt::Debug for Action<T> {
|
|||
Self::ChangeIcon(_icon) => {
|
||||
write!(f, "Action::ChangeIcon(icon)")
|
||||
}
|
||||
Self::Screenshot(_) => write!(f, "Action::Screenshot"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::core::{Color, Rectangle};
|
||||
use crate::graphics::compositor::{self, Information, SurfaceError};
|
||||
use crate::core::{Color, Rectangle, Size};
|
||||
use crate::graphics::compositor::{self, Information};
|
||||
use crate::graphics::damage;
|
||||
use crate::graphics::{Error, Primitive, Viewport};
|
||||
use crate::{Backend, Renderer, Settings};
|
||||
|
|
@ -79,7 +79,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
|
|||
viewport: &Viewport,
|
||||
background_color: Color,
|
||||
overlay: &[T],
|
||||
) -> Result<(), SurfaceError> {
|
||||
) -> Result<(), compositor::SurfaceError> {
|
||||
renderer.with_primitives(|backend, primitives| {
|
||||
present(
|
||||
backend,
|
||||
|
|
@ -91,6 +91,26 @@ 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| {
|
||||
screenshot(
|
||||
surface,
|
||||
backend,
|
||||
primitives,
|
||||
viewport,
|
||||
background_color,
|
||||
overlay,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
|
||||
|
|
@ -156,3 +176,53 @@ pub fn present<T: AsRef<str>>(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn screenshot<T: AsRef<str>>(
|
||||
surface: &mut Surface,
|
||||
backend: &mut Backend,
|
||||
primitives: &[Primitive],
|
||||
viewport: &Viewport,
|
||||
background_color: Color,
|
||||
overlay: &[T],
|
||||
) -> Vec<u8> {
|
||||
let size = viewport.physical_size();
|
||||
|
||||
let mut offscreen_buffer: Vec<u32> =
|
||||
vec![0; size.width as usize * size.height as usize];
|
||||
|
||||
backend.draw(
|
||||
&mut tiny_skia::PixmapMut::from_bytes(
|
||||
bytemuck::cast_slice_mut(&mut offscreen_buffer),
|
||||
size.width,
|
||||
size.height,
|
||||
)
|
||||
.expect("Create offscreen pixel map"),
|
||||
&mut surface.clip_mask,
|
||||
primitives,
|
||||
viewport,
|
||||
&[Rectangle::with_size(Size::new(
|
||||
size.width as f32,
|
||||
size.height as f32,
|
||||
))],
|
||||
background_color,
|
||||
overlay,
|
||||
);
|
||||
|
||||
offscreen_buffer.iter().fold(
|
||||
Vec::with_capacity(offscreen_buffer.len() * 4),
|
||||
|mut acc, pixel| {
|
||||
const A_MASK: u32 = 0xFF_00_00_00;
|
||||
const R_MASK: u32 = 0x00_FF_00_00;
|
||||
const G_MASK: u32 = 0x00_00_FF_00;
|
||||
const B_MASK: u32 = 0x00_00_00_FF;
|
||||
|
||||
let a = ((A_MASK & pixel) >> 24) as u8;
|
||||
let r = ((R_MASK & pixel) >> 16) as u8;
|
||||
let g = ((G_MASK & pixel) >> 8) as u8;
|
||||
let b = (B_MASK & pixel) as u8;
|
||||
|
||||
acc.extend([r, g, b, a]);
|
||||
acc
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::core;
|
||||
use crate::core::{Color, Font, Point, Size};
|
||||
use crate::graphics::backend;
|
||||
use crate::graphics::color;
|
||||
|
|
@ -6,6 +5,7 @@ use crate::graphics::{Primitive, Transformation, Viewport};
|
|||
use crate::quad;
|
||||
use crate::text;
|
||||
use crate::triangle;
|
||||
use crate::{core, offscreen};
|
||||
use crate::{Layer, Settings};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
|
|
@ -123,6 +123,65 @@ impl Backend {
|
|||
self.image_pipeline.end_frame();
|
||||
}
|
||||
|
||||
/// Performs an offscreen render pass. If the `format` selected by WGPU is not
|
||||
/// `wgpu::TextureFormat::Rgba8UnormSrgb`, a conversion compute pipeline will run.
|
||||
///
|
||||
/// Returns `None` if the `frame` is `Rgba8UnormSrgb`, else returns the newly
|
||||
/// converted texture view in `Rgba8UnormSrgb`.
|
||||
pub fn offscreen<T: AsRef<str>>(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
clear_color: Option<Color>,
|
||||
frame: &wgpu::TextureView,
|
||||
format: wgpu::TextureFormat,
|
||||
primitives: &[Primitive],
|
||||
viewport: &Viewport,
|
||||
overlay_text: &[T],
|
||||
texture_extent: wgpu::Extent3d,
|
||||
) -> Option<wgpu::Texture> {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _ = info_span!("iced_wgpu::offscreen", "DRAW").entered();
|
||||
|
||||
self.present(
|
||||
device,
|
||||
queue,
|
||||
encoder,
|
||||
clear_color,
|
||||
frame,
|
||||
primitives,
|
||||
viewport,
|
||||
overlay_text,
|
||||
);
|
||||
|
||||
if format != wgpu::TextureFormat::Rgba8UnormSrgb {
|
||||
log::info!("Texture format is {format:?}; performing conversion to rgba8..");
|
||||
let pipeline = offscreen::Pipeline::new(device);
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.conversion.source_texture"),
|
||||
size: texture_extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let view =
|
||||
texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
pipeline.convert(device, texture_extent, frame, &view, encoder);
|
||||
|
||||
return Some(texture);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn prepare_text(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ pub mod geometry;
|
|||
|
||||
mod backend;
|
||||
mod buffer;
|
||||
mod offscreen;
|
||||
mod quad;
|
||||
mod text;
|
||||
mod triangle;
|
||||
|
|
|
|||
102
wgpu/src/offscreen.rs
Normal file
102
wgpu/src/offscreen.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
/// A simple compute pipeline to convert any texture to Rgba8UnormSrgb.
|
||||
#[derive(Debug)]
|
||||
pub struct Pipeline {
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
let shader =
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.blit.shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
|
||||
"shader/offscreen_blit.wgsl"
|
||||
))),
|
||||
});
|
||||
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.blit.bind_group_layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float {
|
||||
filterable: false,
|
||||
},
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::StorageTexture {
|
||||
access: wgpu::StorageTextureAccess::WriteOnly,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline =
|
||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.blit.pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
module: &shader,
|
||||
entry_point: "main",
|
||||
});
|
||||
|
||||
Self {
|
||||
pipeline,
|
||||
layout: bind_group_layout,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
extent: wgpu::Extent3d,
|
||||
frame: &wgpu::TextureView,
|
||||
view: &wgpu::TextureView,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
) {
|
||||
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.blit.bind_group"),
|
||||
layout: &self.layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(frame),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(view),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let mut compute_pass =
|
||||
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.blit.compute_pass"),
|
||||
});
|
||||
|
||||
compute_pass.set_pipeline(&self.pipeline);
|
||||
compute_pass.set_bind_group(0, &bind, &[]);
|
||||
compute_pass.dispatch_workgroups(extent.width, extent.height, 1);
|
||||
}
|
||||
}
|
||||
22
wgpu/src/shader/offscreen_blit.wgsl
Normal file
22
wgpu/src/shader/offscreen_blit.wgsl
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@group(0) @binding(0) var u_texture: texture_2d<f32>;
|
||||
@group(0) @binding(1) var out_texture: texture_storage_2d<rgba8unorm, write>;
|
||||
|
||||
fn srgb(color: f32) -> f32 {
|
||||
if (color <= 0.0031308) {
|
||||
return 12.92 * color;
|
||||
} else {
|
||||
return (1.055 * (pow(color, (1.0/2.4)))) - 0.055;
|
||||
}
|
||||
}
|
||||
|
||||
@compute @workgroup_size(1)
|
||||
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
// texture coord must be i32 due to a naga bug:
|
||||
// https://github.com/gfx-rs/naga/issues/1997
|
||||
let coords = vec2(i32(id.x), i32(id.y));
|
||||
|
||||
let src: vec4<f32> = textureLoad(u_texture, coords, 0);
|
||||
let srgb_color: vec4<f32> = vec4(srgb(src.x), srgb(src.y), srgb(src.z), src.w);
|
||||
|
||||
textureStore(out_texture, coords, srgb_color);
|
||||
}
|
||||
|
|
@ -16,15 +16,8 @@ impl Blit {
|
|||
format: wgpu::TextureFormat,
|
||||
antialiasing: graphics::Antialiasing,
|
||||
) -> Blit {
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
let sampler =
|
||||
device.create_sampler(&wgpu::SamplerDescriptor::default());
|
||||
|
||||
let constant_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Connect a window with a renderer.
|
||||
use crate::core::Color;
|
||||
use crate::core::{Color, Size};
|
||||
use crate::graphics;
|
||||
use crate::graphics::color;
|
||||
use crate::graphics::compositor;
|
||||
|
|
@ -283,4 +283,145 @@ impl<Theme> 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| {
|
||||
screenshot(
|
||||
self,
|
||||
backend,
|
||||
primitives,
|
||||
viewport,
|
||||
background_color,
|
||||
overlay,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the current surface to an offscreen buffer.
|
||||
///
|
||||
/// Returns RGBA bytes of the texture data.
|
||||
pub fn screenshot<Theme, T: AsRef<str>>(
|
||||
compositor: &Compositor<Theme>,
|
||||
backend: &mut Backend,
|
||||
primitives: &[Primitive],
|
||||
viewport: &Viewport,
|
||||
background_color: Color,
|
||||
overlay: &[T],
|
||||
) -> Vec<u8> {
|
||||
let mut encoder = compositor.device.create_command_encoder(
|
||||
&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.encoder"),
|
||||
},
|
||||
);
|
||||
|
||||
let dimensions = BufferDimensions::new(viewport.physical_size());
|
||||
|
||||
let texture_extent = wgpu::Extent3d {
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = compositor.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.source_texture"),
|
||||
size: texture_extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: compositor.format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let rgba_texture = backend.offscreen(
|
||||
&compositor.device,
|
||||
&compositor.queue,
|
||||
&mut encoder,
|
||||
Some(background_color),
|
||||
&view,
|
||||
compositor.format,
|
||||
primitives,
|
||||
viewport,
|
||||
overlay,
|
||||
texture_extent,
|
||||
);
|
||||
|
||||
let output_buffer =
|
||||
compositor.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("iced_wgpu.offscreen.output_texture_buffer"),
|
||||
size: (dimensions.padded_bytes_per_row * dimensions.height as usize)
|
||||
as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
rgba_texture.unwrap_or(texture).as_image_copy(),
|
||||
wgpu::ImageCopyBuffer {
|
||||
buffer: &output_buffer,
|
||||
layout: wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
texture_extent,
|
||||
);
|
||||
|
||||
let index = compositor.queue.submit(Some(encoder.finish()));
|
||||
|
||||
let slice = output_buffer.slice(..);
|
||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
|
||||
let _ = compositor
|
||||
.device
|
||||
.poll(wgpu::Maintain::WaitForSubmissionIndex(index));
|
||||
|
||||
let mapped_buffer = slice.get_mapped_range();
|
||||
|
||||
mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold(
|
||||
vec![],
|
||||
|mut acc, row| {
|
||||
acc.extend(&row[..dimensions.unpadded_bytes_per_row]);
|
||||
acc
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct BufferDimensions {
|
||||
width: u32,
|
||||
height: u32,
|
||||
unpadded_bytes_per_row: usize,
|
||||
padded_bytes_per_row: usize,
|
||||
}
|
||||
|
||||
impl BufferDimensions {
|
||||
fn new(size: Size<u32>) -> Self {
|
||||
let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA
|
||||
let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256
|
||||
let padded_bytes_per_row_padding =
|
||||
(alignment - unpadded_bytes_per_row % alignment) % alignment;
|
||||
let padded_bytes_per_row =
|
||||
unpadded_bytes_per_row + padded_bytes_per_row_padding;
|
||||
|
||||
Self {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
unpadded_bytes_per_row,
|
||||
padded_bytes_per_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use crate::graphics::compositor::{self, Compositor};
|
|||
use crate::runtime::clipboard;
|
||||
use crate::runtime::program::Program;
|
||||
use crate::runtime::user_interface::{self, UserInterface};
|
||||
use crate::runtime::{Command, Debug};
|
||||
use crate::runtime::{Command, Debug, Screenshot};
|
||||
use crate::style::application::{Appearance, StyleSheet};
|
||||
use crate::{Clipboard, Error, Proxy, Settings};
|
||||
|
||||
|
|
@ -308,6 +308,8 @@ async fn run_instance<A, E, C>(
|
|||
|
||||
run_command(
|
||||
&application,
|
||||
&mut compositor,
|
||||
&mut surface,
|
||||
&mut cache,
|
||||
&state,
|
||||
&mut renderer,
|
||||
|
|
@ -318,7 +320,6 @@ async fn run_instance<A, E, C>(
|
|||
&mut proxy,
|
||||
&mut debug,
|
||||
&window,
|
||||
|| compositor.fetch_information(),
|
||||
);
|
||||
runtime.track(application.subscription().into_recipes());
|
||||
|
||||
|
|
@ -382,6 +383,8 @@ async fn run_instance<A, E, C>(
|
|||
// Update application
|
||||
update(
|
||||
&mut application,
|
||||
&mut compositor,
|
||||
&mut surface,
|
||||
&mut cache,
|
||||
&state,
|
||||
&mut renderer,
|
||||
|
|
@ -392,7 +395,6 @@ async fn run_instance<A, E, C>(
|
|||
&mut debug,
|
||||
&mut messages,
|
||||
&window,
|
||||
|| compositor.fetch_information(),
|
||||
);
|
||||
|
||||
// Update window
|
||||
|
|
@ -645,8 +647,10 @@ where
|
|||
|
||||
/// Updates an [`Application`] by feeding it the provided messages, spawning any
|
||||
/// resulting [`Command`], and tracking its [`Subscription`].
|
||||
pub fn update<A: Application, E: Executor>(
|
||||
pub fn update<A: Application, C, E: Executor>(
|
||||
application: &mut A,
|
||||
compositor: &mut C,
|
||||
surface: &mut C::Surface,
|
||||
cache: &mut user_interface::Cache,
|
||||
state: &State<A>,
|
||||
renderer: &mut A::Renderer,
|
||||
|
|
@ -657,8 +661,8 @@ pub fn update<A: Application, E: Executor>(
|
|||
debug: &mut Debug,
|
||||
messages: &mut Vec<A::Message>,
|
||||
window: &winit::window::Window,
|
||||
graphics_info: impl FnOnce() -> compositor::Information + Copy,
|
||||
) where
|
||||
C: Compositor<Renderer = A::Renderer> + 'static,
|
||||
<A::Renderer as core::Renderer>::Theme: StyleSheet,
|
||||
{
|
||||
for message in messages.drain(..) {
|
||||
|
|
@ -676,6 +680,8 @@ pub fn update<A: Application, E: Executor>(
|
|||
|
||||
run_command(
|
||||
application,
|
||||
compositor,
|
||||
surface,
|
||||
cache,
|
||||
state,
|
||||
renderer,
|
||||
|
|
@ -686,7 +692,6 @@ pub fn update<A: Application, E: Executor>(
|
|||
proxy,
|
||||
debug,
|
||||
window,
|
||||
graphics_info,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -695,8 +700,10 @@ pub fn update<A: Application, E: Executor>(
|
|||
}
|
||||
|
||||
/// Runs the actions of a [`Command`].
|
||||
pub fn run_command<A, E>(
|
||||
pub fn run_command<A, C, E>(
|
||||
application: &A,
|
||||
compositor: &mut C,
|
||||
surface: &mut C::Surface,
|
||||
cache: &mut user_interface::Cache,
|
||||
state: &State<A>,
|
||||
renderer: &mut A::Renderer,
|
||||
|
|
@ -707,10 +714,10 @@ pub fn run_command<A, E>(
|
|||
proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
|
||||
debug: &mut Debug,
|
||||
window: &winit::window::Window,
|
||||
_graphics_info: impl FnOnce() -> compositor::Information + Copy,
|
||||
) where
|
||||
A: Application,
|
||||
E: Executor,
|
||||
C: Compositor<Renderer = A::Renderer> + 'static,
|
||||
<A::Renderer as core::Renderer>::Theme: StyleSheet,
|
||||
{
|
||||
use crate::runtime::command;
|
||||
|
|
@ -802,12 +809,28 @@ pub fn run_command<A, E>(
|
|||
.send_event(tag(window.id().into()))
|
||||
.expect("Send message to event loop");
|
||||
}
|
||||
window::Action::Screenshot(tag) => {
|
||||
let bytes = compositor.screenshot(
|
||||
renderer,
|
||||
surface,
|
||||
state.viewport(),
|
||||
state.background_color(),
|
||||
&debug.overlay(),
|
||||
);
|
||||
|
||||
proxy
|
||||
.send_event(tag(Screenshot::new(
|
||||
bytes,
|
||||
state.physical_size(),
|
||||
)))
|
||||
.expect("Send message to event loop.")
|
||||
}
|
||||
},
|
||||
command::Action::System(action) => match action {
|
||||
system::Action::QueryInformation(_tag) => {
|
||||
#[cfg(feature = "system")]
|
||||
{
|
||||
let graphics_info = _graphics_info();
|
||||
let graphics_info = compositor.fetch_information();
|
||||
let proxy = proxy.clone();
|
||||
|
||||
let _ = std::thread::spawn(move || {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue