gardesk/gardisplay / c037b91

Browse files

fix edge snapping to respect alignment, clear dirty on save

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c037b9125e334c2d9fb042073f8c5e93acf1f5ed
Parents
cc3d7ca
Tree
b5b22d0

2 changed files

StatusFile+-
M gardisplay/src/app.rs 4 1
M gardisplay/src/ui/monitor_view.rs 79 83
gardisplay/src/app.rsmodified
@@ -466,7 +466,10 @@ impl App {
466
             .insert("default".to_string(), profile);
466
             .insert("default".to_string(), profile);
467
 
467
 
468
         match self.config.save() {
468
         match self.config.save() {
469
-            Ok(()) => self.set_status("Saved profile"),
469
+            Ok(()) => {
470
+                self.monitor_view.clear_dirty();
471
+                self.set_status("Saved profile");
472
+            }
470
             Err(e) => {
473
             Err(e) => {
471
                 tracing::error!("failed to save profile: {}", e);
474
                 tracing::error!("failed to save profile: {}", e);
472
                 self.set_status("Failed to save profile");
475
                 self.set_status("Failed to save profile");
gardisplay/src/ui/monitor_view.rsmodified
@@ -370,15 +370,85 @@ impl MonitorView {
370
     }
370
     }
371
 
371
 
372
     /// Apply snapping to a monitor after drag ends.
372
     /// Apply snapping to a monitor after drag ends.
373
-    /// Ensures no gaps - monitors must always be adjacent to at least one other.
373
+    /// Snaps edges to nearby edges while preserving the user's intended position.
374
     fn apply_snap(&mut self, dragged_idx: usize) {
374
     fn apply_snap(&mut self, dragged_idx: usize) {
375
         if self.monitors.len() < 2 {
375
         if self.monitors.len() < 2 {
376
             return;
376
             return;
377
         }
377
         }
378
 
378
 
379
-        // Always snap to nearest to ensure adjacency, using directional awareness
379
+        let mut dragged = self.monitors[dragged_idx].scaled_rect;
380
-        let dragged = self.monitors[dragged_idx].scaled_rect;
380
+        let dragged_right = dragged.x + dragged.width as i32;
381
-        self.snap_to_nearest(dragged_idx, dragged);
381
+        let dragged_bottom = dragged.y + dragged.height as i32;
382
+
383
+        // Find best edge snaps independently for each axis
384
+        let mut best_x_snap: Option<(i32, i32)> = None; // (new_x, distance)
385
+        let mut best_y_snap: Option<(i32, i32)> = None; // (new_y, distance)
386
+
387
+        for (i, other) in self.monitors.iter().enumerate() {
388
+            if i == dragged_idx {
389
+                continue;
390
+            }
391
+
392
+            let other_rect = other.scaled_rect;
393
+            let other_right = other_rect.x + other_rect.width as i32;
394
+            let other_bottom = other_rect.y + other_rect.height as i32;
395
+
396
+            // X-axis edge snaps (for adjacency - touching edges)
397
+            let x_snaps = [
398
+                // Dragged left edge to other right edge (place right of other)
399
+                (other_right, (dragged.x - other_right).abs()),
400
+                // Dragged right edge to other left edge (place left of other)
401
+                (other_rect.x - dragged.width as i32, (dragged_right - other_rect.x).abs()),
402
+                // Dragged left edge to other left edge (align left edges)
403
+                (other_rect.x, (dragged.x - other_rect.x).abs()),
404
+                // Dragged right edge to other right edge (align right edges)
405
+                (other_right - dragged.width as i32, (dragged_right - other_right).abs()),
406
+            ];
407
+
408
+            for (new_x, dist) in x_snaps {
409
+                if dist < SNAP_THRESHOLD {
410
+                    if best_x_snap.map_or(true, |(_, best_dist)| dist < best_dist) {
411
+                        best_x_snap = Some((new_x, dist));
412
+                    }
413
+                }
414
+            }
415
+
416
+            // Y-axis edge snaps
417
+            let y_snaps = [
418
+                // Dragged top edge to other bottom edge (place below other)
419
+                (other_bottom, (dragged.y - other_bottom).abs()),
420
+                // Dragged bottom edge to other top edge (place above other)
421
+                (other_rect.y - dragged.height as i32, (dragged_bottom - other_rect.y).abs()),
422
+                // Dragged top edge to other top edge (align top edges)
423
+                (other_rect.y, (dragged.y - other_rect.y).abs()),
424
+                // Dragged bottom edge to other bottom edge (align bottom edges)
425
+                (other_bottom - dragged.height as i32, (dragged_bottom - other_bottom).abs()),
426
+            ];
427
+
428
+            for (new_y, dist) in y_snaps {
429
+                if dist < SNAP_THRESHOLD {
430
+                    if best_y_snap.map_or(true, |(_, best_dist)| dist < best_dist) {
431
+                        best_y_snap = Some((new_y, dist));
432
+                    }
433
+                }
434
+            }
435
+        }
436
+
437
+        // Apply snaps
438
+        if let Some((new_x, _)) = best_x_snap {
439
+            dragged.x = new_x;
440
+        }
441
+        if let Some((new_y, _)) = best_y_snap {
442
+            dragged.y = new_y;
443
+        }
444
+
445
+        // Check for overlap and resolve if needed
446
+        if self.would_overlap(dragged, dragged_idx) {
447
+            // Revert to original position if we'd overlap
448
+            return;
449
+        }
450
+
451
+        self.monitors[dragged_idx].scaled_rect = dragged;
382
     }
452
     }
383
 
453
 
384
     /// Check if a rect overlaps with any monitor except the specified one.
454
     /// Check if a rect overlaps with any monitor except the specified one.
@@ -404,84 +474,6 @@ impl MonitorView {
404
         a.x < b_right && a_right > b.x && a.y < b_bottom && a_bottom > b.y
474
         a.x < b_right && a_right > b.x && a.y < b_bottom && a_bottom > b.y
405
     }
475
     }
406
 
476
 
407
-    /// Snap a monitor to be adjacent to the nearest other monitor.
408
-    /// Uses directional awareness - snaps to the side the monitor was dropped on.
409
-    /// Ensures no overlaps with other monitors.
410
-    fn snap_to_nearest(&mut self, dragged_idx: usize, dragged: Rect) {
411
-        let dragged_center_x = dragged.x + dragged.width as i32 / 2;
412
-        let dragged_center_y = dragged.y + dragged.height as i32 / 2;
413
-
414
-        let mut best_snap: Option<(i32, i32, i32)> = None; // (new_x, new_y, distance)
415
-
416
-        for (i, other) in self.monitors.iter().enumerate() {
417
-            if i == dragged_idx {
418
-                continue;
419
-            }
420
-
421
-            let other_rect = other.scaled_rect;
422
-            let other_center_x = other_rect.x + other_rect.width as i32 / 2;
423
-            let other_center_y = other_rect.y + other_rect.height as i32 / 2;
424
-
425
-            // Determine which side of the other monitor we're on
426
-            let dx = dragged_center_x - other_center_x;
427
-            let dy = dragged_center_y - other_center_y;
428
-
429
-            // Try all 4 sides and pick the best non-overlapping position
430
-            let candidates = [
431
-                // Right of other
432
-                (other_rect.x + other_rect.width as i32, other_rect.y),
433
-                // Left of other
434
-                (other_rect.x - dragged.width as i32, other_rect.y),
435
-                // Below other
436
-                (other_rect.x, other_rect.y + other_rect.height as i32),
437
-                // Above other
438
-                (other_rect.x, other_rect.y - dragged.height as i32),
439
-            ];
440
-
441
-            // Score each candidate based on direction preference
442
-            for (new_x, new_y) in candidates {
443
-                let candidate_rect = Rect::new(new_x, new_y, dragged.width, dragged.height);
444
-
445
-                // Skip if this position would overlap with another monitor
446
-                if self.would_overlap(candidate_rect, dragged_idx) {
447
-                    continue;
448
-                }
449
-
450
-                // Calculate distance with direction weighting
451
-                let new_center_x = new_x + dragged.width as i32 / 2;
452
-                let new_center_y = new_y + dragged.height as i32 / 2;
453
-
454
-                // Base distance
455
-                let mut dist = (dragged_center_x - new_center_x).abs()
456
-                    + (dragged_center_y - new_center_y).abs();
457
-
458
-                // Penalize positions that don't match the drag direction
459
-                let snap_dx = new_center_x - other_center_x;
460
-                let snap_dy = new_center_y - other_center_y;
461
-
462
-                // If we're dragging more horizontally, prefer horizontal snaps
463
-                if dx.abs() > dy.abs() {
464
-                    if (dx > 0) != (snap_dx > 0) {
465
-                        dist += 1000; // Penalize wrong horizontal direction
466
-                    }
467
-                } else {
468
-                    if (dy > 0) != (snap_dy > 0) {
469
-                        dist += 1000; // Penalize wrong vertical direction
470
-                    }
471
-                }
472
-
473
-                if best_snap.map_or(true, |(_, _, best_dist)| dist < best_dist) {
474
-                    best_snap = Some((new_x, new_y, dist));
475
-                }
476
-            }
477
-        }
478
-
479
-        if let Some((new_x, new_y, _)) = best_snap {
480
-            self.monitors[dragged_idx].scaled_rect.x = new_x;
481
-            self.monitors[dragged_idx].scaled_rect.y = new_y;
482
-        }
483
-    }
484
-
485
     /// Render the monitor view.
477
     /// Render the monitor view.
486
     pub fn render(&self, renderer: &mut Renderer, theme: &Theme) -> anyhow::Result<()> {
478
     pub fn render(&self, renderer: &mut Renderer, theme: &Theme) -> anyhow::Result<()> {
487
         // Background
479
         // Background
@@ -645,11 +637,15 @@ impl MonitorView {
645
     }
637
     }
646
 
638
 
647
     /// Check if layout has been modified.
639
     /// Check if layout has been modified.
648
-    #[allow(dead_code)] // Used in Sprint 3 for RandR application
649
     pub fn is_dirty(&self) -> bool {
640
     pub fn is_dirty(&self) -> bool {
650
         self.dirty
641
         self.dirty
651
     }
642
     }
652
 
643
 
644
+    /// Clear the dirty flag (after save).
645
+    pub fn clear_dirty(&mut self) {
646
+        self.dirty = false;
647
+    }
648
+
653
     /// Get primary monitor name.
649
     /// Get primary monitor name.
654
     #[allow(dead_code)] // Used in Sprint 3 for RandR application
650
     #[allow(dead_code)] // Used in Sprint 3 for RandR application
655
     pub fn primary_name(&self) -> Option<&str> {
651
     pub fn primary_name(&self) -> Option<&str> {