Skip to content

Main classes: Widget and Control #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4dfdce6
Initial commit with Widget, Control and WidgetLabel class definitions…
kmatch98 Feb 2, 2021
a7eced3
Ran black, somehow failed for widget_label.py
kmatch98 Feb 2, 2021
ff05592
Add orientation and flip to switch, simplify animation functions with…
kmatch98 Feb 3, 2021
634f91c
Ran black and pylint, some cleanup
kmatch98 Feb 3, 2021
14303c8
Update widget name to SwitchRound
kmatch98 Feb 3, 2021
7272941
Change filename of example to remove _horizontal_
kmatch98 Feb 3, 2021
adb2dc0
Use .hidden property of TileGrid to handle text 0/1 display control
kmatch98 Feb 7, 2021
57eb704
Arrange folders to match library standard organization
kmatch98 Feb 7, 2021
bdc1f34
Update example to conform to correct directory structure
kmatch98 Feb 7, 2021
3158d43
Added resize function for compatibility with grid_layout
kmatch98 Feb 9, 2021
8072bb4
Some pylint fixes, delete debug print statements
kmatch98 Feb 9, 2021
bc24846
Initial commit of WidgetAnnotation
kmatch98 Feb 12, 2021
14b2543
Add Dial widget, add example for Switch, Dial and WidgetAnnotation an…
kmatch98 Feb 13, 2021
90c443e
Initial commit with Widget, Control and WidgetLabel class definitions…
kmatch98 Feb 2, 2021
3d6d443
Ran black, somehow failed for widget_label.py
kmatch98 Feb 2, 2021
4e2cfec
Merge branch 'main' of https://github.com/kmatch98/Adafruit_CircuitPy…
kmatch98 Feb 13, 2021
7582f2e
Initial commit with Widget, Control and WidgetLabel class definitions…
kmatch98 Feb 2, 2021
4daff4d
Merge branch 'main' of https://github.com/kmatch98/Adafruit_CircuitPy…
kmatch98 Feb 13, 2021
b8de020
Bring up to date with latest main upstream branch
kmatch98 Feb 13, 2021
4245291
Update to include merged changes from upstream
kmatch98 Feb 13, 2021
57fa93b
Ran black on widgets
kmatch98 Feb 13, 2021
7e4e3a4
Add comments to switch_round, add simpletest switch examples
kmatch98 Feb 14, 2021
e3cd623
Add font option for switch_round WidgetLabel
kmatch98 Feb 14, 2021
7aa9902
Add flip_input widget
kmatch98 Feb 15, 2021
759248d
Add flip_input example
kmatch98 Feb 15, 2021
8c5c397
add font file
kmatch98 Feb 15, 2021
4f62512
Streamline animation for flip_input, adapt to script fonts
kmatch98 Feb 16, 2021
c050b9a
Add easing animations for flip_input
kmatch98 Feb 18, 2021
dac960d
Updated animation redraws to allow springy animations
kmatch98 Feb 19, 2021
a0649d6
Add sphinx docs
kmatch98 Feb 19, 2021
5fd70e8
remove unnecessary files for this PR
kmatch98 Feb 19, 2021
a23dab9
restore examples
kmatch98 Feb 19, 2021
b8e8df7
pylint fixes
kmatch98 Feb 19, 2021
6f426d8
end of line to api.rst
kmatch98 Feb 19, 2021
faf2588
Update Widget and Control class definitions, add documentation includ…
kmatch98 Feb 22, 2021
a2fdcca
Update Widget, Control definitions per review feedback, added documen…
kmatch98 Feb 22, 2021
4a6a2c9
add license file
kmatch98 Feb 22, 2021
acb0695
Add default values to Widget x,y
kmatch98 Feb 23, 2021
1a2ed2f
Minor doc tweak
kmatch98 Feb 24, 2021
18e9cf5
Minor doc tweak
kmatch98 Feb 24, 2021
312ccf7
Merge branch 'main_classes' of https://github.com/kmatch98/Adafruit_C…
kmatch98 Mar 3, 2021
db17f9e
Update api.rst
kmatch98 Mar 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions adafruit_displayio_layout/widgets/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2021 Kevin Matocha, Tim Cocks
#
# SPDX-License-Identifier: MIT

