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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue