from gi.repository import GLib, Gtk import gi 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: self._timer_id = GLib.timeout_add(22, self._animate_all) # 45 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 = [] 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 for widget in self._animating_widgets: widget._apply_position() # Single redraw call per container 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_optimal_interval(self): """Calculate optimal frame interval based on widget count""" widget_count = len(self._animating_widgets) if widget_count <= 1: return 16 # 60 FPS elif widget_count <= 2: return 16 # 60 FPS elif widget_count <= 4: return 22 # 45 FPS elif widget_count <= 6: return 33 # 30 FPS else: return 50 # 20 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): if not self._timer_id: return new_interval = self._get_optimal_interval() GLib.source_remove(self._timer_id) self._timer_id = GLib.timeout_add(new_interval, self._animate_all) class SlideRevealer(Gtk.Overlay): def __init__(self, child: Gtk.Widget, direction="right", duration=300, size=None): super().__init__() self.child = child self.direction = direction 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._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 or self._animating: return self._revealed = True self._start_animation(show=True) def hide(self): if not self._revealed or self._animating: return self._revealed = False self._start_animation(show=False) def _start_animation(self, show: bool): 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 self._start_time = GLib.get_monotonic_time() self._animating = True self._show_animation = show if show: self.child.show() self._fixed.move(self.child, *self._get_offscreen_pos_cached()) AnimationManager.get_instance().add_widget(self) 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) if self._show_animation: # Ease out quadratic (no bounce/overshoot) eased = 1 - (1 - t) ** 2 else: eased = t**2 # Ease in quadratic for hide animation self._pending_position = self._get_position_at_progress_cached(eased) if t >= 1.0: self._animating = False self._cached_dimensions = 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 self._fixed.move(self.child, x, 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 int(-w + w * progress), 0 elif self.direction == "right": return int(w - w * progress), 0 elif self.direction == "top": return 0, int(-h + h * progress) elif self.direction == "bottom": return 0, int(h - h * progress) else: # Hiding animation: slide from onscreen (0,0) to offscreen if self.direction == "left": return int(-w * progress), 0 # Slide left (negative x) elif self.direction == "right": return int(w * progress), 0 # Slide right (positive x) elif self.direction == "top": return 0, int(-h * progress) # Slide up (negative y) elif self.direction == "bottom": return 0, int(h * progress) # Slide down (positive y) return 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 def destroy(self): self.stop_animation() super().destroy()