"""
`control`
================================================================================
CircuitPython GUI Control Class for touch-related elements

* Author(s): Kevin Matocha

Implementation Notes
--------------------

**Hardware:**

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases

"""

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git"

# pylint: disable=unsubscriptable-object, unnecessary-pass


class Control:
"""A Control class for responsive elements, including touch response functions for displays.

**IMPORTANT**: The *touch_point* for all functions should be in local coordinates
for this item. That means, any widget should adjust the touchpoint for self.x and
self.y before passing the touchpoint to this set of Control functions.

The Control class uses a state variable **touch_boundary** [x, y, width, height]
that defines the rectangular boundary for touch inputs. The **touch_boundary**
is used by the `contains` function to check when touches are within the Control's
boundary. Note: These **touch_boundary** dimensions are in the Control's local
pixel coordinates. The **x** and **y** values define the upper left corner of the
**touch_boundary**. The **touch_boundary** value should be updated by the sublcass
definiton.

"""

def __init__(
self,
):
self.touch_boundary = (
None # `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):
"""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]
:return: Boolean

"""

# Note: if a widget's `scale` property is > 1, be sure to update the
# `touch_boundary` dimensions to accommodate the `scale` factor
if (self.touch_boundary is not None) and (
(
self.touch_boundary[0]
<= touch_point[0]
<= (self.touch_boundary[0] + self.touch_boundary[2])
)
and (
self.touch_boundary[1]
<= touch_point[1]
<= (self.touch_boundary[1] + self.touch_boundary[3])
)
):
return True
return False

# place holder touch_handler response functions
def selected(self, touch_point):
"""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]
:return: None

"""
pass
301 changes: 301 additions & 0 deletions adafruit_displayio_layout/widgets/widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# SPDX-FileCopyrightText: 2021 Kevin Matocha, Tim Cocks
#
# SPDX-License-Identifier: MIT

"""
`widget`
================================================================================

CircuitPython GUI Widget Class for visual elements

* Author(s): Kevin Matocha

Implementation Notes
--------------------

**Hardware:**

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases

"""

import displayio

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git"

# pylint: disable=too-many-arguments


class Widget(displayio.Group):
"""
A Widget class definition for graphical display elements. The Widget handles
the sizing and positioning of the widget.

:param int x: pixel position
:param int y: pixel position
:param int width: width of the widget in pixels, set to None to auto-size relative to
the height
:param int height: height of the widget in pixels
:param anchor_point: (X,Y) values from 0.0 to 1.0 to define the anchor point relative to the
widget bounding box
:type anchor_point: Tuple[float,float]
:param int anchored_position: (x,y) pixel value for the location of the anchor_point
:type anchored_position: Tuple[int, int]


.. figure:: gui_layout_coordinates.png
:scale: 50 %
:alt: Diagram of layout coordinates

Diagram showing the global and local coordinate systems and the Widget's
associated class variables.

**Widget Class Positioning: Display vs. Local Coordinates**

The Widget class is used to define the position and size of the graphical elements
that define the widget.
The Widget is a subclass of `displayio.Group` and inherits the positioning elements
of `displayio.Group`, including *x*, *y* (in pixels). If the Widget is directly added
to the display, then the *.x* and *.y* positions refer to the pixel position on
the display. (Note: If the Widget is actually held within another Group, then the *.x*
and *.y* of the widget are in that Group's local coordinate system.)

This Widget coordinate system is illustrated in the diagram above, showing the
coordinate systems of a sliding switch widget. The display's origin (x=0, y=0)
is at the upper left corner of the display. In this example the display size is
320 x 240 pixels, so the display's bottom right corner is at display coordinates
(x=320, y=240). The upper left corner of the widget is labeled notionally as
*widget.x* and *widget.y* is set at the display pixel position of (x=100, y=50).


**Local Coordinates: bounding_box**

Other parameters defined in the Widget class use a "local" coordinate system, as
indicated by the red text in the display. These include `bounding_box` and
*touch_boundary*. The `bounding_box` defines the origin of a Widget is at the upper
left corner of the key graphical element of the widget and is set to (0,0) in
widget local coordinates. The `width` and `height` of the `bounding_box` are
defined as the pixel distances that make a mininum box that contains the key
graphical elements of the widget. In the case of this example, the width is
100 pixels and the height is 40 pixels. (Note: If a label is included for a
widget, the label should not be included in the `bounding_box`.)

**Local Coordinates: touch_boundary (inherited from `Control` class)**
This example of a sliding switch reacts to touch using the addition of
inheritance from the `Control` class, so additional dimensional parameters are
included for that class definition. Similar to the definition of
the `bounding_box`, the *touch_boundary* is also defined using the widget's
local coordinate system.

As shown in the diagram, we see that the *touch_boundary* is larger than the
`bounding_box`. The *touch_boundary* should likely be larger than the
`bounding_box` since finger touches are not precise. The use of additional
space around the widget ensures that the widget reacts when the touch is close
enough. In the case of this example, the switch widget provides a *touch_padding*
option to define additional space around the `bounding_box` where touches are
accepted (with the `Control.contains()` function). Looking at the example, we
see that the upper left corner of the *touch_boundary* is (x=-10, y=-10) in widget
local coordinates. This means that the accepted touch boundary starts at 10 pixels
up and 10 pixels left of the upper left corner of the widget. The *touch_boundary*
is 120 pixels wide and 60 pixels high. This confirms that a 10 pixel *touch_padding*
was used, giving additional 10 pixels around the `bounding_box`. Note: If you are
building your own new widgets, the *touch_boundary* tuple can be adjusted directly to
meet whatever needs your widget needs. The *touch_boundary* is used in the
`Control.contains()` function to determine when the Control-type widget was touched.

Note: If a widget does not need to respond to touches (for example a display of a
value), then it should not inherit the `Control` class, and thus will not have a
*touch_boundary*.

**Positioning on the screen: Using x and y or anchor_point and anchored_position**

The Widget class has several options for setting the widget position on the screen.
In the simplest case, you can define the widget's *.x* and *.y* properties to set
the position. (**Reminder**: If your widget is directly shown by the display using
*display.show(my_widget)*), then the *.x* and *.y* positions will be in the display's
coordinate system. But if your widget is held inside of another Group, then its
coordinates will be in that Group's coordinate system.)

The Widget class definition also allows for relative positioning on the screen using
the combination of `anchor_point` and `anchored_position`. This method is useful
when you want your widget to be centered or aligned along one of its edges.

A good example of the use of `anchor_point` and `anchored_position` is in the
`Adafruit "Candy Hearts" learn guide
<https://learn.adafruit.com/circuit-python-tft-gizmo-candy-hearts/how-it-works>`_
related to text positioning.

The `anchor_point` is a Tuple (float, float) that corresponds to the fractional
position of the size of the widget. The upper left corner being
`anchor_point` =(0.0, 0.0) and the lower right corner being `anchor_point` =(1.0, 1.0).
The center of the widget is then `anchor_point` =(0.5, 0.5), halfway along the
x-size and halfway along the y-size. One more example, the center of the bottom
edge is (0.5, 1.0), halfway along the x-size and all the way of the y-size.

Once you define the `anchor_point`, you can now set the `anchored_position`. The
`anchored_position` is the pixel dimension location where you want to put the
`anchor_point`. To learn from example, let's say I want to place my widget so
its bottom right corner is at the bottom right of my display (assume 320 x 240
pixel size display).

First, I want to define the widget reference point to be the bottom right corner of
my widget, so I'll set `anchor_point` =(1.0,1.0). Next, I want that anchor point
on the widget to be placed at the bottom right corner of my display, so I'll set
`anchored_position` =(320,240). In essence, the `anchor_point` is defining the
reference ("anchor") point on the widget (but in relative widget-sized dimensions
using x,y floats between 0.0 and 1.0) and then places that `anchor_point` at the
pixel location specified as the `anchored_position` in pixel dimensions
(x, y are in pixel units on the display).

The reason for using `anchor_point` is so that you
don't need to know the width or height of the widget in advance, you can use
`anchor_point` and it will always adjust for the widget's height and width to
set the position at the `anchored_position` pixel position.

In summary:
- `anchor_point` is x,y tuple (floats) of the relative size of the widget. Upper left
corner is (0.0, 0.0) and lower right is (1.0, 1.0).
- `anchored_position` is in x,y tuple (ints) pixel coordinates where the `anchor_point`
will be placed.

"""

def __init__(
self,
x=0,
y=0,
scale=1,
width=None,
height=None,
anchor_point=None,
anchored_position=None,
**kwargs,
):

super().__init__(x=x, y=y, scale=scale, **kwargs)
# send x,y and scale to Group
# **kwargs should include `max_size`from the subclass implementation
# to define how many graphical elements will be held in the Group that
# makes up this widget
#
# If scale is set > 1, will need to update the Control `touch_boundary`
# to accommodate the larger scale

self._width = width
self._height = height
self._anchor_point = anchor_point
self._anchored_position = anchored_position

# self._bounding_box: pixel extent of the widget [x0, y0, width, height]
# The bounding box should be updated based on the specifics of the widget
if (width is not None) and (height is not None):
self._bounding_box = [0, 0, width, height]
else:
self._bounding_box = [0, 0, 0, 0]

self._update_position()

def resize(self, new_width, new_height):
"""Resizes the widget dimensions (for use with automated layout functions).

**IMPORTANT:** The `resize` function should be overridden by the subclass definition.

The width and height are provided together so the subclass `resize`
function can apply any constraints that require consideration of both width
and height (such as maintaining a Widget's preferred aspect ratio). The Widget should
be resized to the maximum size that can fit within the dimensions defined by
the requested *new_width* and *new_height*. After resizing, the Widget's
`bounding_box` should also be updated.

:param int new_width: target maximum width (in pixels)
:param int new_height: target maximum height (in pixels)
:return: None

"""
self._width = new_width
self._height = new_height

self._bounding_box[2] = new_width
self._bounding_box[3] = new_height

def _update_position(self):
"""
Widget class function for updating the widget's *x* and *y* position based
upon the `anchor_point` and `anchored_position` values. The subclass should
call `_update_position` after the widget is resized.

:return: None
"""

if (self._anchor_point is not None) and (self._anchored_position is not None):
self.x = (
self._anchored_position[0]
- int(self._anchor_point[0] * self._bounding_box[2])
- self._bounding_box[0]
)
self.y = (
self._anchored_position[1]
- int(self._anchor_point[1] * self._bounding_box[3])
- self._bounding_box[1]
)

@property
def width(self):
"""The widget width, in pixels. (getter only)

:return: int
"""
return self._width

@property
def height(self):
"""The widget height, in pixels. (getter only)

:return: int
"""
return self._height

@property
def bounding_box(self):
"""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

@property
def anchor_point(self):
"""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,
(0.5, 0.5) is the Widget's center point, and (1.0, 1.0) is the Widget's lower right corner.

:param anchor_point: In relative units of the Widget size.
:type anchor_point: Tuple[float, float]"""
return self._anchor_point

@anchor_point.setter
def anchor_point(self, new_anchor_point):
self._anchor_point = new_anchor_point
self._update_position()

@property
def anchored_position(self):
"""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`.

:param anchored_position: The (x,y) pixel position for the anchored_position (in pixels).
:type anchored_position: Tuple[int, int]

"""
return self._anchored_position

@anchored_position.setter
def anchored_position(self, new_anchored_position):
self._anchored_position = new_anchored_position
self._update_position()
Loading