/ slider.py
slider.py
1 # SPDX-FileCopyrightText: 2021 Jose David 2 # 3 # SPDX-License-Identifier: MIT 4 """ 5 6 `slider` 7 ================================================================================ 8 A slider widget with a rectangular shape. 9 10 * Author(s): Jose David M. 11 12 Implementation Notes 13 -------------------- 14 15 **Hardware:** 16 17 **Software and Dependencies:** 18 19 * Adafruit CircuitPython firmware for the supported boards: 20 https://github.com/adafruit/circuitpython/releases 21 22 """ 23 24 ################################ 25 # A slider widget for CircuitPython, using displayio and adafruit_display_shapes 26 # 27 # Features: 28 # - slider to represent non discrete values 29 # 30 # Future options to consider: 31 # --------------------------- 32 # different orientations (horizontal, vertical, flipped) 33 # 34 35 from adafruit_display_shapes.roundrect import RoundRect 36 from adafruit_displayio_layout.widgets.widget import Widget 37 from adafruit_displayio_layout.widgets.control import Control 38 from adafruit_displayio_layout.widgets.easing import quadratic_easeinout as easing 39 40 try: 41 from typing import Tuple 42 except ImportError: 43 pass 44 45 __version__ = "0.0.0-auto.0" 46 __repo__ = "https://github.com/jposada202020/CircuitPython_slider.git" 47 48 49 class Slider(Widget, Control): 50 """ 51 52 :param int x: pixel position, defaults to 0 53 :param int y: pixel position, defaults to 0 54 :param int width: width of the slider in pixels. It is recommended to use 100 55 the height will auto-size relative to the width. Defaults to :const:`100` 56 :param int height: height of the slider in pixels, defaults to 40 pixels 57 :param int touch_padding: the width of an additional border surrounding the switch 58 that extends the touch response boundary, defaults to 0 59 60 :param anchor_point: starting point for the annotation line, where ``anchor_point`` is 61 an (A,B) tuple in relative units of the size of the widget, for example (0.0, 0.0) is 62 the upper left corner, and (1.0, 1.0) is the lower right corner of the widget. 63 If :attr:`anchor_point` is `None`, then :attr:`anchored_position` is used to set the 64 annotation line starting point, in widget size relative units. 65 Defaults to :const:`(0.0, 0.0)` 66 :type anchor_point: Tuple[float, float] 67 68 :param anchored_position: pixel position starting point for the annotation line 69 where :attr:`anchored_position` is an (x,y) tuple in pixel units relative to the 70 upper left corner of the widget, in pixel units (default is None). 71 :type anchored_position: Tuple[int, int] 72 73 :param fill_color: (*RGB tuple or 24-bit hex value*) slider fill color, default 74 is :const:`(66, 44, 66)` gray. 75 :param outline_color: (*RGB tuple or 24-bit hex value*) slider outline color, 76 default is :const:`(30, 30, 30)` dark gray. 77 :param background_color: (*RGB tuple or 24-bit hex value*) background color, 78 default is :const:`(255, 255, 255)` white 79 80 :param int switch_stroke: outline stroke width for the switch and background, in pixels, 81 default is 2 82 :param Boolean value: the initial value for the switch, default is False 83 84 85 **Quickstart: Importing and using Slider** 86 87 Here is one way of importing the `Slider` class so you can use it as 88 the name ``Slider``: 89 90 .. code-block:: python 91 92 from adafruit_displayio_layout.widgets.slider import Slider 93 94 Now you can create a Slider at pixel position x=20, y=30 using: 95 96 .. code-block:: python 97 98 my_slider=Slider(x=20, y=30) 99 100 Once your setup your display, you can now add ``my_slider`` to your display using: 101 102 .. code-block:: python 103 104 display.show(my_slider) # add the group to the display 105 106 If you want to have multiple display elements, you can create a group and then 107 append the slider and the other elements to the group. Then, you can add the full 108 group to the display as in this example: 109 110 .. code-block:: python 111 112 my_slider= Slider(20, 30) 113 my_group = displayio.Group() # make a group 114 my_group.append(my_slider) # Add my_slider to the group 115 116 # 117 # Append other display elements to the group 118 # 119 120 display.show(my_group) # add the group to the display 121 122 123 **Summary: Slider Features and input variables** 124 125 The ``Slider`` widget has some options for controlling its position, visible appearance, 126 and value through a collection of input variables: 127 128 - **position**: :const:`x`, ``y`` or ``anchor_point`` and ``anchored_position`` 129 130 - **size**: :const:`width` and ``height`` (recommend to leave ``height`` = None to use 131 preferred aspect ratio) 132 133 - **switch color**: :const:`fill_color`, :const:`outline_color` 134 135 - **background color**: :const:`background_color` 136 137 - **linewidths**: :const:`switch_stroke` 138 139 - **value**: Set ``value`` to the initial value (True or False) 140 141 - **touch boundaries**: :attr:`touch_padding` defines the number of additional pixels 142 surrounding the switch that should respond to a touch. (Note: The ``touch_padding`` 143 variable updates the ``touch_boundary`` Control class variable. The definition of 144 the ``touch_boundary`` is used to determine the region on the Widget that returns 145 `True` in the `when_inside` function.) 146 147 **The Slider Widget** 148 149 .. figure:: slider.png 150 :scale: 100 % 151 :figwidth: 80% 152 :align: center 153 :alt: Diagram of the slider widget. 154 155 This is a diagram of a slider with component parts 156 157 158 """ 159 160 def __init__( 161 self, 162 x: int = 0, 163 y: int = 0, 164 width: int = 100, # recommend to default to 165 height: int = 40, 166 touch_padding: int = 0, 167 anchor_point: Tuple[int, int] = None, 168 anchored_position: Tuple[int, int] = None, 169 fill_color: Tuple[int, int, int] = (66, 44, 66), 170 outline_color: Tuple[int, int, int] = (30, 30, 30), 171 background_color: Tuple[int, int, int] = (255, 255, 255), 172 value: bool = False, 173 **kwargs, 174 ) -> None: 175 Widget.__init__(self, x=x, y=y, height=height, width=width, **kwargs) 176 Control.__init__(self) 177 178 self._knob_width = height // 2 179 self._knob_height = height 180 181 self._knob_x = self._knob_width 182 self._knob_y = self._knob_height 183 184 self._slider_height = height // 5 185 186 self._height = self.height 187 188 # pylint: disable=access-member-before-definition) 189 190 if self._width is None: 191 self._width = 100 192 else: 193 self._width = self.width 194 195 self._fill_color = fill_color 196 self._outline_color = outline_color 197 self._background_color = background_color 198 199 self._switch_stroke = 2 200 201 self._touch_padding = touch_padding 202 203 self._value = value 204 205 self._anchor_point = anchor_point 206 self._anchored_position = anchored_position 207 208 self._create_slider() 209 210 def _create_slider(self) -> None: 211 # The main function that creates the switch display elements 212 self._x_motion = self._width 213 self._y_motion = 0 214 215 self._frame = RoundRect( 216 x=0, 217 y=0, 218 width=self.width, 219 height=self.height, 220 r=4, 221 fill=0x990099, 222 outline=self._outline_color, 223 stroke=self._switch_stroke, 224 ) 225 226 self._switch_handle = RoundRect( 227 x=0, 228 y=0, 229 width=self._knob_width, 230 height=self._knob_height, 231 r=4, 232 fill=self._fill_color, 233 outline=self._outline_color, 234 stroke=self._switch_stroke, 235 ) 236 237 self._switch_roundrect = RoundRect( 238 x=2, 239 y=self.height // 2 - self._slider_height // 2, 240 r=2, 241 width=self._width - 4, 242 height=self._slider_height, 243 fill=self._background_color, 244 outline=self._background_color, 245 stroke=self._switch_stroke, 246 ) 247 248 self._bounding_box = [ 249 0, 250 0, 251 self.width, 252 self._knob_height, 253 ] 254 255 self.touch_boundary = [ 256 self._bounding_box[0] - self._touch_padding, 257 self._bounding_box[1] - self._touch_padding, 258 self._bounding_box[2] + 2 * self._touch_padding, 259 self._bounding_box[3] + 2 * self._touch_padding, 260 ] 261 262 self._switch_initial_x = self._switch_handle.x 263 self._switch_initial_y = self._switch_handle.y 264 265 for _ in range(len(self)): 266 self.pop() 267 268 self.append(self._frame) 269 self.append(self._switch_roundrect) 270 self.append(self._switch_handle) 271 272 self._update_position() 273 274 def _get_offset_position(self, position) -> Tuple[int, int]: 275 x_offset = int(self._x_motion * position // 2) 276 y_offset = int(self._y_motion * position) 277 278 return x_offset, y_offset 279 280 def _draw_position(self, position: Tuple[int, int]) -> None: 281 # apply the "easing" function to the requested position to adjust motion 282 position = easing(position) 283 284 # Get the position offset from the motion function 285 x_offset, y_offset = self._get_offset_position(position) 286 287 # Update the switch x- and y-positions 288 self._switch_handle.x = self._switch_initial_x + x_offset 289 self._switch_handle.y = self._switch_initial_y + y_offset 290 291 def when_selected(self, touch_point: int) -> int: 292 """ 293 Manages internal logic when widget is selected 294 """ 295 296 if touch_point[0] <= self.x + self._knob_width: 297 touch_x = touch_point[0] - self.x 298 else: 299 touch_x = touch_point[0] - self.x - self._knob_width 300 301 touch_y = touch_point[1] - self.y 302 303 self.selected((touch_x, touch_y, 0)) 304 self._switch_handle.x = touch_x 305 return self._switch_handle.x 306 307 def when_inside(self, touch_point: int) -> bool: 308 """Checks if the Widget was touched. 309 310 :param touch_point: x,y location of the screen, in absolute display coordinates. 311 :return: Boolean 312 313 """ 314 touch_x = ( 315 touch_point[0] - self.x 316 ) # adjust touch position for the local position 317 touch_y = touch_point[1] - self.y 318 319 return self.contains((touch_x, touch_y, 0)) 320 321 @property 322 def value(self) -> int: 323 """The current switch value (Boolean). 324 325 :return: Boolean 326 """ 327 return self._value