@@ -1,8 +1,8 @@ |
| 1 | //! Miller columns view (like macOS Finder). | 1 | //! Miller columns view (like macOS Finder). |
| 2 | | 2 | |
| 3 | -use crate::core::{read_directory, sort_entries, EntryType, FileEntry, SortDirection, SortOrder}; | 3 | +use crate::core::{read_directory, sort_entries, EntryType, FileEntry, ImagePreview, SortDirection, SortOrder, is_supported_image}; |
| 4 | use gartk_core::{Color, Modifiers, Point, Rect}; | 4 | use gartk_core::{Color, Modifiers, Point, Rect}; |
| 5 | -use gartk_render::{Renderer, TextStyle}; | 5 | +use gartk_render::{Renderer, TextStyle, Surface}; |
| 6 | use std::collections::HashSet; | 6 | use std::collections::HashSet; |
| 7 | use std::path::PathBuf; | 7 | use std::path::PathBuf; |
| 8 | | 8 | |
@@ -83,6 +83,14 @@ pub struct ColumnView { |
| 83 | preview_column: Option<Column>, | 83 | preview_column: Option<Column>, |
| 84 | /// Path that needs preview loading (set when selection changes to a directory). | 84 | /// Path that needs preview loading (set when selection changes to a directory). |
| 85 | pending_preview_path: Option<PathBuf>, | 85 | pending_preview_path: Option<PathBuf>, |
| | 86 | + /// Path that needs image preview loading. |
| | 87 | + pending_image_preview_path: Option<PathBuf>, |
| | 88 | + /// Loaded image preview. |
| | 89 | + image_preview: Option<ImagePreview>, |
| | 90 | + /// Path of the loaded image preview (for matching). |
| | 91 | + image_preview_path: Option<PathBuf>, |
| | 92 | + /// Cached Cairo surface for the image preview. |
| | 93 | + image_surface: Option<Surface>, |
| 86 | /// View bounds. | 94 | /// View bounds. |
| 87 | bounds: Rect, | 95 | bounds: Rect, |
| 88 | /// Show hidden files. | 96 | /// Show hidden files. |
@@ -111,6 +119,10 @@ impl ColumnView { |
| 111 | current_column: Column::new(Vec::new(), column_bounds), | 119 | current_column: Column::new(Vec::new(), column_bounds), |
| 112 | preview_column: None, | 120 | preview_column: None, |
| 113 | pending_preview_path: None, | 121 | pending_preview_path: None, |
| | 122 | + pending_image_preview_path: None, |
| | 123 | + image_preview: None, |
| | 124 | + image_preview_path: None, |
| | 125 | + image_surface: None, |
| 114 | bounds, | 126 | bounds, |
| 115 | show_hidden: false, | 127 | show_hidden: false, |
| 116 | sort_order: SortOrder::Name, | 128 | sort_order: SortOrder::Name, |
@@ -196,13 +208,40 @@ impl ColumnView { |
| 196 | // Clear current preview while loading | 208 | // Clear current preview while loading |
| 197 | self.preview_column = None; | 209 | self.preview_column = None; |
| 198 | } | 210 | } |
| | 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; |
| 199 | } else { | 216 | } else { |
| 200 | self.pending_preview_path = None; | 217 | self.pending_preview_path = None; |
| 201 | self.preview_column = None; | 218 | self.preview_column = None; |
| | 219 | + |
| | 220 | + // Check if this is an image file that needs preview |
| | 221 | + if is_supported_image(entry.extension().as_deref()) { |
| | 222 | + let needs_load = self.image_preview_path.as_ref() != Some(&entry.path); |
| | 223 | + if needs_load { |
| | 224 | + self.pending_image_preview_path = Some(entry.path.clone()); |
| | 225 | + // Clear current image preview while loading |
| | 226 | + self.image_preview = None; |
| | 227 | + self.image_preview_path = None; |
| | 228 | + self.image_surface = None; |
| | 229 | + } |
| | 230 | + } else { |
| | 231 | + // Not an image - clear image preview |
| | 232 | + self.pending_image_preview_path = None; |
| | 233 | + self.image_preview = None; |
| | 234 | + self.image_preview_path = None; |
| | 235 | + self.image_surface = None; |
| | 236 | + } |
| 202 | } | 237 | } |
| 203 | } else { | 238 | } else { |
| 204 | self.pending_preview_path = None; | 239 | self.pending_preview_path = None; |
| 205 | self.preview_column = None; | 240 | 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; |
| 206 | } | 245 | } |
| 207 | } | 246 | } |
| 208 | | 247 | |
@@ -245,6 +284,37 @@ impl ColumnView { |
| 245 | self.pending_preview_path.is_some() | 284 | self.pending_preview_path.is_some() |
| 246 | } | 285 | } |
| 247 | | 286 | |
| | 287 | + /// Take pending image preview request (path, max_width, max_height). |
| | 288 | + pub fn take_pending_image_preview(&mut self) -> Option<(PathBuf, u32, u32)> { |
| | 289 | + self.pending_image_preview_path.take().map(|path| { |
| | 290 | + let preview_x = self.current_column.bounds.x + self.current_column.bounds.width as i32; |
| | 291 | + let preview_width = (self.bounds.x + self.bounds.width as i32 - preview_x) as u32; |
| | 292 | + let preview_height = self.bounds.height / 2; // Use half height for image |
| | 293 | + (path, preview_width.saturating_sub(32), preview_height.saturating_sub(32)) |
| | 294 | + }) |
| | 295 | + } |
| | 296 | + |
| | 297 | + /// Set loaded image preview. |
| | 298 | + pub fn set_image_preview(&mut self, path: &PathBuf, image: Option<ImagePreview>) { |
| | 299 | + // Only set if this is still the path we're displaying |
| | 300 | + let visible = self.current_column.visible_entries(self.show_hidden); |
| | 301 | + let selected_matches = visible |
| | 302 | + .get(self.current_column.selected) |
| | 303 | + .map(|e| &e.path == path) |
| | 304 | + .unwrap_or(false); |
| | 305 | + |
| | 306 | + if selected_matches { |
| | 307 | + if let Some(ref preview) = image { |
| | 308 | + // Create a Cairo surface from the RGBA data |
| | 309 | + self.image_surface = Surface::from_rgba(&preview.data, preview.width, preview.height).ok(); |
| | 310 | + } else { |
| | 311 | + self.image_surface = None; |
| | 312 | + } |
| | 313 | + self.image_preview = image; |
| | 314 | + self.image_preview_path = Some(path.clone()); |
| | 315 | + } |
| | 316 | + } |
| | 317 | + |
| 248 | /// Get visible entries in current column. | 318 | /// Get visible entries in current column. |
| 249 | pub fn visible_entries(&self) -> Vec<&FileEntry> { | 319 | pub fn visible_entries(&self) -> Vec<&FileEntry> { |
| 250 | self.current_column.visible_entries(self.show_hidden) | 320 | self.current_column.visible_entries(self.show_hidden) |
@@ -460,6 +530,31 @@ impl ColumnView { |
| 460 | changed | 530 | changed |
| 461 | } | 531 | } |
| 462 | | 532 | |
| | 533 | + /// Handle mouse scroll. Returns true if scrolled. |
| | 534 | + pub fn on_scroll(&mut self, delta_y: i32) -> bool { |
| | 535 | + // Scroll the current column |
| | 536 | + let visible = self.visible_entries(); |
| | 537 | + let total_rows = visible.len(); |
| | 538 | + let visible_rows = self.current_column.visible_rows(); |
| | 539 | + |
| | 540 | + if total_rows <= visible_rows { |
| | 541 | + return false; |
| | 542 | + } |
| | 543 | + |
| | 544 | + let max_scroll = total_rows.saturating_sub(visible_rows); |
| | 545 | + let old_offset = self.current_column.scroll_offset; |
| | 546 | + |
| | 547 | + if delta_y < 0 { |
| | 548 | + let rows = ((-delta_y) as usize / 3).max(1); |
| | 549 | + self.current_column.scroll_offset = self.current_column.scroll_offset.saturating_sub(rows); |
| | 550 | + } else if delta_y > 0 { |
| | 551 | + let rows = (delta_y as usize / 3).max(1); |
| | 552 | + self.current_column.scroll_offset = (self.current_column.scroll_offset + rows).min(max_scroll); |
| | 553 | + } |
| | 554 | + |
| | 555 | + self.current_column.scroll_offset != old_offset |
| | 556 | + } |
| | 557 | + |
| 463 | /// Get the entry at the given position (for drag detection). | 558 | /// Get the entry at the given position (for drag detection). |
| 464 | pub fn entry_at_point(&self, pos: Point) -> Option<&FileEntry> { | 559 | pub fn entry_at_point(&self, pos: Point) -> Option<&FileEntry> { |
| 465 | // Check current column (main selection column) | 560 | // Check current column (main selection column) |
@@ -745,6 +840,36 @@ impl ColumnView { |
| 745 | 1.0, | 840 | 1.0, |
| 746 | )?; | 841 | )?; |
| 747 | | 842 | |
| | 843 | + let mut y = self.bounds.y + 16; |
| | 844 | + let x = preview_x + 16; |
| | 845 | + |
| | 846 | + // Render image preview if available |
| | 847 | + if let Some(ref surface) = self.image_surface { |
| | 848 | + // Center the image in the preview area |
| | 849 | + let img_width = surface.width(); |
| | 850 | + let img_height = surface.height(); |
| | 851 | + let max_width = preview_width.saturating_sub(32); |
| | 852 | + |
| | 853 | + let img_x = preview_x + 16 + (max_width.saturating_sub(img_width) / 2) as i32; |
| | 854 | + let img_y = y; |
| | 855 | + |
| | 856 | + // Draw the image using Cairo |
| | 857 | + let ctx = renderer.context()?; |
| | 858 | + ctx.set_source_surface(surface.cairo_surface(), img_x as f64, img_y as f64)?; |
| | 859 | + ctx.paint()?; |
| | 860 | + |
| | 861 | + // Move y below the image with padding |
| | 862 | + y += img_height as i32 + 24; |
| | 863 | + } else if is_supported_image(entry.extension().as_deref()) && self.pending_image_preview_path.is_some() { |
| | 864 | + // Show loading indicator for images |
| | 865 | + let loading_style = TextStyle::new() |
| | 866 | + .font_family(&theme.font_family) |
| | 867 | + .font_size(theme.font_size) |
| | 868 | + .color(theme.item_foreground.with_alpha(0.5)); |
| | 869 | + renderer.text("Loading preview...", x as f64, y as f64, &loading_style)?; |
| | 870 | + y += 40; |
| | 871 | + } |
| | 872 | + |
| 748 | let label_style = TextStyle::new() | 873 | let label_style = TextStyle::new() |
| 749 | .font_family(&theme.font_family) | 874 | .font_family(&theme.font_family) |
| 750 | .font_size(theme.font_size - 1.0) | 875 | .font_size(theme.font_size - 1.0) |
@@ -755,9 +880,6 @@ impl ColumnView { |
| 755 | .font_size(theme.font_size) | 880 | .font_size(theme.font_size) |
| 756 | .color(theme.item_foreground); | 881 | .color(theme.item_foreground); |
| 757 | | 882 | |
| 758 | - let mut y = self.bounds.y + 16; | | |
| 759 | - let x = preview_x + 16; | | |
| 760 | - | | |
| 761 | // File name | 883 | // File name |
| 762 | renderer.text("Name", x as f64, y as f64, &label_style)?; | 884 | renderer.text("Name", x as f64, y as f64, &label_style)?; |
| 763 | y += 18; | 885 | y += 18; |