Compare commits

..
Author SHA1 Message Date
b1157474c9
feat: simplify theme 2026-07-30 20:35:45 -07:00
9bdd556c9d
refactor: use newer helper method in State::new
this should keep it from printing "no image to allocate"
each time when started without an argument
2026-07-30 20:27:34 -07:00
7ce87e2870
feat: optional infobar
literally the title; except on the bottom
2026-07-30 20:17:21 -07:00
97ce6754dd
feat: change scale limits
zooming in a lot is sometimes useful,
but zooming out is basically useless and annoying
(when you want to put it back to covering the full window)

if you really need to zoom out, you can always resize the window
2026-07-30 19:21:12 -07:00
f279c3aa15
feat: f to switch FilterMethod
useful to scale pixelart images nicely.

This makes me consider expanding the status bar to include info;
partially because I don't use window decorations usually
(so I don't see the info in the title)
2026-07-30 19:19:23 -07:00
2 changed files with 73 additions and 38 deletions

View file

@ -1,7 +1,7 @@
use std::{env, path::Path};
use std::{borrow::Cow, env, ffi::OsStr, path::Path};
use iced::{
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, color, event,
Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event,
keyboard::{self, Key, key},
theme, widget, window,
};
@ -20,44 +20,53 @@ enum Message {
struct State {
image: Option<RgbaImage>,
image_display: Option<widget::image::Allocation>,
image_filter: widget::image::FilterMethod,
error: Option<String>,
info: Option<String>,
infobar_shown: bool,
}
impl State {
fn new() -> (Self, Task<Message>) {
let mut state = State::default();
state.error = env::args().nth(1).and_then(|path| {
utils::load_image(path)
.map(|image| state.image = Some(image))
.err()
});
let allocate_image = state.allocate_image();
if let Some(path) = env::args().nth(1) {
match state.load_image(path) {
Err(e) => state.error = Some(e),
Ok(task) => return (state, task),
}
}
(state, allocate_image)
(state, Task::none())
}
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
widget::column([
if let Some(allocation) = self.image_display.as_ref() {
widget::image::viewer(allocation.handle().clone())
.width(Length::Fill)
.height(Length::Fill)
.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() {
widget::text(error).style(widget::text::danger).into()
} else {
widget::space().into()
},
])
.into()
let mut main = Vec::new();
main.push(if let Some(allocation) = self.image_display.as_ref() {
widget::image::viewer(allocation.handle().clone())
.filter_method(self.image_filter)
.max_scale(50.)
.min_scale(1.)
.width(Length::Fill)
.height(Length::Fill)
.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() {
main.push(widget::text(error).style(widget::text::danger).into());
}
if self.infobar_shown && self.info.is_some() {
main.push(widget::text(self.title()).into());
}
widget::column(main).into()
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
@ -107,6 +116,15 @@ impl State {
}
};
}
Key::Character("f") => {
self.image_filter = match self.image_filter {
widget::image::FilterMethod::Linear => widget::image::FilterMethod::Nearest,
widget::image::FilterMethod::Nearest => widget::image::FilterMethod::Linear,
}
}
Key::Character("b") => {
self.infobar_shown = !self.infobar_shown;
}
Key::Character("s") => {
self.save_image();
@ -136,7 +154,16 @@ impl State {
}
fn load_image(&mut self, path: impl AsRef<Path>) -> Result<Task<Message>, String> {
utils::load_image(path).map(|image| {
utils::load_image(&path).map(|image| {
self.info = Some(format!(
"{} {}x{}",
path.as_ref()
.file_name()
.map(OsStr::to_string_lossy)
.unwrap_or(Cow::Borrowed("[no file]")),
image.width(),
image.height(),
));
self.image = Some(image);
self.allocate_image()
})
@ -167,8 +194,11 @@ impl State {
event::listen().map(Message::Event)
}
fn title(&self) -> String {
match self.image.as_ref() {
Some(image) => format!("imagey {}x{}", image.width(), image.height()),
match self.info.as_ref() {
Some(info) => format!(
"imagey {info} {}",
utils::string_from_filter_type(self.image_filter),
),
None => "imagey".into(),
}
}
@ -183,12 +213,9 @@ fn main() -> Result<(), iced::Error> {
.theme(Theme::custom(
"custom",
theme::Palette {
background: color!(0x080808),
background: Color::BLACK,
text: Color::WHITE,
primary: color!(0xff00ff),
success: color!(0x00ff00),
warning: color!(0x880000),
danger: color!(0xff0000),
..theme::Palette::DARK
},
))
.run()

View file

@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
use iced::widget;
use image::{DynamicImage, ImageDecoder, ImageReader, RgbaImage};
use rfd::FileDialog;
@ -79,3 +80,10 @@ pub fn save_image(image: Option<&RgbaImage>) -> Result<(), String> {
Ok(())
}
pub fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static str {
match f {
widget::image::FilterMethod::Linear => "bilinear",
widget::image::FilterMethod::Nearest => "nearest neighbor",
}
}