Files
Modus/widgets/customrevealer.py
T
sankalp 830305c0f3 refactor(widgets): clean up animation timer and easing functions
Remove dead code and redundant blank lines, simplify timer interval selection, and ensure proper realization handling. This improves readability and prevents potential runtime issues.

New Features
-

Enhancements
- Refined timer interval calculation for clarity
- Consolidated easing function stubs for consistency

Bug Fixes
- Fixed missing realize handler connection in SlideRevealer
- Eliminated stray blank line that could affect indentation
2026-02-26 06:54:35 +05:45

320 lines
10 KiB
Python

from gi.repository import GLib, Gtk
import gi
import math
gi.require_version("Gtk", "3.0")
# TODO: UsE BETTER APPROACH IF POSSIBLE
class AnimationManager:
_instance = None
_animating_widgets = set()
_timer_id = None
_containers_to_redraw = set()
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def add_widget(self, widget):
self._animating_widgets.add(widget)
if self._timer_id is None:
# Use 120 FPS for ultra-smooth animations like macOS
self._timer_id = GLib.timeout_add(8, self._animate_all) # 120 FPS
def remove_widget(self, widget):
self._animating_widgets.discard(widget)
if not self._animating_widgets and self._timer_id:
# Stop timer when no widgets are animating
GLib.source_remove(self._timer_id)
self._timer_id = None
# Clear any pending redraws
self._containers_to_redraw.clear()
def _animate_all(self):
# Clear previous frame's redraw queue
self._containers_to_redraw.clear()
widgets_to_remove = []
# Process all animations in a single frame
for widget in list(self._animating_widgets):
if not widget._calculate_position():
widgets_to_remove.append(widget)
else:
container = widget._get_container_for_redraw()
if container:
self._containers_to_redraw.add(container)
# Apply all position changes at once to prevent conflicts
for widget in self._animating_widgets:
widget._apply_position()
# Batch redraw calls to minimize performance impact
for container in self._containers_to_redraw:
container.queue_draw()
# Remove completed animations
for widget in widgets_to_remove:
self.remove_widget(widget)
return len(self._animating_widgets) > 0 # Continue if widgets remain
def get_active_widget_count(self):
"""Return the number of currently animating widgets"""
return len(self._animating_widgets)
def _get_optimal_interval(self):
"""Keep consistent 120 FPS for macOS-like smoothness"""
return 8 # 120 FPS
def _start_timer(self):
interval = self._get_optimal_interval()
self._timer_id = GLib.timeout_add(interval, self._animate_all)
def _adjust_frame_rate(self):
# No need to adjust frame rate anymore - keep it consistent
pass
class MacOSEasing:
"""macOS-style easing functions for natural motion"""
@staticmethod
def ease_out_expo(t):
"""Exponential ease out - fast start, slow end"""
return 1 - math.pow(2, -10 * t) if t != 1 else 1
@staticmethod
def ease_in_out_quart(t):
"""Quartic ease in-out for smooth acceleration/deceleration"""
return 8 * t * t * t * t if t < 0.5 else 1 - math.pow(-2 * t + 2, 4) / 2
@staticmethod
def ease_out_back(t):
"""Back ease out for slight overshoot effect"""
c1 = 1.70158
c3 = c1 + 1
return 1 + c3 * math.pow(t - 1, 3) + c1 * math.pow(t - 1, 2)
@staticmethod
def ease_out_cubic_bezier(t):
"""Custom cubic bezier similar to macOS default (0.25, 0.1, 0.25, 1.0)"""
# Approximation of cubic-bezier(0.25, 0.1, 0.25, 1.0)
return t * t * t * (t * (6 * t - 15) + 10)
@staticmethod
def ease_in_cubic(t):
"""Cubic ease in for smooth acceleration"""
return t * t * t
@staticmethod
def ease_out_quint(t):
"""Quintic ease out for very smooth deceleration"""
return 1 - math.pow(1 - t, 5)
class SlideRevealer(Gtk.Overlay):
def __init__(self, child: Gtk.Widget, direction="right", duration=350, size=None):
super().__init__()
self.child = child
self.direction = direction
self.duration = duration # Slightly faster for snappier feel
self.fixed_size = size
self._revealed = False
self._animating = False
self._start_time = None
self._show_animation = False
self._pending_position = None
self._current_position = (0.0, 0.0) # Use float for sub-pixel positioning
self._animation_id = None # Track individual animation instances
self._fixed = Gtk.Fixed()
self._fixed.set_has_window(False)
self._fixed.add(child)
self.add_overlay(self._fixed)
if self.fixed_size:
self.set_size_request(self.fixed_size[0], self.fixed_size[1])
child.hide()
self.show_all()
else:
child.connect("size-allocate", self._on_size_allocate)
child.hide()
self.show_all()
def _on_size_allocate(self, _widget, allocation):
if not self.fixed_size:
current_req = self.get_size_request()
if (
current_req[0] != allocation.width
or current_req[1] != allocation.height
):
self.set_size_request(allocation.width, allocation.height)
def set_reveal_child(self, reveal: bool):
if reveal:
self.reveal()
else:
self.hide()
def reveal(self):
if self._revealed and not self._animating:
return
self._revealed = True
# Ensure widget is properly laid out before starting animation
if self.get_realized():
self._start_animation(show=True)
else:
# Wait for widget to be realized
def on_realize(*_):
self._start_animation(show=True)
self.disconnect_by_func(on_realize)
self.connect("realize", on_realize)
def hide(self):
if not self._revealed and not self._animating:
return
self._revealed = False
self._start_animation(show=False)
def _start_animation(self, show: bool):
# Stop any existing animation for this widget
if self._animating:
AnimationManager.get_instance().remove_widget(self)
self._cached_dimensions = self._get_dimensions()
if self._cached_dimensions[0] == 0 or self._cached_dimensions[1] == 0:
self._animating = False
return
# Use high-precision monotonic time for smooth animations
self._start_time = GLib.get_monotonic_time()
self._animating = True
self._show_animation = show
self._animation_id = id(self) # Unique ID for this animation instance
if show:
self.child.show()
pos = self._get_offscreen_pos_cached()
self._current_position = (float(pos[0]), float(pos[1]))
self._fixed.move(self.child, int(pos[0]), int(pos[1]))
def start_with_dimensions():
AnimationManager.get_instance().add_widget(self)
return False
# Use idle_add to ensure layout is complete
GLib.idle_add(start_with_dimensions)
def _calculate_position(self):
if not self._animating:
return False
elapsed = (GLib.get_monotonic_time() - self._start_time) / 1000
t = min(elapsed / self.duration, 1.0)
# Use different easing functions for better smoothness
if self._show_animation:
# Use quintic ease out for very smooth revealing
eased = MacOSEasing.ease_out_quint(t)
else:
# Use cubic ease in for smooth hiding
eased = MacOSEasing.ease_in_cubic(t)
self._pending_position = self._get_position_at_progress_cached(eased)
if t >= 1.0:
self._animating = False
self._cached_dimensions = None
self._animation_id = None
if not self._show_animation:
GLib.idle_add(lambda: self.child.hide())
return False
return True
def _apply_position(self):
if self._pending_position:
x, y = self._pending_position
# Use sub-pixel positioning for smoother motion
self._current_position = (x, y)
# Round to nearest pixel for actual positioning
pixel_x, pixel_y = int(round(x)), int(round(y))
self._fixed.move(self.child, pixel_x, pixel_y)
self._pending_position = None
def _get_container_for_redraw(self):
return self
def _get_dimensions(self):
if self.fixed_size:
return self.fixed_size
else:
alloc = self.child.get_allocation()
return alloc.width, alloc.height
def _get_offscreen_pos_cached(self):
w, h = self._cached_dimensions
if self.direction == "left":
return -w, 0
elif self.direction == "right":
return w, 0
elif self.direction == "top":
return 0, -h
elif self.direction == "bottom":
return 0, h
return 0, 0
def _get_position_at_progress_cached(self, progress):
w, h = self._cached_dimensions
if self._show_animation:
# Showing animation: slide from offscreen to onscreen (0,0)
if self.direction == "left":
return -w + w * progress, 0.0
elif self.direction == "right":
return w - w * progress, 0.0
elif self.direction == "top":
return 0.0, -h + h * progress
elif self.direction == "bottom":
return 0.0, h - h * progress
else:
# Hiding animation: slide from onscreen (0,0) to offscreen
if self.direction == "left":
return -w * progress, 0.0 # Slide left (negative x)
elif self.direction == "right":
return w * progress, 0.0 # Slide right (positive x)
elif self.direction == "top":
return 0.0, -h * progress # Slide up (negative y)
elif self.direction == "bottom":
return 0.0, h * progress # Slide down (positive y)
return 0.0, 0.0
def set_slide_direction(self, direction):
self.direction = direction
def is_revealed(self):
return self._revealed
def is_animating(self):
return self._animating
def get_child_revealed(self):
return self._revealed
def stop_animation(self):
if self._animating:
AnimationManager.get_instance().remove_widget(self)
self._animating = False
self._cached_dimensions = None
self._animation_id = None
def destroy(self):
self.stop_animation()
super().destroy()