Compare commits

..
3 changed files with 96 additions and 109 deletions

View file

@ -19,24 +19,21 @@
cargoToml = fromTOML (builtins.readFile ./Cargo.toml); cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
name = cargoToml.package.name; name = cargoToml.package.name;
dlDeps = dlDeps = with pkgs; [
with pkgs; # libdbus, for rfd
[ dbus.lib
# needed for both x11 and wayland # needed for both x11 and wayland
libxkbcommon libxkbcommon
libGL libGL
vulkan-loader
wayland
libx11 libx11
libxcursor libxcursor
libxi libxi
libxcb libxcb
]
++ lib.optionals stdenv.hostPlatform.isLinux [
# libdbus, for rfd
dbus.lib
vulkan-loader
wayland
]; ];
commonArgs = { commonArgs = {

View file

@ -1,9 +1,4 @@
use std::{ use std::{borrow::Cow, env, ffi::OsStr, path::Path};
borrow::Cow,
env,
ffi::OsStr,
path::{Path, PathBuf},
};
use iced::{ use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event,
@ -17,9 +12,7 @@ mod utils;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
ImagePicked(Result<PathBuf, String>),
ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>), ImageDisplayReady(Result<widget::image::Allocation, widget::image::Error>),
SavePathPicked(Result<PathBuf, String>),
Event(iced::Event), Event(iced::Event),
} }
@ -36,7 +29,7 @@ struct State {
} }
impl State { impl State {
fn new() -> (Self, Task<Message>) { fn new() -> (Self, Task<Message>) {
let mut state = Self::default(); let mut state = State::default();
if let Some(path) = env::args().nth(1) { if let Some(path) = env::args().nth(1) {
match state.load_image(path) { match state.load_image(path) {
@ -50,16 +43,7 @@ impl State {
fn view(&self) -> Element<'_, Message, Theme, Renderer> { fn view(&self) -> Element<'_, Message, Theme, Renderer> {
let mut main = Vec::new(); let mut main = Vec::new();
main.push(self.image_display.as_ref().map_or_else( main.push(if let Some(allocation) = self.image_display.as_ref() {
|| {
widget::container(widget::text(include_str!("usage.txt")))
.height(Length::Fill)
.width(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
},
|allocation| {
widget::image::viewer(allocation.handle().clone()) widget::image::viewer(allocation.handle().clone())
.filter_method(self.image_filter) .filter_method(self.image_filter)
.max_scale(50.) .max_scale(50.)
@ -67,8 +51,14 @@ impl State {
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
}, } else {
)); widget::container(widget::text(include_str!("usage.txt")))
.height(Length::Fill)
.width(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
});
if let Some(error) = self.error.as_ref() { if let Some(error) = self.error.as_ref() {
main.push(widget::text(error).style(widget::text::danger).into()); main.push(widget::text(error).style(widget::text::danger).into());
@ -81,26 +71,9 @@ impl State {
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::ImagePicked(result) => match result.and_then(|path| self.load_image(path)) {
Err(e) => self.error = Some(e),
Ok(task) => {
self.error = None;
return task;
}
},
Message::ImageDisplayReady(result) => { Message::ImageDisplayReady(result) => {
self.image_display = Some(result.unwrap()); self.image_display = Some(result.unwrap());
} }
Message::SavePathPicked(result) => {
self.error = result
.and_then(|path| {
self.image.as_ref().map_or_else(
|| Err("no image to save".into()),
|image| image.save(path).map_err(|e| e.to_string()),
)
})
.err();
}
Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed { Message::Event(iced::Event::Keyboard(keyboard::Event::KeyPressed {
key, key,
@ -111,30 +84,39 @@ impl State {
Key::Named(key::Named::Tab) => { Key::Named(key::Named::Tab) => {
if modifiers.shift() { if modifiers.shift() {
return widget::operation::focus_previous(); return widget::operation::focus_previous();
} } else {
return widget::operation::focus_next(); return widget::operation::focus_next();
} }
Key::Character("o") => {
return Task::perform(utils::pick_image(), Message::ImagePicked);
} }
Key::Character("r") => match self.image.as_ref() { Key::Character("o") => match self.pick_and_load_image() {
Ok(task) => {
self.error = None;
return task;
}
Err(e) => self.error = Some(e),
},
Key::Character("r") => {
match self.image.as_ref() {
None => self.error = Some("no image to rotate".into()), None => self.error = Some("no image to rotate".into()),
Some(image) => { Some(image) => {
self.error = None; self.error = None;
self.image = Some(imageops::rotate90(image)); self.image = Some(imageops::rotate90(image));
return self.allocate_image(); return self.allocate_image();
} }
}, };
Key::Character("i") => match self.image.as_mut() { }
Key::Character("i") => {
match self.image.as_mut() {
None => self.error = Some("no image to invert".into()), None => self.error = Some("no image to invert".into()),
Some(image) => { Some(image) => {
self.error = None; self.error = None;
imageops::invert(image); imageops::invert(image);
return self.allocate_image(); return self.allocate_image();
} }
}, };
}
Key::Character("f") => { Key::Character("f") => {
self.image_filter = match self.image_filter { self.image_filter = match self.image_filter {
widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest, widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest,
@ -146,12 +128,7 @@ impl State {
} }
Key::Character("s") => { Key::Character("s") => {
if self.image.is_some() { self.save_image();
self.error = None;
return Task::perform(utils::pick_save_path(), Message::SavePathPicked);
} else {
self.error = Some("no image to save".into())
}
} }
Key::Character("q") => return window::latest().and_then(window::close), Key::Character("q") => return window::latest().and_then(window::close),
@ -183,7 +160,8 @@ impl State {
"{} {}x{}", "{} {}x{}",
path.as_ref() path.as_ref()
.file_name() .file_name()
.map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy), .map(OsStr::to_string_lossy)
.unwrap_or(Cow::Borrowed("[no file]")),
image.width(), image.width(),
image.height(), image.height(),
)); ));
@ -194,6 +172,13 @@ impl State {
self.allocate_image() self.allocate_image()
}) })
} }
fn pick_and_load_image(&mut self) -> Result<Task<Message>, String> {
utils::pick_image().and_then(|path| self.load_image(path))
}
fn save_image(&mut self) {
self.error = utils::save_image(self.image.as_ref()).err();
}
fn allocate_image(&self) -> Task<Message> { fn allocate_image(&self) -> Task<Message> {
let Some(image) = self.image.as_ref() else { let Some(image) = self.image.as_ref() else {
@ -225,15 +210,13 @@ impl State {
event::listen().map(Message::Event) event::listen().map(Message::Event)
} }
fn title(&self) -> String { fn title(&self) -> String {
self.info.as_ref().map_or_else( match self.info.as_ref() {
|| "imagey".into(), Some(info) => format!(
|info| {
format!(
"imagey {info} {}", "imagey {info} {}",
utils::string_from_filter_type(self.image_filter), utils::string_from_filter_type(self.image_filter),
) ),
}, None => "imagey".into(),
) }
} }
} }

View file

@ -2,12 +2,12 @@ use std::path::{Path, PathBuf};
use iced::widget; use iced::widget;
use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage};
use rfd::AsyncFileDialog; use rfd::FileDialog;
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> { pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
load_image_impl(path).map_err(|e| e.to_string()) _load_image(path).map_err(|e| e.to_string())
} }
fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> { fn _load_image(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
let mut decoder = ImageReader::open(path)? let mut decoder = ImageReader::open(path)?
.with_guessed_format()? .with_guessed_format()?
.into_decoder()?; .into_decoder()?;
@ -20,8 +20,8 @@ fn load_image_impl(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
Ok(decoded_image.into_rgba8()) Ok(decoded_image.into_rgba8())
} }
pub async fn pick_image() -> Result<PathBuf, String> { pub fn pick_image() -> Result<PathBuf, String> {
let Some(filehandle) = AsyncFileDialog::new() let Some(path) = FileDialog::new()
.add_filter( .add_filter(
"image", "image",
&[ &[
@ -30,23 +30,30 @@ pub async fn pick_image() -> Result<PathBuf, String> {
], ],
) )
.pick_file() .pick_file()
.await
else { else {
return Err("no path to open provided".into()); return Err("no path to open provided".into());
}; };
Ok(filehandle.path().to_owned()) Ok(path)
} }
pub async fn pick_save_path() -> Result<PathBuf, String> { pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> {
let Some(filehandle) = AsyncFileDialog::new().save_file().await else { let Some(image) = image else {
return Err("no image to save".into());
};
let Some(path) = FileDialog::new().save_file() else {
return Err("no path to save provided".into()); return Err("no path to save provided".into());
}; };
Ok(filehandle.path().to_owned()) if let Err(e) = image.save(&path) {
return Err(e.to_string());
};
Ok(())
} }
pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str { pub fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {
match f { match f {
widget::image::FilterMethod::Linear => "bilinear", widget::image::FilterMethod::Linear => "bilinear",
widget::image::FilterMethod::Nearest => "nearest neighbor", widget::image::FilterMethod::Nearest => "nearest neighbor",