From 9399a008c4d3c648070cc12532725d7d193a8ec1 Mon Sep 17 00:00:00 2001 From: wfx Date: Mon, 19 Jan 2026 19:42:54 +0100 Subject: [PATCH] refactor: improve code quality and consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive code audit addressing naming consistency, dead code removal, and documentation improvements across the entire codebase. - Rename panel_pages.rs → pages_panel.rs for consistent naming - Unify view functions: all panels now use view() convention - Rename header_start/end() → start/end() for brevity - Update all imports and references - Remove Rotation::from_degrees() (only 4 fixed states needed) - Remove TransformState::identity() and is_identity() (Default suffices) - Remove duplicate convenience methods from RasterDocument, VectorDocument, PortableDocument (rotate_cw/ccw, flip_horizontal/vertical) - Remove unused imports and constants (ROTATION_STEP, FULL_ROTATION) - Rename constants: PDF_RENDER_SCALE → PDF_RENDER_QUALITY, PDF_THUMBNAIL_SCALE → PDF_THUMBNAIL_SIZE - Add comprehensive trait documentation explaining type erasure pattern - Document why large enum variants are acceptable - Add #[allow(dead_code)] with explanations for trait API types - Improve all constant and config comments - Collapse nested if statements using Rust 2024 let-chains - Replace single-arm match with if-let - Introduce StateChangeCallback type alias - Apply clippy auto-fixes for better code style --- src/app/document/cache.rs | 18 +- src/app/document/file.rs | 5 +- src/app/document/meta.rs | 8 +- src/app/document/mod.rs | 504 +++++++++++++----- src/app/document/portable.rs | 278 +++++----- src/app/document/raster.rs | 125 +++-- src/app/document/vector.rs | 125 +++-- src/app/message.rs | 4 +- src/app/mod.rs | 8 +- src/app/update.rs | 10 +- src/app/view/header.rs | 12 +- src/app/view/image_viewer.rs | 5 +- src/app/view/mod.rs | 4 +- .../view/{panel_pages.rs => pages_panel.rs} | 10 +- src/app/view/panels.rs | 6 +- src/config.rs | 14 +- src/constant.rs | 22 +- src/i18n.rs | 2 + src/main.rs | 2 + 19 files changed, 744 insertions(+), 418 deletions(-) rename src/app/view/{panel_pages.rs => pages_panel.rs} (89%) diff --git a/src/app/document/cache.rs b/src/app/document/cache.rs index 6660a6c..7389a9c 100644 --- a/src/app/document/cache.rs +++ b/src/app/document/cache.rs @@ -27,7 +27,7 @@ fn ensure_cache_dir() -> Option { /// Generate a cache key from file path, modification time, and page number. /// Format: sha256(path + mtime + page) -fn cache_key(file_path: &Path, page: u32) -> Option { +fn cache_key(file_path: &Path, page: usize) -> Option { let metadata = fs::metadata(file_path).ok()?; let mtime = metadata .modified() @@ -46,7 +46,7 @@ fn cache_key(file_path: &Path, page: u32) -> Option { } /// Get the full path for a cached thumbnail. -fn thumbnail_path(file_path: &Path, page: u32) -> Option { +fn thumbnail_path(file_path: &Path, page: usize) -> Option { let dir = cache_dir()?; let key = cache_key(file_path, page)?; Some(dir.join(format!("{}.{}", key, THUMBNAIL_EXT))) @@ -54,7 +54,7 @@ fn thumbnail_path(file_path: &Path, page: u32) -> Option { /// Load a thumbnail from disk cache. /// Returns None if not cached or cache is invalid. -pub fn load_thumbnail(file_path: &Path, page: u32) -> Option { +pub fn load_thumbnail(file_path: &Path, page: usize) -> Option { let cache_path = thumbnail_path(file_path, page)?; log::debug!("Cache lookup: file={}, page={}", file_path.display(), page); @@ -74,11 +74,11 @@ pub fn load_thumbnail(file_path: &Path, page: u32) -> Option { file_path.display(), page ); - Some(super::create_image_handle(&img)) + Some(super::create_image_handle_from_image(&img)) } /// Save a thumbnail to disk cache. -pub fn save_thumbnail(file_path: &Path, page: u32, image: &DynamicImage) -> Option<()> { +pub fn save_thumbnail(file_path: &Path, page: usize, image: &DynamicImage) -> Option<()> { let dir = ensure_cache_dir()?; let key = cache_key(file_path, page)?; let cache_path = dir.join(format!("{}.{}", key, THUMBNAIL_EXT)); @@ -119,7 +119,8 @@ pub fn save_thumbnail(file_path: &Path, page: u32, image: &DynamicImage) -> Opti } /// Check if a thumbnail exists in cache. -pub fn has_thumbnail(file_path: &Path, page: u32) -> bool { +#[allow(dead_code)] +pub fn has_thumbnail(file_path: &Path, page: usize) -> bool { thumbnail_path(file_path, page) .map(|p| p.exists()) .unwrap_or(false) @@ -128,10 +129,9 @@ pub fn has_thumbnail(file_path: &Path, page: u32) -> bool { /// Clear all cached thumbnails. #[allow(dead_code)] pub fn clear_cache() -> std::io::Result<()> { - if let Some(dir) = cache_dir() { - if dir.exists() { + if let Some(dir) = cache_dir() + && dir.exists() { fs::remove_dir_all(&dir)?; } - } Ok(()) } diff --git a/src/app/document/file.rs b/src/app/document/file.rs index 371c525..048846e 100644 --- a/src/app/document/file.rs +++ b/src/app/document/file.rs @@ -79,11 +79,10 @@ pub fn open_single_file(model: &mut AppModel, path: &Path) { load_document_into_model(model, path); // Refresh folder listing based on parent directory. - if model.document.is_some() { - if let Some(parent) = path.parent() { + if model.document.is_some() + && let Some(parent) = path.parent() { refresh_folder_entries(model, parent, path); } - } } /// Load a document into the model, resetting view state. diff --git a/src/app/document/meta.rs b/src/app/document/meta.rs index 0eb738c..56bf9d2 100644 --- a/src/app/document/meta.rs +++ b/src/app/document/meta.rs @@ -163,13 +163,11 @@ fn extract_exif_from_bytes(data: &[u8]) -> Option { if let Some(field) = exif.get_field(Tag::FNumber, In::PRIMARY) { meta.f_number = Some(format!("f/{}", field.display_value())); } - if let Some(field) = exif.get_field(Tag::PhotographicSensitivity, In::PRIMARY) { - if let Value::Short(ref vals) = field.value { - if let Some(&iso) = vals.first() { + if let Some(field) = exif.get_field(Tag::PhotographicSensitivity, In::PRIMARY) + && let Value::Short(ref vals) = field.value + && let Some(&iso) = vals.first() { meta.iso = Some(iso as u32); } - } - } if let Some(field) = exif.get_field(Tag::FocalLength, In::PRIMARY) { meta.focal_length = Some(field.display_value().to_string()); } diff --git a/src/app/document/mod.rs b/src/app/document/mod.rs index 0f98802..b1f91ce 100644 --- a/src/app/document/mod.rs +++ b/src/app/document/mod.rs @@ -20,42 +20,182 @@ use self::portable::PortableDocument; use self::raster::RasterDocument; use self::vector::VectorDocument; -/// Trait for documents that support multiple pages (PDF, multi-page TIFF, etc.). -pub trait MultiPage { - /// Total number of pages in the document. - fn page_count(&self) -> u32; +// ============================================================================ +// Type Definitions +// ============================================================================ - /// Current page index (0-based). - fn current_page(&self) -> u32; +/// Result type alias for document operations. +pub type DocResult = anyhow::Result; + +/// Rotation state for documents. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Rotation { + /// No rotation (0 degrees). + #[default] + None, + /// 90 degrees clockwise. + Cw90, + /// 180 degrees. + Cw180, + /// 270 degrees clockwise (90 counter-clockwise). + Cw270, +} + +impl Rotation { + /// Rotate clockwise by 90 degrees. + #[must_use] + pub fn rotate_cw(self) -> Self { + match self { + Self::None => Self::Cw90, // 0 → 90 + Self::Cw90 => Self::Cw180, // 90 → 180 + Self::Cw180 => Self::Cw270, // 180 → 270 + Self::Cw270 => Self::None, // 270 → 0 + } + } + + /// Rotate counter-clockwise by 90 degrees. + #[must_use] + pub fn rotate_ccw(self) -> Self { + match self { + Self::None => Self::Cw270, // 0 → 270 + Self::Cw270 => Self::Cw180, // 270 → 180 + Self::Cw180 => Self::Cw90, // 180 → 90 + Self::Cw90 => Self::None, // 90 → 0 + } + } + + /// Convert to degrees (0, 90, 180, 270). + #[must_use] + pub fn to_degrees(self) -> i16 { + match self { + Self::None => 0, + Self::Cw90 => 90, + Self::Cw180 => 180, + Self::Cw270 => 270, + } + } +} + +/// Flip direction for documents. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FlipDirection { + /// Flip along the vertical axis (mirror left-right). + Horizontal, + /// Flip along the horizontal axis (mirror top-bottom). + Vertical, +} + +/// Current transformation state of a document. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct TransformState { + /// Current rotation. + pub rotation: Rotation, + /// Whether flipped horizontally. + pub flip_h: bool, + /// Whether flipped vertically. + pub flip_v: bool, +} + + + +/// Output of a render operation. +/// +/// Used as return type for the `Renderable::render()` trait method. +/// Not constructed externally - only returned by trait implementations. +#[allow(dead_code)] +pub struct RenderOutput { + /// Image handle for display. + pub handle: ImageHandle, + /// Rendered width in pixels. + pub width: u32, + /// Rendered height in pixels. + pub height: u32, +} + +/// Document metadata/information. +/// +/// Used as return type for the `Renderable::info()` trait method. +/// Contains native dimensions and format description before any transformations. +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct DocumentInfo { + /// Native width in pixels (before transforms). + pub width: u32, + /// Native height in pixels (before transforms). + pub height: u32, + /// Document format description. + pub format: String, +} + +// ============================================================================ +// Traits +// ============================================================================ + +/// Trait for documents that can be rendered to an image. +/// +/// This trait is used internally through type erasure via `DocumentContent`. +/// The UI layer calls methods on `DocumentContent`, which delegates to the +/// specific document type implementations (Raster, Vector, Portable). +#[allow(dead_code)] +pub trait Renderable { + /// Render the document at the given scale factor. + fn render(&mut self, scale: f64) -> DocResult; + + /// Get document information (dimensions, format). + fn info(&self) -> DocumentInfo; +} + +/// Trait for documents that support geometric transformations. +pub trait Transformable { + /// Apply a rotation state. + fn rotate(&mut self, rotation: Rotation); + + /// Flip in the given direction. + fn flip(&mut self, direction: FlipDirection); + + /// Get the current transformation state. + fn transform_state(&self) -> TransformState; +} + +/// Trait for documents with multiple pages. +pub trait MultiPage { + /// Get total number of pages. + fn page_count(&self) -> usize; + + /// Get current page index (0-based). + fn current_page(&self) -> usize; /// Navigate to a specific page. - fn goto_page(&mut self, page: u32) -> anyhow::Result<()>; + fn go_to_page(&mut self, page: usize) -> DocResult<()>; +} - /// Check if thumbnails are ready for display. +/// Trait for multi-page documents that support thumbnail generation. +/// +/// Currently implemented only by `PortableDocument` (PDF). +/// Methods are called through `DocumentContent` type erasure. +#[allow(dead_code)] +pub trait MultiPageThumbnails: MultiPage { + /// Get cached thumbnail for a page, if available. + fn get_thumbnail(&self, page: usize) -> Option; + + /// Check if all thumbnails are ready. fn thumbnails_ready(&self) -> bool; - /// Generate thumbnails (uses disk cache when available). - fn generate_thumbnails(&mut self); + /// Get count of thumbnails currently loaded. + fn thumbnails_loaded(&self) -> usize; - /// Get cached thumbnail handle for a specific page. - fn get_thumbnail(&self, page: u32) -> Option; + /// Generate thumbnail for a single page. Returns next page to generate. + fn generate_thumbnail_page(&mut self, page: usize) -> Option; + + /// Generate all thumbnails (blocking). + fn generate_all_thumbnails(&mut self); } -/// Re-export the image handle type for use by submodules. -pub type ImageHandle = cosmic::iced::widget::image::Handle; +// ============================================================================ +// Document Types +// ============================================================================ -/// Create an iced image handle from a DynamicImage. -/// -/// This is the central function for converting rendered images to display handles. -/// Used by raster, vector, and portable document types. -pub fn create_image_handle(img: &image::DynamicImage) -> ImageHandle { - let (w, h) = img.dimensions(); - let rgba = img.to_rgba8(); - let pixels = rgba.into_raw(); - ImageHandle::from_rgba(w, h, pixels) -} - -/// High-level classification of documents. +/// Supported document kinds (for format detection). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DocumentKind { Raster, @@ -63,7 +203,72 @@ pub enum DocumentKind { Portable, } -/// Unified document type used by the application. +impl DocumentKind { + /// Detect document kind from file path. + #[must_use] + pub fn from_path(path: &Path) -> Option { + let ext = path.extension()?.to_str()?.to_lowercase(); + + // SVG + if ext == "svg" || ext == "svgz" { + return Some(Self::Vector); + } + + // PDF + if ext == "pdf" { + return Some(Self::Portable); + } + + // Raster: Check via cosmic/image-rs + if CosmicImageFormat::from_path(path).is_ok() { + return Some(Self::Raster); + } + + None + } +} + +impl fmt::Display for DocumentKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Raster => write!(f, "Raster"), + Self::Vector => write!(f, "Vector"), + Self::Portable => write!(f, "Portable"), + } + } +} + +// ============================================================================ +// Image Handle Helper +// ============================================================================ + +/// Handle for rendered images (compatible with cosmic/iced). +pub type ImageHandle = cosmic::widget::image::Handle; + +/// Create an image handle from RGBA pixel data. +#[must_use] +pub fn create_image_handle(pixels: Vec, width: u32, height: u32) -> ImageHandle { + cosmic::widget::image::Handle::from_rgba(width, height, pixels) +} + +/// Create an image handle from a DynamicImage. +#[must_use] +pub fn create_image_handle_from_image(img: &image::DynamicImage) -> ImageHandle { + let (width, height) = img.dimensions(); + let pixels = img.to_rgba8().into_raw(); + create_image_handle(pixels, width, height) +} + +// ============================================================================ +// Document Content Enum +// ============================================================================ + +/// Type-erased document content. +/// +/// The application only holds one document at a time, so the size difference +/// between variants (536 bytes for Vector vs 184 bytes for Portable) is acceptable. +/// Boxing would add unnecessary indirection without measurable performance benefit. +#[allow(clippy::large_enum_variant)] pub enum DocumentContent { Raster(RasterDocument), Vector(VectorDocument), @@ -73,199 +278,212 @@ pub enum DocumentContent { impl fmt::Debug for DocumentContent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - DocumentContent::Raster(_) => f.write_str("DocumentContent::Raster(..)"), - DocumentContent::Vector(_) => f.write_str("DocumentContent::Vector(..)"), - DocumentContent::Portable(_) => f.write_str("DocumentContent::Portable(..)"), + Self::Raster(_) => write!(f, "DocumentContent::Raster(...)"), + Self::Vector(_) => write!(f, "DocumentContent::Vector(...)"), + Self::Portable(_) => write!(f, "DocumentContent::Portable(...)"), } } } -impl DocumentKind { - /// Derive document kind from file extension. - /// - /// - `pdf` => Portable - /// - `svg` => Vector - /// - supported image extensions (via libcosmic/image_rs ImageFormat) - /// => Raster - /// - /// Returns `None` if the extension is not recognized as any supported kind. - pub fn from_path(path: &Path) -> Option { - let ext_os = path.extension()?; - let ext_str = ext_os.to_str()?; - let ext_lower = ext_str.to_ascii_lowercase(); +// ============================================================================ +// Trait Implementations for DocumentContent +// ============================================================================ - match ext_lower.as_str() { - "pdf" => return Some(DocumentKind::Portable), - "svg" => return Some(DocumentKind::Vector), - _ => {} +impl Renderable for DocumentContent { + fn render(&mut self, scale: f64) -> DocResult { + match self { + Self::Raster(doc) => doc.render(scale), + Self::Vector(doc) => doc.render(scale), + Self::Portable(doc) => doc.render(scale), } + } - // Ask libcosmic/image_rs if this extension corresponds to a known image - // format. If yes, we treat it as a raster document. - if CosmicImageFormat::from_extension(ext_os).is_some() { - return Some(DocumentKind::Raster); + fn info(&self) -> DocumentInfo { + match self { + Self::Raster(doc) => doc.info(), + Self::Vector(doc) => doc.info(), + Self::Portable(doc) => doc.info(), } - - None } } +impl Transformable for DocumentContent { + fn rotate(&mut self, rotation: Rotation) { + match self { + Self::Raster(doc) => doc.rotate(rotation), + Self::Vector(doc) => doc.rotate(rotation), + Self::Portable(doc) => doc.rotate(rotation), + } + } + + fn flip(&mut self, direction: FlipDirection) { + match self { + Self::Raster(doc) => doc.flip(direction), + Self::Vector(doc) => doc.flip(direction), + Self::Portable(doc) => doc.flip(direction), + } + } + + fn transform_state(&self) -> TransformState { + match self { + Self::Raster(doc) => doc.transform_state(), + Self::Vector(doc) => doc.transform_state(), + Self::Portable(doc) => doc.transform_state(), + } + } +} + +// ============================================================================ +// Convenience Methods for DocumentContent +// ============================================================================ + impl DocumentContent { - /// Returns a cloneable image handle for rendering. - /// - /// This is intentionally linear: every concrete document type - /// owns some kind of `ImageHandle`, and the canvas can - /// just call `doc.handle()` without additional branching. - pub fn handle(&self) -> ImageHandle { - match self { - DocumentContent::Raster(doc) => doc.handle.clone(), - DocumentContent::Vector(doc) => doc.handle.clone(), - DocumentContent::Portable(doc) => doc.handle.clone(), - } - } - - /// Returns the native dimensions (width, height) of the document in pixels. - /// - /// For raster images this is the actual pixel size. - /// For vector/portable documents this is the rasterized size at default DPI. - pub fn dimensions(&self) -> (u32, u32) { - match self { - DocumentContent::Raster(doc) => doc.dimensions(), - DocumentContent::Vector(doc) => doc.dimensions(), - DocumentContent::Portable(doc) => doc.dimensions(), - } - } - /// Extract metadata from the document. - /// Requires the file path for file size and EXIF extraction. - pub fn extract_meta(&self, path: &Path) -> meta::DocumentMeta { - match self { - DocumentContent::Raster(doc) => doc.extract_meta(path), - DocumentContent::Vector(doc) => doc.extract_meta(path), - DocumentContent::Portable(doc) => doc.extract_meta(path), - } - } - /// Rotate document 90 degrees clockwise. pub fn rotate_cw(&mut self) { - match self { - DocumentContent::Raster(doc) => doc.rotate_cw(), - DocumentContent::Vector(doc) => doc.rotate_cw(), - DocumentContent::Portable(doc) => doc.rotate_cw(), - } + let new_rotation = self.transform_state().rotation.rotate_cw(); + self.rotate(new_rotation); } /// Rotate document 90 degrees counter-clockwise. pub fn rotate_ccw(&mut self) { - match self { - DocumentContent::Raster(doc) => doc.rotate_ccw(), - DocumentContent::Vector(doc) => doc.rotate_ccw(), - DocumentContent::Portable(doc) => doc.rotate_ccw(), - } + let new_rotation = self.transform_state().rotation.rotate_ccw(); + self.rotate(new_rotation); } /// Flip document horizontally. pub fn flip_horizontal(&mut self) { - match self { - DocumentContent::Raster(doc) => doc.flip_horizontal(), - DocumentContent::Vector(doc) => doc.flip_horizontal(), - DocumentContent::Portable(doc) => doc.flip_horizontal(), - } + self.flip(FlipDirection::Horizontal); } /// Flip document vertically. pub fn flip_vertical(&mut self) { + self.flip(FlipDirection::Vertical); + } + + /// Get document kind. + /// + /// Reserved for future use (format-specific optimizations, statistics). + #[allow(dead_code)] + #[must_use] + pub fn kind(&self) -> DocumentKind { match self { - DocumentContent::Raster(doc) => doc.flip_vertical(), - DocumentContent::Vector(doc) => doc.flip_vertical(), - DocumentContent::Portable(doc) => doc.flip_vertical(), + Self::Raster(_) => DocumentKind::Raster, + Self::Vector(_) => DocumentKind::Vector, + Self::Portable(_) => DocumentKind::Portable, } } /// Check if this document supports multiple pages. + #[must_use] pub fn is_multi_page(&self) -> bool { - match self { - DocumentContent::Portable(doc) => doc.page_count() > 1, - // TODO: RasterDocument for multi-page TIFF - _ => false, - } + self.page_count().is_some_and(|n| n > 1) } - /// Get page count if this is a multi-page document. - pub fn page_count(&self) -> Option { + /// Get page count if applicable. + #[must_use] + pub fn page_count(&self) -> Option { match self { - DocumentContent::Portable(doc) => Some(doc.page_count()), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => Some(doc.page_count()), _ => None, } } - /// Get current page index if this is a multi-page document. - pub fn current_page(&self) -> Option { + /// Get current page index if applicable. + #[must_use] + pub fn current_page(&self) -> Option { match self { - DocumentContent::Portable(doc) => Some(doc.current_page()), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => Some(doc.current_page()), _ => None, } } - /// Navigate to a specific page if this is a multi-page document. - pub fn goto_page(&mut self, page: u32) -> anyhow::Result<()> { + /// Navigate to a specific page. + pub fn go_to_page(&mut self, page: usize) -> DocResult<()> { match self { - DocumentContent::Portable(doc) => doc.goto_page(page), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => doc.go_to_page(page), _ => Err(anyhow::anyhow!("Document does not support multiple pages")), } } - /// Get cached thumbnail handle for a specific page. - pub fn get_thumbnail(&self, page: u32) -> Option { + /// Get cached thumbnail for a page. + #[must_use] + pub fn get_thumbnail(&self, page: usize) -> Option { match self { - DocumentContent::Portable(doc) => doc.get_thumbnail(page), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => doc.get_thumbnail(page), _ => None, } } - /// Check if thumbnails are ready for display. + /// Check if thumbnails are ready. + #[must_use] pub fn thumbnails_ready(&self) -> bool { match self { - DocumentContent::Portable(doc) => doc.thumbnails_ready(), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => doc.thumbnails_ready(), _ => false, } } - /// Get number of thumbnails currently loaded. - pub fn thumbnails_loaded(&self) -> u32 { + /// Get count of loaded thumbnails. + #[must_use] + pub fn thumbnails_loaded(&self) -> usize { match self { - DocumentContent::Portable(doc) => doc.thumbnails_loaded(), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => doc.thumbnails_loaded(), _ => 0, } } - /// Generate a single thumbnail page. Returns next page to generate, or None if done. - pub fn generate_thumbnail_page(&mut self, page: u32) -> Option { + /// Generate thumbnail for a single page. + pub fn generate_thumbnail_page(&mut self, page: usize) -> Option { match self { - DocumentContent::Portable(doc) => doc.generate_thumbnail_page(page), - // TODO: RasterDocument for multi-page TIFF + Self::Portable(doc) => doc.generate_thumbnail_page(page), _ => None, } } - /// Generate all thumbnails at once (blocking). + /// Generate all thumbnails (blocking). + /// + /// Convenience wrapper for `MultiPageThumbnails::generate_all_thumbnails()`. + /// Currently unused - thumbnails are generated incrementally via `generate_thumbnail_page()`. + #[allow(dead_code)] pub fn generate_thumbnails(&mut self) { + if let Self::Portable(doc) = self { doc.generate_all_thumbnails() } + } + + /// Get current image handle for display. + #[must_use] + pub fn handle(&self) -> ImageHandle { match self { - DocumentContent::Portable(doc) => doc.generate_thumbnails(), - // TODO: RasterDocument for multi-page TIFF - _ => {} + Self::Raster(doc) => doc.handle.clone(), + Self::Vector(doc) => doc.handle.clone(), + Self::Portable(doc) => doc.handle.clone(), + } + } + + /// Get current document dimensions. + #[must_use] + pub fn dimensions(&self) -> (u32, u32) { + match self { + Self::Raster(doc) => doc.dimensions(), + Self::Vector(doc) => doc.dimensions(), + Self::Portable(doc) => doc.dimensions(), + } + } + + /// Extract document metadata. + pub fn extract_meta(&self, path: &Path) -> meta::DocumentMeta { + match self { + Self::Raster(doc) => doc.extract_meta(path), + Self::Vector(doc) => doc.extract_meta(path), + Self::Portable(doc) => doc.extract_meta(path), } } } +// ============================================================================ +// Public Utilities +// ============================================================================ + /// Set an image file as desktop wallpaper. -/// -/// Delegates to `utils::set_as_wallpaper` which tries multiple methods. pub fn set_as_wallpaper(path: &Path) { utils::set_as_wallpaper(path); } diff --git a/src/app/document/portable.rs b/src/app/document/portable.rs index 0f690d1..fc4ef2b 100644 --- a/src/app/document/portable.rs +++ b/src/app/document/portable.rs @@ -10,8 +10,11 @@ use cairo::{Context, Format, ImageSurface}; use image::{imageops, DynamicImage, ImageReader}; use poppler::PopplerDocument; -use super::{cache, ImageHandle}; -use crate::constant::{FULL_ROTATION, PDF_RENDER_SCALE, PDF_THUMBNAIL_SCALE, ROTATION_STEP}; +use super::{ + cache, DocResult, DocumentInfo, FlipDirection, ImageHandle, MultiPage, MultiPageThumbnails, + Renderable, RenderOutput, Rotation, TransformState, Transformable, +}; +use crate::constant::{PDF_RENDER_QUALITY, PDF_THUMBNAIL_SIZE}; /// Represents a portable document (PDF). pub struct PortableDocument { @@ -20,11 +23,11 @@ pub struct PortableDocument { /// Path to the source file (for caching). source_path: PathBuf, /// Total number of pages. - page_count: u32, + num_pages: usize, /// Current page index (0-based). - current_page: u32, - /// Rotation in degrees (0, 90, 180, 270). - pub rotation: i16, + page_index: usize, + /// Current transformation state. + transform: TransformState, /// Current rendered page as image. pub rendered: DynamicImage, /// Image handle for display. @@ -39,58 +42,47 @@ impl PortableDocument { let document = PopplerDocument::new_from_file(path, None) .map_err(|e| anyhow::anyhow!("Failed to parse PDF: {}", e))?; - let page_count = document.get_n_pages() as u32; - if page_count == 0 { + let num_pages = document.get_n_pages(); + if num_pages == 0 { return Err(anyhow::anyhow!("PDF has no pages")); } - let rendered = Self::render_page(&document, 0, 0)?; - let handle = super::create_image_handle(&rendered); + let rendered = Self::render_page(&document, 0, Rotation::None)?; + let handle = super::create_image_handle_from_image(&rendered); Ok(Self { document, source_path: path.to_path_buf(), - page_count, - current_page: 0, - rotation: 0, + num_pages, + page_index: 0, + transform: TransformState::default(), rendered, handle, thumbnail_cache: None, }) } - /// Check if all thumbnails are ready. - pub fn thumbnails_ready(&self) -> bool { - self.thumbnail_cache - .as_ref() - .map(|c| c.len() as u32 >= self.page_count) - .unwrap_or(false) - } - /// Get the number of thumbnails currently loaded. - pub fn thumbnails_loaded(&self) -> u32 { - self.thumbnail_cache - .as_ref() - .map(|c| c.len() as u32) - .unwrap_or(0) + pub fn thumbnails_loaded(&self) -> usize { + self.thumbnail_cache.as_ref().map_or(0, Vec::len) } /// Initialize thumbnail cache (empty, ready for incremental loading). - pub fn init_thumbnail_cache(&mut self) { + fn init_thumbnail_cache(&mut self) { if self.thumbnail_cache.is_none() { - self.thumbnail_cache = Some(Vec::with_capacity(self.page_count as usize)); + self.thumbnail_cache = Some(Vec::with_capacity(self.num_pages)); } } /// Generate a single thumbnail page. Returns the next page to generate, or None if done. - pub fn generate_thumbnail_page(&mut self, page: u32) -> Option { + pub fn generate_thumbnail_page(&mut self, page: usize) -> Option { // Initialize cache if needed. self.init_thumbnail_cache(); // Check if we should generate this page. let should_generate = { let cache = self.thumbnail_cache.as_ref()?; - page as usize >= cache.len() && page < self.page_count + page >= cache.len() && page < self.num_pages }; if should_generate { @@ -102,34 +94,24 @@ impl PortableDocument { // Return next page if not done. let next = page + 1; - if next < self.page_count { + if next < self.num_pages { Some(next) } else { None } } - /// Generate all thumbnails at once (legacy, blocking). - pub fn generate_thumbnails(&mut self) { - if self.thumbnails_ready() { - return; - } - self.init_thumbnail_cache(); - for page in 0..self.page_count { - self.generate_thumbnail_page(page); - } - } - /// Load thumbnail from cache or generate and cache it. - fn load_or_generate_thumbnail(&self, page: u32) -> ImageHandle { + fn load_or_generate_thumbnail(&self, page: usize) -> ImageHandle { if let Some(handle) = cache::load_thumbnail(&self.source_path, page) { return handle; } - match Self::render_page_at_scale(&self.document, page, 0, PDF_THUMBNAIL_SCALE) { + match Self::render_page_at_scale(&self.document, page, Rotation::None, PDF_THUMBNAIL_SIZE) + { Ok(img) => { let _ = cache::save_thumbnail(&self.source_path, page, &img); - super::create_image_handle(&img) + super::create_image_handle_from_image(&img) } Err(e) => { log::warn!("Failed to generate thumbnail for page {}: {}", page, e); @@ -141,32 +123,35 @@ impl PortableDocument { /// Render a specific page from the document to an image. fn render_page( document: &PopplerDocument, - page_index: u32, - rotation: i16, + page_index: usize, + rotation: Rotation, ) -> anyhow::Result { - Self::render_page_at_scale(document, page_index, rotation, PDF_RENDER_SCALE) + Self::render_page_at_scale(document, page_index, rotation, PDF_RENDER_QUALITY) } /// Render a specific page at a given scale. fn render_page_at_scale( document: &PopplerDocument, - page_index: u32, - rotation: i16, + page_index: usize, + rotation: Rotation, scale: f64, ) -> anyhow::Result { let page = document - .get_page(page_index as usize) + .get_page(page_index) .ok_or_else(|| anyhow::anyhow!("Failed to get page {}", page_index))?; let (page_width, page_height) = page.get_size(); + let rotation_degrees = rotation.to_degrees(); - let (width, height) = if rotation == 90 || rotation == 270 { + let (width, height) = if rotation_degrees == 90 || rotation_degrees == 270 { (page_height, page_width) } else { (page_width, page_height) }; + #[allow(clippy::cast_possible_truncation)] let scaled_width = (width * scale) as i32; + #[allow(clippy::cast_possible_truncation)] let scaled_height = (height * scale) as i32; let surface = ImageSurface::create(Format::ARgb32, scaled_width, scaled_height) @@ -181,11 +166,11 @@ impl PortableDocument { context.scale(scale, scale); - if rotation != 0 { + if rotation != Rotation::None { let center_x = width / 2.0; let center_y = height / 2.0; context.translate(center_x, center_y); - context.rotate(f64::from(rotation) * std::f64::consts::PI / 180.0); + context.rotate(f64::from(rotation_degrees) * std::f64::consts::PI / 180.0); context.translate(-page_width / 2.0, -page_height / 2.0); } @@ -208,10 +193,17 @@ impl PortableDocument { Ok(image) } - /// Re-render the current page. + /// Re-render the current page with current transform. fn rerender(&mut self) { - match Self::render_page(&self.document, self.current_page, self.rotation) { - Ok(rendered) => { + match Self::render_page(&self.document, self.page_index, self.transform.rotation) { + Ok(mut rendered) => { + // Apply flip transformations to the rendered result + if self.transform.flip_h { + rendered = DynamicImage::ImageRgba8(imageops::flip_horizontal(&rendered)); + } + if self.transform.flip_v { + rendered = DynamicImage::ImageRgba8(imageops::flip_vertical(&rendered)); + } self.rendered = rendered; self.refresh_handle(); } @@ -222,8 +214,8 @@ impl PortableDocument { } /// Rebuild the handle after mutating `rendered`. - pub fn refresh_handle(&mut self) { - self.handle = super::create_image_handle(&self.rendered); + fn refresh_handle(&mut self) { + self.handle = super::create_image_handle_from_image(&self.rendered); } /// Returns the dimensions of the currently rendered page. @@ -231,24 +223,11 @@ impl PortableDocument { (self.rendered.width(), self.rendered.height()) } - /// Navigate to a specific page. - pub fn goto_page(&mut self, page: u32) -> anyhow::Result<()> { - if page >= self.page_count { - return Err(anyhow::anyhow!( - "Page {} out of range (0-{})", - page, - self.page_count - 1 - )); - } - self.current_page = page; - self.rerender(); - Ok(()) - } - /// Navigate to the next page. + #[allow(dead_code)] pub fn next_page(&mut self) -> bool { - if self.current_page + 1 < self.page_count { - self.current_page += 1; + if self.page_index + 1 < self.num_pages { + self.page_index += 1; self.rerender(); true } else { @@ -257,9 +236,10 @@ impl PortableDocument { } /// Navigate to the previous page. + #[allow(dead_code)] pub fn prev_page(&mut self) -> bool { - if self.current_page > 0 { - self.current_page -= 1; + if self.page_index > 0 { + self.page_index -= 1; self.rerender(); true } else { @@ -267,51 +247,109 @@ impl PortableDocument { } } - /// Rotate 90 degrees clockwise. - pub fn rotate_cw(&mut self) { - self.rotation = (self.rotation + ROTATION_STEP).rem_euclid(FULL_ROTATION); - self.rerender(); - } - - /// Rotate 90 degrees counter-clockwise. - pub fn rotate_ccw(&mut self) { - self.rotation = (self.rotation - ROTATION_STEP).rem_euclid(FULL_ROTATION); - self.rerender(); - } - - /// Flip horizontally. - pub fn flip_horizontal(&mut self) { - self.rendered = DynamicImage::ImageRgba8(imageops::flip_horizontal(&self.rendered)); - self.refresh_handle(); - } - - /// Flip vertically. - pub fn flip_vertical(&mut self) { - self.rendered = DynamicImage::ImageRgba8(imageops::flip_vertical(&self.rendered)); - self.refresh_handle(); - } - /// Extract metadata for this portable document. pub fn extract_meta(&self, path: &Path) -> super::meta::DocumentMeta { let (width, height) = self.dimensions(); - super::meta::build_portable_meta(path, width, height, self.page_count) - } - - /// Get total page count. - pub fn page_count(&self) -> u32 { - self.page_count - } - - /// Get current page index (0-based). - pub fn current_page(&self) -> u32 { - self.current_page - } - - /// Get cached thumbnail handle for a specific page. - /// Returns None if thumbnails not yet generated. - pub fn get_thumbnail(&self, page: u32) -> Option { - self.thumbnail_cache - .as_ref() - .and_then(|cache| cache.get(page as usize).cloned()) + #[allow(clippy::cast_possible_truncation)] + super::meta::build_portable_meta(path, width, height, self.num_pages as u32) + } +} + +// ============================================================================ +// Trait Implementations +// ============================================================================ + +impl Renderable for PortableDocument { + fn render(&mut self, _scale: f64) -> DocResult { + // PDF rendering quality is fixed for now (PDF_RENDER_QUALITY) + let (width, height) = self.dimensions(); + Ok(RenderOutput { + handle: self.handle.clone(), + width, + height, + }) + } + + fn info(&self) -> DocumentInfo { + let (width, height) = self.dimensions(); + DocumentInfo { + width, + height, + format: "PDF".to_string(), + } + } +} + +impl Transformable for PortableDocument { + fn rotate(&mut self, rotation: Rotation) { + self.transform.rotation = rotation; + self.rerender(); + } + + fn flip(&mut self, direction: FlipDirection) { + match direction { + FlipDirection::Horizontal => self.transform.flip_h = !self.transform.flip_h, + FlipDirection::Vertical => self.transform.flip_v = !self.transform.flip_v, + } + self.rerender(); + } + + fn transform_state(&self) -> TransformState { + self.transform + } +} + +impl MultiPage for PortableDocument { + fn page_count(&self) -> usize { + self.num_pages + } + + fn current_page(&self) -> usize { + self.page_index + } + + fn go_to_page(&mut self, page: usize) -> DocResult<()> { + if page >= self.num_pages { + return Err(anyhow::anyhow!( + "Page {} out of range (0-{})", + page, + self.num_pages - 1 + )); + } + self.page_index = page; + self.rerender(); + Ok(()) + } +} + +impl MultiPageThumbnails for PortableDocument { + fn thumbnails_ready(&self) -> bool { + self.thumbnail_cache + .as_ref() + .is_some_and(|c| c.len() >= self.num_pages) + } + + fn thumbnails_loaded(&self) -> usize { + PortableDocument::thumbnails_loaded(self) + } + + fn generate_thumbnail_page(&mut self, page: usize) -> Option { + PortableDocument::generate_thumbnail_page(self, page) + } + + fn generate_all_thumbnails(&mut self) { + if self.thumbnails_ready() { + return; + } + self.init_thumbnail_cache(); + for page in 0..self.num_pages { + self.generate_thumbnail_page(page); + } + } + + fn get_thumbnail(&self, page: usize) -> Option { + self.thumbnail_cache + .as_ref() + .and_then(|cache| cache.get(page).cloned()) } } diff --git a/src/app/document/raster.rs b/src/app/document/raster.rs index 32b7016..efbd278 100644 --- a/src/app/document/raster.rs +++ b/src/app/document/raster.rs @@ -1,16 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/document/raster.rs +// +// Raster image document support (PNG, JPEG, WebP, etc.). use std::path::Path; use image::{imageops, DynamicImage, GenericImageView, ImageReader}; -use super::ImageHandle; +use super::{ + DocResult, DocumentInfo, FlipDirection, ImageHandle, Renderable, RenderOutput, Rotation, + TransformState, Transformable, +}; /// Represents a raster image document (PNG, JPEG, WebP, ...). pub struct RasterDocument { /// The decoded image document. document: DynamicImage, + /// Native width (original, before transforms). + native_width: u32, + /// Native height (original, before transforms). + native_height: u32, + /// Current transformation state. + transform: TransformState, /// Cached handle for rendering. pub handle: ImageHandle, } @@ -19,53 +30,103 @@ impl RasterDocument { /// Load a raster document from disk. pub fn open(path: &Path) -> image::ImageResult { let document = ImageReader::open(path)?.decode()?; - let handle = super::create_image_handle(&document); + let (native_width, native_height) = document.dimensions(); + let handle = super::create_image_handle_from_image(&document); - Ok(Self { document, handle }) + Ok(Self { + document, + native_width, + native_height, + transform: TransformState::default(), + handle, + }) } /// Rebuild the handle after mutating `document`. - pub fn refresh_handle(&mut self) { - self.handle = super::create_image_handle(&self.document); + fn refresh_handle(&mut self) { + self.handle = super::create_image_handle_from_image(&self.document); } - /// Returns the native pixel dimensions (width, height). + /// Returns the current pixel dimensions (width, height) after transforms. pub fn dimensions(&self) -> (u32, u32) { self.document.dimensions() } /// Save the current document to disk. + #[allow(dead_code)] pub fn save(&self, path: &Path) -> image::ImageResult<()> { self.document.save(path) } /// Extract metadata for this raster document. pub fn extract_meta(&self, path: &Path) -> super::meta::DocumentMeta { - let (width, height) = self.dimensions(); - super::meta::build_raster_meta(path, &self.document, width, height) - } - - /// Rotate 90 degrees clockwise. - pub fn rotate_cw(&mut self) { - self.document = DynamicImage::ImageRgba8(imageops::rotate90(&self.document)); - self.refresh_handle(); - } - - /// Rotate 90 degrees counter-clockwise. - pub fn rotate_ccw(&mut self) { - self.document = DynamicImage::ImageRgba8(imageops::rotate270(&self.document)); - self.refresh_handle(); - } - - /// Flip horizontally. - pub fn flip_horizontal(&mut self) { - self.document = DynamicImage::ImageRgba8(imageops::flip_horizontal(&self.document)); - self.refresh_handle(); - } - - /// Flip vertically. - pub fn flip_vertical(&mut self) { - self.document = DynamicImage::ImageRgba8(imageops::flip_vertical(&self.document)); - self.refresh_handle(); + super::meta::build_raster_meta(path, &self.document, self.native_width, self.native_height) + } +} + +// ============================================================================ +// Trait Implementations +// ============================================================================ + +impl Renderable for RasterDocument { + fn render(&mut self, _scale: f64) -> DocResult { + // Raster images don't re-render at different scales (lossy), + // we just return the current handle. + let (width, height) = self.dimensions(); + Ok(RenderOutput { + handle: self.handle.clone(), + width, + height, + }) + } + + fn info(&self) -> DocumentInfo { + DocumentInfo { + width: self.native_width, + height: self.native_height, + format: "Raster".to_string(), + } + } +} + +impl Transformable for RasterDocument { + fn rotate(&mut self, rotation: Rotation) { + let current_deg = self.transform.rotation.to_degrees(); + let new_deg = rotation.to_degrees(); + let diff_deg = (new_deg - current_deg + 360) % 360; + + match diff_deg { + 0 => {} + 90 => { + self.document = DynamicImage::ImageRgba8(imageops::rotate90(&self.document)); + } + 180 => { + self.document = DynamicImage::ImageRgba8(imageops::rotate180(&self.document)); + } + 270 => { + self.document = DynamicImage::ImageRgba8(imageops::rotate270(&self.document)); + } + _ => unreachable!("Invalid rotation diff: {}", diff_deg), + } + self.transform.rotation = rotation; + self.refresh_handle(); + } + + fn flip(&mut self, direction: FlipDirection) { + match direction { + FlipDirection::Horizontal => { + self.document = DynamicImage::ImageRgba8(imageops::flip_horizontal(&self.document)); + self.transform.flip_h = !self.transform.flip_h; + } + FlipDirection::Vertical => { + self.document = DynamicImage::ImageRgba8(imageops::flip_vertical(&self.document)); + self.transform.flip_v = !self.transform.flip_v; + } + } + self.refresh_handle(); + } + + fn transform_state(&self) -> TransformState { + self.transform } } diff --git a/src/app/document/vector.rs b/src/app/document/vector.rs index 0d97992..1bbe9dd 100644 --- a/src/app/document/vector.rs +++ b/src/app/document/vector.rs @@ -9,19 +9,11 @@ use image::{imageops, DynamicImage, RgbaImage}; use resvg::tiny_skia::{self, Pixmap}; use resvg::usvg::{Options, Tree}; -use super::ImageHandle; -use crate::constant::{FULL_ROTATION, MIN_PIXMAP_SIZE, ROTATION_STEP}; - -/// Accumulated transformations for a vector document. -#[derive(Debug, Clone, Copy, Default)] -pub struct VectorTransform { - /// Rotation in degrees (0, 90, 180, 270). - pub rotation: i16, - /// Horizontal flip. - pub flip_h: bool, - /// Vertical flip. - pub flip_v: bool, -} +use super::{ + DocResult, DocumentInfo, FlipDirection, ImageHandle, Renderable, RenderOutput, Rotation, + TransformState, Transformable, +}; +use crate::constant::MIN_PIXMAP_SIZE; /// Represents a vector document such as SVG. pub struct VectorDocument { @@ -32,9 +24,9 @@ pub struct VectorDocument { /// Native height of the SVG (from viewBox or height attribute). native_height: u32, /// Current render scale (1.0 = native size). - current_scale: f32, + current_scale: f64, /// Accumulated transformations. - transform: VectorTransform, + transform: TransformState, /// Rasterized image at the current scale. pub rendered: DynamicImage, /// Image handle for display. @@ -59,12 +51,12 @@ impl VectorDocument { let native_width = size.width().ceil() as u32; let native_height = size.height().ceil() as u32; - let transform = VectorTransform::default(); + let transform = TransformState::default(); // Render at native scale (1.0). let (rendered, width, height) = render_document(&document, native_width, native_height, 1.0, &transform)?; - let handle = super::create_image_handle(&rendered); + let handle = super::create_image_handle_from_image(&rendered); Ok(Self { document, @@ -86,9 +78,10 @@ impl VectorDocument { /// Re-render the SVG at a new scale, preserving transformations. /// Returns true if re-rendering occurred. - pub fn render_at_scale(&mut self, scale: f32) -> bool { + #[allow(dead_code)] + pub fn render_at_scale(&mut self, scale: f64) -> bool { // Skip if scale hasn't changed - if (self.current_scale - scale).abs() < f32::EPSILON { + if (self.current_scale - scale).abs() < f64::EPSILON { return false; } @@ -104,7 +97,7 @@ impl VectorDocument { self.rendered = rendered; self.width = width; self.height = height; - self.handle = super::create_image_handle(&self.rendered); + self.handle = super::create_image_handle_from_image(&self.rendered); true } Err(e) => { @@ -114,32 +107,6 @@ impl VectorDocument { } } - /// Rotate 90 degrees clockwise. - pub fn rotate_cw(&mut self) { - self.transform.rotation = - (self.transform.rotation + ROTATION_STEP).rem_euclid(FULL_ROTATION); - self.rerender(); - } - - /// Rotate 90 degrees counter-clockwise. - pub fn rotate_ccw(&mut self) { - self.transform.rotation = - (self.transform.rotation - ROTATION_STEP).rem_euclid(FULL_ROTATION); - self.rerender(); - } - - /// Flip horizontally. - pub fn flip_horizontal(&mut self) { - self.transform.flip_h = !self.transform.flip_h; - self.rerender(); - } - - /// Flip vertically. - pub fn flip_vertical(&mut self) { - self.transform.flip_v = !self.transform.flip_v; - self.rerender(); - } - /// Re-render with current scale and transform. fn rerender(&mut self) { if let Ok((rendered, width, height)) = render_document( @@ -152,7 +119,7 @@ impl VectorDocument { self.rendered = rendered; self.width = width; self.height = height; - self.handle = super::create_image_handle(&self.rendered); + self.handle = super::create_image_handle_from_image(&self.rendered); } } @@ -163,21 +130,67 @@ impl VectorDocument { } } +// ============================================================================ +// Trait Implementations +// ============================================================================ + +impl Renderable for VectorDocument { + fn render(&mut self, scale: f64) -> DocResult { + self.render_at_scale(scale); + Ok(RenderOutput { + handle: self.handle.clone(), + width: self.width, + height: self.height, + }) + } + + fn info(&self) -> DocumentInfo { + DocumentInfo { + width: self.native_width, + height: self.native_height, + format: "SVG".to_string(), + } + } +} + +impl Transformable for VectorDocument { + fn rotate(&mut self, rotation: Rotation) { + self.transform.rotation = rotation; + self.rerender(); + } + + fn flip(&mut self, direction: FlipDirection) { + match direction { + FlipDirection::Horizontal => self.transform.flip_h = !self.transform.flip_h, + FlipDirection::Vertical => self.transform.flip_v = !self.transform.flip_v, + } + self.rerender(); + } + + fn transform_state(&self) -> TransformState { + self.transform + } +} + /// Render the SVG document at a given scale with transformations. fn render_document( document: &Tree, native_width: u32, native_height: u32, - scale: f32, - transform: &VectorTransform, + scale: f64, + transform: &TransformState, ) -> anyhow::Result<(DynamicImage, u32, u32)> { - let width = (((native_width as f32) * scale).ceil() as u32).max(MIN_PIXMAP_SIZE); - let height = (((native_height as f32) * scale).ceil() as u32).max(MIN_PIXMAP_SIZE); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let width = (((native_width as f64) * scale).ceil() as u32).max(MIN_PIXMAP_SIZE); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let height = (((native_height as f64) * scale).ceil() as u32).max(MIN_PIXMAP_SIZE); let mut pixmap = Pixmap::new(width, height).ok_or_else(|| anyhow::anyhow!("Failed to create pixmap"))?; - let ts = tiny_skia::Transform::from_scale(scale, scale); + #[allow(clippy::cast_possible_truncation)] + let scale_f32 = scale as f32; + let ts = tiny_skia::Transform::from_scale(scale_f32, scale_f32); resvg::render(document, ts, &mut pixmap.as_mut()); let mut image = pixmap_to_dynamic_image(&pixmap); @@ -192,10 +205,10 @@ fn render_document( // Apply rotation image = match transform.rotation { - 90 => DynamicImage::ImageRgba8(imageops::rotate90(&image)), - 180 => DynamicImage::ImageRgba8(imageops::rotate180(&image)), - 270 => DynamicImage::ImageRgba8(imageops::rotate270(&image)), - _ => image, + Rotation::Cw90 => DynamicImage::ImageRgba8(imageops::rotate90(&image)), + Rotation::Cw180 => DynamicImage::ImageRgba8(imageops::rotate180(&image)), + Rotation::Cw270 => DynamicImage::ImageRgba8(imageops::rotate270(&image)), + Rotation::None => image, }; let final_width = image.width(); diff --git a/src/app/message.rs b/src/app/message.rs index 3ee69ea..77c48e8 100644 --- a/src/app/message.rs +++ b/src/app/message.rs @@ -14,8 +14,8 @@ pub enum AppMessage { OpenPath(PathBuf), NextDocument, PrevDocument, - GotoPage(u32), - GenerateThumbnailPage(u32), + GotoPage(usize), + GenerateThumbnailPage(usize), // Transformations. RotateCW, diff --git a/src/app/mod.rs b/src/app/mod.rs index 42eb9f7..2755147 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -166,11 +166,11 @@ impl cosmic::Application for Noctua { } fn header_start(&self) -> Vec> { - view::header::header_start(&self.model) + view::header::start(&self.model) } fn header_end(&self) -> Vec> { - view::header::header_end(&self.model) + view::header::end(&self.model) } fn view(&self) -> Element<'_, Self::Message> { @@ -182,7 +182,7 @@ impl cosmic::Application for Noctua { return None; } Some(context_drawer::context_drawer( - view::panels::properties_panel(&self.model), + view::panels::view(&self.model), AppMessage::ToggleContextPage(ContextPage::Properties), )) } @@ -307,7 +307,7 @@ fn thumbnail_refresh_subscription(app: &Noctua) -> Subscription { .model .document .as_ref() - .map_or(false, |doc| doc.is_multi_page() && !doc.thumbnails_ready()); + .is_some_and(|doc| doc.is_multi_page() && !doc.thumbnails_ready()); if needs_refresh { time::every(Duration::from_millis(100)).map(|_| AppMessage::RefreshView) diff --git a/src/app/update.rs b/src/app/update.rs index ba8542a..a11815c 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -39,17 +39,16 @@ pub fn update(model: &mut AppModel, msg: &AppMessage, config: &AppConfig) -> Upd } AppMessage::GotoPage(page) => { - if let Some(doc) = &mut model.document { - if let Err(e) = doc.goto_page(*page) { + if let Some(doc) = &mut model.document + && let Err(e) = doc.go_to_page(*page) { log::error!("Failed to navigate to page {}: {}", page, e); } - } } // ---- Thumbnail generation ------------------------------------------------- AppMessage::GenerateThumbnailPage(page) => { - if let Some(doc) = &mut model.document { - if let Some(next_page) = doc.generate_thumbnail_page(*page) { + if let Some(doc) = &mut model.document + && let Some(next_page) = doc.generate_thumbnail_page(*page) { return UpdateResult::Task(Task::batch([ Task::future(async move { Action::App(AppMessage::GenerateThumbnailPage(next_page)) @@ -57,7 +56,6 @@ pub fn update(model: &mut AppModel, msg: &AppMessage, config: &AppConfig) -> Upd Task::done(Action::App(AppMessage::RefreshView)), ])); } - } } AppMessage::RefreshView => { diff --git a/src/app/view/header.rs b/src/app/view/header.rs index 5970186..cdd9d91 100644 --- a/src/app/view/header.rs +++ b/src/app/view/header.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/view/header.rs // -// Header bar buttons (navigation, rotation, flip). +// Header bar content (navigation, rotation, flip). -use cosmic::iced::{Alignment, Length}; +use cosmic::iced::Length; use cosmic::widget::{button, horizontal_space, icon, row}; use cosmic::Element; @@ -11,8 +11,8 @@ use crate::app::message::AppMessage; use crate::app::model::AppModel; use crate::app::ContextPage; -/// Build the left side of the header bar. -pub fn header_start(model: &AppModel) -> Vec> { +/// Build the start (left) side of the header bar. +pub fn start(model: &AppModel) -> Vec> { let has_doc = model.document.is_some(); // Left: Nav toggle + Navigation @@ -55,8 +55,8 @@ pub fn header_start(model: &AppModel) -> Vec> { ] } -/// Build the right side of the header bar. -pub fn header_end(_model: &AppModel) -> Vec> { +/// Build the end (right) side of the header bar. +pub fn end(_model: &AppModel) -> Vec> { vec![ // Info panel toggle button::icon(icon::from_name("dialog-information-symbolic")) diff --git a/src/app/view/image_viewer.rs b/src/app/view/image_viewer.rs index f647ef3..d484c3f 100644 --- a/src/app/view/image_viewer.rs +++ b/src/app/view/image_viewer.rs @@ -17,6 +17,9 @@ use cosmic::iced::{ContentFit, Element, Length, Pixels, Point, Radians, Rectangl use crate::constant::{OFFSET_EPSILON, SCALE_EPSILON}; +/// Callback type for notifying viewer state changes (scale, offset_x, offset_y). +type StateChangeCallback = Box Message>; + /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] pub struct Viewer { @@ -32,7 +35,7 @@ pub struct Viewer { /// Optional external state to override internal state (scale, offset) external_state: Option<(f32, Vector)>, /// Optional callback to notify state changes - on_state_change: Option Message>>, + on_state_change: Option>, } impl Viewer { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index ba48d5e..eaf330b 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -7,7 +7,7 @@ mod canvas; pub mod footer; pub mod header; mod image_viewer; -pub mod panel_pages; +pub mod pages_panel; pub mod panels; use cosmic::iced::Length; @@ -31,7 +31,7 @@ pub fn nav_bar(model: &AppModel) -> Option>> { return None; } - panel_pages::pages_panel(model).map(|panel| { + pages_panel::view(model).map(|panel| { container(panel.map(Action::App)) .width(Length::Shrink) .height(Length::Fill) diff --git a/src/app/view/panel_pages.rs b/src/app/view/pages_panel.rs similarity index 89% rename from src/app/view/panel_pages.rs rename to src/app/view/pages_panel.rs index e207846..65c15db 100644 --- a/src/app/view/panel_pages.rs +++ b/src/app/view/pages_panel.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/panel_pages.rs +// src/app/view/pages_panel.rs // -// Page thumbnail panel for multi-page documents (PDF, multi-page TIFF). +// Page navigation panel for multi-page documents (PDF, multi-page TIFF, etc.). use cosmic::iced::{Alignment, Length}; use cosmic::widget::{button, column, scrollable, text}; @@ -12,9 +12,9 @@ use crate::app::{AppMessage, AppModel}; use crate::constant::THUMBNAIL_MAX_WIDTH; use crate::fl; -/// Content for the page navigation panel (COSMIC nav_bar). +/// Build the page navigation panel view. /// Returns None if the current document doesn't support multiple pages. -pub fn pages_panel(model: &AppModel) -> Option> { +pub fn view(model: &AppModel) -> Option> { let doc = model.document.as_ref()?; // Only show for multi-page documents. @@ -26,7 +26,7 @@ pub fn pages_panel(model: &AppModel) -> Option> { let loaded = doc.thumbnails_loaded(); let current_page = doc.current_page()?; - let mut content = column::with_capacity(page_count as usize + 1) + let mut content = column::with_capacity(page_count + 1) .spacing(12) .padding([12, 8]) .align_x(Alignment::Center) diff --git a/src/app/view/panels.rs b/src/app/view/panels.rs index d3aa8a6..3d5e7a8 100644 --- a/src/app/view/panels.rs +++ b/src/app/view/panels.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/view/panels.rs // -// Panel content for COSMIC context drawer. +// Properties panel content for COSMIC context drawer. use cosmic::iced::Length; use cosmic::widget::{button, column, divider, horizontal_space, icon, row, text}; @@ -10,8 +10,8 @@ use cosmic::Element; use crate::app::{AppMessage, AppModel}; use crate::fl; -/// Content for the right-side properties panel (context drawer). -pub fn properties_panel(model: &AppModel) -> Element<'static, AppMessage> { +/// Build the properties panel view. +pub fn view(model: &AppModel) -> Element<'static, AppMessage> { let mut content = column::with_capacity(16).spacing(8); // Header with action icons diff --git a/src/config.rs b/src/config.rs index b9dc02b..816ff23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,19 +10,19 @@ use std::path::PathBuf; #[derive(Debug, Clone, CosmicConfigEntry, PartialEq)] #[version = 1] pub struct AppConfig { - /// Optional default directory to open images from. + /// Default directory to open when browsing for documents. pub default_image_dir: Option, - /// Whether the nav bar (left panel) is visible. + /// Show page navigation panel (left sidebar for multi-page documents). pub nav_bar_visible: bool, - /// Whether the context drawer (right panel) is visible. + /// Show properties panel (right sidebar with metadata). pub context_drawer_visible: bool, - /// Scale step factor for keyboard zoom (e.g., 1.1 = 10% per step). + /// Zoom step multiplier for keyboard shortcuts (1.1 = 10% increase per step). pub scale_step: f32, - /// Pan step size in pixels per key press. + /// Pan distance in pixels per arrow key press. pub pan_step: f32, - /// Minimum zoom scale (e.g., 0.1 = 10%). + /// Minimum zoom level (0.1 = 10% of original size). pub min_scale: f32, - /// Maximum zoom scale (e.g., 20.0 = 2000%). + /// Maximum zoom level (8.0 = 800% of original size). pub max_scale: f32, } diff --git a/src/constant.rs b/src/constant.rs index 6089716..fe54fc9 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -3,19 +3,13 @@ // // Application constants that should not be changed by the user. -/// Rotation step in degrees (90 = quarter turn). -pub const ROTATION_STEP: i16 = 90; - -/// Full rotation in degrees (for modulo calculation in angle normalization). -pub const FULL_ROTATION: i16 = 360; - /// Minutes per degree (GPS coordinate conversion: DMS to decimal degrees). pub const MINUTES_PER_DEGREE: f64 = 60.0; /// Seconds per degree (GPS coordinate conversion: DMS to decimal degrees). pub const SECONDS_PER_DEGREE: f64 = 3600.0; -/// Minimum pixmap size for SVG rendering (prevents 0x0 images). +/// Minimum pixmap size for SVG rendering (prevents zero-size pixmaps). pub const MIN_PIXMAP_SIZE: u32 = 1; /// Tolerance for scale comparisons (float precision in zoom synchronization). @@ -24,17 +18,17 @@ pub const SCALE_EPSILON: f32 = 0.0001; /// Tolerance for offset comparisons (float precision in pan synchronization). pub const OFFSET_EPSILON: f32 = 0.01; -/// Maximum thumbnail width in pixels (nav bar page thumbnails). +/// Maximum width in pixels for page navigation thumbnails. pub const THUMBNAIL_MAX_WIDTH: f32 = 100.0; -/// Thumbnail cache directory name. +/// Cache directory name under ~/.cache/ for thumbnail storage. pub const CACHE_DIR: &str = "noctua"; -/// Thumbnail file extension. +/// File extension for cached thumbnails. pub const THUMBNAIL_EXT: &str = "png"; -/// Default render scale for PDF pages. -pub const PDF_RENDER_SCALE: f64 = 2.0; +/// PDF page render quality multiplier (2.0 = double resolution for sharp display). +pub const PDF_RENDER_QUALITY: f64 = 2.0; -/// Thumbnail render scale (smaller for quick rendering). -pub const PDF_THUMBNAIL_SCALE: f64 = 0.25; +/// PDF thumbnail size multiplier (0.25 = 25% for fast preview generation). +pub const PDF_THUMBNAIL_SIZE: f64 = 0.25; diff --git a/src/i18n.rs b/src/i18n.rs index d3f2b10..08c2829 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/i18n.rs +// +// Internationalization (i18n) support. use i18n_embed::{ DefaultLocalizer, LanguageLoader, Localizer, diff --git a/src/main.rs b/src/main.rs index 8de9d5b..a71952d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/main.rs +// +// Application entry point. mod app; mod config;