@@ -1,6 +1,6 @@ |
| 1 | 1 | //! Miller columns view (like macOS Finder). |
| 2 | 2 | |
| 3 | | -use crate::core::{read_directory, sort_entries, EntryType, FileEntry, ImagePreview, SortDirection, SortOrder, is_supported_image}; |
| 3 | +use crate::core::{read_directory, sort_entries, EntryType, FileEntry, ImagePreview, PdfPreview, SortDirection, SortOrder, is_supported_image, is_pdf}; |
| 4 | 4 | use gartk_core::{Color, Modifiers, Point, Rect}; |
| 5 | 5 | use gartk_render::{Renderer, TextStyle, Surface}; |
| 6 | 6 | use std::collections::HashSet; |
@@ -91,6 +91,14 @@ pub struct ColumnView { |
| 91 | 91 | image_preview_path: Option<PathBuf>, |
| 92 | 92 | /// Cached Cairo surface for the image preview. |
| 93 | 93 | image_surface: Option<Surface>, |
| 94 | + /// Path that needs PDF preview loading. |
| 95 | + pending_pdf_preview_path: Option<PathBuf>, |
| 96 | + /// Loaded PDF preview. |
| 97 | + pdf_preview: Option<PdfPreview>, |
| 98 | + /// Path of the loaded PDF preview (for matching). |
| 99 | + pdf_preview_path: Option<PathBuf>, |
| 100 | + /// Cached Cairo surface for the PDF preview. |
| 101 | + pdf_surface: Option<Surface>, |
| 94 | 102 | /// View bounds. |
| 95 | 103 | bounds: Rect, |
| 96 | 104 | /// Show hidden files. |
@@ -123,6 +131,10 @@ impl ColumnView { |
| 123 | 131 | image_preview: None, |
| 124 | 132 | image_preview_path: None, |
| 125 | 133 | image_surface: None, |
| 134 | + pending_pdf_preview_path: None, |
| 135 | + pdf_preview: None, |
| 136 | + pdf_preview_path: None, |
| 137 | + pdf_surface: None, |
| 126 | 138 | bounds, |
| 127 | 139 | show_hidden: false, |
| 128 | 140 | sort_order: SortOrder::Name, |
@@ -208,11 +220,8 @@ impl ColumnView { |
| 208 | 220 | // Clear current preview while loading |
| 209 | 221 | self.preview_column = None; |
| 210 | 222 | } |
| 211 | | - // Clear image preview for directories |
| 212 | | - self.pending_image_preview_path = None; |
| 213 | | - self.image_preview = None; |
| 214 | | - self.image_preview_path = None; |
| 215 | | - self.image_surface = None; |
| 223 | + // Clear image/PDF preview for directories |
| 224 | + self.clear_file_previews(); |
| 216 | 225 | } else { |
| 217 | 226 | self.pending_preview_path = None; |
| 218 | 227 | self.preview_column = None; |
@@ -227,24 +236,50 @@ impl ColumnView { |
| 227 | 236 | self.image_preview_path = None; |
| 228 | 237 | self.image_surface = None; |
| 229 | 238 | } |
| 230 | | - } else { |
| 231 | | - // Not an image - clear image preview |
| 239 | + // Clear PDF preview when showing image |
| 240 | + self.pending_pdf_preview_path = None; |
| 241 | + self.pdf_preview = None; |
| 242 | + self.pdf_preview_path = None; |
| 243 | + self.pdf_surface = None; |
| 244 | + } else if is_pdf(entry.extension().as_deref()) { |
| 245 | + // Check if this is a PDF file that needs preview |
| 246 | + let needs_load = self.pdf_preview_path.as_ref() != Some(&entry.path); |
| 247 | + if needs_load { |
| 248 | + self.pending_pdf_preview_path = Some(entry.path.clone()); |
| 249 | + // Clear current PDF preview while loading |
| 250 | + self.pdf_preview = None; |
| 251 | + self.pdf_preview_path = None; |
| 252 | + self.pdf_surface = None; |
| 253 | + } |
| 254 | + // Clear image preview when showing PDF |
| 232 | 255 | self.pending_image_preview_path = None; |
| 233 | 256 | self.image_preview = None; |
| 234 | 257 | self.image_preview_path = None; |
| 235 | 258 | self.image_surface = None; |
| 259 | + } else { |
| 260 | + // Not an image or PDF - clear both previews |
| 261 | + self.clear_file_previews(); |
| 236 | 262 | } |
| 237 | 263 | } |
| 238 | 264 | } else { |
| 239 | 265 | self.pending_preview_path = None; |
| 240 | 266 | self.preview_column = None; |
| 241 | | - self.pending_image_preview_path = None; |
| 242 | | - self.image_preview = None; |
| 243 | | - self.image_preview_path = None; |
| 244 | | - self.image_surface = None; |
| 267 | + self.clear_file_previews(); |
| 245 | 268 | } |
| 246 | 269 | } |
| 247 | 270 | |
| 271 | + /// Clear all file preview state (image and PDF). |
| 272 | + fn clear_file_previews(&mut self) { |
| 273 | + self.pending_image_preview_path = None; |
| 274 | + self.image_preview = None; |
| 275 | + self.image_preview_path = None; |
| 276 | + self.image_surface = None; |
| 277 | + self.pending_pdf_preview_path = None; |
| 278 | + self.pdf_preview = None; |
| 279 | + self.pdf_preview_path = None; |
| 280 | + self.pdf_surface = None; |
| 281 | + } |
| 282 | + |
| 248 | 283 | /// Get the path that needs preview loading, if any. |
| 249 | 284 | /// Returns the path and sort settings. Returns None if no preview needed. |
| 250 | 285 | pub fn take_pending_preview(&mut self) -> Option<(PathBuf, SortOrder, SortDirection)> { |
@@ -315,6 +350,37 @@ impl ColumnView { |
| 315 | 350 | } |
| 316 | 351 | } |
| 317 | 352 | |
| 353 | + /// Take pending PDF preview request (path, max_width, max_height). |
| 354 | + pub fn take_pending_pdf_preview(&mut self) -> Option<(PathBuf, u32, u32)> { |
| 355 | + self.pending_pdf_preview_path.take().map(|path| { |
| 356 | + let preview_x = self.current_column.bounds.x + self.current_column.bounds.width as i32; |
| 357 | + let preview_width = (self.bounds.x + self.bounds.width as i32 - preview_x) as u32; |
| 358 | + let preview_height = self.bounds.height / 2; // Use half height for PDF |
| 359 | + (path, preview_width.saturating_sub(32), preview_height.saturating_sub(32)) |
| 360 | + }) |
| 361 | + } |
| 362 | + |
| 363 | + /// Set loaded PDF preview. |
| 364 | + pub fn set_pdf_preview(&mut self, path: &PathBuf, pdf: Option<PdfPreview>) { |
| 365 | + // Only set if this is still the path we're displaying |
| 366 | + let visible = self.current_column.visible_entries(self.show_hidden); |
| 367 | + let selected_matches = visible |
| 368 | + .get(self.current_column.selected) |
| 369 | + .map(|e| &e.path == path) |
| 370 | + .unwrap_or(false); |
| 371 | + |
| 372 | + if selected_matches { |
| 373 | + if let Some(ref preview) = pdf { |
| 374 | + // Create a Cairo surface from the RGBA data |
| 375 | + self.pdf_surface = Surface::from_rgba(&preview.data, preview.width, preview.height).ok(); |
| 376 | + } else { |
| 377 | + self.pdf_surface = None; |
| 378 | + } |
| 379 | + self.pdf_preview = pdf; |
| 380 | + self.pdf_preview_path = Some(path.clone()); |
| 381 | + } |
| 382 | + } |
| 383 | + |
| 318 | 384 | /// Get visible entries in current column. |
| 319 | 385 | pub fn visible_entries(&self) -> Vec<&FileEntry> { |
| 320 | 386 | self.current_column.visible_entries(self.show_hidden) |
@@ -860,6 +926,32 @@ impl ColumnView { |
| 860 | 926 | |
| 861 | 927 | // Move y below the image with padding |
| 862 | 928 | y += img_height as i32 + 24; |
| 929 | + } else if let Some(ref surface) = self.pdf_surface { |
| 930 | + // Render PDF preview |
| 931 | + let img_width = surface.width(); |
| 932 | + let img_height = surface.height(); |
| 933 | + let max_width = preview_width.saturating_sub(32); |
| 934 | + |
| 935 | + let img_x = preview_x + 16 + (max_width.saturating_sub(img_width) / 2) as i32; |
| 936 | + let img_y = y; |
| 937 | + |
| 938 | + // Draw the PDF preview using Cairo |
| 939 | + let ctx = renderer.context()?; |
| 940 | + ctx.set_source_surface(surface.cairo_surface(), img_x as f64, img_y as f64)?; |
| 941 | + ctx.paint()?; |
| 942 | + |
| 943 | + // Show page count if available |
| 944 | + if let Some(ref pdf) = self.pdf_preview { |
| 945 | + let page_info = format!("Page 1 of {}", pdf.page_count); |
| 946 | + let page_style = TextStyle::new() |
| 947 | + .font_family(&theme.font_family) |
| 948 | + .font_size(theme.font_size - 2.0) |
| 949 | + .color(theme.item_foreground.with_alpha(0.6)); |
| 950 | + renderer.text(&page_info, (img_x + 4) as f64, (img_y + img_height as i32 + 4) as f64, &page_style)?; |
| 951 | + } |
| 952 | + |
| 953 | + // Move y below the preview with padding |
| 954 | + y += img_height as i32 + 32; |
| 863 | 955 | } else if is_supported_image(entry.extension().as_deref()) && self.pending_image_preview_path.is_some() { |
| 864 | 956 | // Show loading indicator for images |
| 865 | 957 | let loading_style = TextStyle::new() |
@@ -868,6 +960,14 @@ impl ColumnView { |
| 868 | 960 | .color(theme.item_foreground.with_alpha(0.5)); |
| 869 | 961 | renderer.text("Loading preview...", x as f64, y as f64, &loading_style)?; |
| 870 | 962 | y += 40; |
| 963 | + } else if is_pdf(entry.extension().as_deref()) && self.pending_pdf_preview_path.is_some() { |
| 964 | + // Show loading indicator for PDFs |
| 965 | + let loading_style = TextStyle::new() |
| 966 | + .font_family(&theme.font_family) |
| 967 | + .font_size(theme.font_size) |
| 968 | + .color(theme.item_foreground.with_alpha(0.5)); |
| 969 | + renderer.text("Loading PDF preview...", x as f64, y as f64, &loading_style)?; |
| 970 | + y += 40; |
| 871 | 971 | } |
| 872 | 972 | |
| 873 | 973 | let label_style = TextStyle::new() |