gardesk/garfield / 3abf9ab

Browse files

ui: add bookmark reorder via drag in sidebar

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3abf9ab0ae87b7621d6bf2761403c4e300fccc00
Parents
c7ca947
Tree
81e2dd2

2 changed files

StatusFile+-
M garfield/src/app.rs 25 3
M garfield/src/ui/sidebar.rs 224 1
garfield/src/app.rsmodified
@@ -305,7 +305,13 @@ impl App {
305
             return;
305
             return;
306
         }
306
         }
307
 
307
 
308
-        // Check sidebar clicks
308
+        // Check sidebar clicks - try to start bookmark drag first
309
+        if self.sidebar.start_bookmark_drag(pos) {
310
+            // Started potential bookmark drag, don't navigate yet
311
+            return;
312
+        }
313
+
314
+        // Check sidebar clicks (for non-bookmark items)
309
         if let Some(path) = self.sidebar.on_click(pos) {
315
         if let Some(path) = self.sidebar.on_click(pos) {
310
             self.navigate_to(path);
316
             self.navigate_to(path);
311
             return;
317
             return;
@@ -408,7 +414,18 @@ impl App {
408
 
414
 
409
     /// Handle mouse release.
415
     /// Handle mouse release.
410
     fn handle_mouse_release(&mut self, pos: Point) {
416
     fn handle_mouse_release(&mut self, pos: Point) {
411
-        // Handle bookmark drag drop
417
+        // Handle bookmark reorder drag completion
418
+        if self.sidebar.is_bookmark_dragging() {
419
+            self.sidebar.complete_bookmark_drag();
420
+        } else if self.sidebar.bookmark_drag_index().is_some() {
421
+            // Clicked on bookmark but didn't drag - navigate to it
422
+            if let Some(path) = self.sidebar.bookmark_path_at_index() {
423
+                self.navigate_to(path);
424
+            }
425
+            self.sidebar.cancel_bookmark_drag();
426
+        }
427
+
428
+        // Handle bookmark drag drop (dragging from file view to sidebar)
412
         if self.drag_active {
429
         if self.drag_active {
413
             if let Some(path) = self.drag_source_path.take() {
430
             if let Some(path) = self.drag_source_path.take() {
414
                 if self.sidebar.is_bookmark_drop_zone(pos) {
431
                 if self.sidebar.is_bookmark_drop_zone(pos) {
@@ -467,7 +484,12 @@ impl App {
467
             }
484
             }
468
         }
485
         }
469
 
486
 
470
-        // Handle bookmark drag in progress
487
+        // Handle bookmark reorder drag in progress
488
+        if self.sidebar.bookmark_drag_index().is_some() {
489
+            self.sidebar.update_bookmark_drag(pos);
490
+        }
491
+
492
+        // Handle bookmark drag in progress (dragging from file view)
471
         if self.drag_source_path.is_some() {
493
         if self.drag_source_path.is_some() {
472
             // Update current drag position for visual feedback
494
             // Update current drag position for visual feedback
473
             self.drag_current_pos = Some(pos);
495
             self.drag_current_pos = Some(pos);
garfield/src/ui/sidebar.rsmodified
@@ -43,6 +43,14 @@ pub struct Sidebar {
43
     bookmarks_section_y: i32,
43
     bookmarks_section_y: i32,
44
     /// Whether to show drop highlight on bookmarks section.
44
     /// Whether to show drop highlight on bookmarks section.
45
     drop_highlight: bool,
45
     drop_highlight: bool,
46
+    /// Index of bookmark being dragged for reorder.
47
+    bookmark_drag_index: Option<usize>,
48
+    /// Target insert position for bookmark reorder.
49
+    bookmark_drop_index: Option<usize>,
50
+    /// Starting position of bookmark drag.
51
+    bookmark_drag_start: Option<Point>,
52
+    /// Whether bookmark drag is active (past threshold).
53
+    bookmark_drag_active: bool,
46
 }
54
 }
47
 
55
 
48
 impl Sidebar {
56
 impl Sidebar {
@@ -64,6 +72,10 @@ impl Sidebar {
64
             bookmarks_path,
72
             bookmarks_path,
65
             bookmarks_section_y: 0,
73
             bookmarks_section_y: 0,
66
             drop_highlight: false,
74
             drop_highlight: false,
75
+            bookmark_drag_index: None,
76
+            bookmark_drop_index: None,
77
+            bookmark_drag_start: None,
78
+            bookmark_drag_active: false,
67
         };
79
         };
68
         sidebar.populate_default_places();
80
         sidebar.populate_default_places();
69
         sidebar.load_bookmarks();
81
         sidebar.load_bookmarks();
@@ -344,6 +356,125 @@ impl Sidebar {
344
         }
356
         }
345
     }
357
     }
346
 
358
 
359
+    /// Get the bookmark index at a given point (if any).
360
+    fn bookmark_index_at_point(&self, pos: Point) -> Option<usize> {
361
+        for (i, bookmark) in self.bookmarks.iter().enumerate() {
362
+            if bookmark.bounds.contains_point(pos) {
363
+                return Some(i);
364
+            }
365
+        }
366
+        None
367
+    }
368
+
369
+    /// Start dragging a bookmark if the position is over one.
370
+    /// Returns true if drag was started.
371
+    pub fn start_bookmark_drag(&mut self, pos: Point) -> bool {
372
+        if !self.visible || !self.bounds.contains_point(pos) {
373
+            return false;
374
+        }
375
+
376
+        if let Some(index) = self.bookmark_index_at_point(pos) {
377
+            self.bookmark_drag_index = Some(index);
378
+            self.bookmark_drag_start = Some(pos);
379
+            self.bookmark_drag_active = false;
380
+            self.bookmark_drop_index = None;
381
+            true
382
+        } else {
383
+            false
384
+        }
385
+    }
386
+
387
+    /// Update bookmark drag state with current mouse position.
388
+    /// Returns true if drag is active.
389
+    pub fn update_bookmark_drag(&mut self, pos: Point) -> bool {
390
+        if self.bookmark_drag_index.is_none() {
391
+            return false;
392
+        }
393
+
394
+        // Check if past drag threshold
395
+        if !self.bookmark_drag_active {
396
+            if let Some(start) = self.bookmark_drag_start {
397
+                let dx = (pos.x - start.x).abs();
398
+                let dy = (pos.y - start.y).abs();
399
+                if dx > 5 || dy > 5 {
400
+                    self.bookmark_drag_active = true;
401
+                }
402
+            }
403
+        }
404
+
405
+        if !self.bookmark_drag_active {
406
+            return false;
407
+        }
408
+
409
+        // Calculate drop index based on mouse position
410
+        if pos.y < self.bookmarks_section_y || !self.bounds.contains_point(pos) {
411
+            self.bookmark_drop_index = None;
412
+        } else {
413
+            // Find which slot we're closest to
414
+            let mut drop_index = 0;
415
+            for (i, bookmark) in self.bookmarks.iter().enumerate() {
416
+                let mid_y = bookmark.bounds.y + bookmark.bounds.height as i32 / 2;
417
+                if pos.y > mid_y {
418
+                    drop_index = i + 1;
419
+                }
420
+            }
421
+            self.bookmark_drop_index = Some(drop_index);
422
+        }
423
+
424
+        true
425
+    }
426
+
427
+    /// Complete bookmark drag and reorder.
428
+    /// Returns true if reorder occurred.
429
+    pub fn complete_bookmark_drag(&mut self) -> bool {
430
+        let result = if self.bookmark_drag_active {
431
+            if let (Some(from), Some(to)) = (self.bookmark_drag_index, self.bookmark_drop_index) {
432
+                if from != to && to != from + 1 && !self.bookmarks.is_empty() {
433
+                    // Perform the reorder
434
+                    let bookmark = self.bookmarks.remove(from);
435
+                    let new_index = if to > from { to - 1 } else { to };
436
+                    self.bookmarks.insert(new_index, bookmark);
437
+                    self.save_bookmarks();
438
+                    true
439
+                } else {
440
+                    false
441
+                }
442
+            } else {
443
+                false
444
+            }
445
+        } else {
446
+            false
447
+        };
448
+
449
+        self.cancel_bookmark_drag();
450
+        result
451
+    }
452
+
453
+    /// Cancel bookmark drag.
454
+    pub fn cancel_bookmark_drag(&mut self) {
455
+        self.bookmark_drag_index = None;
456
+        self.bookmark_drop_index = None;
457
+        self.bookmark_drag_start = None;
458
+        self.bookmark_drag_active = false;
459
+    }
460
+
461
+    /// Check if bookmark drag is in progress.
462
+    pub fn is_bookmark_dragging(&self) -> bool {
463
+        self.bookmark_drag_active
464
+    }
465
+
466
+    /// Get the index of the bookmark being dragged (for checking if a click was on a bookmark).
467
+    pub fn bookmark_drag_index(&self) -> Option<usize> {
468
+        self.bookmark_drag_index
469
+    }
470
+
471
+    /// Get the path of the bookmark at the current drag index.
472
+    pub fn bookmark_path_at_index(&self) -> Option<PathBuf> {
473
+        self.bookmark_drag_index
474
+            .and_then(|i| self.bookmarks.get(i))
475
+            .map(|b| b.path.clone())
476
+    }
477
+
347
     /// Update bounds.
478
     /// Update bounds.
348
     pub fn set_bounds(&mut self, bounds: Rect) {
479
     pub fn set_bounds(&mut self, bounds: Rect) {
349
         self.bounds = bounds;
480
         self.bounds = bounds;
@@ -565,11 +696,27 @@ impl Sidebar {
565
                 .font_size(theme.font_size - 2.0)
696
                 .font_size(theme.font_size - 2.0)
566
                 .color(theme.item_foreground.with_alpha(0.4));
697
                 .color(theme.item_foreground.with_alpha(0.4));
567
             renderer.text("Ctrl+D to add", (header_x + 4) as f64, y as f64, &hint_style)?;
698
             renderer.text("Ctrl+D to add", (header_x + 4) as f64, y as f64, &hint_style)?;
699
+
700
+            // Draw drop indicator at start if dragging (shouldn't happen but be safe)
701
+            if self.bookmark_drag_active && self.bookmark_drop_index == Some(0) {
702
+                self.render_drop_indicator(renderer, y)?;
703
+            }
568
         } else {
704
         } else {
569
             for i in 0..self.bookmarks.len() {
705
             for i in 0..self.bookmarks.len() {
706
+                // Draw drop indicator before this item if needed
707
+                if self.bookmark_drag_active && self.bookmark_drop_index == Some(i) {
708
+                    self.render_drop_indicator(renderer, y)?;
709
+                }
710
+
570
                 let combined_index = self.places.len() + i;
711
                 let combined_index = self.places.len() + i;
571
                 let is_hovered = self.hovered == Some(combined_index);
712
                 let is_hovered = self.hovered == Some(combined_index);
572
-                y = self.render_item(renderer, combined_index, y, is_hovered, &icon_style, &name_style, &hover_style)?;
713
+                let is_dragging = self.bookmark_drag_index == Some(i);
714
+                y = self.render_bookmark_item(renderer, combined_index, y, is_hovered, is_dragging, &icon_style, &name_style, &hover_style)?;
715
+            }
716
+
717
+            // Draw drop indicator at end if needed
718
+            if self.bookmark_drag_active && self.bookmark_drop_index == Some(self.bookmarks.len()) {
719
+                self.render_drop_indicator(renderer, y)?;
573
             }
720
             }
574
         }
721
         }
575
 
722
 
@@ -626,4 +773,80 @@ impl Sidebar {
626
 
773
 
627
         Ok(y + self.item_height as i32)
774
         Ok(y + self.item_height as i32)
628
     }
775
     }
776
+
777
+    /// Render a bookmark item (supports dimming when being dragged).
778
+    fn render_bookmark_item(
779
+        &mut self,
780
+        renderer: &Renderer,
781
+        index: usize,
782
+        y: i32,
783
+        is_hovered: bool,
784
+        is_dragging: bool,
785
+        icon_style: &TextStyle,
786
+        name_style: &TextStyle,
787
+        hover_style: &TextStyle,
788
+    ) -> anyhow::Result<i32> {
789
+        let theme = renderer.theme();
790
+
791
+        // Get item (need to reborrow to avoid issues)
792
+        let (icon, name) = {
793
+            let item = self.get_item(index).unwrap();
794
+            (item.icon.clone(), item.name.clone())
795
+        };
796
+
797
+        // Update bounds for hit testing
798
+        let item_bounds = Rect::new(
799
+            self.bounds.x,
800
+            y,
801
+            self.bounds.width,
802
+            self.item_height,
803
+        );
804
+
805
+        // Store bounds
806
+        if let Some(item) = self.get_item_mut(index) {
807
+            item.bounds = item_bounds;
808
+        }
809
+
810
+        // Draw hover background (unless being dragged)
811
+        if is_hovered && !is_dragging {
812
+            renderer.fill_rect(item_bounds, theme.item_background)?;
813
+        }
814
+
815
+        // Dim if being dragged
816
+        let alpha = if is_dragging { 0.4 } else { 1.0 };
817
+
818
+        let text_style = if is_hovered && !is_dragging {
819
+            hover_style.clone()
820
+        } else {
821
+            name_style.clone().color(theme.item_foreground.with_alpha(alpha))
822
+        };
823
+
824
+        let icon_style = icon_style.clone().color(theme.item_foreground.with_alpha(0.7 * alpha));
825
+
826
+        // Draw icon
827
+        let icon_x = self.bounds.x + self.padding as i32;
828
+        let text_y = y + (self.item_height as i32 - theme.font_size as i32) / 2;
829
+        renderer.text(&icon, icon_x as f64, text_y as f64, &icon_style)?;
830
+
831
+        // Draw name
832
+        let name_x = icon_x + 20;
833
+        renderer.text(&name, name_x as f64, text_y as f64, &text_style)?;
834
+
835
+        Ok(y + self.item_height as i32)
836
+    }
837
+
838
+    /// Render a drop indicator line.
839
+    fn render_drop_indicator(&self, renderer: &Renderer, y: i32) -> anyhow::Result<()> {
840
+        let theme = renderer.theme();
841
+        let line_y = y - 2;
842
+        renderer.line(
843
+            (self.bounds.x + self.padding as i32) as f64,
844
+            line_y as f64,
845
+            (self.bounds.x + self.bounds.width as i32 - self.padding as i32) as f64,
846
+            line_y as f64,
847
+            theme.selection_background,
848
+            2.0,
849
+        )?;
850
+        Ok(())
851
+    }
629
 }
852
 }