diff --git a/widgets/customrevealer.py b/widgets/customrevealer.py index 874de83..0f26f86 100644 --- a/widgets/customrevealer.py +++ b/widgets/customrevealer.py @@ -1,117 +1,15 @@ -from fabric.utils import GLib, Gtk +from fabric.utils import Gtk import math - -# TODO: UsE BETTER APPROACH IF POSSIBLE +from utils.animator import Animator -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 +# macOS-style easing functions for natural motion +def ease_out_quint(progress): + return 1 - math.pow(1 - progress, 5) -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) +def ease_in_cubic(progress): + return progress * progress * progress class SlideRevealer(Gtk.Overlay): @@ -120,15 +18,19 @@ class SlideRevealer(Gtk.Overlay): self.child = child self.direction = direction - self.duration = duration # Slightly faster for snappier feel + self.duration = duration 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._cached_dimensions = None + + self.animator = Animator( + duration=duration / 1000.0, + timing_function=ease_out_quint, + tick_interval=8, # 120 FPS for smoothness + tick_widget=self, + ) + self.animator.connect("notify::value", self._on_animator_value_changed) + self.animator.connect("finished", self._on_animator_finished) self._fixed = Gtk.Fixed() self._fixed.set_has_window(False) @@ -160,15 +62,14 @@ class SlideRevealer(Gtk.Overlay): self.hide() def reveal(self): - if self._revealed and not self._animating: + if self._revealed and not self.animator.playing: 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) @@ -176,75 +77,50 @@ class SlideRevealer(Gtk.Overlay): self.connect("realize", on_realize) def hide(self): - if not self._revealed and not self._animating: + if not self._revealed and not self.animator.playing: 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.animator.stop() 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 + # Configure animator based on direction 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) + self.animator.timing_function = ease_out_quint + # We animate the 'progress' from 0 to 1 + self.animator.min_value = 0.0 + self.animator.max_value = 1.0 else: - # Use cubic ease in for smooth hiding - eased = MacOSEasing.ease_in_cubic(t) + self.animator.timing_function = ease_in_cubic + self.animator.min_value = 0.0 + self.animator.max_value = 1.0 - self._pending_position = self._get_position_at_progress_cached(eased) + self.animator.play() - 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 _on_animator_value_changed(self, animator, _pspec): + if not self._cached_dimensions: + return - 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 + progress = animator.value + x, y = self._get_position_at_progress_cached(progress) + + # 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.queue_draw() + + def _on_animator_finished(self, _animator): + self._cached_dimensions = None + if not self._revealed: + self.child.hide() def _get_container_for_redraw(self): return self @@ -299,17 +175,13 @@ class SlideRevealer(Gtk.Overlay): return self._revealed def is_animating(self): - return self._animating + return self.animator.playing 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 + self.animator.stop() def destroy(self): self.stop_animation()