Merge pull request #1845 from bungoboingo/feat/offscreen-rendering

Feat: Offscreen Rendering & Screenshots
This commit is contained in:
Héctor Ramón 2023-06-27 20:37:19 +02:00 committed by GitHub
commit f6966268bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 921 additions and 24 deletions

View 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"

View file

@ -0,0 +1,320 @@
use iced::alignment;
use iced::keyboard::KeyCode;
use iced::theme::{Button, Container};
use iced::widget::{button, column, container, image, row, text, text_input};
use iced::window::screenshot::{self, Screenshot};
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<screenshot::CropError>,
x_input_value: Option<u32>,
y_input_value: Option<u32>,
width_input_value: Option<u32>,
height_input_value: Option<u32>,
}
#[derive(Clone, Debug)]
enum Message {
Crop,
Screenshot,
ScreenshotData(Screenshot),
Png,
PngSaved(Result<String, PngError>),
XInputChanged(Option<u32>),
YInputChanged(Option<u32>),
WidthInputChanged(Option<u32>),
HeightInputChanged(Option<u32>),
}
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: None,
y_input_value: None,
width_input_value: None,
height_input_value: None,
},
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 {
self.png_saving = true;
return Command::perform(
save_to_png(screenshot.clone()),
Message::PngSaved,
);
}
}
Message::PngSaved(res) => {
self.png_saving = false;
self.saved_png_path = Some(res);
}
Message::XInputChanged(new_value) => {
self.x_input_value = new_value;
}
Message::YInputChanged(new_value) => {
self.y_input_value = new_value;
}
Message::WidthInputChanged(new_value) => {
self.width_input_value = new_value;
}
Message::HeightInputChanged(new_value) => {
self.height_input_value = new_value;
}
Message::Crop => {
if let Some(screenshot) = &self.screenshot {
let cropped = screenshot.crop(Rectangle::<u32> {
x: self.x_input_value.unwrap_or(0),
y: self.y_input_value.unwrap_or(0),
width: self.width_input_value.unwrap_or(0),
height: self.height_input_value.unwrap_or(0),
});
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
{
image(image::Handle::from_pixels(
screenshot.size.width,
screenshot.size.height,
screenshot.clone(),
))
.content_fit(ContentFit::Contain)
.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::Box)
.width(Length::FillPortion(2))
.height(Length::Fill)
.center_x()
.center_y();
let crop_origin_controls = row![
text("X:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
numeric_input("0", self.x_input_value).map(Message::XInputChanged),
text("Y:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
numeric_input("0", self.y_input_value).map(Message::YInputChanged)
]
.spacing(10)
.align_items(Alignment::Center);
let crop_dimension_controls = row![
text("W:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
numeric_input("0", self.width_input_value)
.map(Message::WidthInputChanged),
text("H:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
numeric_input("0", self.height_input_value)
.map(Message::HeightInputChanged)
]
.spacing(10)
.align_items(Alignment::Center);
let mut crop_controls =
column![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 mut controls = column![
column![
button(centered_text("Screenshot!"))
.padding([10, 20, 10, 20])
.width(Length::Fill)
.on_press(Message::Screenshot),
if !self.png_saving {
button(centered_text("Save as png")).on_press_maybe(
self.screenshot.is_some().then(|| Message::Png),
)
} else {
button(centered_text("Saving...")).style(Button::Secondary)
}
.style(Button::Secondary)
.padding([10, 20, 10, 20])
.width(Length::Fill)
]
.spacing(10),
column![
crop_controls,
button(centered_text("Crop"))
.on_press(Message::Crop)
.style(Button::Destructive)
.padding([10, 20, 10, 20])
.width(Length::Fill),
]
.spacing(10)
.align_items(Alignment::Center),
]
.spacing(40);
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(alignment::Horizontal::Center)
.width(Length::FillPortion(1))
.height(Length::Fill)
.center_y()
.center_x();
let content = row![side_content, image]
.spacing(10)
.width(Length::Fill)
.height(Length::Fill)
.align_items(Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.padding(10)
.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
}
})
}
}
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);
fn numeric_input(
placeholder: &str,
value: Option<u32>,
) -> Element<'_, Option<u32>> {
text_input(
placeholder,
&value
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(String::new),
)
.on_input(move |text| {
if text.is_empty() {
None
} else if let Ok(new_value) = text.parse() {
Some(new_value)
} else {
value
}
})
.width(40)
.into()
}
fn centered_text(content: &str) -> Element<'_, Message> {
text(content)
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.into()
}

View file

@ -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.

View file

@ -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 {

View file

@ -1,7 +1,10 @@
//! Build window-based GUI applications.
mod action;
pub mod screenshot;
pub use action::Action;
pub use screenshot::Screenshot;
use crate::command::{self, Command};
use crate::core::time::Instant;
@ -115,3 +118,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))))
}

View file

@ -1,5 +1,6 @@
use crate::core::window::{Icon, Level, Mode, UserAttention};
use crate::futures::MaybeSend;
use crate::window::Screenshot;
use std::fmt;
@ -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"),
}
}
}

View file

@ -0,0 +1,92 @@
//! Take screenshots of a window.
use crate::core::{Rectangle, Size};
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
/// 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: Arc<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: Arc::new(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: Arc::new(chopped),
size: Size::new(region.width, region.height),
})
}
}
impl AsRef<[u8]> for Screenshot {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
#[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,
}

View file

@ -278,6 +278,7 @@ pub mod widget {
mod native {}
mod renderer {}
mod style {}
mod runtime {}
}
pub use application::Application;

View file

@ -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
},
)
}

165
wgpu/src/color.rs Normal file
View file

@ -0,0 +1,165 @@
use std::borrow::Cow;
pub fn convert(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
source: wgpu::Texture,
format: wgpu::TextureFormat,
) -> wgpu::Texture {
if source.format() == format {
return source;
}
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("iced_wgpu.offscreen.sampler"),
..Default::default()
});
//sampler in 0
let sampler_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.sampler_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
}],
});
let sampler_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.offscreen.sampler.bind_group"),
layout: &sampler_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
}],
});
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.texture_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: false,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
bind_group_layouts: &[&sampler_layout, &texture_layout],
push_constant_ranges: &[],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.offscreen.blit.shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shader/blit.wgsl"
))),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..Default::default()
},
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu.offscreen.conversion.source_texture"),
size: source.size(),
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = &texture.create_view(&wgpu::TextureViewDescriptor::default());
let texture_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.offscreen.blit.texture_bind_group"),
layout: &texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&source
.create_view(&wgpu::TextureViewDescriptor::default()),
),
}],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu.offscreen.blit.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &sampler_bind_group, &[]);
pass.set_bind_group(1, &texture_bind_group, &[]);
pass.draw(0..6, 0..1);
texture
}
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Vertex {
ndc: [f32; 2],
uv: [f32; 2],
}

View file

@ -46,6 +46,7 @@ pub mod geometry;
mod backend;
mod buffer;
mod color;
mod quad;
mod text;
mod triangle;

View file

@ -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 {

View file

@ -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,154 @@ 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());
backend.present(
&compositor.device,
&compositor.queue,
&mut encoder,
Some(background_color),
&view,
primitives,
viewport,
overlay,
);
let texture = crate::color::convert(
&compositor.device,
&mut encoder,
texture,
if color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
);
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(
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,
}
}
}

View file

@ -102,8 +102,17 @@ where
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(on_press);
self
}
/// Sets the message that will be produced when the [`Button`] is pressed,
/// if `Some`.
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press;
self
}

View file

@ -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(window::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 || {