nanogui-experiments/knob.py

248 lines
7.2 KiB
Python

#!/usr/bin/env python3
import math
from nanogui import Color, Cursor, FloatBox, Vector2f, Vector2i, Widget, glfw
from nanogui.nanovg import BUTT, LerpRGBA, NVGwinding, RGB, RGBA, RGBAf
class Knob(Widget):
def __init__(
self,
parent,
label,
default=0.0,
min_val=0.0,
max_val=127.0,
unit="%",
increment=0.1,
size=80,
color=RGBA(192, 192, 192, 255),
):
super().__init__(parent)
self.label = label
# sizes
self._size = size
# value gauge width as ratio of knob radius
self.gauge_width = 0.125
# value indicator line length as ratio of knob radius
self.indicator_size = 0.35
# value and range
self.value = default
self.min_val = min_val
self.max_val = max_val
self.default = default
self.unit = unit
self.increment = increment
# behaviour
self.fine_mode = False
self.scroll_speed = 2.0
self.scroll_increment = (self.max_val - self.min_val) / 100.0 * self.scroll_speed
self.drag_increment = (self.max_val - self.min_val) / 100.0
self.callback = None
self.set_cursor(Cursor.Hand)
# colors
self.color = color
self.knob_color_1 = RGB(86, 92, 95)
self.knob_color_2 = RGB(39, 42, 43)
self.outline_color_1 = RGBA(190, 190, 190, 0.0)
self.outline_color_2 = RGB(23, 23, 23)
@property
def color(self):
return self._color
@color.setter
def color(self, col):
if isinstance(col, Color):
col = RGBAf(col.r, col.g, col.b, col.w)
self._color = col
self.gauge_color = LerpRGBA(RGB(40, 40, 40), col, 0.3)
def preferred_size(self, ctx):
return Vector2i(self._size, self._size)
def _adjust_value(self, val, inc):
if self.fine_mode:
inc = self.increment
new_val = self.value + val * inc
new_val = max(self.min_val, min(self.max_val, new_val))
if new_val != self.value:
self.value = new_val
if self.callback:
self.callback(new_val)
return True
return False
def mouse_enter_event(self, pos: Vector2i, enter: bool):
if enter:
self.request_focus()
self.set_focused(enter)
return True
def mouse_button_event(self, pos: Vector2i, button: int, down: bool, modifiers: int):
if button == glfw.MOUSE_BUTTON_1 and modifiers == glfw.MOD_CONTROL and down:
if self.value != self.default:
self.value = self.default
if self.callback:
self.callback(self.value)
return True
def mouse_drag_event(self, p: Vector2i, rel: Vector2i, button: int, modifiers: int):
self._adjust_value(-rel[1], self.drag_increment)
return True
def scroll_event(self, p: Vector2i, rel: Vector2f):
self._adjust_value(rel[1], self.scroll_increment)
return True
def keyboard_event(self, key, scancode, action, modifiers):
if key in (glfw.KEY_LEFT_SHIFT, glfw.KEY_RIGHT_SHIFT):
if action == glfw.PRESS:
self.fine_mode = True
elif action == glfw.RELEASE:
self.fine_mode = False
return True
elif action in (glfw.PRESS, glfw.REPEAT):
if key == glfw.KEY_UP:
self._adjust_value(1, self.drag_increment)
return True
elif key == glfw.KEY_DOWN:
self._adjust_value(-1, self.drag_increment)
return True
elif key == glfw.KEY_PAGE_UP:
self._adjust_value(10, self.drag_increment)
return True
elif key == glfw.KEY_PAGE_DOWN:
self._adjust_value(-10, self.drag_increment)
return True
return False
def set_callback(self, callback):
if callable(callback):
self.callback = callback
else:
raise TypeError("Not a callable.")
def set_value(self, value, trigger_callback=False):
new_val = max(self.min_val, min(self.max_val, value))
if new_val != self.value:
self.value = new_val
if trigger_callback and self.callback:
self.callback(value)
def create_value_entry(self, parent, editable=True, font_size=20, widget=FloatBox, **kwargs):
entry = widget(parent, **kwargs)
entry.set_size((int(self._size * 1.1), int(font_size * 1.2)))
entry.set_font_size(font_size)
entry.set_editable(editable)
entry.set_spinnable(True)
entry.set_value(self.value)
# entry.number_format("%02.2f") # method missing in Python bindings
entry.set_min_value(self.min_val)
entry.set_max_value(self.max_val)
entry.set_value_increment(self.increment)
if self.unit:
entry.set_units(self.unit)
self.set_callback(entry.set_value)
entry.set_callback(self.set_value)
return entry
def draw(self, ctx):
pos = self.position()
size = self.size()
height = float(size[1])
radius = height / 2.0
gauge_width = radius * self.gauge_width
margin = gauge_width / 2.0
percent_filled = (self.value - self.min_val) / (self.max_val - self.min_val)
knob_diameter = (radius - gauge_width) * 2.0 - margin
indicator_length = radius * self.indicator_size
ctx.Save()
ctx.Translate(pos[0], pos[1])
# Gauge (background)
ctx.BeginPath()
ctx.StrokeWidth(gauge_width)
ctx.StrokeColor(self.gauge_color)
ctx.LineCap(BUTT.ROUND)
ctx.Arc(radius, radius, radius - margin, 0.75 * math.pi, 0.25 * math.pi, NVGwinding.CW)
ctx.Stroke()
# Gauge (fill)
ctx.BeginPath()
ctx.StrokeWidth(gauge_width)
ctx.StrokeColor(self.color)
ctx.LineCap(BUTT.ROUND)
ctx.Arc(
radius,
radius,
radius - margin,
0.75 * math.pi,
(0.75 + 1.5 * percent_filled) * math.pi,
NVGwinding.CW,
)
ctx.Stroke()
# Knob
ctx.BeginPath()
ctx.StrokeWidth(2.0)
outline_paint = ctx.LinearGradient(
0,
0,
0,
height - 10,
self.outline_color_1,
self.outline_color_2,
)
ctx.StrokePaint(outline_paint)
knob_paint = ctx.LinearGradient(
radius, gauge_width, radius, knob_diameter, self.knob_color_1, self.knob_color_2
)
ctx.FillPaint(knob_paint)
ctx.Circle(radius, radius, knob_diameter / 2.0)
ctx.Fill()
ctx.Stroke()
# Indicator
ctx.BeginPath()
ctx.Translate(radius, radius)
ctx.Rotate((2.0 + ((percent_filled - 0.5) * 1.5)) * math.pi)
ctx.StrokeColor(self.color)
ctx.StrokeWidth(gauge_width)
ctx.LineCap(BUTT.ROUND)
indicator_start = radius - margin - indicator_length
ctx.MoveTo(0, -indicator_start)
ctx.LineTo(0, -(indicator_start + indicator_length))
ctx.Stroke()
ctx.Restore()
ctx.ClosePath()