use std::{path::Path, sync::OnceLock};
use crate::img::Image;
use fontdue::{Font, FontSettings};
use pixels::{Pixels, PixelsBuilder, SurfaceTexture, wgpu::Color};
use winit::{
dpi::{LogicalSize, PhysicalSize},
window::Window,
};
static FONT: OnceLock<Font> = OnceLock::new();
const CLEAR_COLOR: Color = Color {
r: 0.01,
g: 0.01,
b: 0.01,
a: 1.00,
};
const PAN_STEP: f32 = 0.1; const ZOOM_STEP: f32 = 0.1;
const MIN_ZOOM: f32 = 0.5;
pub struct ViewOpts {
pub show_label: bool,
pub label: String,
pub resize_window: bool,
pub max_side: Option<u32>,
}
pub struct ImageView {
zoom: f32,
pan: (i32, i32),
pixels: Pixels,
pub image: Image,
scaled: Option<Image>,
label: String,
show_label: bool,
}
impl ImageView {
pub fn new(image_path: &Path, window: &Window, opts: ViewOpts) -> anyhow::Result<Self> {
let mon = window
.current_monitor()
.or_else(|| window.available_monitors().next())
.unwrap();
let scale_factor = mon.scale_factor();
let mon_size = mon.size();
let max_bounds = match opts.max_side {
Some(side) => {
let phys_side = (side as f64 * scale_factor).round() as u32;
(phys_side, phys_side)
}
None => (mon_size.width, mon_size.height),
};
let image = crate::img::read_image(image_path, max_bounds)?;
let (mut width, mut height) = image.size();
if opts.resize_window {
let size = PhysicalSize::new(width as f64, height as f64);
let size = LogicalSize::<f64>::from_physical(size, scale_factor);
window
.request_inner_size(size)
.ok_or(anyhow::Error::msg("Failed to resize window"))?;
} else {
let inner_size = window.inner_size();
width = inner_size.width.max(1);
height = inner_size.height.max(1);
}
let surface_texture = SurfaceTexture::new(width, height, &window);
let pixels = PixelsBuilder::new(width, height, surface_texture)
.clear_color(CLEAR_COLOR)
.build()?;
let mut view = Self {
zoom: 1.,
pan: (0, 0),
pixels,
image,
scaled: None,
label: opts.label,
show_label: opts.show_label,
};
if !opts.resize_window {
view.resize(width, height, true)?;
} else {
view.update();
}
Ok(view)
}
pub fn draw(&self) -> bool {
self.pixels.render().is_ok()
}
pub fn advance(&mut self) -> bool {
self.update();
self.draw()
}
fn update(&mut self) {
self.clamp_pan();
let image = self.scaled.as_mut().unwrap_or(&mut self.image);
view_buffer_window(&mut self.pixels, image, self.pan);
if self.show_label {
self.draw_label();
}
}
pub fn resize(&mut self, width: u32, height: u32, fit_image: bool) -> anyhow::Result<()> {
self.pixels.resize_surface(width, height)?;
self.pixels.resize_buffer(width, height)?;
if fit_image {
let (w, h) = self.image.size();
let zoom = (width as f32 / w as f32).min(height as f32 / h as f32);
self.set_zoom(zoom);
}
Ok(())
}
pub fn zoom_in(&mut self) {
self.set_zoom(self.zoom + ZOOM_STEP);
}
pub fn zoom_out(&mut self) {
self.set_zoom((self.zoom - ZOOM_STEP).max(MIN_ZOOM));
}
fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom;
self.scaled = Some(self.image.scaled(self.zoom));
self.update();
self.draw();
}
pub fn pan_up(&mut self) {
let (_, height) = self.image_size();
self.pan.1 -= (height as f32 * PAN_STEP).floor() as i32;
self.update();
self.draw();
}
pub fn pan_down(&mut self) {
let (_, height) = self.image_size();
self.pan.1 += (height as f32 * PAN_STEP).floor() as i32;
self.update();
self.draw();
}
pub fn pan_right(&mut self) {
let (width, _) = self.image_size();
self.pan.0 += (width as f32 * PAN_STEP).floor() as i32;
self.update();
self.draw();
}
pub fn pan_left(&mut self) {
let (width, _) = self.image_size();
self.pan.0 -= (width as f32 * PAN_STEP).floor() as i32;
self.update();
self.draw();
}
fn image_size(&self) -> (u32, u32) {
let image = self.scaled.as_ref().unwrap_or(&self.image);
image.size()
}
fn clamp_pan(&mut self) {
let (im_w, im_h) = self.image_size();
let texture = self.pixels.texture();
let (tx_w, tx_h) = (texture.width(), texture.height());
let x_limit = ((im_w as f32 / 2. - tx_w as f32 / 2.).floor() as i32).max(0);
let y_limit = ((im_h as f32 / 2. - tx_h as f32 / 2.).floor() as i32).max(0);
self.pan.0 = self.pan.0.clamp(-x_limit, x_limit);
self.pan.1 = self.pan.1.clamp(-y_limit, y_limit);
}
pub fn toggle_label(&mut self) {
self.show_label = !self.show_label;
self.update();
self.draw();
}
pub fn is_label_visible(&self) -> bool {
self.show_label
}
fn draw_label(&mut self) {
let font = FONT.get_or_init(|| {
let font_data = include_bytes!("../font.ttf") as &[u8];
Font::from_bytes(font_data, FontSettings::default()).expect("Failed to load font.ttf")
});
let texture = self.pixels.texture();
let width = texture.width() as i32;
let height = texture.height() as i32;
let frame = self.pixels.frame_mut();
let font_size = 20.0;
let padding = 15.0;
let mut total_width = 0.0;
for c in self.label.chars() {
total_width += font.metrics(c, font_size).advance_width;
}
let start_x = width as f32 - total_width - padding;
let start_y = height as f32 - font_size - padding;
let passes = [
(2.0, 2.0, 0.0), (0.0, 0.0, 255.0), ];
for (offset_x, offset_y, color_val) in passes {
let mut cursor_x = start_x + offset_x;
let cursor_y = start_y + offset_y;
for c in self.label.chars() {
let (metrics, bitmap) = font.rasterize(c, font_size);
for (i, &coverage) in bitmap.iter().enumerate() {
let lx = (i % metrics.width) as i32;
let ly = (i / metrics.width) as i32;
let px = cursor_x as i32 + metrics.xmin + lx;
let py =
cursor_y as i32 + font_size as i32 - metrics.height as i32 - metrics.ymin
+ ly;
if px >= 0 && px < width && py >= 0 && py < height && coverage > 0 {
let idx = ((py * width + px) * 4) as usize;
let alpha = coverage as f32 / 255.0;
for color_channel in 0..3 {
let bg = frame[idx + color_channel] as f32;
frame[idx + color_channel] =
((color_val * alpha) + (bg * (1.0 - alpha))) as u8;
}
let bg_alpha = frame[idx + 3] as f32;
frame[idx + 3] = ((255.0 * alpha) + (bg_alpha * (1.0 - alpha))) as u8;
}
}
cursor_x += metrics.advance_width;
}
}
}
}
fn view_buffer_window(pixels: &mut Pixels, image: &mut Image, pan: (i32, i32)) {
let texture = pixels.texture();
let (w, h) = (texture.width(), texture.height());
let size = image.size();
let data = image.next_frame();
let view = buffer_window(data, size, (w, h), pan);
pixels.frame_mut().copy_from_slice(&view);
}
fn buffer_window(
buffer: &[u8],
(img_width, img_height): (u32, u32),
(win_width, win_height): (u32, u32),
(offset_x, offset_y): (i32, i32), ) -> Vec<u8> {
const CHANNELS: usize = 4;
let padding_x = (win_width.saturating_sub(img_width) / 2) as usize;
let padding_y = (win_height.saturating_sub(img_height) / 2) as usize;
let (start_x, end_x) = centered_span(img_width as i32, win_width as i32, offset_x);
let (start_y, end_y) = centered_span(img_height as i32, win_height as i32, offset_y);
let end_y = end_y.min(img_height as usize);
let slice_width = (end_x - start_x).min(img_width as usize);
let mut result = vec![0u8; (win_width * win_height) as usize * CHANNELS];
for (i, y) in (start_y..end_y).enumerate() {
let a = flat_idx(start_x, y, img_width as usize) * CHANNELS;
let b = a + slice_width * CHANNELS;
let im_row = &buffer[a..b];
let x = padding_x;
let y = padding_y + i;
let idx = flat_idx(x, y, win_width as usize) * CHANNELS;
let end_idx = idx + slice_width * CHANNELS;
result[idx..end_idx].copy_from_slice(im_row);
}
result
}
fn flat_idx(x: usize, y: usize, w: usize) -> usize {
y * w + x
}
fn centered_span(img_dim: i32, win_dim: i32, offset: i32) -> (usize, usize) {
let img_center = img_dim / 2 + offset;
let win_center = win_dim / 2;
let start = (img_center - win_center).max(0).min(img_dim) as usize;
let end = start + win_dim as usize;
(start, end)
}