diff --git a/adafruit_button/__init__.py b/adafruit_button/__init__.py new file mode 100644 index 0000000..2fa5ba8 --- /dev/null +++ b/adafruit_button/__init__.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_button` +================================================================================ + +UI Buttons for displayio + + +* Author(s): Limor Fried + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" +from adafruit_button.button import Button diff --git a/adafruit_button.py b/adafruit_button/button.py similarity index 73% rename from adafruit_button.py rename to adafruit_button/button.py index 8a2154d..007a8f1 100644 --- a/adafruit_button.py +++ b/adafruit_button/button.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT """ -`adafruit_button` +`adafruit_button.button` ================================================================================ UI Buttons for displayio @@ -22,24 +22,15 @@ """ from micropython import const -import displayio -from adafruit_display_text.label import Label from adafruit_display_shapes.rect import Rect from adafruit_display_shapes.roundrect import RoundRect +from adafruit_button.button_base import ButtonBase, _check_color __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Button.git" -def _check_color(color): - # if a tuple is supplied, convert it to a RGB number - if isinstance(color, tuple): - r, g, b = color - return int((r << 16) + (g << 8) + (b & 0xFF)) - return color - - -class Button(displayio.Group): +class Button(ButtonBase): # pylint: disable=too-many-instance-attributes, too-many-locals """Helper class for creating UI buttons for ``displayio``. @@ -141,26 +132,27 @@ def __init__( selected_outline=None, selected_label=None ): - super().__init__(x=x, y=y) - self.x = x - self.y = y - self._width = width - self._height = height - self._font = label_font - self._selected = False - self.name = name - self._label = label + super().__init__( + x=x, + y=y, + width=width, + height=height, + name=name, + label=label, + label_font=label_font, + label_color=label_color, + selected_label=selected_label, + ) + self.body = self.fill = self.shadow = None self.style = style self._fill_color = _check_color(fill_color) self._outline_color = _check_color(outline_color) - self._label_color = label_color - self._label_font = label_font + # Selecting inverts the button colors! self._selected_fill = _check_color(selected_fill) self._selected_outline = _check_color(selected_outline) - self._selected_label = _check_color(selected_label) if self.selected_fill is None and fill_color is not None: self.selected_fill = (~self._fill_color) & 0xFFFFFF @@ -173,64 +165,17 @@ def __init__( self.label = label - @property - def label(self): - """The text label of the button""" - return self._label.text - - @label.setter - def label(self, newtext): - if self._label and self and (self[-1] == self._label): - self.pop() - - self._label = None - if not newtext or (self._label_color is None): # no new text - return # nothing to do! - - if not self._label_font: - raise RuntimeError("Please provide label font") - self._label = Label(self._label_font, text=newtext) - dims = self._label.bounding_box - if dims[2] >= self.width or dims[3] >= self.height: - while len(self._label.text) > 1 and ( - dims[2] >= self.width or dims[3] >= self.height - ): - self._label.text = "{}.".format(self._label.text[:-2]) - dims = self._label.bounding_box - if len(self._label.text) <= 1: - raise RuntimeError("Button not large enough for label") - self._label.x = (self.width - dims[2]) // 2 - self._label.y = self.height // 2 - self._label.color = self._label_color - self.append(self._label) - - if (self.selected_label is None) and (self._label_color is not None): - self.selected_label = (~self._label_color) & 0xFFFFFF - - @property - def selected(self): - """Selected inverts the colors.""" - return self._selected - - @selected.setter - def selected(self, value): - if value == self._selected: - return # bail now, nothing more to do - self._selected = value + def _subclass_selected_behavior(self, value): if self._selected: new_fill = self.selected_fill new_out = self.selected_outline - new_label = self.selected_label else: new_fill = self._fill_color new_out = self._outline_color - new_label = self._label_color # update all relevant colors! if self.body is not None: self.body.fill = new_fill self.body.outline = new_out - if self._label is not None: - self._label.color = new_label @property def group(self): @@ -295,25 +240,6 @@ def selected_outline(self, new_color): if self.selected: self.body.outline = self._selected_outline - @property - def selected_label(self): - """The font color of the button when selected""" - return self._selected_label - - @selected_label.setter - def selected_label(self, new_color): - self._selected_label = _check_color(new_color) - - @property - def label_color(self): - """The font color of the button""" - return self._label_color - - @label_color.setter - def label_color(self, new_color): - self._label_color = _check_color(new_color) - self._label.color = self._label_color - @property def width(self): """The width of the button""" diff --git a/adafruit_button/button_base.py b/adafruit_button/button_base.py new file mode 100644 index 0000000..d33cfc4 --- /dev/null +++ b/adafruit_button/button_base.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_button.button` +================================================================================ + +UI Buttons for displayio + + +* Author(s): Limor Fried + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" +from adafruit_display_text.bitmap_label import Label +from displayio import Group + + +def _check_color(color): + # if a tuple is supplied, convert it to a RGB number + if isinstance(color, tuple): + r, g, b = color + return int((r << 16) + (g << 8) + (b & 0xFF)) + return color + + +class ButtonBase(Group): + # pylint: disable=too-many-instance-attributes + """Superclass for creating UI buttons for ``displayio``. + + :param x: The x position of the button. + :param y: The y position of the button. + :param width: The width of the button in tiles. + :param height: The height of the button in tiles. + :param name: A name, or miscellaneous string that is stored on the button. + :param label: The text that appears inside the button. Defaults to not displaying the label. + :param label_font: The button label font. + :param label_color: The color of the button label text. Defaults to 0x0. + :param selected_label: Text that appears when selected + """ + + def __init__( + self, + *, + x, + y, + width, + height, + name=None, + label=None, + label_font=None, + label_color=0x0, + selected_label=None + ): + super().__init__(x=x, y=y) + self.x = x + self.y = y + self._width = width + self._height = height + self._font = label_font + self._selected = False + self.name = name + self._label = label + self._label_color = label_color + self._label_font = label_font + self._selected_label = _check_color(selected_label) + + @property + def label(self): + """The text label of the button""" + return self._label.text + + @label.setter + def label(self, newtext): + if self._label and self and (self[-1] == self._label): + self.pop() + + self._label = None + if not newtext or (self._label_color is None): # no new text + return # nothing to do! + + if not self._label_font: + raise RuntimeError("Please provide label font") + self._label = Label(self._label_font, text=newtext) + dims = self._label.bounding_box + if dims[2] >= self.width or dims[3] >= self.height: + while len(self._label.text) > 1 and ( + dims[2] >= self.width or dims[3] >= self.height + ): + self._label.text = "{}.".format(self._label.text[:-2]) + dims = self._label.bounding_box + if len(self._label.text) <= 1: + raise RuntimeError("Button not large enough for label") + self._label.x = (self.width - dims[2]) // 2 + self._label.y = self.height // 2 + self._label.color = ( + self._label_color if not self.selected else self._selected_label + ) + self.append(self._label) + + if (self.selected_label is None) and (self._label_color is not None): + self.selected_label = (~self._label_color) & 0xFFFFFF + + def _subclass_selected_behavior(self, value): + # Subclasses should overide this! + pass + + @property + def selected(self): + """Selected inverts the colors.""" + return self._selected + + @selected.setter + def selected(self, value): + if value == self._selected: + return # bail now, nothing more to do + self._selected = value + + if self._selected: + new_label = self.selected_label + else: + new_label = self._label_color + if self._label is not None: + self._label.color = new_label + + self._subclass_selected_behavior(value) + + @property + def selected_label(self): + """The font color of the button when selected""" + return self._selected_label + + @selected_label.setter + def selected_label(self, new_color): + self._selected_label = _check_color(new_color) + + @property + def label_color(self): + """The font color of the button""" + return self._label_color + + @label_color.setter + def label_color(self, new_color): + self._label_color = _check_color(new_color) + self._label.color = self._label_color diff --git a/adafruit_button/sprite_button.py b/adafruit_button/sprite_button.py new file mode 100644 index 0000000..c2f02ba --- /dev/null +++ b/adafruit_button/sprite_button.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_button.button` +================================================================================ + +Bitmap 3x3 Spritesheet based UI Button for displayio + + +* Author(s): Tim Cocks + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" +from adafruit_imageload.tilegrid_inflator import inflate_tilegrid +from adafruit_imageload import load +from adafruit_button.button_base import ButtonBase + + +class SpriteButton(ButtonBase): + """Helper class for creating 3x3 Bitmap Spritesheet UI buttons for ``displayio``. + + :param x: The x position of the button. + :param y: The y position of the button. + :param width: The width of the button in tiles. + :param height: The height of the button in tiles. + :param name: A name, or miscellaneous string that is stored on the button. + :param label: The text that appears inside the button. Defaults to not displaying the label. + :param label_font: The button label font. + :param label_color: The color of the button label text. Defaults to 0x0. + :param selected_label: Text that appears when selected + :param string bmp_path: The path of the 3x3 spritesheet Bitmap file + :param string selected_bmp_path: The path of the 3x3 spritesheet Bitmap file to use when pressed + :param int or tuple transparent_index: Index(s) that will be made transparent on the Palette + """ + + def __init__( + self, + *, + x, + y, + width, + height, + name=None, + label=None, + label_font=None, + label_color=0x0, + selected_label=None, + bmp_path=None, + selected_bmp_path=None, + transparent_index=None + ): + if bmp_path is None: + raise ValueError("Please supply bmp_path. It cannot be None.") + + super().__init__( + x=x, + y=y, + width=width, + height=height, + name=name, + label=label, + label_font=label_font, + label_color=label_color, + selected_label=selected_label, + ) + + self._bmp, self._bmp_palette = load(bmp_path) + + self._selected_bmp = None + self._selected_bmp_palette = None + self._selected = False + + if selected_bmp_path is not None: + self._selected_bmp, self._selected_bmp_palette = load(selected_bmp_path) + if transparent_index is not None: + if isinstance(transparent_index, tuple): + for _index in transparent_index: + self._selected_bmp_palette.make_transparent(_index) + elif isinstance(transparent_index, int): + self._selected_bmp_palette.make_transparent(0) + + self._btn_tilegrid = inflate_tilegrid( + bmp_obj=self._bmp, + bmp_palette=self._bmp_palette, + target_size=( + width // (self._bmp.width // 3), + height // (self._bmp.height // 3), + ), + transparent_index=transparent_index, + ) + self.append(self._btn_tilegrid) + + self.label = label + + @property + def width(self): + """The width of the button""" + return self._width + + @property + def height(self): + """The height of the button""" + return self._height + + def contains(self, point): + """Used to determine if a point is contained within a button. For example, + ``button.contains(touch)`` where ``touch`` is the touch point on the screen will allow for + determining that a button has been touched. + """ + return (self.x <= point[0] <= self.x + self.width) and ( + self.y <= point[1] <= self.y + self.height + ) + + def _subclass_selected_behavior(self, value): + if self._selected: + if self._selected_bmp is not None: + self._btn_tilegrid.bitmap = self._selected_bmp + self._btn_tilegrid.pixel_shader = self._selected_bmp_palette + else: + self._btn_tilegrid.bitmap = self._bmp + self._btn_tilegrid.pixel_shader = self._bmp_palette diff --git a/docs/api.rst b/docs/api.rst index 49c1de1..b604b89 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,3 +6,12 @@ .. automodule:: adafruit_button :members: + +.. automodule:: adafruit_button.button_base + :members: + +.. automodule:: adafruit_button.button + :members: + +.. automodule:: adafruit_button.sprite_button + :members: diff --git a/docs/examples.rst b/docs/examples.rst index 3960d39..30c5250 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -33,3 +33,12 @@ A soundboard made with buttons .. literalinclude:: ../examples/display_button_soundboard.py :caption: examples/display_button_soundboard.py :linenos: + +Sprite Button +------------- + +Custom sprite button + +.. literalinclude:: ../examples/display_button_spritebutton_simpletest.py + :caption: examples/display_button_spritebutton_simpletest.py + :linenos: diff --git a/examples/bmps/gradient_button_0.bmp b/examples/bmps/gradient_button_0.bmp new file mode 100644 index 0000000..bfa8cfa Binary files /dev/null and b/examples/bmps/gradient_button_0.bmp differ diff --git a/examples/bmps/gradient_button_0.bmp.license b/examples/bmps/gradient_button_0.bmp.license new file mode 100644 index 0000000..8f7990c --- /dev/null +++ b/examples/bmps/gradient_button_0.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT diff --git a/examples/bmps/gradient_button_1.bmp b/examples/bmps/gradient_button_1.bmp new file mode 100644 index 0000000..ba6d75c Binary files /dev/null and b/examples/bmps/gradient_button_1.bmp differ diff --git a/examples/bmps/gradient_button_1.bmp.license b/examples/bmps/gradient_button_1.bmp.license new file mode 100644 index 0000000..8f7990c --- /dev/null +++ b/examples/bmps/gradient_button_1.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT diff --git a/examples/display_button_spritebutton_simpletest.py b/examples/display_button_spritebutton_simpletest.py new file mode 100644 index 0000000..8f78c48 --- /dev/null +++ b/examples/display_button_spritebutton_simpletest.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT +import time +import board +import displayio +import adafruit_touchscreen +import terminalio +from adafruit_button.sprite_button import SpriteButton + +# These pins are used as both analog and digital! XL, XR and YU must be analog +# and digital capable. YD just need to be digital +ts = adafruit_touchscreen.Touchscreen( + board.TOUCH_XL, + board.TOUCH_XR, + board.TOUCH_YD, + board.TOUCH_YU, + calibration=((5200, 59000), (5800, 57000)), + size=(board.DISPLAY.width, board.DISPLAY.height), +) + +# Make the display context +main_group = displayio.Group() +board.DISPLAY.show(main_group) + +BUTTON_WIDTH = 10 * 16 +BUTTON_HEIGHT = 3 * 16 +BUTTON_MARGIN = 20 + +font = terminalio.FONT + +buttons = [] + + +button_0 = SpriteButton( + x=BUTTON_MARGIN, + y=BUTTON_MARGIN, + width=BUTTON_WIDTH, + height=BUTTON_HEIGHT, + label="button0", + label_font=font, + bmp_path="bmps/gradient_button_0.bmp", + selected_bmp_path="bmps/gradient_button_1.bmp", + transparent_index=0, +) + +buttons.append(button_0) + +for b in buttons: + main_group.append(b) +while True: + p = ts.touch_point + if p: + print(p) + for i, b in enumerate(buttons): + if b.contains(p): + print("Button %d pressed" % i) + b.selected = True + b.label = "pressed" + else: + b.selected = False + b.label = "button0" + + else: + for i, b in enumerate(buttons): + if b.selected: + b.selected = False + b.label = "button0" + time.sleep(0.01) diff --git a/optional_requirements.txt b/optional_requirements.txt index d4e27c4..9c5fce5 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -1,3 +1,5 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense + +adafruit-circuitpython-ImageLoad diff --git a/pyproject.toml b/pyproject.toml index 2026c32..4b1b695 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ dynamic = ["dependencies", "optional-dependencies"] [tool.setuptools] -py-modules = ["adafruit_button"] +packages = ["adafruit_button"] [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]}