/ 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