diff --git a/adafruit_displayio_layout/layouts/grid_layout.py b/adafruit_displayio_layout/layouts/grid_layout.py index 5d7ebeb..a882f23 100644 --- a/adafruit_displayio_layout/layouts/grid_layout.py +++ b/adafruit_displayio_layout/layouts/grid_layout.py @@ -24,7 +24,7 @@ """ try: # Used only for typing - from typing import Tuple + from typing import Any, List, Optional, Tuple, Union except ImportError: pass @@ -61,18 +61,18 @@ class GridLayout(displayio.Group): # pylint: disable=too-many-instance-attributes def __init__( self, - x, - y, - width, - height, - grid_size, - cell_padding=0, - divider_lines=False, - h_divider_line_rows=None, - v_divider_line_cols=None, - divider_line_color=0xFFFFFF, - cell_anchor_point=(0.0, 0.0), - ): + x: int, + y: int, + width: int, + height: int, + grid_size: tuple[int, int], + cell_padding: int = 0, + divider_lines: bool = False, + h_divider_line_rows: Union[Tuple[int, ...], List[int], None] = None, + v_divider_line_cols: Union[Tuple[int, ...], List[int], None] = None, + divider_line_color: int = 0xFFFFFF, + cell_anchor_point: Tuple[float, float] = (0.0, 0.0), + ) -> None: super().__init__(x=x, y=y) self.x = x self.y = y @@ -80,13 +80,13 @@ def __init__( self._height = height self.grid_size = grid_size self.cell_padding = cell_padding - self._cell_content_list = [] + self._cell_content_list: List[dict[str, Any]] = [] self._cell_anchor_point = cell_anchor_point - self._divider_lines = [] + self._divider_lines: List[dict[str, Any]] = [] self._divider_color = divider_line_color - self.h_divider_line_rows = h_divider_line_rows - self.v_divider_line_cols = v_divider_line_cols + self.h_divider_line_rows = h_divider_line_rows or tuple() + self.v_divider_line_cols = v_divider_line_cols or tuple() self._divider_lines_enabled = ( (divider_lines is True) @@ -95,19 +95,14 @@ def __init__( ) if divider_lines: - if self.h_divider_line_rows is None: + if h_divider_line_rows is None: self.h_divider_line_rows = [] for _y in range(self.grid_size[1] + 1): self.h_divider_line_rows.append(_y) - if self.v_divider_line_cols is None: + if v_divider_line_cols is None: self.v_divider_line_cols = [] for _x in range(self.grid_size[0] + 1): self.v_divider_line_cols.append(_x) - else: - if not h_divider_line_rows: - self.h_divider_line_rows = tuple() - if not v_divider_line_cols: - self.v_divider_line_cols = tuple() # use at least 1 padding so that content is inside the divider lines if cell_padding == 0 and ( @@ -115,7 +110,7 @@ def __init__( ): self.cell_padding = 1 - def _layout_cells(self): + def _layout_cells(self) -> None: # pylint: disable=too-many-locals, too-many-branches, too-many-statements for cell in self._cell_content_list: if cell["content"] not in self: @@ -382,8 +377,12 @@ def _layout_cells(self): self.append(line_obj["tilegrid"]) def add_content( - self, cell_content, grid_position, cell_size, cell_anchor_point=None - ): + self, + cell_content: displayio.Group, + grid_position: Tuple[int, int], + cell_size: Tuple[int, int], + cell_anchor_point: Optional[Tuple[float, ...]] = None, + ) -> None: """Add a child to the grid. :param cell_content: the content to add to this cell e.g. label, button, etc... @@ -412,7 +411,7 @@ def add_content( self._cell_content_list.append(sub_view_obj) self._layout_cells() - def get_cell(self, cell_coordinates): + def get_cell(self, cell_coordinates: Tuple[int, int]) -> displayio.Group: """ Return a cells content based on the cell_coordinates. Raises KeyError if coordinates were not found in the GridLayout. diff --git a/adafruit_displayio_layout/widgets/cartesian.py b/adafruit_displayio_layout/widgets/cartesian.py index 66790a4..be45ac7 100644 --- a/adafruit_displayio_layout/widgets/cartesian.py +++ b/adafruit_displayio_layout/widgets/cartesian.py @@ -36,7 +36,7 @@ pass try: - from typing import Tuple + from typing import Any, List, Optional, Tuple except ImportError: pass @@ -178,7 +178,7 @@ def __init__( tick_color: int = 0xFFFFFF, major_tick_stroke: int = 1, major_tick_length: int = 5, - tick_label_font=terminalio.FONT, + tick_label_font: terminalio.FONT = terminalio.FONT, font_color: int = 0xFFFFFF, pointer_radius: int = 1, pointer_color: int = 0xFFFFFF, @@ -186,7 +186,7 @@ def __init__( nudge_x: int = 0, nudge_y: int = 0, verbose: bool = False, - **kwargs, + **kwargs: Any, ) -> None: super().__init__(**kwargs) @@ -317,12 +317,12 @@ def __init__( self.append(self._screen_tilegrid) self.append(self._corner_tilegrid) - self._pointer = None - self._circle_palette = None - self.plot_line_point = None + self._pointer: Optional[vectorio.Circle] = None + self._circle_palette: Optional[displayio.Palette] = None + self.plot_line_point: List[Tuple[int, int]] = [] @staticmethod - def _get_font_height(font, scale: int) -> Tuple[int, int]: + def _get_font_height(font: terminalio.FONT, scale: int) -> Tuple[int, int]: if hasattr(font, "get_bounding_box"): font_height = int(scale * font.get_bounding_box()[1]) font_width = int(scale * font.get_bounding_box()[0]) @@ -448,6 +448,7 @@ def _draw_ticks(self) -> None: def _draw_pointers(self, x: int, y: int) -> None: self._circle_palette = displayio.Palette(1) + self._circle_palette[0] = self._pointer_color self._pointer = vectorio.Circle( radius=self._pointer_radius, x=x, y=y, pixel_shader=self._circle_palette @@ -455,7 +456,7 @@ def _draw_pointers(self, x: int, y: int) -> None: self.append(self._pointer) - def _calc_local_xy(self, x: int, y: int) -> (int, int): + def _calc_local_xy(self, x: int, y: int) -> Tuple[int, int]: local_x = ( int((x - self._xrange[0]) * self._factorx * self._valuex) + self._nudge_x ) @@ -469,24 +470,24 @@ def _calc_local_xy(self, x: int, y: int) -> (int, int): ) return (local_x, local_y) - def _check_local_x_in_range(self, local_x): + def _check_local_x_in_range(self, local_x: int) -> bool: return 0 <= local_x < self.width - def _check_local_y_in_range(self, local_y): + def _check_local_y_in_range(self, local_y: int) -> bool: return 0 <= local_y < self.height - def _check_local_xy_in_range(self, local_x, local_y): + def _check_local_xy_in_range(self, local_x: int, local_y: int) -> bool: return self._check_local_x_in_range(local_x) and self._check_local_y_in_range( local_y ) - def _check_x_in_range(self, x): + def _check_x_in_range(self, x: int) -> bool: return self._xrange[0] <= x <= self._xrange[1] - def _check_y_in_range(self, y): + def _check_y_in_range(self, y: int) -> bool: return self._yrange[0] <= y <= self._yrange[1] - def _check_xy_in_range(self, x, y): + def _check_xy_in_range(self, x: int, y: int) -> bool: return self._check_x_in_range(x) and self._check_y_in_range(y) def _add_point(self, x: int, y: int) -> None: @@ -587,6 +588,7 @@ def update_pointer(self, x: int, y: int) -> None: :return: None rtype: None """ + self._add_point(x, y) if not self._pointer: self._draw_pointers( @@ -609,6 +611,7 @@ def add_plot_line(self, x: int, y: int) -> None: rtype: None """ + self._add_point(x, y) if len(self.plot_line_point) > 1: bitmaptools.draw_line( @@ -620,7 +623,7 @@ def add_plot_line(self, x: int, y: int) -> None: 1, ) - def clear_plot_lines(self, palette_index=5): + def clear_plot_lines(self, palette_index: int = 5) -> None: """clear_plot_lines function. clear all added lines @@ -631,5 +634,5 @@ def clear_plot_lines(self, palette_index=5): rtype: None """ - self.plot_line_point = None + self.plot_line_point = [] self._plot_bitmap.fill(palette_index) diff --git a/adafruit_displayio_layout/widgets/control.py b/adafruit_displayio_layout/widgets/control.py index 1aa8a87..f9e5a2b 100644 --- a/adafruit_displayio_layout/widgets/control.py +++ b/adafruit_displayio_layout/widgets/control.py @@ -21,6 +21,12 @@ """ +try: + from typing import Optional, Tuple +except ImportError: + pass + + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" @@ -46,20 +52,24 @@ class Control: def __init__( self, - ): + ) -> None: self.touch_boundary = ( - None # `self.touch_boundary` should be updated by the subclass + 0, + 0, + 0, + 0, # `self.touch_boundary` should be updated by the subclass ) # Tuple of [x, y, width, height]: [int, int, int, int] all in pixel units # where x,y define the upper left corner # and width and height define the size of the `touch_boundary` - def contains(self, touch_point): + def contains(self, touch_point: Tuple[int, int, Optional[int]]) -> bool: """Checks if the Control was touched. Returns True if the touch_point is within the Control's touch_boundary. - :param touch_point: x,y location of the screen, converted to local coordinates. - :type touch_point: Tuple[x,y] + :param touch_point: x, y, p location of the screen, converted to local coordinates, plus + an optional pressure value for screens that support it. + :type touch_point: Tuple[int, int, Optional[int]] :return: Boolean """ @@ -82,11 +92,12 @@ def contains(self, touch_point): return False # place holder touch_handler response functions - def selected(self, touch_point): + def selected(self, touch_point: Tuple[int, int, Optional[int]]) -> None: """Response function when Control is selected. Should be overridden by subclass. - :param touch_point: x,y location of the screen, converted to local coordinates. - :type touch_point: Tuple[x,y] + :param touch_point: x, y, p location of the screen, converted to local coordinates, plus + an optional pressure value for screens that support it. + :type touch_point: Tuple[int, int, Optional[int]] :return: None """ diff --git a/adafruit_displayio_layout/widgets/easing.py b/adafruit_displayio_layout/widgets/easing.py index 56432b8..9a1e9a9 100644 --- a/adafruit_displayio_layout/widgets/easing.py +++ b/adafruit_displayio_layout/widgets/easing.py @@ -81,7 +81,7 @@ # Modeled after the line y = x -def linear_interpolation(pos): +def linear_interpolation(pos: float) -> float: """ Easing function for animations: Linear Interpolation. """ @@ -89,7 +89,7 @@ def linear_interpolation(pos): # Modeled after the parabola y = x^2 -def quadratic_easein(pos): +def quadratic_easein(pos: float) -> float: """ Easing function for animations: Quadratic Ease In """ @@ -97,7 +97,7 @@ def quadratic_easein(pos): # Modeled after the parabola y = -x^2 + 2x -def quadratic_easeout(pos): +def quadratic_easeout(pos: float) -> float: """ Easing function for animations: Quadratic Ease Out. """ @@ -107,7 +107,7 @@ def quadratic_easeout(pos): # Modeled after the piecewise quadratic # y = (1/2)((2x)^2) ; [0, 0.5) # y = -(1/2)((2x-1)*(2x-3) - 1) ; [0.5, 1] -def quadratic_easeinout(pos): +def quadratic_easeinout(pos: float) -> float: """ Easing function for animations: Quadratic Ease In & Out """ @@ -117,7 +117,7 @@ def quadratic_easeinout(pos): # Modeled after the cubic y = x^3 -def cubic_easein(pos): +def cubic_easein(pos: float) -> float: """ Easing function for animations: Cubic Ease In """ @@ -125,7 +125,7 @@ def cubic_easein(pos): # Modeled after the cubic y = (x - 1)^3 + 1 -def cubic_easeout(pos): +def cubic_easeout(pos: float) -> float: """ Easing function for animations: Cubic Ease Out """ @@ -136,7 +136,7 @@ def cubic_easeout(pos): # Modeled after the piecewise cubic # y = (1/2)((2x)^3) ; [0, 0.5) # y = (1/2)((2x-2)^3 + 2) ; [0.5, 1] -def cubic_easeinout(pos): +def cubic_easeinout(pos: float) -> float: """ Easing function for animations: Cubic Ease In & Out """ @@ -147,7 +147,7 @@ def cubic_easeinout(pos): # Modeled after the quartic x^4 -def quartic_easein(pos): +def quartic_easein(pos: float) -> float: """ Easing function for animations: Quartic Ease In """ @@ -155,7 +155,7 @@ def quartic_easein(pos): # Modeled after the quartic y = 1 - (x - 1)^4 -def quartic_easeout(pos): +def quartic_easeout(pos: float) -> float: """ Easing function for animations: Quartic Ease Out """ @@ -166,7 +166,7 @@ def quartic_easeout(pos): # Modeled after the piecewise quartic # y = (1/2)((2x)^4) ; [0, 0.5) # y = -(1/2)((2x-2)^4 - 2) ; [0.5, 1] -def quartic_easeinout(pos): +def quartic_easeinout(pos: float) -> float: """ Easing function for animations: Quartic Ease In & Out """ @@ -177,7 +177,7 @@ def quartic_easeinout(pos): # Modeled after the quintic y = x^5 -def quintic_easein(pos): +def quintic_easein(pos: float) -> float: """ Easing function for animations: Quintic Ease In """ @@ -185,7 +185,7 @@ def quintic_easein(pos): # Modeled after the quintic y = (x - 1)^5 + 1 -def quintic_easeout(pos): +def quintic_easeout(pos: float) -> float: """ Easing function for animations: Quintic Ease Out """ @@ -196,7 +196,7 @@ def quintic_easeout(pos): # Modeled after the piecewise quintic # y = (1/2)((2x)^5) ; [0, 0.5) # y = (1/2)((2x-2)^5 + 2) ; [0.5, 1] -def quintic_easeinout(pos): +def quintic_easeinout(pos: float) -> float: """ Easing function for animations: Quintic Ease In & Out """ @@ -207,7 +207,7 @@ def quintic_easeinout(pos): # Modeled after quarter-cycle of sine wave -def sine_easein(pos): +def sine_easein(pos: float) -> float: """ Easing function for animations: Sine Ease In """ @@ -215,7 +215,7 @@ def sine_easein(pos): # Modeled after quarter-cycle of sine wave (different phase) -def sine_easeout(pos): +def sine_easeout(pos: float) -> float: """ Easing function for animations: Sine Ease Out """ @@ -223,7 +223,7 @@ def sine_easeout(pos): # Modeled after half sine wave -def sine_easeinout(pos): +def sine_easeinout(pos: float) -> float: """ Easing function for animations: Sine Ease In & Out """ @@ -231,7 +231,7 @@ def sine_easeinout(pos): # Modeled after shifted quadrant IV of unit circle -def circular_easein(pos): +def circular_easein(pos: float) -> float: """ Easing function for animations: Circular Ease In """ @@ -239,7 +239,7 @@ def circular_easein(pos): # Modeled after shifted quadrant II of unit circle -def circular_easeout(pos): +def circular_easeout(pos: float) -> float: """ Easing function for animations: Circular Ease Out """ @@ -249,7 +249,7 @@ def circular_easeout(pos): # Modeled after the piecewise circular function # y = (1/2)(1 - sqrt(1 - 4x^2)) ; [0, 0.5) # y = (1/2)(sqrt(-(2x - 3)*(2x - 1)) + 1) ; [0.5, 1] -def circular_easeinout(pos): +def circular_easeinout(pos: float) -> float: """ Easing function for animations: Circular Ease In & Out """ @@ -259,7 +259,7 @@ def circular_easeinout(pos): # Modeled after the exponential function y = 2^(10(x - 1)) -def exponential_easein(pos): +def exponential_easein(pos: float) -> float: """ Easing function for animations: Exponential Ease In """ @@ -269,7 +269,7 @@ def exponential_easein(pos): # Modeled after the exponential function y = -2^(-10x) + 1 -def exponential_easeout(pos): +def exponential_easeout(pos: float) -> float: """ Easing function for animations: Exponential Ease Out """ @@ -281,7 +281,7 @@ def exponential_easeout(pos): # Modeled after the piecewise exponential # y = (1/2)2^(10(2x - 1)) ; [0,0.5) # y = -(1/2)*2^(-10(2x - 1))) + 1 ; [0.5,1] -def exponential_easeinout(pos): +def exponential_easeinout(pos: float) -> float: """ Easing function for animations: Exponential Ease In & Out """ @@ -293,7 +293,7 @@ def exponential_easeinout(pos): # Modeled after the damped sine wave y = sin(13pi/2*x)*pow(2, 10 * (x - 1)) -def elastic_easein(pos): +def elastic_easein(pos: float) -> float: """ Easing function for animations: Elastic Ease In """ @@ -301,7 +301,7 @@ def elastic_easein(pos): # Modeled after the damped sine wave y = sin(-13pi/2*(x + 1))*pow(2, -10x) + 1 -def elastic_easeout(pos): +def elastic_easeout(pos: float) -> float: """ Easing function for animations: Elastic Ease Out """ @@ -311,7 +311,7 @@ def elastic_easeout(pos): # Modeled after the piecewise exponentially-damped sine wave: # y = (1/2)*sin(13pi/2*(2*x))*pow(2, 10 * ((2*x) - 1)) ; [0,0.5) # y = (1/2)*(sin(-13pi/2*((2x-1)+1))*pow(2,-10(2*x-1)) + 2) ; [0.5, 1] -def elastic_easeinout(pos): +def elastic_easeinout(pos: float) -> float: """ Easing function for animations: Elastic Ease In & Out """ @@ -324,7 +324,7 @@ def elastic_easeinout(pos): # Modeled after the overshooting cubic y = x^3-x*sin(x*pi) -def back_easein(pos): +def back_easein(pos: float) -> float: """ Easing function for animations: Back Ease In """ @@ -332,7 +332,7 @@ def back_easein(pos): # Modeled after overshooting cubic y = 1-((1-x)^3-(1-x)*sin((1-x)*pi)) -def back_easeout(pos): +def back_easeout(pos: float) -> float: """ Easing function for animations: Back Ease Out """ @@ -343,7 +343,7 @@ def back_easeout(pos): # Modeled after the piecewise overshooting cubic function: # y = (1/2)*((2x)^3-(2x)*sin(2*x*pi)) ; [0, 0.5) # y = (1/2)*(1-((1-x)^3-(1-x)*sin((1-x)*pi))+1) ; [0.5, 1] -def back_easeinout(pos): +def back_easeinout(pos: float) -> float: """ Easing function for animations: Back Ease In & Out """ @@ -354,14 +354,14 @@ def back_easeinout(pos): return 0.5 * (1 - (fos * fos * fos - fos * math.sin(fos * math.pi))) + 0.5 -def bounce_easein(pos): +def bounce_easein(pos: float) -> float: """ Easing function for animations: Bounce Ease In """ return 1 - bounce_easeout(1 - pos) -def bounce_easeout(pos): +def bounce_easeout(pos: float) -> float: """ Easing function for animations: Bounce Ease Out """ @@ -374,7 +374,7 @@ def bounce_easeout(pos): return (54 / 5.0 * pos * pos) - (513 / 25.0 * pos) + 268 / 25.0 -def bounce_easeinout(pos): +def bounce_easeinout(pos: float) -> float: """ Easing function for animations: Bounce Ease In & Out """ diff --git a/adafruit_displayio_layout/widgets/flip_input.py b/adafruit_displayio_layout/widgets/flip_input.py index e8a20ee..2245069 100644 --- a/adafruit_displayio_layout/widgets/flip_input.py +++ b/adafruit_displayio_layout/widgets/flip_input.py @@ -38,6 +38,11 @@ from adafruit_displayio_layout.widgets.easing import back_easeinout as easein from adafruit_displayio_layout.widgets.easing import back_easeinout as easeout +try: + from typing import Any, List, Optional, Tuple +except ImportError: + pass + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" @@ -88,25 +93,25 @@ class FlipInput(Widget, Control): def __init__( self, - display, + display: displayio.Display, *, - value_list=None, - font=FONT, - font_scale=1, - color=0xFFFFFF, - value=0, # initial value, index into the value_list - arrow_touch_padding=0, # additional touch padding on the arrow sides of the Widget - arrow_color=0x333333, - arrow_outline=0x555555, - arrow_height=30, - arrow_width=30, - arrow_gap=5, - alt_touch_padding=0, # touch padding on the non-arrow sides of the Widget - horizontal=True, - animation_time=None, - cool_down=0.0, - **kwargs, - ): + value_list: List[str], + font: FONT = FONT, + font_scale: int = 1, + color: int = 0xFFFFFF, + value: int = 0, # initial value, index into the value_list + arrow_touch_padding: int = 0, # additional touch padding on the arrow sides of the Widget + arrow_color: int = 0x333333, + arrow_outline: int = 0x555555, + arrow_height: int = 30, + arrow_width: int = 30, + arrow_gap: int = 5, + alt_touch_padding: int = 0, # touch padding on the non-arrow sides of the Widget + horizontal: bool = True, + animation_time: Optional[float] = None, + cool_down: float = 0.0, + **kwargs: Any, + ) -> None: super().__init__(**kwargs) # Group elements for the FlipInput. # 0. The text @@ -183,6 +188,14 @@ def __init__( xposition = xposition + glyph.shift_x + # Something is wrong if left, right, top, or bottom are still None here + assert ( + right is not None + and left is not None + and top is not None + and bottom is not None + ) + self._bounding_box = [ 0, 0, @@ -212,7 +225,7 @@ def __init__( if horizontal: # horizontal orientation, add arrow padding to x-dimension and # alt_padding to y-dimension - self.touch_boundary = [ + self.touch_boundary = ( self._bounding_box[0] - self._arrow_gap - arrow_height @@ -221,10 +234,10 @@ def __init__( self._bounding_box[2] + 2 * (self._arrow_gap + arrow_height + self._arrow_touch_padding), self._bounding_box[3] + 2 * self._alt_touch_padding, - ] + ) else: # vertical orientation, add arrow padding to y-dimension and # alt_padding to x-dimension - self.touch_boundary = [ + self.touch_boundary = ( self._bounding_box[0] - self._alt_touch_padding, self._bounding_box[1] - self._arrow_gap @@ -233,7 +246,7 @@ def __init__( self._bounding_box[2] + 2 * self._alt_touch_padding, self._bounding_box[3] + 2 * (self._arrow_gap + arrow_height + self._arrow_touch_padding), - ] + ) # create the Up/Down arrows self._update_position() # call Widget superclass function to reposition @@ -331,7 +344,7 @@ def __init__( ) # Draw function to update the current value - def _update_value(self, new_value, animate=True): + def _update_value(self, new_value: int, animate: bool = True) -> None: if ( (self._animation_time is not None) @@ -376,20 +389,20 @@ def _update_value(self, new_value, animate=True): start_bitmap.blit(0, 0, self._label.bitmap) # get the bitmap1 position offsets - bitmap1_offset = [ + bitmap1_offset = ( -1 * self._left + self._label.tilegrid.x, -1 * self._top + self._label.tilegrid.y, - ] + ) # hide the label group self.pop(0) # update the value label and get the bitmap offsets self._label.text = str(self.value_list[new_value]) - bitmap2_offset = [ + bitmap2_offset = ( -1 * self._left + self._label.tilegrid.x, -1 * self._top + self._label.tilegrid.y, - ] + ) # animate between old and new bitmaps _animate_bitmap( @@ -424,7 +437,7 @@ def _update_value(self, new_value, animate=True): self._display.auto_refresh = True self._update_position() # call Widget superclass function to reposition - def _ok_to_change(self): # checks state variable and timers to determine + def _ok_to_change(self) -> bool: # checks state variable and timers to determine # if an update is allowed if self._cool_down < 0: # if cool_down is negative, require ``released`` # to be called before next change @@ -433,7 +446,9 @@ def _ok_to_change(self): # checks state variable and timers to determine return False # cool_down time has not transpired return True - def contains(self, touch_point): # overrides, then calls Control.contains(x,y) + def contains( + self, touch_point: Tuple[int, int, Optional[int]] + ) -> bool: # overrides, then calls Control.contains(x,y) """Returns True if the touch_point is within the widget's touch_boundary.""" ###### @@ -449,7 +464,7 @@ def contains(self, touch_point): # overrides, then calls Control.contains(x,y) return super().contains((touch_x, touch_y, 0)) - def selected(self, touch_point): + def selected(self, touch_point: Tuple[int, int, Optional[int]]) -> None: """Response function when the Control is selected. Increases value when upper half is pressed and decreases value when lower half is pressed.""" @@ -489,14 +504,14 @@ def selected(self, touch_point): time.monotonic() ) # value changed, so update cool_down timer - def released(self): + def released(self) -> None: """Response function when the Control is released. Resets the state variables for handling situation when ``cool_down`` is < 0 that requires `released()` before reacting another another `selected()`.""" self._pressed = False @property - def value(self): + def value(self) -> int: """The value index displayed on the widget. For the setter, the input can either be an `int` index into the ``value_list`` or can be a `str` that matches one of the items in the ``value_list``. If `int`, @@ -508,7 +523,9 @@ def value(self): return self._value @value.setter - def value(self, new_value): # Set the value based on the index or on the string. + def value( + self, new_value: int | str + ) -> int | None: # Set the value based on the index or on the string. if isinstance(new_value, str): # for an input string, search the value_list try: new_value = self.value_list.index(new_value) @@ -528,14 +545,14 @@ def value(self, new_value): # Set the value based on the index or on the string # draw_position - Draws two bitmaps into the target bitmap with offsets. # Allows values < 0.0 and > 1.0 for "springy" easing functions def _draw_position( - target_bitmap, - bitmap1, - bitmap1_offset, - bitmap2, - bitmap2_offset, - position=0.0, - horizontal=True, -): + target_bitmap: displayio.Bitmap, + bitmap1: displayio.Bitmap, + bitmap1_offset: Tuple[int, int], + bitmap2: displayio.Bitmap, + bitmap2_offset: Tuple[int, int], + position: float = 0.0, + horizontal: bool = True, +) -> None: x_offset1 = bitmap1_offset[0] y_offset1 = bitmap1_offset[1] @@ -582,7 +599,16 @@ def _draw_position( # pylint: disable=invalid-name # _blit_constrained: Copies bitmaps with constraints to the dimensions -def _blit_constrained(target, x, y, source, x1=None, y1=None, x2=None, y2=None): +def _blit_constrained( + target: displayio.Bitmap, + x: int, + y: int, + source: displayio.Bitmap, + x1: Optional[int] = None, + y1: Optional[int] = None, + x2: Optional[int] = None, + y2: Optional[int] = None, +) -> None: if x1 is None: x1 = 0 if y1 is None: @@ -625,17 +651,17 @@ def _blit_constrained(target, x, y, source, x1=None, y1=None, x2=None, y2=None): # _animate_bitmap - performs animation of scrolling between two bitmaps def _animate_bitmap( - display, - target_bitmap, - bitmap1, - bitmap1_offset, - bitmap2, - bitmap2_offset, - start_position, - end_position, - animation_time, - horizontal, -): + display: displayio.Display, + target_bitmap: displayio.Bitmap, + bitmap1: displayio.Bitmap, + bitmap1_offset: Tuple[int, int], + bitmap2: displayio.Bitmap, + bitmap2_offset: Tuple[int, int], + start_position: float, + end_position: float, + animation_time: float, + horizontal: bool, +) -> None: start_time = time.monotonic() diff --git a/adafruit_displayio_layout/widgets/icon_animated.py b/adafruit_displayio_layout/widgets/icon_animated.py index 0e64cf1..943c436 100644 --- a/adafruit_displayio_layout/widgets/icon_animated.py +++ b/adafruit_displayio_layout/widgets/icon_animated.py @@ -31,6 +31,12 @@ from adafruit_displayio_layout.widgets.easing import quadratic_easeout as easein from adafruit_displayio_layout.widgets.easing import quadratic_easein as easeout +try: + from typing import Any, Optional, Tuple + from displayio import Display # pylint: disable=ungrouped-imports +except ImportError: + pass + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" @@ -79,8 +85,12 @@ class IconAnimated(IconWidget): @classmethod def init_class( - cls, display=None, max_scale=1.5, max_icon_size=(80, 80), max_color_depth=256 - ): + cls, + display: Optional[Display], + max_scale: float = 1.5, + max_icon_size: Tuple[int, int] = (80, 80), + max_color_depth: int = 256, + ) -> None: """ Initializes the IconAnimated Class variables, including preallocating memory buffers for the icon zoom bitmap and icon zoom palette. @@ -134,14 +144,14 @@ def init_class( def __init__( self, - label_text, - icon, - on_disk=False, - scale=None, - angle=4, - animation_time=0.15, - **kwargs, - ): + label_text: str, + icon: str, + on_disk: bool = False, + scale: Optional[float] = None, + angle: float = 4, + animation_time: float = 0.15, + **kwargs: Any, + ) -> None: if self.__class__.display is None: raise ValueError( @@ -169,11 +179,11 @@ def __init__( self._angle = (angle / 360) * 2 * pi # in degrees, convert to radians self._zoomed = False # state variable for zoom status - def zoom_animation(self, touch_point): + def zoom_animation(self, touch_point: Tuple[int, int, Optional[int]]) -> None: """Performs zoom animation when icon is pressed. :param touch_point: x,y location of the screen. - :type touch_point: Tuple[x,y] + :type touch_point: Tuple[int, int, Optional[int]] :return: None """ @@ -200,6 +210,9 @@ def zoom_animation(self, touch_point): animation_bitmap = self.__class__.bitmap_buffer animation_palette = self.__class__.palette_buffer + # For mypy, if class is configured correctly this must be true + assert self.__class__.display is not None + # store the current display refresh setting refresh_status = self.__class__.display.auto_refresh @@ -264,11 +277,11 @@ def zoom_animation(self, touch_point): self._zoomed = True - def zoom_out_animation(self, touch_point): + def zoom_out_animation(self, touch_point: Tuple[int, int, Optional[int]]) -> None: """Performs un-zoom animation when icon is released. :param touch_point: x,y location of the screen. - :type touch_point: Tuple[x,y] + :type touch_point: Tuple[int, int, Optional[int]] :return: None """ @@ -277,6 +290,9 @@ def zoom_out_animation(self, touch_point): animation_bitmap = self.__class__.bitmap_buffer animation_palette = self.__class__.palette_buffer + # For mypy, if class is configured correctly this must be true + assert self.__class__.display is not None + # store the current display refresh setting refresh_status = self.__class__.display.auto_refresh @@ -284,6 +300,7 @@ def zoom_out_animation(self, touch_point): # Animation: shrink down to the original size start_time = time.monotonic() + while True: elapsed_time = time.monotonic() - start_time position = max(0.0, easeout(1 - (elapsed_time / self._animation_time))) diff --git a/adafruit_displayio_layout/widgets/icon_widget.py b/adafruit_displayio_layout/widgets/icon_widget.py index 8d559e4..fd993e3 100644 --- a/adafruit_displayio_layout/widgets/icon_widget.py +++ b/adafruit_displayio_layout/widgets/icon_widget.py @@ -30,6 +30,12 @@ from adafruit_displayio_layout.widgets.control import Control from adafruit_displayio_layout.widgets.widget import Widget +try: + from typing import Any, Optional, Tuple +except ImportError: + pass + + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" @@ -61,13 +67,13 @@ class IconWidget(Widget, Control): def __init__( self, - label_text, - icon, - on_disk=False, - transparent_index=None, - label_background=None, - **kwargs - ): + label_text: str, + icon: str, + on_disk: bool = False, + transparent_index: Optional[int] = None, + label_background: Optional[int] = None, + **kwargs: Any + ) -> None: super().__init__(**kwargs) self._icon = icon @@ -95,20 +101,23 @@ def __init__( _label.background_color = label_background self.append(_label) - self.touch_boundary = ( + self.touch_boundary: Tuple[int, int, int, int] = ( 0, 0, image.width, image.height + _label.bounding_box[3], ) - def contains(self, touch_point): # overrides, then calls Control.contains(x,y) + def contains( + self, touch_point: Tuple[int, int, Optional[int]] + ) -> bool: # overrides, then calls Control.contains(x,y) """Checks if the IconWidget was touched. Returns True if the touch_point is within the IconWidget's touch_boundary. - :param touch_point: x,y location of the screen, converted to local coordinates. - :type touch_point: Tuple[x,y] + :param touch_point: x, y, p location of the screen, converted to local coordinates, plus + an optional pressure value for screens that support it. + :type touch_point: Tuple[int, int, Optional[int]] :return: Boolean """ diff --git a/adafruit_displayio_layout/widgets/switch_round.py b/adafruit_displayio_layout/widgets/switch_round.py index 76efe76..0ada0ca 100644 --- a/adafruit_displayio_layout/widgets/switch_round.py +++ b/adafruit_displayio_layout/widgets/switch_round.py @@ -44,6 +44,11 @@ # modify the "easing" function that is imported to change the switch animation behaviour from adafruit_displayio_layout.widgets.easing import back_easeinout as easing +try: + from typing import Any, Optional, Tuple, Union +except ImportError: + pass + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" @@ -419,32 +424,43 @@ class functions. The `Widget` class handles the overall sizing and positioning def __init__( self, - x=0, - y=0, - width=None, # recommend to default to - height=40, - touch_padding=0, - horizontal=True, # horizontal orientation - flip=False, # flip the direction of the switch movement - anchor_point=None, - anchored_position=None, - fill_color_off=(66, 44, 66), - fill_color_on=(0, 100, 0), - outline_color_off=(30, 30, 30), - outline_color_on=(0, 60, 0), - background_color_off=(255, 255, 255), - background_color_on=(0, 60, 0), - background_outline_color_off=None, # default to background_color_off - background_outline_color_on=None, # default to background_color_on - switch_stroke=2, - text_stroke=None, # default to switch_stroke - display_button_text=True, - animation_time=0.2, # animation duration (in seconds) - value=False, # initial value - **kwargs, - ): + x: int = 0, + y: int = 0, + width: Optional[int] = None, # recommend to default to + height: int = 40, + touch_padding: int = 0, + horizontal: bool = True, # horizontal orientation + flip: bool = False, # flip the direction of the switch movement + anchor_point: Optional[Tuple[float, float]] = None, + anchored_position: Optional[Tuple[int, int]] = None, + fill_color_off: Union[Tuple[int, int, int], int] = (66, 44, 66), + fill_color_on: Union[Tuple[int, int, int], int] = (0, 100, 0), + outline_color_off: Union[Tuple[int, int, int], int] = (30, 30, 30), + outline_color_on: Union[Tuple[int, int, int], int] = (0, 60, 0), + background_color_off: Union[Tuple[int, int, int], int] = (255, 255, 255), + background_color_on: Union[Tuple[int, int, int], int] = (0, 60, 0), + background_outline_color_off: Union[ + Tuple[int, int, int], int, None + ] = None, # default to background_color_off + background_outline_color_on: Union[ + Tuple[int, int, int], int, None + ] = None, # default to background_color_on + switch_stroke: int = 2, + text_stroke: Optional[int] = None, # default to switch_stroke + display_button_text: bool = True, + animation_time: float = 0.2, # animation duration (in seconds) + value: bool = False, # initial value + **kwargs: Any, + ) -> None: + + self._radius = height // 2 + + # If width is not provided, then use the preferred aspect ratio + if width is None: + width = 4 * self._radius # initialize the Widget superclass (x, y, scale) + # self._height and self._width are set in the super call super().__init__(x=x, y=y, height=height, width=width, **kwargs) # Group elements for SwitchRound: # 0. switch_roundrect: The switch background @@ -460,17 +476,6 @@ def __init__( self._horizontal = horizontal self._flip = flip - # height and width internal variables are treated before considering rotation - self._height = self.height - self._radius = self.height // 2 - - # If width is not provided, then use the preferred aspect ratio - if self._width is None: - self._width = 4 * self._radius - else: - self._width = self.width - print("width set!") - if background_outline_color_off is None: background_outline_color_off = background_color_off if background_outline_color_on is None: @@ -505,12 +510,15 @@ def __init__( self._create_switch() - def _create_switch(self): + def _create_switch(self) -> None: # The main function that creates the switch display elements switch_x = self._radius switch_y = self._radius + # These are Optional[int] values, let mypy know they should never be None here + assert self._height is not None and self._width is not None + # Define the motion "keyframes" that define the switch movement if self._horizontal: # horizontal switch orientation self._x_motion = self._width - 2 * self._radius - 1 @@ -614,12 +622,12 @@ def _create_switch(self): self._width, ] - self.touch_boundary = [ + self.touch_boundary = ( self._bounding_box[0] - self._touch_padding, self._bounding_box[1] - self._touch_padding, self._bounding_box[2] + 2 * self._touch_padding, self._bounding_box[3] + 2 * self._touch_padding, - ] + ) # Store initial positions of moving elements to be used in _draw_function self._switch_initial_x = self._switch_circle.x @@ -661,7 +669,7 @@ def _create_switch(self): # due to any changes that might have occurred in the bounding_box self._update_position() - def _get_offset_position(self, position): + def _get_offset_position(self, position: float) -> Tuple[int, int, float]: # Function to calculate the offset position (x, y, angle) of the moving # elements of an animated widget. Designed to be flexible depending upon # the widget's desired response. @@ -683,7 +691,7 @@ def _get_offset_position(self, position): return x_offset, y_offset, angle_offset - def _draw_position(self, position): + def _draw_position(self, position: float) -> None: # Draw the position of the slider. # The position parameter is a float between 0 and 1 (0= off, 1= on). @@ -733,7 +741,7 @@ def _draw_position(self, position): self._text_0.hidden = False self._text_1.hidden = True - def _animate_switch(self): + def _animate_switch(self) -> None: # The animation function for the switch. # 1. Move the switch # 2. Update the self._value to the opposite of its current value. @@ -756,10 +764,10 @@ def _animate_switch(self): if self._animation_time == 0: if not self._value: - position = 1 + position = 1.0 self._draw_position(1) else: - position = 0 + position = 0.0 self._draw_position(0) else: # animate over time # constrain the elapsed time @@ -789,11 +797,12 @@ def _animate_switch(self): self._value = False break - def selected(self, touch_point): + def selected(self, touch_point: Tuple[int, int, Optional[int]]) -> None: """Response function when Switch is selected. When selected, the switch position and value is changed with an animation. - :param touch_point: x,y location of the screen, in absolute display coordinates. + :param touch_point: x, y, p location of the screen, converted to local coordinates, plus + an optional pressure value for screens that support it. :return: None """ @@ -809,11 +818,14 @@ def selected(self, touch_point): # touch_point is adjusted for group's x,y position before sending to super() super().selected((touch_x, touch_y, 0)) - def contains(self, touch_point): # overrides, then calls Control.contains(x,y) + def contains( + self, touch_point: Tuple[int, int, Optional[int]] + ) -> bool: # overrides, then calls Control.contains(x,y) """Checks if the Widget was touched. Returns True if the touch_point is within the Control's touch_boundary. - :param touch_point: x,y location of the screen, in absolute display coordinates. + :param touch_point: x, y, p location of the screen, converted to local coordinates, plus + an optional pressure value for screens that support it. :return: Boolean """ @@ -825,7 +837,7 @@ def contains(self, touch_point): # overrides, then calls Control.contains(x,y) return super().contains((touch_x, touch_y, 0)) @property - def value(self): + def value(self) -> bool: """The current switch value (Boolean). :return: Boolean @@ -833,17 +845,19 @@ def value(self): return self._value @value.setter - def value(self, new_value): + def value(self, new_value: bool) -> None: if new_value != self._value: - fake_touch_point = [0, 0, 0] # send an arbitrary touch_point + fake_touch_point = (0, 0, 0) # send an arbitrary touch_point self.selected(fake_touch_point) @property - def width(self): + def width(self) -> int: + # Type is Optional[int], let mypy know that it can't be None here + assert self._width is not None return self._width @width.setter - def width(self, new_width): + def width(self, new_width: int) -> None: if self._width is None: self._width = 4 * self._radius else: @@ -851,16 +865,18 @@ def width(self, new_width): self._create_switch() @property - def height(self): + def height(self) -> int: + # Type is Optional[int], let mypy know that it can't be None here + assert self._height is not None return self._height @height.setter - def height(self, new_height): + def height(self, new_height: int) -> None: self._height = new_height self._radius = new_height // 2 self._create_switch() - def resize(self, new_width, new_height): + def resize(self, new_width: int, new_height: int) -> None: """Resize the switch to a new requested width and height. :param int new_width: requested maximum width @@ -893,7 +909,7 @@ def resize(self, new_width, new_height): ###### color support functions ###### -def _color_to_tuple(value): +def _color_to_tuple(value: Union[Tuple[int, int, int], int]) -> Tuple[int, int, int]: """Converts a color from a 24-bit integer to a tuple. :param value: RGB LED desired value - can be a RGB tuple or a 24-bit integer. """ @@ -905,12 +921,16 @@ def _color_to_tuple(value): r = value >> 16 g = (value >> 8) & 0xFF b = value & 0xFF - return [r, g, b] + return (r, g, b) raise ValueError("Color must be a tuple or 24-bit integer value.") -def _color_fade(start_color, end_color, fraction): +def _color_fade( + start_color: Union[Tuple[int, int, int], int], + end_color: Union[Tuple[int, int, int], int], + fraction: float, +) -> Tuple[int, ...]: """Linear extrapolation of a color between two RGB colors (tuple or 24-bit integer). :param start_color: starting color :param end_color: ending color @@ -930,4 +950,4 @@ def _color_fade(start_color, end_color, fraction): faded_color[i] = start_color[i] - int( (start_color[i] - end_color[i]) * fraction ) - return faded_color + return tuple(faded_color) diff --git a/adafruit_displayio_layout/widgets/widget.py b/adafruit_displayio_layout/widgets/widget.py index a44603c..af84194 100644 --- a/adafruit_displayio_layout/widgets/widget.py +++ b/adafruit_displayio_layout/widgets/widget.py @@ -24,6 +24,12 @@ import displayio +try: + from typing import Optional, Tuple +except ImportError: + pass + + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git" @@ -166,14 +172,14 @@ class Widget(displayio.Group): def __init__( self, - x=0, - y=0, - scale=1, - width=None, - height=None, - anchor_point=None, - anchored_position=None, - ): + x: int = 0, + y: int = 0, + scale: int = 1, + width: Optional[int] = None, + height: Optional[int] = None, + anchor_point: Optional[Tuple[float, float]] = None, + anchored_position: Optional[Tuple[int, int]] = None, + ) -> None: super().__init__(x=x, y=y, scale=scale) # send x,y and scale to Group @@ -195,7 +201,7 @@ def __init__( self._update_position() - def resize(self, new_width, new_height): + def resize(self, new_width: int, new_height: int) -> None: """Resizes the widget dimensions (for use with automated layout functions). **IMPORTANT:** The `resize` function should be overridden by the subclass definition. @@ -218,7 +224,7 @@ def resize(self, new_width, new_height): self._bounding_box[2] = new_width self._bounding_box[3] = new_height - def _update_position(self): + def _update_position(self) -> None: """ Widget class function for updating the widget's *x* and *y* position based upon the `anchor_point` and `anchored_position` values. The subclass should @@ -240,31 +246,31 @@ def _update_position(self): ) @property - def width(self): + def width(self) -> int: """The widget width, in pixels. (getter only) :return: int """ - return self._width + return self._width or 0 @property - def height(self): + def height(self) -> int: """The widget height, in pixels. (getter only) :return: int """ - return self._height + return self._height or 0 @property - def bounding_box(self): + def bounding_box(self) -> Tuple[int, ...]: """The boundary of the widget. [x, y, width, height] in Widget's local coordinates (in pixels). (getter only) :return: Tuple[int, int, int, int]""" - return self._bounding_box + return tuple(self._bounding_box) @property - def anchor_point(self): + def anchor_point(self) -> Optional[Tuple[float, float]]: """The anchor point for positioning the widget, works in concert with `anchored_position` The relative (X,Y) position of the widget where the anchored_position is placed. For example (0.0, 0.0) is the Widget's upper left corner, @@ -275,12 +281,12 @@ def anchor_point(self): return self._anchor_point @anchor_point.setter - def anchor_point(self, new_anchor_point): + def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None: self._anchor_point = new_anchor_point self._update_position() @property - def anchored_position(self): + def anchored_position(self) -> Optional[Tuple[int, int]]: """The anchored position (in pixels) for positioning the widget, works in concert with `anchor_point`. The `anchored_position` is the x,y pixel position for the placement of the Widget's `anchor_point`. @@ -292,6 +298,6 @@ def anchored_position(self): return self._anchored_position @anchored_position.setter - def anchored_position(self, new_anchored_position): + def anchored_position(self, new_anchored_position: Tuple[int, int]) -> None: self._anchored_position = new_anchored_position self._update_position()