Files
Modus/widgets/customrevealer.py
T

320 lines
11 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()