-
Notifications
You must be signed in to change notification settings - Fork 16
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
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 a7eced3
Ran black, somehow failed for widget_label.py
kmatch98 ff05592
Add orientation and flip to switch, simplify animation functions with…
kmatch98 634f91c
Ran black and pylint, some cleanup
kmatch98 14303c8
Update widget name to SwitchRound
kmatch98 7272941
Change filename of example to remove _horizontal_
kmatch98 adb2dc0
Use .hidden property of TileGrid to handle text 0/1 display control
kmatch98 57eb704
Arrange folders to match library standard organization
kmatch98 bdc1f34
Update example to conform to correct directory structure
kmatch98 3158d43
Added resize function for compatibility with grid_layout
kmatch98 8072bb4
Some pylint fixes, delete debug print statements
kmatch98 bc24846
Initial commit of WidgetAnnotation
kmatch98 14b2543
Add Dial widget, add example for Switch, Dial and WidgetAnnotation an…
kmatch98 90c443e
Initial commit with Widget, Control and WidgetLabel class definitions…
kmatch98 3d6d443
Ran black, somehow failed for widget_label.py
kmatch98 4e2cfec
Merge branch 'main' of https://github.com/kmatch98/Adafruit_CircuitPy…
kmatch98 7582f2e
Initial commit with Widget, Control and WidgetLabel class definitions…
kmatch98 4daff4d
Merge branch 'main' of https://github.com/kmatch98/Adafruit_CircuitPy…
kmatch98 b8de020
Bring up to date with latest main upstream branch
kmatch98 4245291
Update to include merged changes from upstream
kmatch98 57fa93b
Ran black on widgets
kmatch98 7e4e3a4
Add comments to switch_round, add simpletest switch examples
kmatch98 e3cd623
Add font option for switch_round WidgetLabel
kmatch98 7aa9902
Add flip_input widget
kmatch98 759248d
Add flip_input example
kmatch98 8c5c397
add font file
kmatch98 4f62512
Streamline animation for flip_input, adapt to script fonts
kmatch98 c050b9a
Add easing animations for flip_input
kmatch98 dac960d
Updated animation redraws to allow springy animations
kmatch98 a0649d6
Add sphinx docs
kmatch98 5fd70e8
remove unnecessary files for this PR
kmatch98 a23dab9
restore examples
kmatch98 b8e8df7
pylint fixes
kmatch98 6f426d8
end of line to api.rst
kmatch98 faf2588
Update Widget and Control class definitions, add documentation includ…
kmatch98 a2fdcca
Update Widget, Control definitions per review feedback, added documen…
kmatch98 4a6a2c9
add license file
kmatch98 acb0695
Add default values to Widget x,y
kmatch98 1a2ed2f
Minor doc tweak
kmatch98 18e9cf5
Minor doc tweak
kmatch98 312ccf7
Merge branch 'main_classes' of https://github.com/kmatch98/Adafruit_C…
kmatch98 db17f9e
Update api.rst
kmatch98 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.