Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5d5756121a |
|||
|
9d4e24e809 |
|||
|
94c905db38 |
3 changed files with 109 additions and 96 deletions
31
flake.nix
31
flake.nix
|
|
@ -19,22 +19,25 @@
|
||||||
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
|
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
name = cargoToml.package.name;
|
name = cargoToml.package.name;
|
||||||
|
|
||||||
dlDeps = with pkgs; [
|
dlDeps =
|
||||||
# libdbus, for rfd
|
with pkgs;
|
||||||
dbus.lib
|
[
|
||||||
|
# needed for both x11 and wayland
|
||||||
|
libxkbcommon
|
||||||
|
libGL
|
||||||
|
|
||||||
# needed for both x11 and wayland
|
libx11
|
||||||
libxkbcommon
|
libxcursor
|
||||||
libGL
|
libxi
|
||||||
vulkan-loader
|
libxcb
|
||||||
|
]
|
||||||
|
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
|
# libdbus, for rfd
|
||||||
|
dbus.lib
|
||||||
|
|
||||||
wayland
|
vulkan-loader
|
||||||
|
wayland
|
||||||
libx11
|
];
|
||||||
libxcursor
|
|
||||||
libxi
|
|
||||||
libxcb
|
|
||||||
];
|
|
||||||
|
|
||||||
commonArgs = {
|
commonArgs = {
|
||||||
# all that's needed for artifacts and checks
|
# all that's needed for artifacts and checks
|
||||||
|
|
|
||||||
141
src/main.rs
141
src/main.rs
|
|
@ -1,4 +1,9 @@
|
||||||
use std::{borrow::Cow, env, ffi::OsStr, path::Path};
|
use std::{
|
||||||
|
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,
|
||||||
|
|
@ -12,7 +17,9 @@ 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),
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +36,7 @@ struct State {
|
||||||
}
|
}
|
||||||
impl State {
|
impl State {
|
||||||
fn new() -> (Self, Task<Message>) {
|
fn new() -> (Self, Task<Message>) {
|
||||||
let mut state = State::default();
|
let mut state = Self::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) {
|
||||||
|
|
@ -43,22 +50,25 @@ 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(if let Some(allocation) = self.image_display.as_ref() {
|
main.push(self.image_display.as_ref().map_or_else(
|
||||||
widget::image::viewer(allocation.handle().clone())
|
|| {
|
||||||
.filter_method(self.image_filter)
|
widget::container(widget::text(include_str!("usage.txt")))
|
||||||
.max_scale(50.)
|
.height(Length::Fill)
|
||||||
.min_scale(1.)
|
.width(Length::Fill)
|
||||||
.width(Length::Fill)
|
.align_x(Alignment::Center)
|
||||||
.height(Length::Fill)
|
.align_y(Alignment::Center)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
},
|
||||||
widget::container(widget::text(include_str!("usage.txt")))
|
|allocation| {
|
||||||
.height(Length::Fill)
|
widget::image::viewer(allocation.handle().clone())
|
||||||
.width(Length::Fill)
|
.filter_method(self.image_filter)
|
||||||
.align_x(Alignment::Center)
|
.max_scale(50.)
|
||||||
.align_y(Alignment::Center)
|
.min_scale(1.)
|
||||||
.into()
|
.width(Length::Fill)
|
||||||
});
|
.height(Length::Fill)
|
||||||
|
.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());
|
||||||
|
|
@ -71,9 +81,26 @@ 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,
|
||||||
|
|
@ -84,39 +111,30 @@ 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") => match self.pick_and_load_image() {
|
Key::Character("o") => {
|
||||||
Ok(task) => {
|
return Task::perform(utils::pick_image(), Message::ImagePicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Character("r") => match self.image.as_ref() {
|
||||||
|
None => self.error = Some("no image to rotate".into()),
|
||||||
|
Some(image) => {
|
||||||
self.error = None;
|
self.error = None;
|
||||||
return task;
|
self.image = Some(imageops::rotate90(image));
|
||||||
|
return self.allocate_image();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Key::Character("i") => match self.image.as_mut() {
|
||||||
|
None => self.error = Some("no image to invert".into()),
|
||||||
|
Some(image) => {
|
||||||
|
self.error = None;
|
||||||
|
imageops::invert(image);
|
||||||
|
return self.allocate_image();
|
||||||
}
|
}
|
||||||
Err(e) => self.error = Some(e),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
Key::Character("r") => {
|
|
||||||
match self.image.as_ref() {
|
|
||||||
None => self.error = Some("no image to rotate".into()),
|
|
||||||
Some(image) => {
|
|
||||||
self.error = None;
|
|
||||||
self.image = Some(imageops::rotate90(image));
|
|
||||||
return self.allocate_image();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Key::Character("i") => {
|
|
||||||
match self.image.as_mut() {
|
|
||||||
None => self.error = Some("no image to invert".into()),
|
|
||||||
Some(image) => {
|
|
||||||
self.error = None;
|
|
||||||
imageops::invert(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,
|
||||||
|
|
@ -128,7 +146,12 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Character("s") => {
|
Key::Character("s") => {
|
||||||
self.save_image();
|
if self.image.is_some() {
|
||||||
|
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),
|
||||||
|
|
@ -160,8 +183,7 @@ impl State {
|
||||||
"{} {}x{}",
|
"{} {}x{}",
|
||||||
path.as_ref()
|
path.as_ref()
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(OsStr::to_string_lossy)
|
.map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy),
|
||||||
.unwrap_or(Cow::Borrowed("[no file]")),
|
|
||||||
image.width(),
|
image.width(),
|
||||||
image.height(),
|
image.height(),
|
||||||
));
|
));
|
||||||
|
|
@ -172,13 +194,6 @@ 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 {
|
||||||
|
|
@ -210,13 +225,15 @@ impl State {
|
||||||
event::listen().map(Message::Event)
|
event::listen().map(Message::Event)
|
||||||
}
|
}
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
match self.info.as_ref() {
|
self.info.as_ref().map_or_else(
|
||||||
Some(info) => format!(
|
|| "imagey".into(),
|
||||||
"imagey {info} {}",
|
|info| {
|
||||||
utils::string_from_filter_type(self.image_filter),
|
format!(
|
||||||
),
|
"imagey {info} {}",
|
||||||
None => "imagey".into(),
|
utils::string_from_filter_type(self.image_filter),
|
||||||
}
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
29
src/utils.rs
29
src/utils.rs
|
|
@ -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::FileDialog;
|
use rfd::AsyncFileDialog;
|
||||||
|
|
||||||
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
|
pub fn load_image(path: impl AsRef<Path>) -> Result<RgbaImage, String> {
|
||||||
_load_image(path).map_err(|e| e.to_string())
|
load_image_impl(path).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
fn _load_image(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
|
fn load_image_impl(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(path: impl AsRef<Path>) -> ImageResult<RgbaImage> {
|
||||||
|
|
||||||
Ok(decoded_image.into_rgba8())
|
Ok(decoded_image.into_rgba8())
|
||||||
}
|
}
|
||||||
pub fn pick_image() -> Result<PathBuf, String> {
|
pub async fn pick_image() -> Result<PathBuf, String> {
|
||||||
let Some(path) = FileDialog::new()
|
let Some(filehandle) = AsyncFileDialog::new()
|
||||||
.add_filter(
|
.add_filter(
|
||||||
"image",
|
"image",
|
||||||
&[
|
&[
|
||||||
|
|
@ -30,30 +30,23 @@ pub 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(path)
|
Ok(filehandle.path().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> {
|
pub async fn pick_save_path() -> Result<PathBuf, String> {
|
||||||
let Some(image) = image else {
|
let Some(filehandle) = AsyncFileDialog::new().save_file().await 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());
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = image.save(&path) {
|
Ok(filehandle.path().to_owned())
|
||||||
return Err(e.to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {
|
pub const 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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue