diff --git a/TODO.md b/TODO.md index 7b68932..a294328 100644 --- a/TODO.md +++ b/TODO.md @@ -111,11 +111,11 @@ - [ ] 1/3 - 2/3 layouts - [ ] Picture-in-picture style layouts -### 11. Animation Effects +### 11. Animation Effects ✅ COMPLETED **Priority: Low** -- [ ] Add smooth transitions when snapping windows -- [ ] Animate zone overlay appearance/disappearance -- [ ] Visual feedback when window snaps to zone +- [x] Add smooth transitions when snapping windows +- [x] Animate zone overlay appearance/disappearance +- [x] Visual feedback when window snaps to zone ### 12. Settings Panel ✅ COMPLETED **Priority: Medium** diff --git a/extension.js b/extension.js index 59833dc..f3e4fff 100644 --- a/extension.js +++ b/extension.js @@ -1044,9 +1044,9 @@ ZoneManager.prototype = { _showZonesOverlay: function() { if (this.isShowing) return; - + let monitor = Main.layoutManager.primaryMonitor; - + // Create overlay container this.overlay = new St.Widget({ style_class: 'gridsnap-overlay', @@ -1071,6 +1071,14 @@ ZoneManager.prototype = { Main.layoutManager.addChrome(this.overlay); this.isShowing = true; + + // Fade in animation + this.overlay.opacity = 0; + this.overlay.ease({ + opacity: 255, + duration: 200, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); }, _createZoneActor: function(zone, monitor, index) { @@ -1125,14 +1133,24 @@ ZoneManager.prototype = { _hideZonesOverlay: function() { if (!this.isShowing) return; - - if (this.overlay) { - Main.layoutManager.removeChrome(this.overlay); - this.overlay.destroy(); - this.overlay = null; - } - + this.isShowing = false; + + if (this.overlay) { + // Fade out animation + this.overlay.ease({ + opacity: 0, + duration: 150, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + if (this.overlay) { + Main.layoutManager.removeChrome(this.overlay); + this.overlay.destroy(); + this.overlay = null; + } + } + }); + } }, _snapWindowToZone: function(window) { @@ -1159,6 +1177,7 @@ ZoneManager.prototype = { if (targetZone) { this._moveWindowToZone(window, targetZone, monitor); + this._showSnapFeedback(targetZone, monitor); // Show notification if enabled if (this.settings.getValue('notification-on-snap')) { @@ -1181,13 +1200,51 @@ ZoneManager.prototype = { if (zoneIndex >= layout.zones.length) return; let monitor = Main.layoutManager.primaryMonitor; - this._moveWindowToZone(window, layout.zones[zoneIndex], monitor); + let targetZone = layout.zones[zoneIndex]; + this._moveWindowToZone(window, targetZone, monitor); + this._showSnapFeedback(targetZone, monitor); // Show notification if enabled if (this.settings.getValue('notification-on-snap')) { Main.notify('GridSnap', 'Window snapped to zone ' + (zoneIndex + 1)); } }, + + _showSnapFeedback: function(zone, monitor) { + // Create a flash effect on the zone to show snap feedback + let x = monitor.x + Math.round(monitor.width * zone.x); + let y = monitor.y + Math.round(monitor.height * zone.y); + let width = Math.round(monitor.width * zone.width); + let height = Math.round(monitor.height * zone.height); + + let flashActor = new St.Widget({ + style_class: 'gridsnap-flash', + x: x, + y: y, + width: width, + height: height + }); + + flashActor.set_style( + 'border: 4px solid rgba(100, 255, 100, 1.0);' + + 'background-color: rgba(100, 255, 100, 0.3);' + + 'border-radius: 4px;' + ); + + Main.layoutManager.addChrome(flashActor); + + // Animate flash effect + flashActor.opacity = 255; + flashActor.ease({ + opacity: 0, + duration: 400, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + Main.layoutManager.removeChrome(flashActor); + flashActor.destroy(); + } + }); + }, _moveWindowToZone: function(window, zone, monitor) { if (!window || !zone || !monitor) return; @@ -1206,8 +1263,26 @@ ZoneManager.prototype = { // Use Mainloop.idle_add to defer the operation // This ensures the unmaximize completes before move/resize Mainloop.idle_add(Lang.bind(this, function() { - // Move and resize window (false = programmatic operation, not user-initiated) - window.move_resize_frame(false, x, y, width, height); + // Get window actor for animation + let actor = window.get_compositor_private(); + if (actor) { + // Animate window movement + actor.ease({ + x: x, + y: y, + width: width, + height: height, + duration: 250, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + // Set final position precisely after animation + window.move_resize_frame(false, x, y, width, height); + } + }); + } else { + // Fallback to instant move if no actor + window.move_resize_frame(false, x, y, width, height); + } return false; // Don't repeat })); },