gardesk/garfield / 9f89f23

Browse files

grid: optimize rubber band selection, fix status bar update

- throttle render requests during drag to ~60fps
- add visible_indices cache to avoid repeated allocations
- pre-compute colors and font sizes outside render loop
- call final selection update in stop_drag before clearing state
- add select_by_path to Tab for selecting renamed files
- add lightweight status bar update for drag operations
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9f89f2331c86e3d06840cac90b322c731f4cebe2
Parents
cbd3758
Tree
38726f3

4 changed files

StatusFile+-
M garfield/src/app.rs 44 27
M garfield/src/ui/grid_view.rs 117 48
M garfield/src/ui/status_bar.rs 5 0
M garfield/src/ui/tab.rs 23 0
garfield/src/app.rsmodified
@@ -9,7 +9,7 @@ use garfield::ui::pane::SplitDirection;
9
 use garfield::ui::{AddressBar, AppPickerDialog, AppPickerResult, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, InputDialog, InputResult, Pane, ProgressDialog, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT};
9
 use garfield::ui::{AddressBar, AppPickerDialog, AppPickerResult, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, InputDialog, InputResult, Pane, ProgressDialog, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT};
10
 use anyhow::Result;
10
 use anyhow::Result;
11
 use gartk_core::{InputEvent, Key, MouseButton, Point, Rect, Theme};
11
 use gartk_core::{InputEvent, Key, MouseButton, Point, Rect, Theme};
12
-use gartk_render::{Renderer, Surface, TextStyle};
12
+use gartk_render::{Renderer, TextStyle};
13
 use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
13
 use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
14
 use std::path::PathBuf;
14
 use std::path::PathBuf;
15
 use std::time::Instant;
15
 use std::time::Instant;
@@ -567,6 +567,8 @@ impl App {
567
                     tab.on_click(pos, modifiers);
567
                     tab.on_click(pos, modifiers);
568
                 }
568
                 }
569
             }
569
             }
570
+            // Update status bar with new selection
571
+            self.update_status_bar();
570
 
572
 
571
             // Capture drag source from entry at click position (for bookmark drag)
573
             // Capture drag source from entry at click position (for bookmark drag)
572
             let entry_at_click = self.focused_pane()
574
             let entry_at_click = self.focused_pane()
@@ -632,6 +634,8 @@ impl App {
632
         self.pane_resize_path = None;
634
         self.pane_resize_path = None;
633
         self.sidebar_resizing = false;
635
         self.sidebar_resizing = false;
634
 
636
 
637
+        let was_dragging = self.focused_pane().map(|p| p.is_dragging()).unwrap_or(false);
638
+
635
         if let Some(pane) = self.focused_pane_mut() {
639
         if let Some(pane) = self.focused_pane_mut() {
636
             if pane.is_resizing() {
640
             if pane.is_resizing() {
637
                 pane.stop_resize();
641
                 pane.stop_resize();
@@ -640,6 +644,11 @@ impl App {
640
                 pane.stop_drag();
644
                 pane.stop_drag();
641
             }
645
             }
642
         }
646
         }
647
+
648
+        // Update status bar after any mouse release (ensures rubber band selection is reflected)
649
+        if was_dragging {
650
+            self.update_status_bar();
651
+        }
643
     }
652
     }
644
 
653
 
645
     /// Handle mouse move. Returns true if a redraw is needed.
654
     /// Handle mouse move. Returns true if a redraw is needed.
@@ -745,12 +754,23 @@ impl App {
745
         needs_redraw |= self.sidebar.on_mouse_move(pos);
754
         needs_redraw |= self.sidebar.on_mouse_move(pos);
746
         needs_redraw |= self.tab_bar.on_mouse_move(pos);
755
         needs_redraw |= self.tab_bar.on_mouse_move(pos);
747
 
756
 
757
+        let mut is_dragging = false;
758
+        let mut selection_count = 0;
748
         if let Some(pane) = self.focused_pane_mut() {
759
         if let Some(pane) = self.focused_pane_mut() {
749
             if let Some(tab) = pane.active_tab_mut() {
760
             if let Some(tab) = pane.active_tab_mut() {
750
                 needs_redraw |= tab.on_mouse_move(pos);
761
                 needs_redraw |= tab.on_mouse_move(pos);
762
+                is_dragging = tab.is_dragging();
763
+                if is_dragging {
764
+                    selection_count = tab.selection_count();
765
+                }
751
             }
766
             }
752
         }
767
         }
753
 
768
 
769
+        // Update status bar selection count if rubber band is active (lightweight update)
770
+        if is_dragging {
771
+            self.status_bar.update_selection_count(selection_count);
772
+        }
773
+
754
         needs_redraw
774
         needs_redraw
755
     }
