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,
|
background_color: Color,
|
||||||
overlay: &[T],
|
overlay: &[T],
|
||||||
) -> Result<(), SurfaceError>;
|
) -> 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`].
|
/// Result of an unsuccessful call to [`Compositor::present`].
|
||||||
|
|
@ -82,7 +95,7 @@ pub enum SurfaceError {
|
||||||
OutOfMemory,
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct Information {
|
pub struct Information {
|
||||||
/// Contains the graphics adapter.
|
/// 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 {
|
enum Candidate {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
//! Build window-based GUI applications.
|
//! Build window-based GUI applications.
|
||||||
mod action;
|
mod action;
|
||||||
|
|
||||||
|
pub mod screenshot;
|
||||||
|
|
||||||
pub use action::Action;
|
pub use action::Action;
|
||||||
|
pub use screenshot::Screenshot;
|
||||||
|
|
||||||
use crate::command::{self, Command};
|
use crate::command::{self, Command};
|
||||||
use crate::core::time::Instant;
|
use crate::core::time::Instant;
|
||||||
|
|
@ -115,3 +118,10 @@ pub fn fetch_id<Message>(
|
||||||
pub fn change_icon<Message>(icon: Icon) -> Command<Message> {
|
pub fn change_icon<Message>(icon: Icon) -> Command<Message> {
|
||||||
Command::single(command::Action::Window(Action::ChangeIcon(icon)))
|
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::core::window::{Icon, Level, Mode, UserAttention};
|
||||||
use crate::futures::MaybeSend;
|
use crate::futures::MaybeSend;
|
||||||
|
use crate::window::Screenshot;
|
||||||
|
|
||||||
use std::fmt;
|
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
|
/// - **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.
|
/// said, it's usually in the same ballpark as on Windows.
|
||||||
ChangeIcon(Icon),
|
ChangeIcon(Icon),
|
||||||
|
/// Screenshot the viewport of the window.
|
||||||
|
Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Action<T> {
|
impl<T> Action<T> {
|
||||||
|
|
@ -118,6 +121,11 @@ impl<T> Action<T> {
|
||||||
Self::ChangeLevel(level) => Action::ChangeLevel(level),
|
Self::ChangeLevel(level) => Action::ChangeLevel(level),
|
||||||
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))),
|
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))),
|
||||||
Self::ChangeIcon(icon) => Action::ChangeIcon(icon),
|
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) => {
|
Self::ChangeIcon(_icon) => {
|
||||||
write!(f, "Action::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 native {}
|
||||||
mod renderer {}
|
mod renderer {}
|
||||||
mod style {}
|
mod style {}
|
||||||
|
mod runtime {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use application::Application;
|
pub use application::Application;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::core::{Color, Rectangle};
|
use crate::core::{Color, Rectangle, Size};
|
||||||
use crate::graphics::compositor::{self, Information, SurfaceError};
|
use crate::graphics::compositor::{self, Information};
|
||||||
use crate::graphics::damage;
|
use crate::graphics::damage;
|
||||||
use crate::graphics::{Error, Primitive, Viewport};
|
use crate::graphics::{Error, Primitive, Viewport};
|
||||||
use crate::{Backend, Renderer, Settings};
|
use crate::{Backend, Renderer, Settings};
|
||||||
|
|
@ -79,7 +79,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
|
||||||
viewport: &Viewport,
|
viewport: &Viewport,
|
||||||
background_color: Color,
|
background_color: Color,
|
||||||
overlay: &[T],
|
overlay: &[T],
|
||||||
) -> Result<(), SurfaceError> {
|
) -> Result<(), compositor::SurfaceError> {
|
||||||
renderer.with_primitives(|backend, primitives| {
|
renderer.with_primitives(|backend, primitives| {
|
||||||
present(
|
present(
|
||||||
backend,
|
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) {
|
pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
|
||||||
|
|
@ -156,3 +176,53 @@ pub fn present<T: AsRef<str>>(
|
||||||
|
|
||||||
Ok(())
|
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 backend;
|
||||||
mod buffer;
|
mod buffer;
|
||||||
|
mod color;
|
||||||
mod quad;
|
mod quad;
|
||||||
mod text;
|
mod text;
|
||||||
mod triangle;
|
mod triangle;
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,8 @@ impl Blit {
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
antialiasing: graphics::Antialiasing,
|
antialiasing: graphics::Antialiasing,
|
||||||
) -> Blit {
|
) -> Blit {
|
||||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
let sampler =
|
||||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
device.create_sampler(&wgpu::SamplerDescriptor::default());
|
||||||
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 constant_layout =
|
let constant_layout =
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! Connect a window with a renderer.
|
//! Connect a window with a renderer.
|
||||||
use crate::core::Color;
|
use crate::core::{Color, Size};
|
||||||
use crate::graphics;
|
use crate::graphics;
|
||||||
use crate::graphics::color;
|
use crate::graphics::color;
|
||||||
use crate::graphics::compositor;
|
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.
|
/// Sets the message that will be produced when the [`Button`] is pressed.
|
||||||
///
|
///
|
||||||
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
/// Unless `on_press` is called, the [`Button`] will be disabled.
|
||||||
pub fn on_press(mut self, msg: Message) -> Self {
|
pub fn on_press(mut self, on_press: Message) -> Self {
|
||||||
self.on_press = Some(msg);
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,8 @@ async fn run_instance<A, E, C>(
|
||||||
|
|
||||||
run_command(
|
run_command(
|
||||||
&application,
|
&application,
|
||||||
|
&mut compositor,
|
||||||
|
&mut surface,
|
||||||
&mut cache,
|
&mut cache,
|
||||||
&state,
|
&state,
|
||||||
&mut renderer,
|
&mut renderer,
|
||||||
|
|
@ -318,7 +320,6 @@ async fn run_instance<A, E, C>(
|
||||||
&mut proxy,
|
&mut proxy,
|
||||||
&mut debug,
|
&mut debug,
|
||||||
&window,
|
&window,
|
||||||
|| compositor.fetch_information(),
|
|
||||||
);
|
);
|
||||||
runtime.track(application.subscription().into_recipes());
|
runtime.track(application.subscription().into_recipes());
|
||||||
|
|
||||||
|
|
@ -382,6 +383,8 @@ async fn run_instance<A, E, C>(
|
||||||
// Update application
|
// Update application
|
||||||
update(
|
update(
|
||||||
&mut application,
|
&mut application,
|
||||||
|
&mut compositor,
|
||||||
|
&mut surface,
|
||||||
&mut cache,
|
&mut cache,
|
||||||
&state,
|
&state,
|
||||||
&mut renderer,
|
&mut renderer,
|
||||||
|
|
@ -392,7 +395,6 @@ async fn run_instance<A, E, C>(
|
||||||
&mut debug,
|
&mut debug,
|
||||||
&mut messages,
|
&mut messages,
|
||||||
&window,
|
&window,
|
||||||
|| compositor.fetch_information(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update window
|
// Update window
|
||||||
|
|
@ -645,8 +647,10 @@ where
|
||||||
|
|
||||||
/// Updates an [`Application`] by feeding it the provided messages, spawning any
|
/// Updates an [`Application`] by feeding it the provided messages, spawning any
|
||||||
/// resulting [`Command`], and tracking its [`Subscription`].
|
/// resulting [`Command`], and tracking its [`Subscription`].
|
||||||
pub fn update<A: Application, E: Executor>(
|
pub fn update<A: Application, C, E: Executor>(
|
||||||
application: &mut A,
|
application: &mut A,
|
||||||
|
compositor: &mut C,
|
||||||
|
surface: &mut C::Surface,
|
||||||
cache: &mut user_interface::Cache,
|
cache: &mut user_interface::Cache,
|
||||||
state: &State<A>,
|
state: &State<A>,
|
||||||
renderer: &mut A::Renderer,
|
renderer: &mut A::Renderer,
|
||||||
|
|
@ -657,8 +661,8 @@ pub fn update<A: Application, E: Executor>(
|
||||||
debug: &mut Debug,
|
debug: &mut Debug,
|
||||||
messages: &mut Vec<A::Message>,
|
messages: &mut Vec<A::Message>,
|
||||||
window: &winit::window::Window,
|
window: &winit::window::Window,
|
||||||
graphics_info: impl FnOnce() -> compositor::Information + Copy,
|
|
||||||
) where
|
) where
|
||||||
|
C: Compositor<Renderer = A::Renderer> + 'static,
|
||||||
<A::Renderer as core::Renderer>::Theme: StyleSheet,
|
<A::Renderer as core::Renderer>::Theme: StyleSheet,
|
||||||
{
|
{
|
||||||
for message in messages.drain(..) {
|
for message in messages.drain(..) {
|
||||||
|
|
@ -676,6 +680,8 @@ pub fn update<A: Application, E: Executor>(
|
||||||
|
|
||||||
run_command(
|
run_command(
|
||||||
application,
|
application,
|
||||||
|
compositor,
|
||||||
|
surface,
|
||||||
cache,
|
cache,
|
||||||
state,
|
state,
|
||||||
renderer,
|
renderer,
|
||||||
|
|
@ -686,7 +692,6 @@ pub fn update<A: Application, E: Executor>(
|
||||||
proxy,
|
proxy,
|
||||||
debug,
|
debug,
|
||||||
window,
|
window,
|
||||||
graphics_info,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -695,8 +700,10 @@ pub fn update<A: Application, E: Executor>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the actions of a [`Command`].
|
/// Runs the actions of a [`Command`].
|
||||||
pub fn run_command<A, E>(
|
pub fn run_command<A, C, E>(
|
||||||
application: &A,
|
application: &A,
|
||||||
|
compositor: &mut C,
|
||||||
|
surface: &mut C::Surface,
|
||||||
cache: &mut user_interface::Cache,
|
cache: &mut user_interface::Cache,
|
||||||
state: &State<A>,
|
state: &State<A>,
|
||||||
renderer: &mut A::Renderer,
|
renderer: &mut A::Renderer,
|
||||||
|
|
@ -707,10 +714,10 @@ pub fn run_command<A, E>(
|
||||||
proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
|
proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
|
||||||
debug: &mut Debug,
|
debug: &mut Debug,
|
||||||
window: &winit::window::Window,
|
window: &winit::window::Window,
|
||||||
_graphics_info: impl FnOnce() -> compositor::Information + Copy,
|
|
||||||
) where
|
) where
|
||||||
A: Application,
|
A: Application,
|
||||||
E: Executor,
|
E: Executor,
|
||||||
|
C: Compositor<Renderer = A::Renderer> + 'static,
|
||||||
<A::Renderer as core::Renderer>::Theme: StyleSheet,
|
<A::Renderer as core::Renderer>::Theme: StyleSheet,
|
||||||
{
|
{
|
||||||
use crate::runtime::command;
|
use crate::runtime::command;
|
||||||
|
|
@ -802,12 +809,28 @@ pub fn run_command<A, E>(
|
||||||
.send_event(tag(window.id().into()))
|
.send_event(tag(window.id().into()))
|
||||||
.expect("Send message to event loop");
|
.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 {
|
command::Action::System(action) => match action {
|
||||||
system::Action::QueryInformation(_tag) => {
|
system::Action::QueryInformation(_tag) => {
|
||||||
#[cfg(feature = "system")]
|
#[cfg(feature = "system")]
|
||||||
{
|
{
|
||||||
let graphics_info = _graphics_info();
|
let graphics_info = compositor.fetch_information();
|
||||||
let proxy = proxy.clone();
|
let proxy = proxy.clone();
|
||||||
|
|
||||||
let _ = std::thread::spawn(move || {
|
let _ = std::thread::spawn(move || {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue