# scale.py Extension to micro-gui providing the Scale class # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2021 Peter Hinch # Usage: # from gui.widgets.scale import Scale from gui.core.ugui import LinearIO, display from hardware_setup import ssd # Display driver for Writer from gui.core.writer import Writer from gui.core.colors import * dolittle = lambda *_ : None class Scale(LinearIO): def __init__(self, writer, row, col, *, ticks=200, legendcb=None, tickcb=None, height=0, width=100, bdcolor=None, fgcolor=None, bgcolor=None, callback=dolittle, args=[], pointercolor=None, fontcolor=None, prcolor=None, value=0.0, active=False): if ticks % 2: raise ValueError('ticks arg must be divisible by 2') self.ticks = ticks self.tickcb = tickcb def lcb(f): return '{:3.1f}'.format(f) self.legendcb = legendcb if legendcb is not None else lcb bgcolor = BLACK if bgcolor is None else bgcolor text_ht = writer.font.height() ctrl_ht = 12 # Minimum height for ticks # Add 2 pixel internal border to give a little more space min_ht = text_ht + 6 # Ht of text, borders and gap between text and ticks if height < min_ht + ctrl_ht: height = min_ht + ctrl_ht # min workable height else: ctrl_ht = height - min_ht # adjust ticks for greater height width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._to_int(value), active, prcolor) super()._set_callbacks(callback, args) self.minval = -1.0 # By default scales run from -1.0 to +1.0 self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor self.x0 = col + 2 self.x1 = col + self.width - 2 self.y0 = row + 2 self.y1 = row + self.height - 2 self.ptrcolor = pointercolor if pointercolor is not None else self.fgcolor # Define tick dimensions ytop = self.y0 + text_ht + 2 # Top of scale graphic (2 pixel gap) ycl = ytop + (self.y1 - ytop) // 2 # Centre line self.sdl = round(ctrl_ht * 1 / 3) # Length of small tick. self.sdy0 = ycl - self.sdl // 2 self.mdl = round(ctrl_ht * 2 / 3) # Medium tick self.mdy0 = ycl - self.mdl // 2 self.ldl = ctrl_ht # Large tick self.ldy0 = ycl - self.ldl // 2 self.draw = True # Ensure a redraw on next refresh # Run callback (e.g. to set dynamic colors) self.callback(self, *self.args) def show(self): wri = self.writer x0: int = self.x0 # Internal rectangle occupied by scale and text x1: int = self.x1 y0: int = self.y0 y1: int = self.y1 if super().show(): # Scale is drawn using ints. Each division is 10 units. val: int = self._value # 0..ticks*10 # iv increments for each tick. Its value modulo N determines tick length iv: int # val / 10 at a tick position d: int # val % 10: offset relative to a tick position fx: int # X offset of current tick in value units if val >= 100: # Whole LHS of scale will be drawn iv, d = divmod(val - 100, 10) # Initial value fx = 10 - d iv += 1 else: # Scale will scroll right iv = 0 fx = 100 - val # Window shows 20 divisions, each of which corresponds to 10 units of value. # So pixels per unit value == win_width/200 win_width: int = x1 - x0 ticks: int = self.ticks # Total # of ticks visible and hidden txtcolor = GREY if self.greyed_out() else self.fontcolor while True: x: int = x0 + (fx * win_width) // 200 # Current X position ys: int # Start Y position for tick yl: int # tick length if x > x1 or iv > ticks: # Out of space or data (scroll left) break if not iv % 10: txt = self.legendcb(self._fvalue(iv * 10)) tlen = wri.stringlen(txt) Writer.set_textpos(ssd, y0, min(x, x1 - tlen)) wri.setcolor(txtcolor, self.bgcolor) wri.printstring(txt) wri.setcolor() ys = self.ldy0 # Large tick yl = self.ldl elif not iv % 5: ys = self.mdy0 yl = self.mdl else: ys = self.sdy0 yl = self.sdl if self.tickcb is None: color = self.fgcolor else: color = self.tickcb(self._fvalue(iv * 10), self.fgcolor) display.vline(x, ys, yl, color) # Draw tick fx += 10 iv += 1 display.vline(x0 + (x1 - x0) // 2, y0, y1 - y0, self.ptrcolor) # Draw pointer def _to_int(self, v): return round((v + 1.0) * self.ticks * 5) # 0..self.ticks*10 def _fvalue(self, v=None): return v / (5 * self.ticks) - 1.0 def value(self, val=None): # User method to get or set value if val is not None: val = min(max(val, - 1.0), 1.0) v = self._to_int(val) if v != self._value: self._value = v self.draw = True # Ensure a redraw on next refresh self.callback(self, *self.args) return self._fvalue(self._value)