775
     }
756
 
776
 
@@ -2380,6 +2400,8 @@ impl App {
2380
                     });
2400
                     });
2381
                 }
2401
                 }
2382
                 self.status_bar.set_status_message(format!("Renamed to '{}'", new_name));
2402
                 self.status_bar.set_status_message(format!("Renamed to '{}'", new_name));
2403
+                // Update status bar with new entry count
2404
+                self.update_status_bar();
2383
             }
2405
             }
2384
             Some(Err(msg)) => {
2406
             Some(Err(msg)) => {
2385
                 self.status_bar.set_status_message(format!("Rename failed: {}", msg));
2407
                 self.status_bar.set_status_message(format!("Rename failed: {}", msg));
@@ -2705,33 +2727,28 @@ impl App {
2705
     /// Blit the rendered surface to the window.
2727
     /// Blit the rendered surface to the window.
2706
     fn blit_surface(&mut self) -> Result<()> {
2728
     fn blit_surface(&mut self) -> Result<()> {
2707
         let size = self.renderer.size();
2729
         let size = self.renderer.size();
2708
-        let conn = self.window.connection();
2730
+        let window_id = self.window.id();
2709
-
2731
+        let depth = self.window.depth();
2710
-        let ctx = self.renderer.context()?;
2732
+        let gc = self.gc;
2711
-        ctx.target().flush();
2733
+        let conn = self.window.connection().clone();
2712
-
2734
+
2713
-        let mut temp_surface = Surface::new(size.width, size.height)?;
2735
+        // Access surface data directly without copying, blit to X11
2714
-        let temp_ctx = temp_surface.context()?;
2736
+        self.renderer.surface_mut().with_data(|data| {
2715
-        temp_ctx.set_source_surface(self.renderer.surface().cairo_surface(), 0.0, 0.0)?;
2737
+            let _ = conn.inner().put_image(
2716
-        temp_ctx.paint()?;
2738
+                ImageFormat::Z_PIXMAP,
2717
-        drop(temp_ctx);
2739
+                window_id,
2718
-
2740
+                gc,
2719
-        let data = temp_surface.data()?;
2741
+                size.width as u16,
2720
-
2742
+                size.height as u16,
2721
-        conn.inner().put_image(
2743
+                0,
2722
-            ImageFormat::Z_PIXMAP,
2744
+                0,
2723
-            self.window.id(),
2745
+                0,
2724
-            self.gc,
2746
+                depth,
2725
-            size.width as u16,
2747
+                data,
2726
-            size.height as u16,
2748
+            );
2727
-            0,
2749
+        })?;
2728
-            0,
2729
-            0,
2730
-            self.window.depth(),
2731
-            &data,
2732
-        )?;
2733
 
2750
 
2734
-        conn.flush()?;
2751
+        self.window.connection().flush()?;
2735
 
2752
 
2736
         Ok(())
2753
         Ok(())
2737
     }
2754
     }
garfield/src/ui/grid_view.rsmodified
@@ -5,6 +5,7 @@ use crate::ui::tab::RenameState;
5
 use gartk_core::{Color, Modifiers, Point, Rect};
5
 use gartk_core::{Color, Modifiers, Point, Rect};
6
 use gartk_render::{Renderer, TextAlign, TextStyle};
6
 use gartk_render::{Renderer, TextAlign, TextStyle};
7
 use std::collections::HashSet;
7
 use std::collections::HashSet;
8
+use std::time::{Duration, Instant};
8
 
9
 
9
 /// Padding around cells.
10
 /// Padding around cells.
10
 pub const CELL_PADDING: u32 = 8;
11
 pub const CELL_PADDING: u32 = 8;
@@ -69,6 +70,8 @@ impl IconSize {
69
 pub struct GridView {
70
 pub struct GridView {
70
     /// Entries to display.
71
     /// Entries to display.
71
     entries: Vec<FileEntry>,
72
     entries: Vec<FileEntry>,
73
+    /// Cached indices of visible entries (respecting hidden filter).
74
+    visible_indices: Vec<usize>,
72
     /// Currently focused index (for keyboard nav).
75
     /// Currently focused index (for keyboard nav).
73
     focused: usize,
76
     focused: usize,
74
     /// Selected indices (for multi-select).
77
     /// Selected indices (for multi-select).
@@ -91,6 +94,10 @@ pub struct GridView {
91
     drag_current: Option<Point>,
94
     drag_current: Option<Point>,
92
     /// Icon size setting.
95
     /// Icon size setting.
93
     icon_size: IconSize,
96
     icon_size: IconSize,
97
+    /// Last time rubber band selection was updated (for throttling).
98
+    last_selection_update: Option<Instant>,
99
+    /// Last time we requested a redraw during drag (for frame rate limiting).
100
+    last_drag_render: Option<Instant>,
94
 }
101
 }
95
 
102
 
96
 impl GridView {
103
 impl GridView {
@@ -100,6 +107,7 @@ impl GridView {
100
         let columns = Self::calculate_columns_for_size(bounds.width, icon_size);
107
         let columns = Self::calculate_columns_for_size(bounds.width, icon_size);
101
         Self {
108
         Self {
102
             entries: Vec::new(),
109
             entries: Vec::new(),
110
+            visible_indices: Vec::new(),
103
             focused: 0,
111
             focused: 0,
104
             selected: HashSet::new(),
112
             selected: HashSet::new(),
105
             selection_anchor: None,
113
             selection_anchor: None,
@@ -111,9 +119,21 @@ impl GridView {
111
             drag_start: None,
119
             drag_start: None,
112
             drag_current: None,
120
             drag_current: None,
113
             icon_size,
121
             icon_size,
122
+            last_selection_update: None,
123
+            last_drag_render: None,
114
         }
124
         }
115
     }
125
     }
116
 
126
 
127
+    /// Rebuild the visible indices cache.
128
+    fn rebuild_visible_cache(&mut self) {
129
+        self.visible_indices = self.entries
130
+            .iter()
131
+            .enumerate()
132
+            .filter(|(_, e)| self.show_hidden || !e.hidden)
133
+            .map(|(i, _)| i)
134
+            .collect();
135
+    }
136
+
117
     /// Calculate number of columns that fit in the given width for a specific icon size.
137
     /// Calculate number of columns that fit in the given width for a specific icon size.
118
     fn calculate_columns_for_size(width: u32, icon_size: IconSize) -> usize {
138
     fn calculate_columns_for_size(width: u32, icon_size: IconSize) -> usize {
119
         let cell_size = icon_size.cell_size();
139
         let cell_size = icon_size.cell_size();
@@ -144,6 +164,7 @@ impl GridView {
144
     /// Set the entries to display.
164
     /// Set the entries to display.
145
     pub fn set_entries(&mut self, entries: Vec<FileEntry>) {
165
     pub fn set_entries(&mut self, entries: Vec<FileEntry>) {
146
         self.entries = entries;
166
         self.entries = entries;
167
+        self.rebuild_visible_cache();
147
         self.focused = 0;
168
         self.focused = 0;
148
         self.selected.clear();
169
         self.selected.clear();
149
         self.selected.insert(0);
170
         self.selected.insert(0);
@@ -152,17 +173,29 @@ impl GridView {
152
     }
173
     }
153
 
174
 
154
     /// Get visible entries (respecting hidden filter).
175
     /// Get visible entries (respecting hidden filter).
176
+    /// Uses cached indices for efficiency.
155
     pub fn visible_entries(&self) -> Vec<&FileEntry> {
177
     pub fn visible_entries(&self) -> Vec<&FileEntry> {
156
-        self.entries
178
+        self.visible_indices
157
             .iter()
179
             .iter()
158
-            .filter(|e| self.show_hidden || !e.hidden)
180
+            .filter_map(|&i| self.entries.get(i))
159
             .collect()
181
             .collect()
160
     }
182
     }
161
 
183
 
184
+    /// Get visible entry count without allocation.
185
+    #[inline]
186
+    pub fn visible_count(&self) -> usize {
187
+        self.visible_indices.len()
188
+    }
189
+
190
+    /// Get entry at visible index without allocation.
191
+    #[inline]
192
+    pub fn visible_entry(&self, visible_idx: usize) -> Option<&FileEntry> {
193
+        self.visible_indices.get(visible_idx).and_then(|&i| self.entries.get(i))
194
+    }
195
+
162
     /// Get the currently focused entry.
196
     /// Get the currently focused entry.
163
     pub fn selected_entry(&self) -> Option<&FileEntry> {
197
     pub fn selected_entry(&self) -> Option<&FileEntry> {
164
-        let visible = self.visible_entries();
198
+        self.visible_entry(self.focused)
165
-        visible.get(self.focused).copied()
166
     }
199
     }
167
 
200
 
168
     /// Get the focused index.
201
     /// Get the focused index.
@@ -181,10 +214,9 @@ impl GridView {
181
 
214
 
182
     /// Get all selected entries.
215
     /// Get all selected entries.
183
     pub fn selected_entries(&self) -> Vec<&FileEntry> {
216
     pub fn selected_entries(&self) -> Vec<&FileEntry> {
184
-        let visible = self.visible_entries();
185
         self.selected
217
         self.selected
186
             .iter()
218
             .iter()
187
-            .filter_map(|&i| visible.get(i).copied())
219
+            .filter_map(|&i| self.visible_entry(i))
188
             .collect()
220
             .collect()
189
     }
221
     }
190
 
222
 
@@ -207,7 +239,8 @@ impl GridView {
207
     /// Toggle hidden files visibility.
239
     /// Toggle hidden files visibility.
208
     pub fn toggle_hidden(&mut self) {
240
     pub fn toggle_hidden(&mut self) {
209
         self.show_hidden = !self.show_hidden;
241
         self.show_hidden = !self.show_hidden;
210
-        let visible_count = self.visible_entries().len();
242
+        self.rebuild_visible_cache();
243
+        let visible_count = self.visible_count();
211
         if self.focused >= visible_count && visible_count > 0 {
244
         if self.focused >= visible_count && visible_count > 0 {
212
             self.focused = visible_count - 1;
245
             self.focused = visible_count - 1;
213
         }
246
         }
@@ -229,7 +262,7 @@ impl GridView {
229
 
262
 
230
     /// Move selection down (to next row).
263
     /// Move selection down (to next row).
231
     pub fn select_next(&mut self) {
264
     pub fn select_next(&mut self) {
232
-        let visible_count = self.visible_entries().len();
265
+        let visible_count = self.visible_count();
233
         if self.focused + self.columns < visible_count {
266
         if self.focused + self.columns < visible_count {
234
             self.focused += self.columns;
267
             self.focused += self.columns;
235
         } else if self.focused < visible_count.saturating_sub(1) {
268
         } else if self.focused < visible_count.saturating_sub(1) {
@@ -248,7 +281,7 @@ impl GridView {
248
 
281
 
249
     /// Move selection right.
282
     /// Move selection right.
250
     pub fn select_right(&mut self) {
283
     pub fn select_right(&mut self) {
251
-        let visible_count = self.visible_entries().len();
284
+        let visible_count = self.visible_count();
252
         if self.focused + 1 < visible_count {
285
         if self.focused + 1 < visible_count {
253
             self.focused += 1;
286
             self.focused += 1;
254
             self.update_single_selection();
287
             self.update_single_selection();
@@ -283,7 +316,7 @@ impl GridView {
283
 
316
 
284
     /// Jump to last entry.
317
     /// Jump to last entry.
285
     pub fn select_last(&mut self) {
318
     pub fn select_last(&mut self) {
286
-        let visible_count = self.visible_entries().len();
319
+        let visible_count = self.visible_count();
287
         if visible_count > 0 {
320
         if visible_count > 0 {
288
             self.focused = visible_count - 1;
321
             self.focused = visible_count - 1;
289
             self.update_single_selection();
322
             self.update_single_selection();
@@ -303,7 +336,7 @@ impl GridView {
303
 
336
 
304
     /// Page down.
337
     /// Page down.
305
     pub fn page_down(&mut self) {
338
     pub fn page_down(&mut self) {
306
-        let visible_count = self.visible_entries().len();
339
+        let visible_count = self.visible_count();
307
         let page_size = self.visible_rows() * self.columns;
340
         let page_size = self.visible_rows() * self.columns;
308
         self.focused = (self.focused + page_size).min(visible_count.saturating_sub(1));
341
         self.focused = (self.focused + page_size).min(visible_count.saturating_sub(1));
309
         self.update_single_selection();
342
         self.update_single_selection();
@@ -311,7 +344,7 @@ impl GridView {
311
 
344
 
312
     /// Select all entries (Ctrl+A).
345
     /// Select all entries (Ctrl+A).
313
     pub fn select_all(&mut self) {
346
     pub fn select_all(&mut self) {
314
-        let visible_count = self.visible_entries().len();
347
+        let visible_count = self.visible_count();
315
         self.selected = (0..visible_count).collect();
348
         self.selected = (0..visible_count).collect();
316
     }
349
     }
317
 
350
 
@@ -344,8 +377,32 @@ impl GridView {
344
         // Handle rubber band drag
377
         // Handle rubber band drag
345
         if self.drag_start.is_some() {
378
         if self.drag_start.is_some() {
346
             self.drag_current = Some(pos);
379
             self.drag_current = Some(pos);
347
-            self.update_rubber_band_selection();
380
+
348
-            return true; // Rubber band always needs redraw
381
+            // Throttle both selection updates AND render requests to ~60fps
382
+            let should_update = match self.last_selection_update {
383
+                Some(last) => last.elapsed() >= Duration::from_millis(16),
384
+                None => true,
385
+            };
386
+
387
+            if should_update {
388
+                self.update_rubber_band_selection();
389
+                self.last_selection_update = Some(Instant::now());
390
+                self.last_drag_render = Some(Instant::now());
391
+                return true; // Request redraw at throttled rate
392
+            }
393
+
394
+            // Also limit render requests independently (in case selection didn't change)
395
+            let should_render = match self.last_drag_render {
396
+                Some(last) => last.elapsed() >= Duration::from_millis(16),
397
+                None => true,
398
+            };
399
+
400
+            if should_render {
401
+                self.last_drag_render = Some(Instant::now());
402
+                return true;
403
+            }
404
+
405
+            return false; // Skip redraw, we're within throttle window
349
         }
406
         }
350
 
407
 
351
         let old_hovered = self.hovered;
408
         let old_hovered = self.hovered;
@@ -355,9 +412,9 @@ impl GridView {
355
             return self.hovered != old_hovered;
412
             return self.hovered != old_hovered;
356
         }
413
         }
357
 
414
 
358
-        let visible = self.visible_entries();
415
+        let visible_count = self.visible_count();
359
         let start_index = self.scroll_offset * self.columns;
416
         let start_index = self.scroll_offset * self.columns;
360
-        let end_index = (start_index + self.visible_rows() * self.columns).min(visible.len());
417
+        let end_index = (start_index + self.visible_rows() * self.columns).min(visible_count);
361
 
418
 
362
         self.hovered = None;
419
         self.hovered = None;
363
         for i in start_index..end_index {
420
         for i in start_index..end_index {
@@ -375,6 +432,8 @@ impl GridView {
375
         self.drag_start = Some(pos);
432
         self.drag_start = Some(pos);
376
         self.drag_current = Some(pos);
433
         self.drag_current = Some(pos);
377
         self.selected.clear();
434
         self.selected.clear();
435
+        self.last_selection_update = None;
436
+        self.last_drag_render = None;
378
     }
437
     }
379
 
438
 
380
     /// Check if rubber band drag is active.
439
     /// Check if rubber band drag is active.
@@ -384,8 +443,12 @@ impl GridView {
384
 
443
 
385
     /// Stop rubber band selection.
444
     /// Stop rubber band selection.
386
     pub fn stop_drag(&mut self) {
445
     pub fn stop_drag(&mut self) {
446
+        // Final selection update before clearing drag state
447
+        self.update_rubber_band_selection();
387
         self.drag_start = None;
448
         self.drag_start = None;
388
         self.drag_current = None;
449
         self.drag_current = None;
450
+        self.last_selection_update = None;
451
+        self.last_drag_render = None;
389
     }
452
     }
390
 
453
 
391
     /// Get the rubber band rectangle (if dragging).
454
     /// Get the rubber band rectangle (if dragging).
@@ -404,9 +467,9 @@ impl GridView {
404
     /// Update selection based on rubber band rectangle.
467
     /// Update selection based on rubber band rectangle.
405
     fn update_rubber_band_selection(&mut self) {
468
     fn update_rubber_band_selection(&mut self) {
406
         if let Some(band) = self.rubber_band_rect() {
469
         if let Some(band) = self.rubber_band_rect() {
407
-            let visible = self.visible_entries();
470
+            let visible_count = self.visible_count();
408
             let start_index = self.scroll_offset * self.columns;
471
             let start_index = self.scroll_offset * self.columns;
409
-            let end_index = (start_index + self.visible_rows() * self.columns).min(visible.len());
472
+            let end_index = (start_index + self.visible_rows() * self.columns).min(visible_count);
410
 
473
 
411
             self.selected.clear();
474
             self.selected.clear();
412
             for i in start_index..end_index {
475
             for i in start_index..end_index {
@@ -424,13 +487,13 @@ impl GridView {
424
             return None;
487
             return None;
425
         }
488
         }
426
 
489
 
427
-        let visible = self.visible_entries();
490
+        let visible_count = self.visible_count();
428
         let start_index = self.scroll_offset * self.columns;
491
         let start_index = self.scroll_offset * self.columns;
429
-        let end_index = (start_index + self.visible_rows() * self.columns).min(visible.len());
492
+        let end_index = (start_index + self.visible_rows() * self.columns).min(visible_count);
430
 
493
 
431
         for i in start_index..end_index {
494
         for i in start_index..end_index {
432
             if self.cell_bounds(i).contains_point(pos) {
495
             if self.cell_bounds(i).contains_point(pos) {
433
-                return visible.get(i).copied();
496
+                return self.visible_entry(i);
434
             }
497
             }
435
         }
498
         }
436
 
499
 
@@ -443,9 +506,9 @@ impl GridView {
443
             return None;
506
             return None;
444
         }
507
         }
445
 
508
 
446
-        let visible = self.visible_entries();
509
+        let visible_count = self.visible_count();
447
         let start_index = self.scroll_offset * self.columns;
510
         let start_index = self.scroll_offset * self.columns;
448
-        let end_index = (start_index + self.visible_rows() * self.columns).min(visible.len());
511
+        let end_index = (start_index + self.visible_rows() * self.columns).min(visible_count);
449
 
512
 
450
         for i in start_index..end_index {
513
         for i in start_index..end_index {
451
             if self.cell_bounds(i).contains_point(pos) {
514
             if self.cell_bounds(i).contains_point(pos) {
@@ -499,14 +562,30 @@ impl GridView {
499
     /// Render the grid view.
562
     /// Render the grid view.
500
     pub fn render(&self, renderer: &Renderer, rename_state: Option<&RenameState>) -> anyhow::Result<()> {
563
     pub fn render(&self, renderer: &Renderer, rename_state: Option<&RenameState>) -> anyhow::Result<()> {
501
         let theme = renderer.theme();
564
         let theme = renderer.theme();
502
-        let visible = self.visible_entries();
565
+        let visible_count = self.visible_count();
503
         let visible_rows = self.visible_rows();
566
         let visible_rows = self.visible_rows();
504
 
567
 
505
         let start_index = self.scroll_offset * self.columns;
568
         let start_index = self.scroll_offset * self.columns;
506
-        let end_index = (start_index + visible_rows * self.columns).min(visible.len());
569
+        let end_index = (start_index + visible_rows * self.columns).min(visible_count);
570
+
571
+        // Pre-compute colors to avoid parsing hex strings in the loop
572
+        let dir_color = Color::new(0.36, 0.62, 0.85, 1.0); // #5c9fd8
573
+        let symlink_color = Color::new(0.78, 0.47, 0.87, 1.0); // #c678dd
574
+
575
+        // Pre-compute icon font size
576
+        let icon_font_size = match self.icon_size {
577
+            IconSize::Small => 24.0,
578
+            IconSize::Medium => 32.0,
579
+            IconSize::Large => 48.0,
580
+        };
581
+        let name_font_size = self.icon_size.font_size();
582
+        let icon_size_px = self.icon_size.icon_size();
507
 
583
 
508
         for i in start_index..end_index {
584
         for i in start_index..end_index {
509
-            let entry = visible[i];
585
+            let entry = match self.visible_entry(i) {
586
+                Some(e) => e,
587
+                None => continue,
588
+            };
510
             let cell = self.cell_bounds(i);
589
             let cell = self.cell_bounds(i);
511
 
590
 
512
             // Skip cells outside visible area
591
             // Skip cells outside visible area
@@ -545,18 +624,12 @@ impl GridView {
545
                 theme.selection_foreground
624
                 theme.selection_foreground
546
             } else {
625
             } else {
547
                 match entry.entry_type {
626
                 match entry.entry_type {
548
-                    EntryType::Directory => Color::from_hex("#5c9fd8").unwrap_or(theme.item_foreground),
627
+                    EntryType::Directory => dir_color,
549
-                    EntryType::Symlink => Color::from_hex("#c678dd").unwrap_or(theme.item_foreground),
628
+                    EntryType::Symlink => symlink_color,
550
                     _ => theme.item_foreground,
629
                     _ => theme.item_foreground,
551
                 }
630
                 }
552
             };
631
             };
553
 
632
 
554
-            // Scale icon font size based on icon size setting
555
-            let icon_font_size = match self.icon_size {
556
-                IconSize::Small => 24.0,
557
-                IconSize::Medium => 32.0,
558
-                IconSize::Large => 48.0,
559
-            };
560
             let icon_style = TextStyle::new()
633
             let icon_style = TextStyle::new()
561
                 .font_family(&theme.font_family)
634
                 .font_family(&theme.font_family)
562
                 .font_size(icon_font_size)
635
                 .font_size(icon_font_size)
@@ -568,12 +641,11 @@ impl GridView {
568
             renderer.text_centered(icon, Point::new(icon_center_x, icon_center_y), &icon_style)?;
641
             renderer.text_centered(icon, Point::new(icon_center_x, icon_center_y), &icon_style)?;
569
 
642
 
570
             // Rectangle for the text area below the icon
643
             // Rectangle for the text area below the icon
571
-            let icon_size = self.icon_size.icon_size();
572
             let text_rect = Rect::new(
644
             let text_rect = Rect::new(
573
                 cell.x + 4,
645
                 cell.x + 4,
574
-                cell.y + icon_size as i32 + 8,
646
+                cell.y + icon_size_px as i32 + 8,
575
                 cell.width - 8,
647
                 cell.width - 8,
576
-                cell.height - icon_size - 12,
648
+                cell.height - icon_size_px - 12,
577
             );
649
             );
578
 
650
 
579
             if is_renaming {
651
             if is_renaming {
@@ -591,25 +663,22 @@ impl GridView {
591
                     theme.item_foreground
663
                     theme.item_foreground
592
                 };
664
                 };
593
 
665
 
594
-                let name_font_size = self.icon_size.font_size();
666
+                // Use Pango CENTER alignment for proper text centering
595
                 let name_style = TextStyle::new()
667
                 let name_style = TextStyle::new()
596
                     .font_family(&theme.font_family)
668
                     .font_family(&theme.font_family)
597
                     .font_size(name_font_size)
669
                     .font_size(name_font_size)
598
-                    .color(name_color);
670
+                    .color(name_color)
599
-
600
-                // Use Pango CENTER alignment for proper text centering (like Dolphin/Nautilus)
601
-                let name_style = name_style.clone()
602
                     .align(TextAlign::Center)
671
                     .align(TextAlign::Center)
603
                     .ellipsize(true)
672
                     .ellipsize(true)
604
                     .max_width((cell.width - 8) as i32);
673
                     .max_width((cell.width - 8) as i32);
605
 
674
 
606
-                // Add "@" suffix for symlinks
675
+                // Render text - avoid allocation for non-symlinks
607
-                let display_name = if entry.is_symlink {
676
+                if entry.is_symlink {
608
-                    format!("{}@", entry.name)
677
+                    let display_name = format!("{}@", entry.name);
678
+                    renderer.text_in_rect(&display_name, text_rect, &name_style)?;
609
                 } else {
679
                 } else {
610
-                    entry.name.clone()
680
+                    renderer.text_in_rect(&entry.name, text_rect, &name_style)?;
611
-                };
681
+                }
612
-                renderer.text_in_rect(&display_name, text_rect, &name_style)?;
613
             }
682
             }
614
         }
683
         }
615
 
684
 
garfield/src/ui/status_bar.rsmodified
@@ -52,6 +52,11 @@ impl StatusBar {
52
         self.selected_size = selected_size;
52
         self.selected_size = selected_size;
53
     }
53
     }
54
 
54
 
55
+    /// Light-weight update of just the selection count (for drag operations).
56
+    pub fn update_selection_count(&mut self, selected_count: usize) {
57
+        self.selected_count = selected_count;
58
+    }
59
+
55
     /// Set the current view mode name.
60
     /// Set the current view mode name.
56
     pub fn set_view_mode(&mut self, mode: &str) {
61
     pub fn set_view_mode(&mut self, mode: &str) {
57
         self.view_mode = mode.to_string();
62
         self.view_mode = mode.to_string();
garfield/src/ui/tab.rsmodified
@@ -578,12 +578,35 @@ impl Tab {
578
         match rename_path(&entry_path, new_name) {
578
         match rename_path(&entry_path, new_name) {
579
             Ok(new_path) => {
579
             Ok(new_path) => {
580
                 self.refresh();
580
                 self.refresh();
581
+                // Select the renamed file
582
+                self.select_by_path(&new_path);
581
                 Ok((entry_path, new_path, new_name.to_string()))
583
                 Ok((entry_path, new_path, new_name.to_string()))
582
             }
584
             }
583
             Err(e) => Err(e.to_string()),
585
             Err(e) => Err(e.to_string()),
584
         }
586
         }
585
     }
587
     }
586
 
588
 
589
+    /// Select a file by its path.
590
+    fn select_by_path(&mut self, path: &std::path::Path) {
591
+        let visible = self.visible_entries();
592
+        for (i, entry) in visible.iter().enumerate() {
593
+            if entry.path == path {
594
+                match self.view_mode {
595
+                    ViewMode::List => {
596
+                        self.list_view.set_focused(i);
597
+                    }
598
+                    ViewMode::Grid => {
599
+                        self.grid_view.set_focused(i);
600
+                    }
601
+                    ViewMode::Columns => {
602
+                        self.column_view.set_focused(i);
603
+                    }
604
+                }
605
+                return;
606
+            }
607
+        }
608
+    }
609
+
587
     /// Handle keyboard input during rename. Returns true if handled.
610
     /// Handle keyboard input during rename. Returns true if handled.
588
     pub fn handle_rename_key(&mut self, key: &Key) -> bool {
611
     pub fn handle_rename_key(&mut self, key: &Key) -> bool {
589
         let state = match self.renaming.as_mut() {
612
         let state = match self.renaming.as_mut() {