284 lines
8.8 KiB
Rust
284 lines
8.8 KiB
Rust
use iced::keyboard;
|
|
use iced::widget::{
|
|
button, center_y, column, container, image, row, text, text_input,
|
|
};
|
|
use iced::window;
|
|
use iced::window::screenshot::{self, Screenshot};
|
|
use iced::{
|
|
Center, ContentFit, Element, Fill, FillPortion, Rectangle, Subscription,
|
|
Task,
|
|
};
|
|
|
|
use ::image as img;
|
|
use ::image::ColorType;
|
|
|
|
fn main() -> iced::Result {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
iced::application("Screenshot - Iced", Example::update, Example::view)
|
|
.subscription(Example::subscription)
|
|
.run()
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Example {
|
|
screenshot: Option<(Screenshot, image::Handle)>,
|
|
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,
|
|
Screenshotted(Screenshot),
|
|
Png,
|
|
PngSaved(Result<String, PngError>),
|
|
XInputChanged(Option<u32>),
|
|
YInputChanged(Option<u32>),
|
|
WidthInputChanged(Option<u32>),
|
|
HeightInputChanged(Option<u32>),
|
|
}
|
|
|
|
impl Example {
|
|
fn update(&mut self, message: Message) -> Task<Message> {
|
|
match message {
|
|
Message::Screenshot => {
|
|
return window::get_latest()
|
|
.and_then(window::screenshot)
|
|
.map(Message::Screenshotted);
|
|
}
|
|
Message::Screenshotted(screenshot) => {
|
|
self.screenshot = Some((
|
|
screenshot.clone(),
|
|
image::Handle::from_rgba(
|
|
screenshot.size.width,
|
|
screenshot.size.height,
|
|
screenshot.bytes,
|
|
),
|
|
));
|
|
}
|
|
Message::Png => {
|
|
if let Some((screenshot, _handle)) = &self.screenshot {
|
|
self.png_saving = true;
|
|
|
|
return Task::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, _handle)) = &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.clone(),
|
|
image::Handle::from_rgba(
|
|
screenshot.size.width,
|
|
screenshot.size.height,
|
|
screenshot.bytes,
|
|
),
|
|
));
|
|
self.crop_error = None;
|
|
}
|
|
Err(crop_error) => {
|
|
self.crop_error = Some(crop_error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Task::none()
|
|
}
|
|
|
|
fn view(&self) -> Element<'_, Message> {
|
|
let image: Element<Message> =
|
|
if let Some((_screenshot, handle)) = &self.screenshot {
|
|
image(handle)
|
|
.content_fit(ContentFit::Contain)
|
|
.width(Fill)
|
|
.height(Fill)
|
|
.into()
|
|
} else {
|
|
text("Press the button to take a screenshot!").into()
|
|
};
|
|
|
|
let image = center_y(image)
|
|
.height(FillPortion(2))
|
|
.padding(10)
|
|
.style(container::rounded_box);
|
|
|
|
let crop_origin_controls = row![
|
|
text("X:").width(30),
|
|
numeric_input("0", self.x_input_value).map(Message::XInputChanged),
|
|
text("Y:").width(30),
|
|
numeric_input("0", self.y_input_value).map(Message::YInputChanged)
|
|
]
|
|
.spacing(10)
|
|
.align_y(Center);
|
|
|
|
let crop_dimension_controls = row![
|
|
text("W:").width(30),
|
|
numeric_input("0", self.width_input_value)
|
|
.map(Message::WidthInputChanged),
|
|
text("H:").width(30),
|
|
numeric_input("0", self.height_input_value)
|
|
.map(Message::HeightInputChanged)
|
|
]
|
|
.spacing(10)
|
|
.align_y(Center);
|
|
|
|
let crop_controls =
|
|
column![crop_origin_controls, crop_dimension_controls]
|
|
.push_maybe(
|
|
self.crop_error
|
|
.as_ref()
|
|
.map(|error| text!("Crop error! \n{error}")),
|
|
)
|
|
.spacing(10)
|
|
.align_x(Center);
|
|
|
|
let controls = {
|
|
let save_result =
|
|
self.saved_png_path.as_ref().map(
|
|
|png_result| match png_result {
|
|
Ok(path) => format!("Png saved as: {path:?}!"),
|
|
Err(PngError(error)) => {
|
|
format!("Png could not be saved due to:\n{}", error)
|
|
}
|
|
},
|
|
);
|
|
|
|
column![
|
|
column![
|
|
button(centered_text("Screenshot!"))
|
|
.padding([10, 20])
|
|
.width(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])
|
|
.width(Fill)
|
|
]
|
|
.spacing(10),
|
|
column![
|
|
crop_controls,
|
|
button(centered_text("Crop"))
|
|
.on_press(Message::Crop)
|
|
.style(button::danger)
|
|
.padding([10, 20])
|
|
.width(Fill),
|
|
]
|
|
.spacing(10)
|
|
.align_x(Center),
|
|
]
|
|
.push_maybe(save_result.map(text))
|
|
.spacing(40)
|
|
};
|
|
|
|
let side_content = center_y(controls);
|
|
|
|
let content = row![side_content, image]
|
|
.spacing(10)
|
|
.width(Fill)
|
|
.height(Fill)
|
|
.align_y(Center);
|
|
|
|
container(content).padding(10).into()
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<Message> {
|
|
use keyboard::key;
|
|
|
|
keyboard::on_key_press(|key, _modifiers| {
|
|
if let keyboard::Key::Named(key::Named::F5) = key {
|
|
Some(Message::Screenshot)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn save_to_png(screenshot: Screenshot) -> Result<String, PngError> {
|
|
let path = "screenshot.png".to_string();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
img::save_buffer(
|
|
&path,
|
|
&screenshot.bytes,
|
|
screenshot.size.width,
|
|
screenshot.size.height,
|
|
ColorType::Rgba8,
|
|
)
|
|
.map(|_| path)
|
|
.map_err(|error| PngError(error.to_string()))
|
|
})
|
|
.await
|
|
.expect("Blocking task to finish")
|
|
}
|
|
|
|
#[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_default(),
|
|
)
|
|
.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(Fill).align_x(Center).into()
|
|
}
|