Files
Modus/utils/wiggle_bar.py
T

175 lines
5.5 KiB
Python

import math
from fabric.core.service import Service, Signal
from gi.repository import Gtk, Gdk, GLib
import gi
gi.require_version("Gtk", "3.0")
class WigglyWidget(Gtk.DrawingArea, Service):
@Signal
def on_seek(self, ratio: float) -> None: ...
def __init__(self):
super().__init__(name="wiggle-bar")
self.phase = 0
self.set_size_request(-1, 20)
self.connect("draw", self.on_draw)
# mouse events
self.add_events(
Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.POINTER_MOTION_MASK
)
self.connect("button-press-event", self.on_button_press)
self.connect("motion-notify-event", self.on_motion)
self.connect("button-release-event", self.on_button_release)
GLib.timeout_add(16, self.update) # 60 FPS
self.value = 0.0
self.amplitude = 2
self.dragging = False
self.pause = False
self.show_all()
def animate_amplitude_to(self):
if abs(self.amplitude - self.amplitude_target) < 0.01:
self.amplitude = self.amplitude_target
self.queue_draw()
return False
else:
self.amplitude += self.amplitude_step
self.queue_draw()
return True
def update_amplitude(self, decrease):
if decrease:
self.amplitude_target = 0
self.amplitude_step = (0 - self.amplitude) / 10.0
else:
self.amplitude_target = 2
self.amplitude_step = (2 - self.amplitude) / 10.0
# animation stops when False is returned btw
GLib.timeout_add(16, self.animate_amplitude_to)
def update_value_from_x(self, x):
width = self.get_allocated_width()
self.value = max(0.0, min(1.0, x / width))
self.on_seek(self.value)
self.queue_draw()
def on_button_press(self, widget, event):
self.dragging = True
self.update_amplitude(True)
self.update_value_from_x(event.x)
return True
# def animate_progress_to(self):
# print("frame")
# if self.value < self.value_target:
# self.value = self.value_target
# print(self.value)
# self.queue_draw()
# return False
# else:
# self.value += self.value_step
# self.queue_draw()
# return True
# def update_value_from_signal(self, new_value):
# self.value_target = min(1.0, new_value)
# steps = 60 # 1s / 16ms ≈ 60 frames
# self.value_step = (self.value_target - self.value) / steps
# GLib.timeout_add(16, self.animate_progress_to)
def update_value_from_signal(self, new_value):
self.value = min(1.0, new_value)
def on_motion(self, widget, event):
if self.dragging and not self.pause:
self.update_value_from_x(event.x)
cursor = Gdk.Cursor.new(Gdk.CursorType.HAND1)
widget.get_window().set_cursor(cursor)
return True
def on_button_release(self, widget, event):
if self.pause == False:
self.dragging = False
self.update_amplitude(False)
return True
def update(self):
if self.dragging == False:
self.phase += 0.1
# if (self.value<1):
# self.value += 0.001
self.queue_draw()
return True
def on_draw(self, widget, cr):
alloc_width = self.get_allocated_width()
height = self.get_allocated_height()
center_y = height / 2
# self.amplitude = 2
frequency = 0.3
slider_diameter = 6
width = int(alloc_width * self.value) - slider_diameter + 1
cr.set_source_rgb(1, 1, 1) # color here btw
cr.set_line_width(2)
last_x = 0 # fallback values in case the slider gets dragged out of range
last_y = 0
cr.move_to(0, center_y)
for x in range(width):
y = center_y + self.amplitude * math.sin((x * frequency) + self.phase)
cr.line_to(x, y)
last_x, last_y = x, y
cr.stroke()
# Set the color using CSS - the widget should have CSS styling applied
# that sets the color to var(--primary). For now, use a fallback approach.
style_context = self.get_style_context()
try:
# Get the foreground color which should be styled with CSS
color = style_context.get_color(Gtk.StateFlags.NORMAL)
cr.set_source_rgba(color.red, color.green, color.blue, color.alpha)
except:
# Fallback to a nice blue color that matches the primary theme
cr.set_source_rgb(0.2, 0.6, 1.0)
rect_width = 6
arc_radius = slider_diameter / 2
self.draw_rounded_rect(cr, last_x, height, rect_width, rect_width, arc_radius)
cr.fill()
cr.stroke()
cr.set_source_rgb(1, 1, 1)
cr.set_line_width(1)
# This is for anti aliasing. When line width becomes 1px and the position is not an integer, two lines of opacity 50% is drawn on two pixel rows.
# This moves the line to make it pixel perfect hence not needing anti aliasing
cr.translate(0, 0.5)
cr.move_to(last_x + 2 * arc_radius, height / 2)
cr.line_to(alloc_width, height / 2)
cr.stroke()
def draw_rounded_rect(self, cr, x, y, width, _height, radius):
cr.new_sub_path()
cr.arc(x + width - radius, radius, radius, -math.pi, 0)
cr.arc(x + width - radius, y - radius, radius, 0, -math.pi)
cr.close_path()