Skip to main content

zenity_rs/ui/
file_select.rs

1//! File selection dialog implementation with enhanced UI.
2
3use std::{
4    collections::HashSet,
5    fs::{self, Metadata},
6    path::{Path, PathBuf},
7    time::SystemTime,
8};
9
10use crate::{
11    backend::{MouseButton, Window, WindowEvent, create_window},
12    error::Error,
13    render::{Canvas, Font, Rgba, rgb},
14    ui::{
15        BASE_BUTTON_HEIGHT, BASE_BUTTON_SPACING, BASE_CORNER_RADIUS, Colors, KEY_BACKSPACE,
16        KEY_DOWN, KEY_ESCAPE, KEY_RETURN, KEY_UP,
17        widgets::{Widget, button::Button, text_input::TextInput},
18    },
19};
20
21// Layout constants (logical, at scale 1.0)
22const BASE_WINDOW_WIDTH: u32 = 700;
23const BASE_WINDOW_HEIGHT: u32 = 500;
24const BASE_PADDING: u32 = 12;
25const BASE_SIDEBAR_WIDTH: u32 = 160;
26const BASE_TOOLBAR_HEIGHT: u32 = 36;
27const BASE_PATH_BAR_HEIGHT: u32 = 32;
28const BASE_SEARCH_WIDTH: u32 = 200;
29const BASE_ITEM_HEIGHT: u32 = 28;
30const BASE_ICON_SIZE: u32 = 20;
31const BASE_SECTION_HEADER_HEIGHT: u32 = 22;
32
33// Column widths (logical)
34const BASE_NAME_COL_WIDTH: u32 = 280;
35const BASE_SIZE_COL_WIDTH: u32 = 80;
36const BASE_COLUMN_HEADER_HEIGHT: u32 = 28;
37const BASE_FILENAME_ROW_HEIGHT: u32 = 58;
38const BASE_FOOTER_HEIGHT: u32 = 44;
39const BASE_CONTENT_GAP: u32 = 12;
40const BASE_FILENAME_LABEL_HEIGHT: u32 = 20;
41
42/// File selection dialog result.
43#[derive(Debug, Clone)]
44pub enum FileSelectResult {
45    Selected(PathBuf),
46    SelectedMultiple(Vec<PathBuf>),
47    Cancelled,
48    Closed,
49}
50
51impl FileSelectResult {
52    pub fn exit_code(&self) -> i32 {
53        match self {
54            FileSelectResult::Selected(_) | FileSelectResult::SelectedMultiple(_) => 0,
55            FileSelectResult::Cancelled => 1,
56            FileSelectResult::Closed => 1,
57        }
58    }
59}
60
61/// Quick access location.
62#[derive(Clone)]
63struct QuickAccess {
64    name: &'static str,
65    path: PathBuf,
66    icon: QuickAccessIcon,
67}
68
69#[derive(Clone, Copy)]
70enum QuickAccessIcon {
71    Home,
72    Desktop,
73    Documents,
74    Downloads,
75    Pictures,
76    Music,
77    Videos,
78}
79
80/// Represents a mounted drive
81#[derive(Clone)]
82struct MountPoint {
83    device: String,
84    mount_point: PathBuf,
85    label: Option<String>,
86}
87
88/// Icon for mount point type
89#[derive(Clone, Copy)]
90enum MountIcon {
91    UsbDrive,
92    ExternalHdd,
93    Optical,
94    Generic,
95}
96
97/// File filter pattern.
98#[derive(Debug, Clone)]
99pub struct FileFilter {
100    pub name: String,
101    pub patterns: Vec<String>,
102}
103
104/// File selection dialog builder.
105pub struct FileSelectBuilder {
106    title: String,
107    directory: bool,
108    save: bool,
109    filename: String,
110    start_path: Option<PathBuf>,
111    width: Option<u32>,
112    height: Option<u32>,
113    colors: Option<&'static Colors>,
114    filters: Vec<FileFilter>,
115    multiple: bool,
116    separator: String,
117}
118
119impl FileSelectBuilder {
120    pub fn new() -> Self {
121        Self {
122            title: String::new(),
123            directory: false,
124            save: false,
125            filename: String::new(),
126            start_path: None,
127            width: None,
128            height: None,
129            colors: None,
130            filters: Vec::new(),
131            multiple: false,
132            separator: String::from(" "),
133        }
134    }
135
136    pub fn title(mut self, title: &str) -> Self {
137        self.title = title.to_string();
138        self
139    }
140
141    pub fn directory(mut self, directory: bool) -> Self {
142        self.directory = directory;
143        self
144    }
145
146    pub fn save(mut self, save: bool) -> Self {
147        self.save = save;
148        self
149    }
150
151    pub fn filename(mut self, filename: &str) -> Self {
152        self.filename = filename.to_string();
153        self
154    }
155
156    pub fn start_path(mut self, path: &Path) -> Self {
157        self.start_path = Some(path.to_path_buf());
158        self
159    }
160
161    pub fn colors(mut self, colors: &'static Colors) -> Self {
162        self.colors = Some(colors);
163        self
164    }
165
166    pub fn width(mut self, width: u32) -> Self {
167        self.width = Some(width);
168        self
169    }
170
171    pub fn height(mut self, height: u32) -> Self {
172        self.height = Some(height);
173        self
174    }
175
176    pub fn add_filter(mut self, filter: FileFilter) -> Self {
177        self.filters.push(filter);
178        self
179    }
180
181    pub fn multiple(mut self, multiple: bool) -> Self {
182        self.multiple = multiple;
183        self
184    }
185
186    pub fn separator(mut self, separator: &str) -> Self {
187        self.separator = separator.to_string();
188        self
189    }
190
191    pub fn show(self) -> Result<FileSelectResult, Error> {
192        let colors = self.colors.unwrap_or_else(|| crate::ui::detect_theme());
193
194        // Use custom dimensions if provided, otherwise use defaults
195        let logical_width = self.width.unwrap_or(BASE_WINDOW_WIDTH);
196        let logical_height = self.height.unwrap_or(BASE_WINDOW_HEIGHT);
197
198        // Create window with LOGICAL dimensions first
199        let mut window = create_window(logical_width as u16, logical_height as u16)?;
200        let title = if self.title.is_empty() {
201            if self.directory {
202                "Select Directory"
203            } else if self.save {
204                "Save File"
205            } else {
206                "Open File"
207            }
208        } else {
209            &self.title
210        };
211        window.set_title(title)?;
212
213        // Get the actual scale factor from the window (compositor scale)
214        let scale = window.scale_factor();
215
216        // Now create everything at PHYSICAL scale
217        let font = Font::load(scale);
218
219        // Scale dimensions for physical rendering
220        let window_width = (logical_width as f32 * scale) as u32;
221        let window_height = (logical_height as f32 * scale) as u32;
222        let padding = (BASE_PADDING as f32 * scale) as u32;
223        let sidebar_width = (BASE_SIDEBAR_WIDTH as f32 * scale) as u32;
224        let toolbar_height = (BASE_TOOLBAR_HEIGHT as f32 * scale) as u32;
225        let path_bar_height = (BASE_PATH_BAR_HEIGHT as f32 * scale) as u32;
226        let search_width = (BASE_SEARCH_WIDTH as f32 * scale) as u32;
227        let item_height = (BASE_ITEM_HEIGHT as f32 * scale) as u32;
228        let name_col_width = (BASE_NAME_COL_WIDTH as f32 * scale) as u32;
229        let size_col_width = (BASE_SIZE_COL_WIDTH as f32 * scale) as u32;
230
231        // Build quick access locations
232        let quick_access = build_quick_access();
233
234        // Load mounted drives
235        let mounted_drives = get_mounted_drives();
236
237        // Create UI elements at physical scale
238        let mut ok_button = Button::new(if self.save { "Save" } else { "Open" }, &font, scale);
239        let mut cancel_button = Button::new("Cancel", &font, scale);
240
241        // Search input
242        let mut search_input = TextInput::new(search_width).with_placeholder("Search...");
243
244        // Save mode flag
245        let save_mode = self.save && !self.directory;
246
247        // Navigation history
248        let mut history: Vec<PathBuf> = Vec::new();
249        let mut history_index: usize = 0;
250
251        // Current state
252        let mut current_dir = self
253            .start_path
254            .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")));
255        history.push(current_dir.clone());
256
257        let mut all_entries: Vec<DirEntry> = Vec::new();
258        let mut filtered_entries: Vec<usize> = Vec::new(); // Indices into all_entries
259        let mut selected_indices: HashSet<usize> = HashSet::new();
260        let mut scroll_offset: usize = 0;
261        let mut show_hidden = false;
262        let mut search_text = String::new();
263        let mut hovered_quick_access: Option<usize> = None;
264        let mut hovered_entry: Option<usize> = None;
265        let mut hovered_drive: Option<usize> = None;
266
267        // Tab-completion state for filename input (save mode)
268        let mut completion_matches: Vec<String> = Vec::new();
269        let mut completion_popup_index: usize = 0;
270
271        // Tab-completion state for search input
272        let mut search_matches: Vec<String> = Vec::new();
273        let mut search_popup_index: usize = 0;
274
275        let mut window_dragging = false;
276
277        // Scrollbar thumb dragging state
278        let mut thumb_drag = false;
279        let mut thumb_drag_offset: Option<i32> = None;
280        let mut scrollbar_hovered = false;
281
282        // Load initial directory
283        load_directory(&current_dir, &mut all_entries, self.directory, show_hidden);
284        update_filtered(
285            &all_entries,
286            &search_text,
287            &mut filtered_entries,
288            &self.filters,
289        );
290
291        // Calculate layout in physical coordinates
292        let filename_row_height = if save_mode {
293            (BASE_FILENAME_ROW_HEIGHT as f32 * scale) as u32
294        } else {
295            0
296        };
297        let content_gap = (BASE_CONTENT_GAP as f32 * scale) as u32;
298        let footer_height = (BASE_FOOTER_HEIGHT as f32 * scale) as u32;
299        let sidebar_x = padding as i32;
300        let sidebar_y = (padding + toolbar_height + content_gap) as i32;
301        let sidebar_h = window_height
302            - padding * 2
303            - toolbar_height
304            - content_gap
305            - footer_height
306            - filename_row_height;
307
308        let main_x = (padding + sidebar_width + content_gap) as i32;
309        let main_y = sidebar_y;
310        let main_w = window_width - padding * 2 - sidebar_width - content_gap;
311        let main_h = sidebar_h;
312
313        let header_offset = (BASE_COLUMN_HEADER_HEIGHT as f32 * scale) as u32;
314        let list_y = main_y + path_bar_height as i32 + header_offset as i32;
315        let list_h = main_h - path_bar_height - header_offset;
316        let visible_items = (list_h / item_height) as usize;
317
318        // Calculate section heights
319        let section_header_height = (BASE_SECTION_HEADER_HEIGHT as f32 * scale) as u32;
320        let item_height_scaled = item_height;
321        let gap_between_sections = content_gap;
322
323        // Position buttons
324        let button_y =
325            (window_height - padding - (BASE_BUTTON_HEIGHT as f32 * scale) as u32) as i32;
326        let mut bx = window_width as i32 - padding as i32;
327        bx -= cancel_button.width() as i32;
328        cancel_button.set_position(bx, button_y);
329        bx -= (BASE_BUTTON_SPACING as f32 * scale) as i32 + ok_button.width() as i32;
330        ok_button.set_position(bx, button_y);
331
332        // Position filename area (label above, full-width input below, save mode only)
333        let filename_y = button_y - filename_row_height as i32;
334        let filename_label_h = (BASE_FILENAME_LABEL_HEIGHT as f32 * scale) as i32;
335        let mut filename_input = if save_mode {
336            let mut input = TextInput::new(main_w).with_placeholder("Enter filename...");
337            if !self.filename.is_empty() {
338                input = input.with_default_text(&self.filename);
339            }
340            input.set_focus(true);
341            input.set_position(main_x, filename_y + filename_label_h);
342            Some(input)
343        } else {
344            None
345        };
346
347        // Position search input
348        let search_x = window_width as i32 - padding as i32 - search_width as i32;
349        let search_y = padding as i32 + (2.0 * scale) as i32;
350        search_input.set_position(search_x, search_y);
351
352        // Create canvas at PHYSICAL dimensions
353        let mut canvas = Canvas::new(window_width, window_height);
354        let mut mouse_x = 0i32;
355        let mut mouse_y = 0i32;
356
357        // Draw function - captures scaled variables from enclosing scope
358        let draw = |canvas: &mut Canvas,
359                    colors: &Colors,
360                    font: &Font,
361                    current_dir: &Path,
362                    quick_access: &[QuickAccess],
363                    all_entries: &[DirEntry],
364                    filtered_entries: &[usize],
365                    selected_indices: &HashSet<usize>,
366                    scroll_offset: usize,
367                    hovered_quick_access: Option<usize>,
368                    hovered_entry: Option<usize>,
369                    show_hidden: bool,
370                    search_input: &TextInput,
371                    ok_button: &Button,
372                    cancel_button: &Button,
373                    history: &[PathBuf],
374                    history_index: usize,
375                    mounted_drives: &[MountPoint],
376                    hovered_drive: Option<usize>,
377                    scale: f32,
378                    scrollbar_hovered: bool,
379                    filename_input: Option<&TextInput>| {
380            let width = canvas.width() as f32;
381            let height = canvas.height() as f32;
382            let radius = BASE_CORNER_RADIUS * scale;
383
384            canvas.fill_dialog_bg(
385                width,
386                height,
387                colors.window_bg,
388                colors.window_border,
389                colors.window_shadow,
390                radius,
391            );
392
393            // Toolbar background
394            let toolbar_bg = darken(colors.window_bg, 0.03);
395            canvas.fill_rect(
396                0.0,
397                0.0,
398                window_width as f32,
399                (toolbar_height + padding) as f32,
400                toolbar_bg,
401            );
402
403            // Navigation buttons
404            let nav_y = padding as i32 + (4.0 * scale) as i32;
405            let can_back = history_index > 0;
406            let can_forward = history_index + 1 < history.len();
407
408            // Back button
409            draw_nav_button(
410                canvas,
411                padding as i32,
412                nav_y,
413                "<",
414                can_back,
415                colors,
416                font,
417                scale,
418            );
419            // Forward button
420            draw_nav_button(
421                canvas,
422                (padding as f32 + 32.0 * scale) as i32,
423                nav_y,
424                ">",
425                can_forward,
426                colors,
427                font,
428                scale,
429            );
430            // Up button
431            let can_up = current_dir.parent().is_some();
432            draw_nav_button(
433                canvas,
434                (padding as f32 + 68.0 * scale) as i32,
435                nav_y,
436                "^",
437                can_up,
438                colors,
439                font,
440                scale,
441            );
442            // Home button
443            draw_nav_button(
444                canvas,
445                (padding as f32 + 104.0 * scale) as i32,
446                nav_y,
447                "~",
448                true,
449                colors,
450                font,
451                scale,
452            );
453            // Hidden files toggle
454            let toggle_x = (padding as f32 + 150.0 * scale) as i32;
455            draw_toggle(
456                canvas,
457                toggle_x,
458                nav_y,
459                ".*",
460                show_hidden,
461                colors,
462                font,
463                scale,
464            );
465
466            // Search input
467            search_input.draw_to(canvas, colors, font);
468
469            // Sidebar
470            let sidebar_bg = darken(colors.window_bg, 0.02);
471            canvas.fill_rounded_rect(
472                sidebar_x as f32,
473                sidebar_y as f32,
474                sidebar_width as f32,
475                sidebar_h as f32,
476                6.0 * scale,
477                sidebar_bg,
478            );
479
480            // ===== PLACES SECTION =====
481            draw_section_header(
482                canvas,
483                sidebar_x,
484                sidebar_y + (8.0 * scale) as i32,
485                "PLACES",
486                colors,
487                font,
488                scale,
489            );
490
491            let places_items_start_y =
492                sidebar_y + (8.0 * scale) as i32 + section_header_height as i32;
493            for (i, qa) in quick_access.iter().enumerate() {
494                let y = places_items_start_y + (i as i32 * item_height_scaled as i32);
495                let is_hovered = hovered_quick_access == Some(i);
496                let is_current = qa.path == current_dir;
497
498                if is_current {
499                    canvas.fill_rounded_rect(
500                        (sidebar_x + (4.0 * scale) as i32) as f32,
501                        y as f32,
502                        (sidebar_width - (8.0 * scale) as u32) as f32,
503                        28.0 * scale,
504                        4.0 * scale,
505                        colors.input_border_focused,
506                    );
507                } else if is_hovered {
508                    canvas.fill_rounded_rect(
509                        (sidebar_x + (4.0 * scale) as i32) as f32,
510                        y as f32,
511                        (sidebar_width - (8.0 * scale) as u32) as f32,
512                        28.0 * scale,
513                        4.0 * scale,
514                        darken(colors.window_bg, 0.05),
515                    );
516                }
517
518                draw_quick_access_icon(
519                    canvas,
520                    sidebar_x + (12.0 * scale) as i32,
521                    y + (4.0 * scale) as i32,
522                    qa.icon,
523                    colors,
524                    scale,
525                );
526
527                let text_color = if is_current {
528                    rgb(255, 255, 255)
529                } else {
530                    colors.text
531                };
532                let name_canvas = font.render(qa.name).with_color(text_color).finish();
533                canvas.draw_canvas(
534                    &name_canvas,
535                    sidebar_x + (36.0 * scale) as i32,
536                    y + (6.0 * scale) as i32,
537                );
538            }
539
540            // ===== DRIVES SECTION =====
541            if !mounted_drives.is_empty() {
542                let drives_section_y = places_items_start_y
543                    + (quick_access.len() as i32 * item_height_scaled as i32)
544                    + gap_between_sections as i32;
545
546                draw_section_header(
547                    canvas,
548                    sidebar_x,
549                    drives_section_y,
550                    "DRIVES",
551                    colors,
552                    font,
553                    scale,
554                );
555
556                let drives_items_start_y = drives_section_y + section_header_height as i32;
557                for (i, drive) in mounted_drives.iter().enumerate() {
558                    let y = drives_items_start_y + (i as i32 * item_height_scaled as i32);
559                    let is_hovered = hovered_drive == Some(i);
560                    let is_current = drive.mount_point == current_dir;
561
562                    if is_current {
563                        canvas.fill_rounded_rect(
564                            (sidebar_x + (4.0 * scale) as i32) as f32,
565                            y as f32,
566                            (sidebar_width - (8.0 * scale) as u32) as f32,
567                            28.0 * scale,
568                            4.0 * scale,
569                            colors.input_border_focused,
570                        );
571                    } else if is_hovered {
572                        canvas.fill_rounded_rect(
573                            (sidebar_x + (4.0 * scale) as i32) as f32,
574                            y as f32,
575                            (sidebar_width - (8.0 * scale) as u32) as f32,
576                            28.0 * scale,
577                            4.0 * scale,
578                            darken(colors.window_bg, 0.05),
579                        );
580                    }
581
582                    let icon = get_mount_icon(&drive.device);
583                    draw_mount_icon(
584                        canvas,
585                        sidebar_x + (12.0 * scale) as i32,
586                        y + (6.0 * scale) as i32,
587                        icon,
588                        colors,
589                        scale,
590                    );
591
592                    let display_name = drive.label.as_deref().unwrap_or_else(|| {
593                        drive
594                            .mount_point
595                            .file_name()
596                            .and_then(|n| n.to_str())
597                            .unwrap_or(&drive.device)
598                    });
599                    let truncated_name = truncate_name(display_name, 18);
600
601                    let text_color = if is_current {
602                        rgb(255, 255, 255)
603                    } else {
604                        colors.text
605                    };
606                    let name_canvas = font.render(&truncated_name).with_color(text_color).finish();
607                    canvas.draw_canvas(
608                        &name_canvas,
609                        sidebar_x + (36.0 * scale) as i32,
610                        y + (6.0 * scale) as i32,
611                    );
612                }
613            }
614
615            // Main area background
616            canvas.fill_rounded_rect(
617                main_x as f32,
618                main_y as f32,
619                main_w as f32,
620                main_h as f32,
621                6.0 * scale,
622                colors.input_bg,
623            );
624
625            // Path bar (breadcrumbs)
626            draw_breadcrumbs(
627                canvas,
628                main_x + (8.0 * scale) as i32,
629                main_y + (6.0 * scale) as i32,
630                main_w - (16.0 * scale) as u32,
631                current_dir,
632                colors,
633                font,
634            );
635
636            // Column headers
637            let header_y = main_y + path_bar_height as i32;
638            let header_bg = darken(colors.input_bg, 0.03);
639            canvas.fill_rect(
640                main_x as f32,
641                header_y as f32,
642                main_w as f32,
643                26.0 * scale,
644                header_bg,
645            );
646
647            let header_text = rgb(150, 150, 150);
648            let name_header = font.render("Name").with_color(header_text).finish();
649            canvas.draw_canvas(
650                &name_header,
651                main_x + (32.0 * scale) as i32,
652                header_y + (5.0 * scale) as i32,
653            );
654            let size_header = font.render("Size").with_color(header_text).finish();
655            canvas.draw_canvas(
656                &size_header,
657                main_x + name_col_width as i32 + (8.0 * scale) as i32,
658                header_y + (5.0 * scale) as i32,
659            );
660            let date_header = font.render("Modified").with_color(header_text).finish();
661            canvas.draw_canvas(
662                &date_header,
663                main_x + name_col_width as i32 + size_col_width as i32 + (16.0 * scale) as i32,
664                header_y + (5.0 * scale) as i32,
665            );
666
667            // Separator line
668            canvas.fill_rect(
669                main_x as f32,
670                (header_y + (26.0 * scale) as i32) as f32,
671                main_w as f32,
672                1.0,
673                colors.input_border,
674            );
675
676            // File list
677            let list_x = main_x;
678            for (vi, &ei) in filtered_entries
679                .iter()
680                .skip(scroll_offset)
681                .take(visible_items)
682                .enumerate()
683            {
684                let entry = &all_entries[ei];
685                let y = list_y + (vi as u32 * item_height) as i32;
686                let is_selected = selected_indices.contains(&ei);
687                let is_hovered = hovered_entry == Some(ei);
688
689                // Alternating background
690                let row_bg = if vi % 2 == 1 {
691                    darken(colors.input_bg, 0.02)
692                } else {
693                    colors.input_bg
694                };
695
696                // Selection/hover highlight
697                if is_selected {
698                    canvas.fill_rect(
699                        (list_x + 2) as f32,
700                        y as f32,
701                        (main_w - 4) as f32,
702                        item_height as f32,
703                        colors.input_border_focused,
704                    );
705                } else if is_hovered {
706                    canvas.fill_rect(
707                        (list_x + 2) as f32,
708                        y as f32,
709                        (main_w - 4) as f32,
710                        item_height as f32,
711                        darken(colors.input_bg, 0.06),
712                    );
713                } else {
714                    canvas.fill_rect(
715                        list_x as f32,
716                        y as f32,
717                        main_w as f32,
718                        item_height as f32,
719                        row_bg,
720                    );
721                }
722
723                // Icon
724                let icon_x = list_x + (8.0 * scale) as i32;
725                let icon_y = y + (4.0 * scale) as i32;
726                if entry.is_dir {
727                    draw_folder_icon(canvas, icon_x, icon_y, colors, scale);
728                } else {
729                    draw_file_icon(canvas, icon_x, icon_y, &entry.name, colors, scale);
730                }
731
732                // Name
733                let text_color = if is_selected {
734                    rgb(255, 255, 255)
735                } else {
736                    colors.text
737                };
738                let display_name = truncate_name(&entry.name, 35);
739                let name_canvas = font.render(&display_name).with_color(text_color).finish();
740                canvas.draw_canvas(
741                    &name_canvas,
742                    list_x + (32.0 * scale) as i32,
743                    y + (6.0 * scale) as i32,
744                );
745
746                // Size (for files)
747                if !entry.is_dir {
748                    let size_str = format_size(entry.size);
749                    let size_color = if is_selected {
750                        rgb(220, 220, 220)
751                    } else {
752                        rgb(140, 140, 140)
753                    };
754                    let size_canvas = font.render(&size_str).with_color(size_color).finish();
755                    canvas.draw_canvas(
756                        &size_canvas,
757                        list_x + name_col_width as i32 + (8.0 * scale) as i32,
758                        y + (6.0 * scale) as i32,
759                    );
760                }
761
762                // Date
763                let date_str = format_date(entry.modified);
764                let date_color = if is_selected {
765                    rgb(220, 220, 220)
766                } else {
767                    rgb(140, 140, 140)
768                };
769                let date_canvas = font.render(&date_str).with_color(date_color).finish();
770                canvas.draw_canvas(
771                    &date_canvas,
772                    list_x + name_col_width as i32 + size_col_width as i32 + (16.0 * scale) as i32,
773                    y + (6.0 * scale) as i32,
774                );
775            }
776
777            // Scrollbar
778            if filtered_entries.len() > visible_items {
779                let scrollbar_width = if scrollbar_hovered {
780                    12.0 * scale
781                } else {
782                    8.0 * scale
783                };
784                let scrollbar_x = main_x + main_w as i32 - scrollbar_width as i32;
785                let scrollbar_h = list_h as f32;
786                let thumb_h = (visible_items as f32 / filtered_entries.len() as f32 * scrollbar_h)
787                    .max(20.0 * scale);
788                let thumb_y = scroll_offset as f32 / filtered_entries.len() as f32 * scrollbar_h;
789
790                // Track
791                canvas.fill_rounded_rect(
792                    scrollbar_x as f32,
793                    list_y as f32,
794                    scrollbar_width - 2.0 * scale,
795                    scrollbar_h,
796                    3.0 * scale,
797                    darken(colors.input_bg, 0.05),
798                );
799                // Thumb
800                canvas.fill_rounded_rect(
801                    scrollbar_x as f32,
802                    list_y as f32 + thumb_y,
803                    scrollbar_width - 2.0 * scale,
804                    thumb_h,
805                    3.0 * scale,
806                    if scrollbar_hovered {
807                        colors.input_border_focused
808                    } else {
809                        colors.input_border
810                    },
811                );
812            }
813
814            // Border
815            canvas.stroke_rounded_rect(
816                main_x as f32,
817                main_y as f32,
818                main_w as f32,
819                main_h as f32,
820                6.0 * scale,
821                colors.input_border,
822                1.0,
823            );
824
825            // Filename input (save mode): label above, input below
826            if let Some(fi) = filename_input {
827                let label = title;
828                let label_canvas = font.render(label).with_color(colors.text).finish();
829                canvas.draw_canvas(&label_canvas, main_x, filename_y + (2.0 * scale) as i32);
830                fi.draw_to(canvas, colors, font);
831            }
832
833            // Buttons
834            ok_button.draw_to(canvas, colors, font);
835            cancel_button.draw_to(canvas, colors, font);
836
837            // Status bar
838            let status = format!("{} items", filtered_entries.len());
839            let status_canvas = font.render(&status).with_color(rgb(120, 120, 120)).finish();
840            canvas.draw_canvas(&status_canvas, main_x, button_y + (8.0 * scale) as i32);
841        };
842
843        // Initial draw
844        draw(
845            &mut canvas,
846            colors,
847            &font,
848            &current_dir,
849            &quick_access,
850            &all_entries,
851            &filtered_entries,
852            &selected_indices,
853            scroll_offset,
854            hovered_quick_access,
855            hovered_entry,
856            show_hidden,
857            &search_input,
858            &ok_button,
859            &cancel_button,
860            &history,
861            history_index,
862            &mounted_drives,
863            hovered_drive,
864            scale,
865            scrollbar_hovered,
866            filename_input.as_ref(),
867        );
868        if save_mode && !completion_matches.is_empty() {
869            let visible = completion_matches.len().min(MAX_POPUP_ITEMS);
870            let popup_h = (visible as i32) * POPUP_ITEM_HEIGHT + 2;
871            draw_completion_popup(
872                &mut canvas,
873                &font,
874                colors,
875                &completion_matches,
876                completion_popup_index,
877                main_x,
878                filename_y + filename_label_h - popup_h,
879                main_w,
880            );
881        }
882        if !search_matches.is_empty() && search_input.has_focus() {
883            draw_completion_popup(
884                &mut canvas,
885                &font,
886                colors,
887                &search_matches,
888                search_popup_index,
889                search_x,
890                search_y + 32,
891                search_width,
892            );
893        }
894        window.set_contents(&canvas)?;
895        window.show()?;
896
897        // Event loop
898        loop {
899            let event = window.wait_for_event()?;
900            let mut needs_redraw = false;
901
902            match &event {
903                WindowEvent::CloseRequested => return Ok(FileSelectResult::Closed),
904                WindowEvent::RedrawRequested => needs_redraw = true,
905                WindowEvent::CursorEnter(pos) | WindowEvent::CursorMove(pos) => {
906                    if window_dragging {
907                        let _ = window.start_drag();
908                        window_dragging = false;
909                    }
910
911                    mouse_x = pos.x as i32;
912                    mouse_y = pos.y as i32;
913
914                    // Handle scrollbar thumb dragging
915                    if thumb_drag && !filtered_entries.is_empty() {
916                        let scrollbar_y = list_y;
917
918                        if mouse_x >= main_x
919                            && mouse_x < main_x + main_w as i32
920                            && mouse_y >= list_y
921                            && mouse_y < list_y + list_h as i32
922                        {
923                            let visible_items = (list_h / item_height) as usize;
924                            let total_items = filtered_entries.len();
925                            let max_scroll = total_items.saturating_sub(visible_items);
926
927                            if max_scroll > 0 {
928                                let scrollbar_h_f32 = list_h as f32 - 8.0 * scale;
929                                let thumb_h_f32 = (visible_items as f32 / total_items as f32
930                                    * scrollbar_h_f32)
931                                    .max(20.0 * scale);
932                                let thumb_h = thumb_h_f32 as i32;
933                                let max_thumb_y = scrollbar_h_f32 as i32 - thumb_h;
934
935                                let offset = thumb_drag_offset.unwrap_or(thumb_h / 2);
936                                let thumb_y =
937                                    (mouse_y - scrollbar_y - offset).clamp(0, max_thumb_y);
938                                let scroll_ratio = if max_thumb_y > 0 {
939                                    thumb_y as f32 / max_thumb_y as f32
940                                } else {
941                                    0.0
942                                };
943                                scroll_offset = ((scroll_ratio * max_scroll as f32) as usize)
944                                    .clamp(0, max_scroll);
945                                needs_redraw = true;
946                            }
947                        }
948                    }
949
950                    // Update hover states (only when not dragging)
951                    if !thumb_drag {
952                        let old_qa = hovered_quick_access;
953                        let old_entry = hovered_entry;
954                        let old_drive = hovered_drive;
955
956                        // Check places hover
957                        hovered_quick_access = None;
958                        hovered_entry = None;
959                        hovered_drive = None;
960
961                        if mouse_x >= sidebar_x
962                            && mouse_x < sidebar_x + sidebar_width as i32
963                            && mouse_y >= sidebar_y
964                        {
965                            let places_items_start_y =
966                                sidebar_y + (8.0 * scale) as i32 + section_header_height as i32;
967                            let rel_y = mouse_y - places_items_start_y;
968                            if rel_y >= 0 {
969                                let idx = (rel_y as f32 / item_height_scaled as f32) as usize;
970                                if idx < quick_access.len() {
971                                    hovered_quick_access = Some(idx);
972                                }
973                            }
974
975                            if !mounted_drives.is_empty() {
976                                let drives_section_y = places_items_start_y
977                                    + (quick_access.len() as i32 * item_height_scaled as i32)
978                                    + gap_between_sections as i32;
979                                let drives_items_start_y =
980                                    drives_section_y + section_header_height as i32;
981                                let rel_y = mouse_y - drives_items_start_y;
982                                if rel_y >= 0 {
983                                    let idx = (rel_y as f32 / item_height_scaled as f32) as usize;
984                                    if idx < mounted_drives.len() {
985                                        hovered_drive = Some(idx);
986                                    }
987                                }
988                            }
989                        }
990
991                        // Check file list hover (only if not over scrollbar)
992                        let scrollbar_width = if scrollbar_hovered {
993                            12.0 * scale
994                        } else {
995                            8.0 * scale
996                        };
997                        let scrollbar_x = main_x + main_w as i32 - scrollbar_width as i32;
998
999                        // Update scrollbar hover state
1000                        scrollbar_hovered = mouse_x >= scrollbar_x
1001                            && mouse_x < main_x + main_w as i32
1002                            && mouse_y >= list_y
1003                            && mouse_y < list_y + list_h as i32
1004                            && !filtered_entries.is_empty();
1005
1006                        if mouse_x >= main_x
1007                            && mouse_x < scrollbar_x
1008                            && mouse_y >= list_y
1009                            && mouse_y < list_y + list_h as i32
1010                        {
1011                            let rel_y = (mouse_y - list_y) as usize;
1012                            let idx = scroll_offset + rel_y / item_height as usize;
1013                            if idx < filtered_entries.len() {
1014                                hovered_entry = Some(filtered_entries[idx]);
1015                            }
1016                        }
1017
1018                        if old_qa != hovered_quick_access
1019                            || old_entry != hovered_entry
1020                            || old_drive != hovered_drive
1021                        {
1022                            needs_redraw = true;
1023                        }
1024                    }
1025                }
1026                WindowEvent::ButtonPress(MouseButton::Left, _) => {
1027                    window_dragging = true;
1028                    let mut clicking_scrollbar = false;
1029
1030                    // Check if clicking anywhere in scrollbar area (thumb OR track)
1031                    if !filtered_entries.is_empty() {
1032                        let scrollbar_width = if scrollbar_hovered {
1033                            12.0 * scale
1034                        } else {
1035                            8.0 * scale
1036                        };
1037                        let scrollbar_x = main_x + main_w as i32 - scrollbar_width as i32;
1038
1039                        // Block all clicks in scrollbar area
1040                        if mouse_x >= scrollbar_x
1041                            && mouse_x < main_x + main_w as i32
1042                            && mouse_y >= list_y
1043                            && mouse_y < list_y + list_h as i32
1044                        {
1045                            clicking_scrollbar = true;
1046
1047                            // Now check if clicking specifically on the thumb for dragging
1048                            let scrollbar_y = list_y;
1049                            let visible_items = (list_h / item_height) as usize;
1050                            let total_items = filtered_entries.len();
1051
1052                            if visible_items < total_items {
1053                                let scrollbar_h_f32 = list_h as f32 - 8.0 * scale;
1054                                let thumb_h_f32 = (visible_items as f32 / total_items as f32
1055                                    * scrollbar_h_f32)
1056                                    .max(20.0 * scale);
1057                                let thumb_h = thumb_h_f32 as i32;
1058
1059                                let max_scroll = total_items - visible_items;
1060                                let max_thumb_y = scrollbar_h_f32 as i32 - thumb_h;
1061                                let thumb_y = if max_thumb_y > 0 {
1062                                    ((scroll_offset as f32 / max_scroll as f32)
1063                                        * max_thumb_y as f32)
1064                                        as i32
1065                                } else {
1066                                    0
1067                                };
1068
1069                                let rel_y = mouse_y - scrollbar_y;
1070                                if mouse_x >= scrollbar_x
1071                                    && mouse_x < scrollbar_x + scrollbar_width as i32
1072                                    && rel_y >= scrollbar_y as i32 + thumb_y
1073                                    && rel_y < scrollbar_y as i32 + thumb_y + thumb_h
1074                                {
1075                                    thumb_drag = true;
1076                                    thumb_drag_offset = Some(mouse_y - (scrollbar_y + thumb_y));
1077                                }
1078                            }
1079                        }
1080                    }
1081
1082                    // Toolbar buttons
1083                    let nav_y = padding as i32 + (4.0 * scale) as i32;
1084                    let btn_size = (28.0 * scale) as i32;
1085                    if mouse_y >= nav_y && mouse_y < nav_y + btn_size {
1086                        // Back
1087                        if mouse_x >= padding as i32 && mouse_x < padding as i32 + btn_size {
1088                            if history_index > 0 {
1089                                history_index -= 1;
1090                                navigate_to_directory(
1091                                    history[history_index].clone(),
1092                                    &mut current_dir,
1093                                    &mut history,
1094                                    &mut history_index,
1095                                    &mut all_entries,
1096                                    self.directory,
1097                                    show_hidden,
1098                                    &search_text,
1099                                    &mut filtered_entries,
1100                                    &mut selected_indices,
1101                                    &mut scroll_offset,
1102                                    &self.filters,
1103                                );
1104                                needs_redraw = true;
1105                            }
1106                        }
1107                        // Forward
1108                        else if mouse_x >= (padding as f32 + 32.0 * scale) as i32
1109                            && mouse_x < (padding as f32 + 60.0 * scale) as i32
1110                        {
1111                            if history_index + 1 < history.len() {
1112                                history_index += 1;
1113                                navigate_to_directory(
1114                                    history[history_index].clone(),
1115                                    &mut current_dir,
1116                                    &mut history,
1117                                    &mut history_index,
1118                                    &mut all_entries,
1119                                    self.directory,
1120                                    show_hidden,
1121                                    &search_text,
1122                                    &mut filtered_entries,
1123                                    &mut selected_indices,
1124                                    &mut scroll_offset,
1125                                    &self.filters,
1126                                );
1127                                needs_redraw = true;
1128                            }
1129                        }
1130                        // Up
1131                        else if mouse_x >= (padding as f32 + 68.0 * scale) as i32
1132                            && mouse_x < (padding as f32 + 96.0 * scale) as i32
1133                        {
1134                            if let Some(parent) = current_dir.parent() {
1135                                navigate_to_directory(
1136                                    parent.to_path_buf(),
1137                                    &mut current_dir,
1138                                    &mut history,
1139                                    &mut history_index,
1140                                    &mut all_entries,
1141                                    self.directory,
1142                                    show_hidden,
1143                                    &search_text,
1144                                    &mut filtered_entries,
1145                                    &mut selected_indices,
1146                                    &mut scroll_offset,
1147                                    &self.filters,
1148                                );
1149                                needs_redraw = true;
1150                            }
1151                        }
1152                        // Home
1153                        else if mouse_x >= (padding as f32 + 104.0 * scale) as i32
1154                            && mouse_x < (padding as f32 + 132.0 * scale) as i32
1155                        {
1156                            if let Some(home) = dirs::home_dir() {
1157                                navigate_to_directory(
1158                                    home,
1159                                    &mut current_dir,
1160                                    &mut history,
1161                                    &mut history_index,
1162                                    &mut all_entries,
1163                                    self.directory,
1164                                    show_hidden,
1165                                    &search_text,
1166                                    &mut filtered_entries,
1167                                    &mut selected_indices,
1168                                    &mut scroll_offset,
1169                                    &self.filters,
1170                                );
1171                                needs_redraw = true;
1172                            }
1173                        }
1174                        // Hidden toggle
1175                        else if mouse_x >= (padding as f32 + 150.0 * scale) as i32
1176                            && mouse_x < (padding as f32 + 178.0 * scale) as i32
1177                        {
1178                            show_hidden = !show_hidden;
1179                            load_directory(
1180                                &current_dir,
1181                                &mut all_entries,
1182                                self.directory,
1183                                show_hidden,
1184                            );
1185                            update_filtered(
1186                                &all_entries,
1187                                &search_text,
1188                                &mut filtered_entries,
1189                                &self.filters,
1190                            );
1191                            selected_indices.clear();
1192                            scroll_offset = 0;
1193                            needs_redraw = true;
1194                        }
1195                    }
1196
1197                    // Quick access click
1198                    if !clicking_scrollbar {
1199                        if let Some(idx) = hovered_quick_access {
1200                            let qa = &quick_access[idx];
1201                            navigate_to_directory(
1202                                qa.path.clone(),
1203                                &mut current_dir,
1204                                &mut history,
1205                                &mut history_index,
1206                                &mut all_entries,
1207                                self.directory,
1208                                show_hidden,
1209                                &search_text,
1210                                &mut filtered_entries,
1211                                &mut selected_indices,
1212                                &mut scroll_offset,
1213                                &self.filters,
1214                            );
1215                            needs_redraw = true;
1216                        }
1217
1218                        // Drive click
1219                        if let Some(idx) = hovered_drive {
1220                            let drive = &mounted_drives[idx];
1221                            navigate_to_directory(
1222                                drive.mount_point.clone(),
1223                                &mut current_dir,
1224                                &mut history,
1225                                &mut history_index,
1226                                &mut all_entries,
1227                                self.directory,
1228                                show_hidden,
1229                                &search_text,
1230                                &mut filtered_entries,
1231                                &mut selected_indices,
1232                                &mut scroll_offset,
1233                                &self.filters,
1234                            );
1235                            needs_redraw = true;
1236                        }
1237
1238                        // File list click
1239                        if let Some(ei) = hovered_entry {
1240                            if self.multiple {
1241                                // Toggle selection in multiple mode
1242                                if selected_indices.contains(&ei) {
1243                                    selected_indices.remove(&ei);
1244                                } else {
1245                                    selected_indices.insert(ei);
1246                                }
1247                            } else {
1248                                // Single click - activate if already selected (double click behavior)
1249                                if selected_indices.contains(&ei) {
1250                                    let entry = &all_entries[ei];
1251                                    if entry.is_dir {
1252                                        navigate_to(
1253                                            entry.path.clone(),
1254                                            &mut current_dir,
1255                                            &mut history,
1256                                            &mut history_index,
1257                                        );
1258                                        load_directory(
1259                                            &current_dir,
1260                                            &mut all_entries,
1261                                            self.directory,
1262                                            show_hidden,
1263                                        );
1264                                        update_filtered(
1265                                            &all_entries,
1266                                            &search_text,
1267                                            &mut filtered_entries,
1268                                            &self.filters,
1269                                        );
1270                                        selected_indices.clear();
1271                                        scroll_offset = 0;
1272                                    } else if save_mode {
1273                                        // In save mode, double-click on file populates filename
1274                                        if let Some(ref mut fi) = filename_input {
1275                                            fi.set_text(&entry.name);
1276                                            completion_matches.clear();
1277                                            completion_popup_index = 0;
1278                                        }
1279                                    } else if !self.directory {
1280                                        return Ok(FileSelectResult::Selected(entry.path.clone()));
1281                                    }
1282                                } else {
1283                                    selected_indices.clear();
1284                                    selected_indices.insert(ei);
1285                                    // In save mode, single click on file populates filename input
1286                                    if save_mode {
1287                                        let entry = &all_entries[ei];
1288                                        if !entry.is_dir {
1289                                            if let Some(ref mut fi) = filename_input {
1290                                                fi.set_text(&entry.name);
1291                                                completion_matches.clear();
1292                                                completion_popup_index = 0;
1293                                            }
1294                                        }
1295                                    }
1296                                }
1297                            }
1298                            needs_redraw = true;
1299                        }
1300                    }
1301
1302                    // Input focus management
1303                    let in_search = mouse_x >= search_x
1304                        && mouse_x < search_x + search_width as i32
1305                        && mouse_y >= search_y
1306                        && mouse_y < search_y + (32.0 * scale) as i32;
1307
1308                    if save_mode {
1309                        // In save mode, filename input keeps focus unless search is clicked
1310                        if in_search {
1311                            search_input.set_focus(true);
1312                            if let Some(ref mut fi) = filename_input {
1313                                fi.set_focus(false);
1314                            }
1315                            // Clear filename popup when switching to search
1316                            completion_matches.clear();
1317                            completion_popup_index = 0;
1318                        } else {
1319                            search_input.set_focus(false);
1320                            if let Some(ref mut fi) = filename_input {
1321                                fi.set_focus(true);
1322                            }
1323                            // Clear search popup when switching to filename
1324                            search_matches.clear();
1325                            search_popup_index = 0;
1326                            search_input.set_completion(None);
1327                        }
1328                    } else {
1329                        if !in_search && !search_matches.is_empty() {
1330                            search_matches.clear();
1331                            search_popup_index = 0;
1332                            search_input.set_completion(None);
1333                        }
1334                        search_input.set_focus(in_search);
1335                    }
1336                }
1337                WindowEvent::ButtonRelease(_, _) => {
1338                    window_dragging = false;
1339                    thumb_drag = false;
1340                    thumb_drag_offset = None;
1341                }
1342                WindowEvent::Scroll(direction) => {
1343                    match direction {
1344                        crate::backend::ScrollDirection::Up => {
1345                            if scroll_offset > 0 {
1346                                scroll_offset = scroll_offset.saturating_sub(3);
1347                                needs_redraw = true;
1348                            }
1349                        }
1350                        crate::backend::ScrollDirection::Down => {
1351                            if scroll_offset + visible_items < filtered_entries.len() {
1352                                scroll_offset = (scroll_offset + 3)
1353                                    .min(filtered_entries.len().saturating_sub(visible_items));
1354                                needs_redraw = true;
1355                            }
1356                        }
1357                        _ => {}
1358                    }
1359                }
1360                WindowEvent::KeyPress(key_event) => {
1361                    let filename_has_focus =
1362                        filename_input.as_ref().map_or(false, |fi| fi.has_focus());
1363
1364                    if key_event.keysym == KEY_ESCAPE {
1365                        if search_input.has_focus() {
1366                            if !search_matches.is_empty() {
1367                                // Close search popup first
1368                                search_matches.clear();
1369                                search_popup_index = 0;
1370                                search_input.set_completion(None);
1371                            } else {
1372                                search_input.set_focus(false);
1373                                // In save mode, return focus to filename input
1374                                if let Some(ref mut fi) = filename_input {
1375                                    fi.set_focus(true);
1376                                }
1377                            }
1378                            needs_redraw = true;
1379                        } else if filename_has_focus {
1380                            if !completion_matches.is_empty() {
1381                                // Close popup first
1382                                completion_matches.clear();
1383                                completion_popup_index = 0;
1384                                if let Some(ref mut fi) = filename_input {
1385                                    fi.set_completion(None);
1386                                }
1387                            } else {
1388                                if let Some(ref mut fi) = filename_input {
1389                                    fi.set_focus(false);
1390                                }
1391                            }
1392                            needs_redraw = true;
1393                        } else {
1394                            return Ok(FileSelectResult::Cancelled);
1395                        }
1396                    }
1397                    if !search_input.has_focus() && !filename_has_focus {
1398                        match key_event.keysym {
1399                            KEY_UP => {
1400                                if !filtered_entries.is_empty() {
1401                                    let new_index =
1402                                        if let Some(&sel) = selected_indices.iter().next() {
1403                                            if let Some(pos) =
1404                                                filtered_entries.iter().position(|&e| e == sel)
1405                                            {
1406                                                if pos > 0 {
1407                                                    Some(filtered_entries[pos - 1])
1408                                                } else {
1409                                                    Some(sel)
1410                                                }
1411                                            } else {
1412                                                Some(filtered_entries[0])
1413                                            }
1414                                        } else {
1415                                            Some(filtered_entries[0])
1416                                        };
1417
1418                                    if let Some(idx) = new_index {
1419                                        if self.multiple {
1420                                            if selected_indices.contains(&idx) {
1421                                                selected_indices.remove(&idx);
1422                                            } else {
1423                                                selected_indices.insert(idx);
1424                                            }
1425                                        } else {
1426                                            selected_indices.clear();
1427                                            selected_indices.insert(idx);
1428                                        }
1429
1430                                        if let Some(pos) =
1431                                            filtered_entries.iter().position(|&e| e == idx)
1432                                        {
1433                                            if pos < scroll_offset {
1434                                                scroll_offset = pos;
1435                                            }
1436                                        }
1437                                        needs_redraw = true;
1438                                    }
1439                                }
1440                            }
1441                            KEY_DOWN => {
1442                                if !filtered_entries.is_empty() {
1443                                    let new_index =
1444                                        if let Some(&sel) = selected_indices.iter().next() {
1445                                            if let Some(pos) =
1446                                                filtered_entries.iter().position(|&e| e == sel)
1447                                            {
1448                                                if pos + 1 < filtered_entries.len() {
1449                                                    Some(filtered_entries[pos + 1])
1450                                                } else {
1451                                                    Some(sel)
1452                                                }
1453                                            } else {
1454                                                Some(filtered_entries[0])
1455                                            }
1456                                        } else {
1457                                            Some(filtered_entries[0])
1458                                        };
1459
1460                                    if let Some(idx) = new_index {
1461                                        if self.multiple {
1462                                            if selected_indices.contains(&idx) {
1463                                                selected_indices.remove(&idx);
1464                                            } else {
1465                                                selected_indices.insert(idx);
1466                                            }
1467                                        } else {
1468                                            selected_indices.clear();
1469                                            selected_indices.insert(idx);
1470                                        }
1471
1472                                        if let Some(pos) =
1473                                            filtered_entries.iter().position(|&e| e == idx)
1474                                        {
1475                                            if pos + 1 >= scroll_offset + visible_items {
1476                                                scroll_offset = pos + 1 - visible_items + 1;
1477                                            }
1478                                        }
1479                                        needs_redraw = true;
1480                                    }
1481                                }
1482                            }
1483                            KEY_RETURN => {
1484                                if self.multiple && !selected_indices.is_empty() {
1485                                    let selected_files: Vec<PathBuf> = selected_indices
1486                                        .iter()
1487                                        .filter(|&ei| !all_entries[*ei].is_dir)
1488                                        .map(|&ei| all_entries[ei].path.clone())
1489                                        .collect();
1490                                    if !selected_files.is_empty() {
1491                                        return Ok(FileSelectResult::SelectedMultiple(
1492                                            selected_files,
1493                                        ));
1494                                    }
1495                                } else if let Some(&sel) = selected_indices.iter().next() {
1496                                    let entry = &all_entries[sel];
1497                                    if entry.is_dir {
1498                                        navigate_to_directory(
1499                                            entry.path.clone(),
1500                                            &mut current_dir,
1501                                            &mut history,
1502                                            &mut history_index,
1503                                            &mut all_entries,
1504                                            self.directory,
1505                                            show_hidden,
1506                                            &search_text,
1507                                            &mut filtered_entries,
1508                                            &mut selected_indices,
1509                                            &mut scroll_offset,
1510                                            &self.filters,
1511                                        );
1512                                        needs_redraw = true;
1513                                    } else if !self.directory {
1514                                        return Ok(FileSelectResult::Selected(entry.path.clone()));
1515                                    }
1516                                }
1517                            }
1518                            KEY_BACKSPACE => {
1519                                if let Some(parent) = current_dir.parent() {
1520                                    navigate_to_directory(
1521                                        parent.to_path_buf(),
1522                                        &mut current_dir,
1523                                        &mut history,
1524                                        &mut history_index,
1525                                        &mut all_entries,
1526                                        self.directory,
1527                                        show_hidden,
1528                                        &search_text,
1529                                        &mut filtered_entries,
1530                                        &mut selected_indices,
1531                                        &mut scroll_offset,
1532                                        &self.filters,
1533                                    );
1534                                    needs_redraw = true;
1535                                }
1536                            }
1537                            _ => {}
1538                        }
1539                    }
1540                }
1541                _ => {}
1542            }
1543
1544            // Process search input (with completion popup)
1545            {
1546                let mut search_popup_handled = false;
1547
1548                // Handle search popup keyboard navigation
1549                if !search_matches.is_empty() && search_input.has_focus() {
1550                    if let WindowEvent::KeyPress(key_event) = &event {
1551                        const POPUP_KEY_UP: u32 = 0xff52;
1552                        const POPUP_KEY_DOWN: u32 = 0xff54;
1553                        match key_event.keysym {
1554                            POPUP_KEY_UP => {
1555                                if search_popup_index > 0 {
1556                                    search_popup_index -= 1;
1557                                } else {
1558                                    search_popup_index = search_matches.len() - 1;
1559                                }
1560                                let text = search_input.text().to_string();
1561                                let name = &search_matches[search_popup_index];
1562                                if name.to_lowercase().starts_with(&text.to_lowercase()) {
1563                                    let pc = text.chars().count();
1564                                    search_input
1565                                        .set_completion(Some(name.chars().skip(pc).collect()));
1566                                } else {
1567                                    search_input.set_completion(None);
1568                                }
1569                                needs_redraw = true;
1570                                search_popup_handled = true;
1571                            }
1572                            POPUP_KEY_DOWN => {
1573                                search_popup_index =
1574                                    (search_popup_index + 1) % search_matches.len();
1575                                let text = search_input.text().to_string();
1576                                let name = &search_matches[search_popup_index];
1577                                if name.to_lowercase().starts_with(&text.to_lowercase()) {
1578                                    let pc = text.chars().count();
1579                                    search_input
1580                                        .set_completion(Some(name.chars().skip(pc).collect()));
1581                                } else {
1582                                    search_input.set_completion(None);
1583                                }
1584                                needs_redraw = true;
1585                                search_popup_handled = true;
1586                            }
1587                            _ => {}
1588                        }
1589                    }
1590                }
1591
1592                // Handle click on search popup item
1593                if !search_matches.is_empty() && search_input.has_focus() {
1594                    if let WindowEvent::ButtonPress(MouseButton::Left, _) = &event {
1595                        let popup_x = search_x;
1596                        let popup_y = search_y + 32;
1597                        let popup_w = search_width as i32;
1598                        let visible = search_matches.len().min(MAX_POPUP_ITEMS) as i32;
1599                        let popup_h = visible * POPUP_ITEM_HEIGHT + 2;
1600                        if mouse_x >= popup_x
1601                            && mouse_x < popup_x + popup_w
1602                            && mouse_y >= popup_y
1603                            && mouse_y < popup_y + popup_h
1604                        {
1605                            let idx = ((mouse_y - popup_y - 1) / POPUP_ITEM_HEIGHT) as usize;
1606                            if idx < search_matches.len().min(MAX_POPUP_ITEMS) {
1607                                search_input.set_text(&search_matches[idx]);
1608                                search_matches.clear();
1609                                search_popup_index = 0;
1610                                let new_search = search_input.text().to_lowercase();
1611                                if new_search != search_text {
1612                                    search_text = new_search;
1613                                    update_filtered(
1614                                        &all_entries,
1615                                        &search_text,
1616                                        &mut filtered_entries,
1617                                        &self.filters,
1618                                    );
1619                                    selected_indices.clear();
1620                                    scroll_offset = 0;
1621                                }
1622                                needs_redraw = true;
1623                                search_popup_handled = true;
1624                            }
1625                        }
1626                    }
1627                }
1628
1629                if !search_popup_handled {
1630                    let search_text_before = search_input.text().to_string();
1631                    if search_input.process_event(&event) {
1632                        needs_redraw = true;
1633                    }
1634                    let new_search = search_input.text().to_lowercase();
1635                    if new_search != search_text {
1636                        search_text = new_search;
1637                        update_filtered(
1638                            &all_entries,
1639                            &search_text,
1640                            &mut filtered_entries,
1641                            &self.filters,
1642                        );
1643                        selected_indices.clear();
1644                        scroll_offset = 0;
1645                    }
1646                    // Detect text change → recompute search completions
1647                    if search_input.text() != search_text_before {
1648                        search_popup_index = 0;
1649                        let text = search_input.text().to_string();
1650                        search_matches = find_all_completions(
1651                            &all_entries,
1652                            &text,
1653                            MAX_POPUP_ITEMS,
1654                            false,
1655                            false,
1656                        );
1657                        // Only show ghost text if the first match is a prefix match
1658                        if !search_matches.is_empty()
1659                            && search_matches[0]
1660                                .to_lowercase()
1661                                .starts_with(&text.to_lowercase())
1662                        {
1663                            let pc = text.chars().count();
1664                            search_input
1665                                .set_completion(Some(search_matches[0].chars().skip(pc).collect()));
1666                        } else {
1667                            search_input.set_completion(None);
1668                        }
1669                    }
1670                    // Tab pressed → accept highlighted completion
1671                    if search_input.was_tab_pressed() {
1672                        let text = search_input.text().to_string();
1673                        if !text.is_empty() {
1674                            search_matches = find_all_completions(
1675                                &all_entries,
1676                                &text,
1677                                MAX_POPUP_ITEMS,
1678                                false,
1679                                false,
1680                            );
1681                            search_popup_index = 0;
1682                            if !search_matches.is_empty()
1683                                && search_matches[0]
1684                                    .to_lowercase()
1685                                    .starts_with(&text.to_lowercase())
1686                            {
1687                                let pc = text.chars().count();
1688                                search_input.set_completion(Some(
1689                                    search_matches[0].chars().skip(pc).collect(),
1690                                ));
1691                            } else {
1692                                search_input.set_completion(None);
1693                            }
1694                        }
1695                        // Re-filter after tab acceptance changed the text
1696                        let new_search = search_input.text().to_lowercase();
1697                        if new_search != search_text {
1698                            search_text = new_search;
1699                            update_filtered(
1700                                &all_entries,
1701                                &search_text,
1702                                &mut filtered_entries,
1703                                &self.filters,
1704                            );
1705                            selected_indices.clear();
1706                            scroll_offset = 0;
1707                        }
1708                        needs_redraw = true;
1709                    }
1710                    // Enter with popup open -> accept highlighted item
1711                    if search_input.was_submitted() && !search_matches.is_empty() {
1712                        search_input.set_text(&search_matches[search_popup_index]);
1713                        search_matches.clear();
1714                        search_popup_index = 0;
1715                        let new_search = search_input.text().to_lowercase();
1716                        if new_search != search_text {
1717                            search_text = new_search;
1718                            update_filtered(
1719                                &all_entries,
1720                                &search_text,
1721                                &mut filtered_entries,
1722                                &self.filters,
1723                            );
1724                            selected_indices.clear();
1725                            scroll_offset = 0;
1726                        }
1727                        needs_redraw = true;
1728                    }
1729                }
1730            }
1731
1732            // Process filename input (save mode)
1733            if let Some(ref mut fi) = filename_input {
1734                let mut popup_handled = false;
1735
1736                // Handle popup keyboard navigation before passing event to input
1737                if !completion_matches.is_empty() {
1738                    if let WindowEvent::KeyPress(key_event) = &event {
1739                        const POPUP_KEY_UP: u32 = 0xff52;
1740                        const POPUP_KEY_DOWN: u32 = 0xff54;
1741                        match key_event.keysym {
1742                            POPUP_KEY_UP => {
1743                                if completion_popup_index > 0 {
1744                                    completion_popup_index -= 1;
1745                                } else {
1746                                    completion_popup_index = completion_matches.len() - 1;
1747                                }
1748                                let prefix = fi.text().to_string();
1749                                let name = &completion_matches[completion_popup_index];
1750                                let pc = prefix.chars().count();
1751                                fi.set_completion(Some(name.chars().skip(pc).collect()));
1752                                needs_redraw = true;
1753                                popup_handled = true;
1754                            }
1755                            POPUP_KEY_DOWN => {
1756                                completion_popup_index =
1757                                    (completion_popup_index + 1) % completion_matches.len();
1758                                let prefix = fi.text().to_string();
1759                                let name = &completion_matches[completion_popup_index];
1760                                let pc = prefix.chars().count();
1761                                fi.set_completion(Some(name.chars().skip(pc).collect()));
1762                                needs_redraw = true;
1763                                popup_handled = true;
1764                            }
1765                            _ => {}
1766                        }
1767                    }
1768                }
1769
1770                // Handle click on popup item
1771                if !completion_matches.is_empty() {
1772                    if let WindowEvent::ButtonPress(MouseButton::Left, _) = &event {
1773                        let popup_x = main_x;
1774                        let popup_w = main_w as i32;
1775                        let visible = completion_matches.len().min(MAX_POPUP_ITEMS) as i32;
1776                        let popup_h = visible * POPUP_ITEM_HEIGHT + 2;
1777                        let popup_y = filename_y + filename_label_h - popup_h;
1778                        if mouse_x >= popup_x
1779                            && mouse_x < popup_x + popup_w
1780                            && mouse_y >= popup_y
1781                            && mouse_y < popup_y + popup_h
1782                        {
1783                            let idx = ((mouse_y - popup_y - 1) / POPUP_ITEM_HEIGHT) as usize;
1784                            if idx < completion_matches.len().min(MAX_POPUP_ITEMS) {
1785                                fi.set_text(&completion_matches[idx]);
1786                                completion_matches.clear();
1787                                completion_popup_index = 0;
1788                                needs_redraw = true;
1789                                popup_handled = true;
1790                            }
1791                        }
1792                    }
1793                }
1794
1795                if !popup_handled {
1796                    let text_before = fi.text().to_string();
1797                    if fi.process_event(&event) {
1798                        needs_redraw = true;
1799                    }
1800                    // Detect text change -> recompute completions
1801                    if fi.text() != text_before {
1802                        completion_popup_index = 0;
1803                        let prefix = fi.text().to_string();
1804                        completion_matches = find_all_completions(
1805                            &all_entries,
1806                            &prefix,
1807                            MAX_POPUP_ITEMS,
1808                            true,
1809                            true,
1810                        );
1811                        if !completion_matches.is_empty() {
1812                            let pc = prefix.chars().count();
1813                            fi.set_completion(Some(
1814                                completion_matches[0].chars().skip(pc).collect(),
1815                            ));
1816                        } else {
1817                            fi.set_completion(None);
1818                        }
1819                    }
1820                    // Tab pressed -> accept highlighted completion
1821                    if fi.was_tab_pressed() {
1822                        let prefix = fi.text().to_string();
1823                        if !prefix.is_empty() {
1824                            // Recompute matches from new text (Tab may have accepted a suffix)
1825                            completion_matches = find_all_completions(
1826                                &all_entries,
1827                                &prefix,
1828                                MAX_POPUP_ITEMS,
1829                                true,
1830                                true,
1831                            );
1832                            completion_popup_index = 0;
1833                            if !completion_matches.is_empty() {
1834                                let pc = prefix.chars().count();
1835                                fi.set_completion(Some(
1836                                    completion_matches[0].chars().skip(pc).collect(),
1837                                ));
1838                            } else {
1839                                fi.set_completion(None);
1840                            }
1841                        }
1842                        needs_redraw = true;
1843                    }
1844                    if fi.was_submitted() {
1845                        // If popup is open, accept the highlighted item instead of submitting
1846                        if !completion_matches.is_empty() {
1847                            fi.set_text(&completion_matches[completion_popup_index]);
1848                            completion_matches.clear();
1849                            completion_popup_index = 0;
1850                            needs_redraw = true;
1851                        } else {
1852                            let name = fi.text().trim().to_string();
1853                            if !name.is_empty() {
1854                                return Ok(FileSelectResult::Selected(current_dir.join(&name)));
1855                            }
1856                        }
1857                    }
1858                }
1859            }
1860
1861            // Process buttons
1862            needs_redraw |= ok_button.process_event(&event);
1863            needs_redraw |= cancel_button.process_event(&event);
1864
1865            if ok_button.was_clicked() {
1866                // In save mode, use filename input text
1867                if save_mode {
1868                    if let Some(ref fi) = filename_input {
1869                        let name = fi.text().trim().to_string();
1870                        if !name.is_empty() {
1871                            return Ok(FileSelectResult::Selected(current_dir.join(&name)));
1872                        }
1873                    }
1874                } else if self.multiple && !selected_indices.is_empty() {
1875                    let selected_files: Vec<PathBuf> = selected_indices
1876                        .iter()
1877                        .filter(|&ei| !all_entries[*ei].is_dir)
1878                        .map(|&ei| all_entries[ei].path.clone())
1879                        .collect();
1880                    if !selected_files.is_empty() {
1881                        return Ok(FileSelectResult::SelectedMultiple(selected_files));
1882                    }
1883                } else if let Some(&sel) = selected_indices.iter().next() {
1884                    let entry = &all_entries[sel];
1885                    return Ok(FileSelectResult::Selected(entry.path.clone()));
1886                } else if self.directory {
1887                    return Ok(FileSelectResult::Selected(current_dir.clone()));
1888                }
1889            }
1890
1891            if cancel_button.was_clicked() {
1892                return Ok(FileSelectResult::Cancelled);
1893            }
1894
1895            // Batch pending events
1896            while let Some(ev) = window.poll_for_event()? {
1897                match &ev {
1898                    WindowEvent::CloseRequested => {
1899                        return Ok(FileSelectResult::Closed);
1900                    }
1901                    WindowEvent::CursorEnter(pos) | WindowEvent::CursorMove(pos) => {
1902                        mouse_x = pos.x as i32;
1903                        mouse_y = pos.y as i32;
1904                    }
1905                    WindowEvent::ButtonPress(button, _modifiers)
1906                        if *button == MouseButton::Left =>
1907                    {
1908                        if !filtered_entries.is_empty() {
1909                            let scrollbar_x = main_x + main_w as i32 - (8.0 * scale) as i32;
1910                            let scrollbar_y = list_y;
1911
1912                            if mouse_x >= main_x
1913                                && mouse_x < main_x + main_w as i32
1914                                && mouse_y >= list_y
1915                                && mouse_y < list_y + list_h as i32
1916                            {
1917                                let visible_items = (list_h / item_height) as usize;
1918                                let total_items = filtered_entries.len();
1919
1920                                if visible_items < total_items {
1921                                    let scrollbar_h_f32 = list_h as f32 - 8.0 * scale;
1922                                    let thumb_h_f32 = (visible_items as f32 / total_items as f32
1923                                        * scrollbar_h_f32)
1924                                        .max(20.0 * scale);
1925                                    let thumb_h = thumb_h_f32 as i32;
1926
1927                                    let max_scroll = total_items - visible_items;
1928                                    let max_thumb_y = scrollbar_h_f32 as i32 - thumb_h;
1929                                    let thumb_y = if max_thumb_y > 0 {
1930                                        ((scroll_offset as f32 / max_scroll as f32)
1931                                            * max_thumb_y as f32)
1932                                            as i32
1933                                    } else {
1934                                        0
1935                                    };
1936
1937                                    let rel_y = mouse_y - scrollbar_y;
1938                                    if mouse_x >= scrollbar_x
1939                                        && mouse_x < scrollbar_x + (6.0 * scale) as i32
1940                                        && rel_y >= thumb_y
1941                                        && rel_y < thumb_y + thumb_h
1942                                    {
1943                                        thumb_drag = true;
1944                                        thumb_drag_offset = Some(mouse_y - (scrollbar_y + thumb_y));
1945                                    }
1946                                }
1947                            }
1948                        }
1949                    }
1950                    WindowEvent::ButtonRelease(_, _) => {
1951                        thumb_drag = false;
1952                        thumb_drag_offset = None;
1953                    }
1954                    _ => {}
1955                }
1956
1957                needs_redraw |= ok_button.process_event(&ev);
1958                needs_redraw |= cancel_button.process_event(&ev);
1959            }
1960
1961            if needs_redraw {
1962                draw(
1963                    &mut canvas,
1964                    colors,
1965                    &font,
1966                    &current_dir,
1967                    &quick_access,
1968                    &all_entries,
1969                    &filtered_entries,
1970                    &selected_indices,
1971                    scroll_offset,
1972                    hovered_quick_access,
1973                    hovered_entry,
1974                    show_hidden,
1975                    &search_input,
1976                    &ok_button,
1977                    &cancel_button,
1978                    &history,
1979                    history_index,
1980                    &mounted_drives,
1981                    hovered_drive,
1982                    scale,
1983                    scrollbar_hovered,
1984                    filename_input.as_ref(),
1985                );
1986                if save_mode && !completion_matches.is_empty() {
1987                    let visible = completion_matches.len().min(MAX_POPUP_ITEMS);
1988                    let popup_h = (visible as i32) * POPUP_ITEM_HEIGHT + 2;
1989                    draw_completion_popup(
1990                        &mut canvas,
1991                        &font,
1992                        colors,
1993                        &completion_matches,
1994                        completion_popup_index,
1995                        main_x,
1996                        filename_y + filename_label_h - popup_h,
1997                        main_w,
1998                    );
1999                }
2000                if !search_matches.is_empty() && search_input.has_focus() {
2001                    draw_completion_popup(
2002                        &mut canvas,
2003                        &font,
2004                        colors,
2005                        &search_matches,
2006                        search_popup_index,
2007                        search_x,
2008                        search_y + 32,
2009                        search_width,
2010                    );
2011                }
2012                window.set_contents(&canvas)?;
2013            }
2014        }
2015    }
2016}
2017
2018impl Default for FileSelectBuilder {
2019    fn default() -> Self {
2020        Self::new()
2021    }
2022}
2023
2024// Helper types and functions
2025
2026struct DirEntry {
2027    name: String,
2028    path: PathBuf,
2029    is_dir: bool,
2030    size: u64,
2031    modified: Option<SystemTime>,
2032}
2033
2034fn build_quick_access() -> Vec<QuickAccess> {
2035    let mut items = Vec::new();
2036
2037    if let Some(home) = dirs::home_dir() {
2038        items.push(QuickAccess {
2039            name: "Home",
2040            path: home,
2041            icon: QuickAccessIcon::Home,
2042        });
2043    }
2044    if let Some(desktop) = dirs::desktop_dir() {
2045        items.push(QuickAccess {
2046            name: "Desktop",
2047            path: desktop,
2048            icon: QuickAccessIcon::Desktop,
2049        });
2050    }
2051    if let Some(docs) = dirs::document_dir() {
2052        items.push(QuickAccess {
2053            name: "Documents",
2054            path: docs,
2055            icon: QuickAccessIcon::Documents,
2056        });
2057    }
2058    if let Some(dl) = dirs::download_dir() {
2059        items.push(QuickAccess {
2060            name: "Downloads",
2061            path: dl,
2062            icon: QuickAccessIcon::Downloads,
2063        });
2064    }
2065    if let Some(pics) = dirs::picture_dir() {
2066        items.push(QuickAccess {
2067            name: "Pictures",
2068            path: pics,
2069            icon: QuickAccessIcon::Pictures,
2070        });
2071    }
2072    if let Some(music) = dirs::audio_dir() {
2073        items.push(QuickAccess {
2074            name: "Music",
2075            path: music,
2076            icon: QuickAccessIcon::Music,
2077        });
2078    }
2079    if let Some(videos) = dirs::video_dir() {
2080        items.push(QuickAccess {
2081            name: "Videos",
2082            path: videos,
2083            icon: QuickAccessIcon::Videos,
2084        });
2085    }
2086
2087    items
2088}
2089
2090fn get_mounted_drives() -> Vec<MountPoint> {
2091    let mut drives = Vec::new();
2092
2093    // Parse /run/mount/utab for user-mounted drives (much cleaner than /proc/mounts)
2094    if let Ok(content) = std::fs::read_to_string("/run/mount/utab") {
2095        for line in content.lines() {
2096            let mut device: Option<String> = None;
2097            let mut mount_point: Option<PathBuf> = None;
2098
2099            // Parse KEY=VALUE pairs
2100            for pair in line.split_whitespace() {
2101                let mut kv = pair.split('=');
2102                if let Some(key) = kv.next() {
2103                    let value = kv.next();
2104                    match key {
2105                        "SRC" => {
2106                            device = value.map(|v| v.to_string());
2107                        }
2108                        "TARGET" => {
2109                            mount_point = value.map(PathBuf::from);
2110                        }
2111                        _ => {}
2112                    }
2113                }
2114            }
2115
2116            // We have both source and target, create a mount point entry
2117            if let (Some(dev), Some(mp)) = (device, mount_point) {
2118                // Skip root filesystem
2119                if mp.as_os_str() == "/" {
2120                    continue;
2121                }
2122
2123                let label = get_volume_label(&dev);
2124
2125                drives.push(MountPoint {
2126                    device: dev,
2127                    mount_point: mp,
2128                    label,
2129                });
2130            }
2131        }
2132    }
2133
2134    drives
2135}
2136
2137fn get_volume_label(device: &str) -> Option<String> {
2138    use std::process::Command;
2139
2140    let output = Command::new("lsblk")
2141        .args(["-o", "LABEL", "-n", device])
2142        .output()
2143        .ok()?;
2144
2145    let label = String::from_utf8_lossy(&output.stdout).trim().to_string();
2146
2147    if label.is_empty() { None } else { Some(label) }
2148}
2149
2150fn get_mount_icon(device: &str) -> MountIcon {
2151    // Check for USB by looking for symlink in /dev/disk/by-id/usb-*
2152    let is_usb = device
2153        .strip_prefix("/dev/")
2154        .map(|_dev| {
2155            std::fs::read_dir("/dev/disk/by-id")
2156                .ok()
2157                .map(|entries| {
2158                    entries
2159                        .filter_map(|e| e.ok())
2160                        .filter(|e| e.file_name().to_string_lossy().starts_with("usb-"))
2161                        .any(|e| {
2162                            e.path()
2163                                .canonicalize()
2164                                .ok()
2165                                .as_ref()
2166                                .and_then(|p| p.to_str())
2167                                .map(|p| device.contains(p))
2168                                .unwrap_or(false)
2169                        })
2170                })
2171                .unwrap_or(false)
2172        })
2173        .unwrap_or(false);
2174
2175    if is_usb {
2176        return MountIcon::UsbDrive;
2177    }
2178
2179    if device.starts_with("/dev/sr") || device.starts_with("/dev/scd") {
2180        return MountIcon::Optical;
2181    }
2182
2183    if device.starts_with("/dev/nvme") || device.starts_with("/dev/mmc") {
2184        return MountIcon::ExternalHdd;
2185    }
2186
2187    MountIcon::Generic
2188}
2189
2190fn load_directory(path: &Path, entries: &mut Vec<DirEntry>, dirs_only: bool, show_hidden: bool) {
2191    entries.clear();
2192
2193    if let Some(parent) = path.parent() {
2194        entries.push(DirEntry {
2195            name: "..".to_string(),
2196            path: parent.to_path_buf(),
2197            is_dir: true,
2198            size: 0,
2199            modified: None,
2200        });
2201    }
2202
2203    let mut dirs: Vec<DirEntry> = Vec::new();
2204    let mut files: Vec<DirEntry> = Vec::new();
2205
2206    if let Ok(read_dir) = fs::read_dir(path) {
2207        for entry in read_dir.flatten() {
2208            let name = entry.file_name().to_string_lossy().to_string();
2209
2210            if !show_hidden && name.starts_with('.') {
2211                continue;
2212            }
2213
2214            let metadata = entry.path().metadata().ok();
2215            let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
2216
2217            if dirs_only && !is_dir {
2218                continue;
2219            }
2220
2221            let size = metadata.as_ref().map(Metadata::len).unwrap_or(0);
2222            let modified = metadata.as_ref().and_then(|m| m.modified().ok());
2223
2224            let de = DirEntry {
2225                name,
2226                path: entry.path(),
2227                is_dir,
2228                size,
2229                modified,
2230            };
2231
2232            if is_dir {
2233                dirs.push(de);
2234            } else {
2235                files.push(de);
2236            }
2237        }
2238    }
2239
2240    dirs.sort_by_key(|a| a.name.to_lowercase());
2241    files.sort_by_key(|a| a.name.to_lowercase());
2242
2243    entries.extend(dirs);
2244    entries.extend(files);
2245}
2246
2247fn update_filtered(
2248    all: &[DirEntry],
2249    search: &str,
2250    filtered: &mut Vec<usize>,
2251    filters: &[FileFilter],
2252) {
2253    filtered.clear();
2254    for (i, entry) in all.iter().enumerate() {
2255        let matches_search = search.is_empty() || entry.name.to_lowercase().contains(search);
2256        if entry.is_dir {
2257            if matches_search {
2258                filtered.push(i);
2259            }
2260        } else {
2261            let matches_filter = filters.is_empty() || matches_any_filter(&entry.name, filters);
2262            if matches_filter && matches_search {
2263                filtered.push(i);
2264            }
2265        }
2266    }
2267}
2268
2269fn matches_any_filter(name: &str, filters: &[FileFilter]) -> bool {
2270    let name_lower = name.to_lowercase();
2271    for filter in filters {
2272        for pattern in &filter.patterns {
2273            if matches_pattern(&name_lower, pattern) {
2274                return true;
2275            }
2276        }
2277    }
2278    false
2279}
2280
2281fn matches_pattern(name: &str, pattern: &str) -> bool {
2282    let pattern_lower = pattern.to_lowercase();
2283    if pattern_lower == "*" {
2284        return true;
2285    }
2286
2287    if pattern_lower.starts_with("*") && pattern_lower.ends_with("*") {
2288        let inner = &pattern_lower[1..pattern_lower.len() - 1];
2289        name.contains(inner)
2290    } else if let Some(suffix) = pattern_lower.strip_prefix("*") {
2291        name.ends_with(suffix)
2292    } else if pattern_lower.ends_with("*") {
2293        let prefix = &pattern_lower[..pattern_lower.len() - 1];
2294        name.starts_with(prefix)
2295    } else {
2296        name == pattern_lower
2297    }
2298}
2299
2300fn navigate_to(
2301    dest: PathBuf,
2302    current: &mut PathBuf,
2303    history: &mut Vec<PathBuf>,
2304    index: &mut usize,
2305) {
2306    // Truncate forward history
2307    history.truncate(*index + 1);
2308    history.push(dest.clone());
2309    *index = history.len() - 1;
2310    *current = dest;
2311}
2312
2313#[allow(clippy::too_many_arguments)]
2314fn navigate_to_directory(
2315    dest: PathBuf,
2316    current_dir: &mut PathBuf,
2317    history: &mut Vec<PathBuf>,
2318    history_index: &mut usize,
2319    all_entries: &mut Vec<DirEntry>,
2320    directory_mode: bool,
2321    show_hidden: bool,
2322    search_text: &str,
2323    filtered_entries: &mut Vec<usize>,
2324    selected_indices: &mut HashSet<usize>,
2325    scroll_offset: &mut usize,
2326    filters: &[FileFilter],
2327) {
2328    if dest.exists() {
2329        navigate_to(dest, current_dir, history, history_index);
2330        load_directory(current_dir, all_entries, directory_mode, show_hidden);
2331        update_filtered(all_entries, search_text, filtered_entries, filters);
2332        selected_indices.clear();
2333        *scroll_offset = 0;
2334    }
2335}
2336
2337/// Returns all file entry names matching `prefix` (case-insensitive), up to `max` items.
2338fn find_all_completions(
2339    entries: &[DirEntry],
2340    text: &str,
2341    max: usize,
2342    files_only: bool,
2343    prefix_only: bool,
2344) -> Vec<String> {
2345    if text.is_empty() {
2346        return Vec::new();
2347    }
2348    let text_lower = text.to_lowercase();
2349    entries
2350        .iter()
2351        .filter(|e| {
2352            (!files_only || !e.is_dir) && {
2353                let name_lower = e.name.to_lowercase();
2354                if prefix_only {
2355                    name_lower.starts_with(&text_lower)
2356                } else {
2357                    name_lower.contains(&text_lower)
2358                }
2359            }
2360        })
2361        .take(max)
2362        .map(|e| e.name.clone())
2363        .collect()
2364}
2365
2366const POPUP_ITEM_HEIGHT: i32 = 26;
2367const MAX_POPUP_ITEMS: usize = 8;
2368
2369fn draw_completion_popup(
2370    canvas: &mut Canvas,
2371    font: &Font,
2372    colors: &Colors,
2373    matches: &[String],
2374    selected: usize,
2375    x: i32,
2376    y: i32,
2377    width: u32,
2378) {
2379    if matches.is_empty() {
2380        return;
2381    }
2382    let visible = matches.len().min(MAX_POPUP_ITEMS);
2383    let popup_h = (visible as i32) * POPUP_ITEM_HEIGHT + 2; // 1px border top+bottom
2384
2385    // Background
2386    canvas.fill_rounded_rect(
2387        x as f32,
2388        y as f32,
2389        width as f32,
2390        popup_h as f32,
2391        4.0,
2392        colors.input_bg,
2393    );
2394    // Border
2395    canvas.stroke_rounded_rect(
2396        x as f32,
2397        y as f32,
2398        width as f32,
2399        popup_h as f32,
2400        4.0,
2401        colors.input_border_focused,
2402        1.0,
2403    );
2404
2405    for (i, name) in matches.iter().take(visible).enumerate() {
2406        let item_y = y + 1 + (i as i32) * POPUP_ITEM_HEIGHT;
2407
2408        // Highlight selected item
2409        if i == selected {
2410            canvas.fill_rect(
2411                (x + 1) as f32,
2412                item_y as f32,
2413                (width - 2) as f32,
2414                POPUP_ITEM_HEIGHT as f32,
2415                colors.input_border_focused,
2416            );
2417        }
2418
2419        let text_color = if i == selected {
2420            colors.input_bg
2421        } else {
2422            colors.text
2423        };
2424        let label = font.render(name).with_color(text_color).finish();
2425        let text_y = item_y + (POPUP_ITEM_HEIGHT - label.height() as i32) / 2;
2426        canvas.draw_canvas(&label, x + 6, text_y);
2427    }
2428}
2429
2430fn darken(color: Rgba, amount: f32) -> Rgba {
2431    rgb(
2432        (color.r as f32 * (1.0 - amount)) as u8,
2433        (color.g as f32 * (1.0 - amount)) as u8,
2434        (color.b as f32 * (1.0 - amount)) as u8,
2435    )
2436}
2437
2438fn truncate_name(name: &str, max_len: usize) -> String {
2439    if name.chars().count() > max_len {
2440        format!("{}...", name.chars().take(max_len - 3).collect::<String>())
2441    } else {
2442        name.to_string()
2443    }
2444}
2445
2446fn format_size(bytes: u64) -> String {
2447    if bytes < 1024 {
2448        format!("{} B", bytes)
2449    } else if bytes < 1024 * 1024 {
2450        format!("{:.1} KB", bytes as f64 / 1024.0)
2451    } else if bytes < 1024 * 1024 * 1024 {
2452        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
2453    } else {
2454        format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
2455    }
2456}
2457
2458fn format_date(time: Option<SystemTime>) -> String {
2459    match time {
2460        Some(t) => {
2461            let duration = t.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
2462            let secs = duration.as_secs();
2463            // Simple date format (just show relative or basic)
2464            let now = SystemTime::now()
2465                .duration_since(SystemTime::UNIX_EPOCH)
2466                .unwrap_or_default()
2467                .as_secs();
2468            let diff = now.saturating_sub(secs);
2469
2470            if diff < 60 {
2471                "Just now".to_string()
2472            } else if diff < 3600 {
2473                format!("{} min ago", diff / 60)
2474            } else if diff < 86400 {
2475                format!("{} hrs ago", diff / 3600)
2476            } else if diff < 86400 * 7 {
2477                format!("{} days ago", diff / 86400)
2478            } else {
2479                // Convert to date-ish
2480                let days_since_epoch = secs / 86400;
2481                let years = 1970 + days_since_epoch / 365;
2482                format!("{}", years)
2483            }
2484        }
2485        None => "-".to_string(),
2486    }
2487}
2488
2489#[allow(clippy::too_many_arguments)]
2490fn draw_nav_button(
2491    canvas: &mut Canvas,
2492    x: i32,
2493    y: i32,
2494    label: &str,
2495    enabled: bool,
2496    colors: &Colors,
2497    font: &Font,
2498    scale: f32,
2499) {
2500    let bg = if enabled {
2501        colors.button
2502    } else {
2503        darken(colors.button, 0.1)
2504    };
2505    let size = 28.0 * scale;
2506    canvas.fill_rounded_rect(x as f32, y as f32, size, size, 4.0 * scale, bg);
2507
2508    let text_color = if enabled {
2509        colors.button_text
2510    } else {
2511        rgb(100, 100, 100)
2512    };
2513    let tc = font.render(label).with_color(text_color).finish();
2514    canvas.draw_canvas(&tc, x + (10.0 * scale) as i32, y + (6.0 * scale) as i32);
2515}
2516
2517#[allow(clippy::too_many_arguments)]
2518fn draw_toggle(
2519    canvas: &mut Canvas,
2520    x: i32,
2521    y: i32,
2522    label: &str,
2523    active: bool,
2524    colors: &Colors,
2525    font: &Font,
2526    scale: f32,
2527) {
2528    let bg = if active {
2529        colors.input_border_focused
2530    } else {
2531        colors.button
2532    };
2533    let size = 28.0 * scale;
2534    canvas.fill_rounded_rect(x as f32, y as f32, size, size, 4.0 * scale, bg);
2535
2536    let text_color = if active {
2537        rgb(255, 255, 255)
2538    } else {
2539        colors.button_text
2540    };
2541    let tc = font.render(label).with_color(text_color).finish();
2542    canvas.draw_canvas(&tc, x + (6.0 * scale) as i32, y + (6.0 * scale) as i32);
2543}
2544
2545fn draw_breadcrumbs(
2546    canvas: &mut Canvas,
2547    x: i32,
2548    y: i32,
2549    max_w: u32,
2550    path: &Path,
2551    colors: &Colors,
2552    font: &Font,
2553) {
2554    let components: Vec<_> = path.components().collect();
2555
2556    // Calculate the total width needed for full breadcrumbs
2557    let mut total_width = 0i32;
2558    let ellipsis_width = font
2559        .render("...")
2560        .with_color(rgb(120, 120, 120))
2561        .finish()
2562        .width() as i32
2563        + 8;
2564    let sep_width = font
2565        .render(" / ")
2566        .with_color(rgb(100, 100, 100))
2567        .finish()
2568        .width() as i32;
2569
2570    for (i, comp) in components.iter().enumerate() {
2571        let name = comp.as_os_str().to_string_lossy();
2572        let display = if name.is_empty() { "/" } else { &name };
2573        total_width += font
2574            .render(display)
2575            .with_color(colors.text)
2576            .finish()
2577            .width() as i32;
2578
2579        if i < components.len() - 1 && !matches!(comp, std::path::Component::RootDir) {
2580            total_width += sep_width;
2581        }
2582    }
2583
2584    // Determine how many components to show
2585    let num_components = components.len();
2586    let components_to_show = if total_width > max_w as i32 {
2587        // Try showing fewer components, starting from the end
2588        (1..=num_components.min(4))
2589            .rev()
2590            .find(|n| {
2591                let start = num_components - n;
2592                let mut test_width = if start > 0 { ellipsis_width } else { 0 };
2593
2594                for (i, comp) in components.iter().enumerate().skip(start) {
2595                    let name = comp.as_os_str().to_string_lossy();
2596                    let display = if name.is_empty() { "/" } else { &name };
2597                    test_width += font
2598                        .render(display)
2599                        .with_color(colors.text)
2600                        .finish()
2601                        .width() as i32;
2602
2603                    if i < num_components - 1 && !matches!(comp, std::path::Component::RootDir) {
2604                        test_width += sep_width;
2605                    }
2606                }
2607
2608                test_width <= max_w as i32
2609            })
2610            .unwrap_or(1)
2611    } else {
2612        num_components
2613    };
2614
2615    let start = num_components - components_to_show;
2616
2617    let mut cx = x;
2618    let available_width = max_w as i32;
2619
2620    if start > 0 {
2621        let tc = font.render("...").with_color(rgb(120, 120, 120)).finish();
2622        canvas.draw_canvas(&tc, cx, y);
2623        cx += tc.width() as i32 + 8;
2624    }
2625
2626    for (i, comp) in components.iter().enumerate().skip(start) {
2627        let name = comp.as_os_str().to_string_lossy();
2628        let display = if name.is_empty() { "/" } else { &name };
2629
2630        let is_last = i == num_components - 1;
2631        let is_root = matches!(comp, std::path::Component::RootDir);
2632        let text_color = if is_last {
2633            colors.text
2634        } else {
2635            rgb(120, 120, 120)
2636        };
2637
2638        let tc = font.render(display).with_color(text_color).finish();
2639
2640        // Check if this component would overflow
2641        let remaining_width = available_width - (cx - x);
2642        if tc.width() as i32 > remaining_width && is_last {
2643            // Truncate the last component to fit
2644            let chars: Vec<char> = display.chars().collect();
2645            let ellipsis = font.render("...").with_color(text_color).finish();
2646            let ellipsis_w = ellipsis.width() as i32;
2647            let max_text_w = remaining_width - ellipsis_w;
2648
2649            if max_text_w > 0 {
2650                let mut truncated = String::new();
2651                let mut current_w = 0i32;
2652
2653                for c in chars {
2654                    let c_canvas = font
2655                        .render(c.to_string().as_str())
2656                        .with_color(text_color)
2657                        .finish();
2658                    if current_w + c_canvas.width() as i32 > max_text_w {
2659                        truncated.push('…');
2660                        break;
2661                    }
2662                    truncated.push(c);
2663                    current_w += c_canvas.width() as i32;
2664                }
2665
2666                let truncated_tc = font.render(&truncated).with_color(text_color).finish();
2667                canvas.draw_canvas(&truncated_tc, cx, y);
2668                cx += truncated_tc.width() as i32;
2669            }
2670        } else {
2671            canvas.draw_canvas(&tc, cx, y);
2672            cx += tc.width() as i32;
2673        }
2674
2675        if !is_last && !is_root {
2676            let sep = font.render(" / ").with_color(rgb(100, 100, 100)).finish();
2677            canvas.draw_canvas(&sep, cx, y);
2678            cx += sep.width() as i32;
2679        }
2680    }
2681}
2682
2683fn draw_folder_icon(canvas: &mut Canvas, x: i32, y: i32, colors: &Colors, scale: f32) {
2684    let folder_color = rgb(240, 180, 70); // Golden folder
2685    let icon_size = BASE_ICON_SIZE as f32 * scale;
2686    // Folder body
2687    canvas.fill_rounded_rect(
2688        x as f32,
2689        (y + (4.0 * scale) as i32) as f32,
2690        icon_size,
2691        14.0 * scale,
2692        2.0 * scale,
2693        folder_color,
2694    );
2695    // Folder tab
2696    canvas.fill_rounded_rect(
2697        x as f32,
2698        y as f32,
2699        10.0 * scale,
2700        6.0 * scale,
2701        2.0 * scale,
2702        folder_color,
2703    );
2704    let _ = colors;
2705}
2706
2707fn draw_file_icon(canvas: &mut Canvas, x: i32, y: i32, name: &str, colors: &Colors, scale: f32) {
2708    let ext = name.rsplit('.').next().unwrap_or("").to_lowercase();
2709    let icon_size = BASE_ICON_SIZE as f32 * scale;
2710
2711    let icon_color = match ext.as_str() {
2712        "rs" => rgb(220, 120, 70),          // Rust orange
2713        "py" => rgb(70, 130, 180),          // Python blue
2714        "js" | "ts" => rgb(240, 220, 80),   // JS yellow
2715        "html" | "htm" => rgb(220, 80, 50), // HTML red
2716        "css" => rgb(80, 120, 200),         // CSS blue
2717        "json" | "yaml" | "yml" | "toml" => rgb(150, 150, 150),
2718        "md" | "txt" => rgb(180, 180, 180),
2719        "png" | "jpg" | "jpeg" | "gif" | "svg" => rgb(100, 180, 100), // Green for images
2720        _ => rgb(160, 160, 160),
2721    };
2722
2723    // File body
2724    canvas.fill_rounded_rect(
2725        x as f32,
2726        y as f32,
2727        16.0 * scale,
2728        icon_size,
2729        2.0 * scale,
2730        icon_color,
2731    );
2732    // Folded corner
2733    canvas.fill_rect(
2734        (x + (10.0 * scale) as i32) as f32,
2735        y as f32,
2736        6.0 * scale,
2737        6.0 * scale,
2738        darken(icon_color, 0.2),
2739    );
2740    let _ = colors;
2741}
2742
2743fn draw_quick_access_icon(
2744    canvas: &mut Canvas,
2745    x: i32,
2746    y: i32,
2747    icon: QuickAccessIcon,
2748    colors: &Colors,
2749    scale: f32,
2750) {
2751    let color = match icon {
2752        QuickAccessIcon::Home => rgb(100, 180, 100),
2753        QuickAccessIcon::Desktop => rgb(120, 120, 200),
2754        QuickAccessIcon::Documents => rgb(200, 180, 100),
2755        QuickAccessIcon::Downloads => rgb(100, 160, 220),
2756        QuickAccessIcon::Pictures => rgb(180, 120, 180),
2757        QuickAccessIcon::Music => rgb(220, 120, 120),
2758        QuickAccessIcon::Videos => rgb(180, 100, 200),
2759    };
2760
2761    canvas.fill_rounded_rect(
2762        x as f32,
2763        y as f32,
2764        16.0 * scale,
2765        16.0 * scale,
2766        3.0 * scale,
2767        color,
2768    );
2769    let _ = colors;
2770}
2771
2772fn draw_section_header(
2773    canvas: &mut Canvas,
2774    x: i32,
2775    y: i32,
2776    label: &str,
2777    colors: &Colors,
2778    font: &Font,
2779    scale: f32,
2780) {
2781    let header_color = rgb(140, 140, 140);
2782    let header_canvas = font.render(label).with_color(header_color).finish();
2783    canvas.draw_canvas(&header_canvas, x + (4.0 * scale) as i32, y);
2784
2785    canvas.fill_rect(
2786        x as f32,
2787        (y + (18.0 * scale) as i32) as f32,
2788        (BASE_SIDEBAR_WIDTH as f32 * scale) - (8.0 * scale),
2789        1.0,
2790        darken(colors.window_bg, 0.05),
2791    );
2792}
2793
2794fn draw_mount_icon(
2795    canvas: &mut Canvas,
2796    x: i32,
2797    y: i32,
2798    icon: MountIcon,
2799    colors: &Colors,
2800    scale: f32,
2801) {
2802    let icon_size = 16.0 * scale;
2803    let color = match icon {
2804        MountIcon::UsbDrive => rgb(100, 200, 200),
2805        MountIcon::ExternalHdd => rgb(150, 150, 180),
2806        MountIcon::Optical => rgb(200, 150, 100),
2807        MountIcon::Generic => rgb(140, 140, 140),
2808    };
2809
2810    canvas.fill_rounded_rect(x as f32, y as f32, icon_size, icon_size, 3.0 * scale, color);
2811
2812    match icon {
2813        MountIcon::UsbDrive => {
2814            canvas.fill_rect(
2815                (x + (6.0 * scale) as i32) as f32,
2816                (y + (10.0 * scale) as i32) as f32,
2817                4.0 * scale,
2818                4.0 * scale,
2819                rgb(50, 50, 50),
2820            );
2821        }
2822        MountIcon::Optical => {
2823            canvas.fill_rounded_rect(
2824                (x + (6.0 * scale) as i32) as f32,
2825                (y + (6.0 * scale) as i32) as f32,
2826                4.0 * scale,
2827                4.0 * scale,
2828                2.0 * scale,
2829                rgb(50, 50, 50),
2830            );
2831        }
2832        _ => {}
2833    }
2834
2835    let _ = colors;
2836}