148 lines
4.5 KiB
Python
148 lines
4.5 KiB
Python
from fabric.utils import Gdk, Gtk, cairo
|
|
|
|
from shared.widgets.animator import Animator, ease_in_out
|
|
|
|
|
|
class SmoothSwitch(Gtk.DrawingArea):
|
|
"""
|
|
Cairo-drawn switch with animated thumb, drop-in replacement for Switch.
|
|
Emits 'user-toggled' (bool) only on actual user clicks.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
active: bool = False,
|
|
width: int = 44,
|
|
height: int = 24,
|
|
on_user_toggle=None,
|
|
style_classes: list | None = None,
|
|
v_align=None,
|
|
v_expand: bool = False,
|
|
h_expand: bool = False,
|
|
**kwargs,
|
|
):
|
|
super().__init__(**kwargs)
|
|
self._active = active
|
|
self._width = width
|
|
self._height = height
|
|
self._on_user_toggle = on_user_toggle
|
|
self._anim_value = 1.0 if active else 0.0
|
|
|
|
self.set_size_request(width, height)
|
|
self.set_hexpand(h_expand)
|
|
self.set_vexpand(v_expand)
|
|
if v_align is not None:
|
|
if isinstance(v_align, str):
|
|
v_align = {
|
|
"fill": Gtk.Align.FILL,
|
|
"start": Gtk.Align.START,
|
|
"end": Gtk.Align.END,
|
|
"center": Gtk.Align.CENTER,
|
|
"baseline": Gtk.Align.BASELINE,
|
|
}[v_align]
|
|
self.set_valign(v_align)
|
|
|
|
if style_classes:
|
|
ctx = self.get_style_context()
|
|
for cls in style_classes:
|
|
ctx.add_class(cls)
|
|
|
|
self._animator = Animator(
|
|
timing_function=ease_in_out,
|
|
duration=0.2,
|
|
min_value=0.0,
|
|
max_value=1.0,
|
|
tick_widget=self,
|
|
)
|
|
if active:
|
|
self._animator.value = 1.0
|
|
|
|
self._animator.connect("notify::value", lambda *_: self.queue_draw())
|
|
|
|
self.add_events(
|
|
Gdk.EventMask.BUTTON_PRESS_MASK
|
|
| Gdk.EventMask.ENTER_NOTIFY_MASK
|
|
| Gdk.EventMask.LEAVE_NOTIFY_MASK
|
|
)
|
|
self.connect("button-press-event", self._on_click)
|
|
self.connect("draw", self._on_draw)
|
|
self.connect("enter-notify-event", lambda *_: self.queue_draw())
|
|
self.connect("leave-notify-event", lambda *_: self.queue_draw())
|
|
self.show_all()
|
|
|
|
def get_active(self) -> bool:
|
|
return self._active
|
|
|
|
def set_active(self, value: bool):
|
|
"""Programmatic set — does NOT emit user-toggled."""
|
|
if value == self._active:
|
|
return
|
|
self._active = value
|
|
self._animate_to(1.0 if value else 0.0)
|
|
|
|
def _on_click(self, _, event):
|
|
if event.button != 1:
|
|
return False
|
|
self._active = not self._active
|
|
self._animate_to(1.0 if self._active else 0.0)
|
|
if self._on_user_toggle:
|
|
self._on_user_toggle(self._active)
|
|
return True
|
|
|
|
def _animate_to(self, target: float):
|
|
self._animator.pause()
|
|
self._animator.min_value = self._animator.value
|
|
self._animator.max_value = target
|
|
self._animator.value = self._animator.min_value
|
|
self._animator.play()
|
|
|
|
def _on_draw(self, _, cr: cairo.Context):
|
|
w, h = self._width, self._height
|
|
r = h / 2
|
|
margin = 2
|
|
t = self._animator.value
|
|
|
|
style = self.get_style_context()
|
|
|
|
if self._active or t > 0.0:
|
|
style.add_class("checked")
|
|
else:
|
|
style.remove_class("checked")
|
|
|
|
off_color = style.get_background_color(Gtk.StateFlags.NORMAL)
|
|
|
|
on_color = style.get_background_color(Gtk.StateFlags.CHECKED)
|
|
|
|
if self._active or t > 0.5:
|
|
thumb_color = style.get_color(Gtk.StateFlags.CHECKED)
|
|
else:
|
|
thumb_color = style.get_color(Gtk.StateFlags.NORMAL)
|
|
|
|
track_r = off_color.red + (on_color.red - off_color.red) * t
|
|
track_g = off_color.green + (on_color.green - off_color.green) * t
|
|
track_b = off_color.blue + (on_color.blue - off_color.blue) * t
|
|
track_a = off_color.alpha + (on_color.alpha - off_color.alpha) * t
|
|
|
|
cr.new_sub_path()
|
|
cr.arc(r, r, r, 0.5 * 3.14159, 1.5 * 3.14159)
|
|
cr.arc(w - r, r, r, -0.5 * 3.14159, 0.5 * 3.14159)
|
|
cr.close_path()
|
|
cr.set_source_rgba(track_r, track_g, track_b, track_a)
|
|
cr.fill()
|
|
|
|
thumb_r = r - margin
|
|
travel = w - 2 * r
|
|
thumb_x = r + travel * t
|
|
thumb_y = r
|
|
|
|
cr.arc(thumb_x, thumb_y, thumb_r, 0, 2 * 3.14159)
|
|
cr.set_source_rgba(
|
|
thumb_color.red,
|
|
thumb_color.green,
|
|
thumb_color.blue,
|
|
thumb_color.alpha,
|
|
)
|
|
cr.fill()
|
|
|
|
return False
|