Merge pull request #1845 from bungoboingo/feat/offscreen-rendering
Feat: Offscreen Rendering & Screenshots
This commit is contained in:
commit
f6966268bb
15 changed files with 921 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"
|
||||
320
examples/screenshot/src/main.rs
Normal file
320
examples/screenshot/src/main.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
runtime/src/window/screenshot.rs
Normal file
92
runtime/src/window/screenshot.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -278,6 +278,7 @@ pub mod widget {
|
|||
mod native {}
|
||||
mod renderer {}
|
||||
mod style {}
|
||||
mod runtime {}
|
||||
}
|
||||
|
||||
pub use application::Application;
|
||||
|
|
|
|||
|
|
@ -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
165
wgpu/src/color.rs
Normal 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],
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ pub mod geometry;
|
|||
|
||||
mod backend;
|
||||
mod buffer;
|
||||
mod color;
|
||||
mod quad;
|
||||
mod text;
|
||||
mod triangle;
|
||||
|
|
|
|||
|
|
@ -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,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 || {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue