Files
Modus/widgets/customrevealer.py
T

259 lines
8.1 KiB
Python

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=600, 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()