#!/usr/bin/env python3 import math from nanogui import Color, Cursor, FloatBox, Vector2f, Vector2i, Widget, glfw from nanogui.nanovg import BUTT, RGB, RGBA, RGBAf, LerpRGBA, NVGwinding, 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()