gardesk/garfield / 8f57948

Browse files

ui: add tab reorder via drag in tab bar

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
8f5794861e0da378e599fc564219d22fcbde3f7f
Parents
e61c363
Tree
cfe20ae

3 changed files

StatusFile+-
M garfield/src/app.rs 37 3
M garfield/src/ui/pane.rs 22 0
M garfield/src/ui/tab_bar.rs 148 0
garfield/src/app.rsmodified
@@ -289,13 +289,18 @@ impl App {
289289
             return;
290290
         }
291291
 
292
-        // Check tab bar clicks
292
+        // Check tab bar clicks - try to start tab drag first
293
+        if self.tab_bar.start_drag(pos) {
294
+            // Started potential tab drag, don't switch yet
295
+            return;
296
+        }
297
+
298
+        // Handle tab bar close button clicks (non-drag)
293299
         if let Some((tab_index, is_close)) = self.tab_bar.on_click(pos) {
294300
             if is_close {
295301
                 self.close_tab(tab_index);
296
-            } else {
297
-                self.switch_tab(tab_index);
298302
             }
303
+            // Tab selection happens on mouse release if not dragged
299304
             return;
300305
         }
301306
 
@@ -414,6 +419,19 @@ impl App {
414419
 
415420
     /// Handle mouse release.
416421
     fn handle_mouse_release(&mut self, pos: Point) {
422
+        // Handle tab reorder drag completion
423
+        if self.tab_bar.is_dragging() {
424
+            if let Some((from, to)) = self.tab_bar.complete_drag() {
425
+                self.reorder_tab(from, to);
426
+            }
427
+        } else if self.tab_bar.dragging_tab().is_some() {
428
+            // Clicked on tab but didn't drag - select it
429
+            if let Some(index) = self.tab_bar.dragging_tab() {
430
+                self.switch_tab(index);
431
+            }
432
+            self.tab_bar.cancel_drag();
433
+        }
434
+
417435
         // Handle bookmark reorder drag completion
418436
         if self.sidebar.is_bookmark_dragging() {
419437
             self.sidebar.complete_bookmark_drag();
@@ -484,6 +502,11 @@ impl App {
484502
             }
485503
         }
486504
 
505
+        // Handle tab reorder drag in progress
506
+        if self.tab_bar.dragging_tab().is_some() {
507
+            self.tab_bar.update_drag(pos);
508
+        }
509
+
487510
         // Handle bookmark reorder drag in progress
488511
         if self.sidebar.bookmark_drag_index().is_some() {
489512
             self.sidebar.update_bookmark_drag(pos);
@@ -851,6 +874,17 @@ impl App {
851874
         self.update_status_bar();
852875
     }
853876
 
877
+    /// Reorder a tab from one position to another.
878
+    fn reorder_tab(&mut self, from: usize, to: usize) {
879
+        if let Some(pane) = self.focused_pane_mut() {
880
+            pane.reorder_tab(from, to);
881
+        }
882
+
883
+        self.sync_tab_bar();
884
+        self.sync_breadcrumb();
885
+        self.update_status_bar();
886
+    }
887
+
854888
     /// Cycle to next tab.
855889
     fn next_tab(&mut self) {
856890
         if let Some(pane) = self.focused_pane_mut() {
garfield/src/ui/pane.rsmodified
@@ -486,6 +486,28 @@ impl Pane {
486486
         }
487487
     }
488488
 
489
+    /// Reorder a tab from one position to another.
490
+    pub fn reorder_tab(&mut self, from: usize, to: usize) {
491
+        if let Pane::Leaf { tabs, active_tab, .. } = self {
492
+            if from >= tabs.len() || to > tabs.len() {
493
+                return;
494
+            }
495
+
496
+            let tab = tabs.remove(from);
497
+            let new_index = if to > from { to - 1 } else { to };
498
+            tabs.insert(new_index, tab);
499
+
500
+            // Update active tab index if affected
501
+            if *active_tab == from {
502
+                *active_tab = new_index;
503
+            } else if from < *active_tab && new_index >= *active_tab {
504
+                *active_tab = active_tab.saturating_sub(1);
505
+            } else if from > *active_tab && new_index <= *active_tab {
506
+                *active_tab = (*active_tab + 1).min(tabs.len() - 1);
507
+            }
508
+        }
509
+    }
510
+
489511
     /// Get tab count.
490512
     pub fn tab_count(&self) -> usize {
491513
         match self {
garfield/src/ui/tab_bar.rsmodified
@@ -41,6 +41,14 @@ pub struct TabBar {
4141
     hovered_close: Option<usize>,
4242
     /// Cached tab bounds.
4343
     tab_bounds: Vec<Rect>,
44
+    /// Tab being dragged (index).
45
+    dragging_tab: Option<usize>,
46
+    /// Drag start position.
47
+    drag_start: Option<Point>,
48
+    /// Whether drag is active (past threshold).
49
+    drag_active: bool,
50
+    /// Target drop position for reorder.
51
+    drop_target: Option<usize>,
4452
 }
4553
 
4654
 impl TabBar {
@@ -53,6 +61,10 @@ impl TabBar {
5361
             hovered_tab: None,
5462
             hovered_close: None,
5563
             tab_bounds: Vec::new(),
64
+            dragging_tab: None,
65
+            drag_start: None,
66
+            drag_active: false,
67
+            drop_target: None,
5668
         }
5769
     }
5870
 
@@ -164,6 +176,107 @@ impl TabBar {
164176
         self.hovered_close = None;
165177
     }
166178
 
179
+    /// Start potential tab drag.
180
+    pub fn start_drag(&mut self, pos: Point) -> bool {
181
+        if !self.bounds.contains_point(pos) {
182
+            return false;
183
+        }
184
+
185
+        for (i, tab_bounds) in self.tab_bounds.iter().enumerate() {
186
+            if tab_bounds.contains_point(pos) {
187
+                // Don't drag if clicking close button
188
+                if let Some(close_bounds) = self.close_button_bounds(i) {
189
+                    if close_bounds.contains_point(pos) {
190
+                        return false;
191
+                    }
192
+                }
193
+                self.dragging_tab = Some(i);
194
+                self.drag_start = Some(pos);
195
+                self.drag_active = false;
196
+                self.drop_target = None;
197
+                return true;
198
+            }
199
+        }
200
+        false
201
+    }
202
+
203
+    /// Update tab drag with current mouse position.
204
+    /// Returns true if drag is active.
205
+    pub fn update_drag(&mut self, pos: Point) -> bool {
206
+        if self.dragging_tab.is_none() {
207
+            return false;
208
+        }
209
+
210
+        // Check if past drag threshold
211
+        if !self.drag_active {
212
+            if let Some(start) = self.drag_start {
213
+                let dx = (pos.x - start.x).abs();
214
+                if dx > 5 {
215
+                    self.drag_active = true;
216
+                }
217
+            }
218
+        }
219
+
220
+        if !self.drag_active {
221
+            return false;
222
+        }
223
+
224
+        // Calculate drop target based on position
225
+        if !self.bounds.contains_point(pos) {
226
+            self.drop_target = None;
227
+        } else {
228
+            // Find which slot we're closest to
229
+            let mut target = 0;
230
+            for (i, tab_bounds) in self.tab_bounds.iter().enumerate() {
231
+                let mid_x = tab_bounds.x + tab_bounds.width as i32 / 2;
232
+                if pos.x > mid_x {
233
+                    target = i + 1;
234
+                }
235
+            }
236
+            self.drop_target = Some(target);
237
+        }
238
+
239
+        true
240
+    }
241
+
242
+    /// Complete tab drag and return reorder info if any (from, to).
243
+    pub fn complete_drag(&mut self) -> Option<(usize, usize)> {
244
+        let result = if self.drag_active {
245
+            if let (Some(from), Some(to)) = (self.dragging_tab, self.drop_target) {
246
+                if from != to && to != from + 1 {
247
+                    Some((from, to))
248
+                } else {
249
+                    None
250
+                }
251
+            } else {
252
+                None
253
+            }
254
+        } else {
255
+            None
256
+        };
257
+
258
+        self.cancel_drag();
259
+        result
260
+    }
261
+
262
+    /// Cancel tab drag.
263
+    pub fn cancel_drag(&mut self) {
264
+        self.dragging_tab = None;
265
+        self.drag_start = None;
266
+        self.drag_active = false;
267
+        self.drop_target = None;
268
+    }
269
+
270
+    /// Check if currently dragging.
271
+    pub fn is_dragging(&self) -> bool {
272
+        self.drag_active
273
+    }
274
+
275
+    /// Get dragging tab index.
276
+    pub fn dragging_tab(&self) -> Option<usize> {
277
+        self.dragging_tab
278
+    }
279
+
167280
     /// Render the tab bar.
168281
     pub fn render(&self, renderer: &Renderer) -> anyhow::Result<()> {
169282
         let theme = renderer.theme();
@@ -185,6 +298,26 @@ impl TabBar {
185298
         for (i, (tab, bounds)) in self.tabs.iter().zip(self.tab_bounds.iter()).enumerate() {
186299
             let is_active = i == self.active_index;
187300
             let is_hovered = self.hovered_tab == Some(i);
301
+            let is_being_dragged = self.drag_active && self.dragging_tab == Some(i);
302
+
303
+            // Dim the tab being dragged
304
+            if is_being_dragged {
305
+                renderer.fill_rect(*bounds, theme.item_background.with_alpha(0.3))?;
306
+                continue; // Skip rest of rendering for dragged tab
307
+            }
308
+
309
+            // Draw drop indicator before this tab if needed
310
+            if self.drag_active && self.drop_target == Some(i) {
311
+                let indicator_x = bounds.x - 2;
312
+                renderer.line(
313
+                    indicator_x as f64,
314
+                    (bounds.y + 4) as f64,
315
+                    indicator_x as f64,
316
+                    (bounds.y + bounds.height as i32 - 4) as f64,
317
+                    theme.selection_background,
318
+                    3.0,
319
+                )?;
320
+            }
188321
 
189322
             // Tab background
190323
             if is_active {
@@ -286,6 +419,21 @@ impl TabBar {
286419
             }
287420
         }
288421
 
422
+        // Draw drop indicator at end if dropping after last tab
423
+        if self.drag_active && self.drop_target == Some(self.tabs.len()) {
424
+            if let Some(last_bounds) = self.tab_bounds.last() {
425
+                let indicator_x = last_bounds.x + last_bounds.width as i32 + 2;
426
+                renderer.line(
427
+                    indicator_x as f64,
428
+                    (last_bounds.y + 4) as f64,
429
+                    indicator_x as f64,
430
+                    (last_bounds.y + last_bounds.height as i32 - 4) as f64,
431
+                    theme.selection_background,
432
+                    3.0,
433
+                )?;
434
+            }
435
+        }
436
+
289437
         Ok(())
290438
     }
291439
 }