Skip to content

Commit 77ede11

Browse files
authored
Merge pull request #5 from kmatch98/main_classes
Main classes: `Widget` and `Control`
2 parents c721e4d + db17f9e commit 77ede11

File tree

5 files changed

+406
-0
lines changed

5 files changed

+406
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# SPDX-FileCopyrightText: 2021 Kevin Matocha, Tim Cocks
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
`control`
7+
================================================================================
8+
CircuitPython GUI Control Class for touch-related elements
9+
10+
* Author(s): Kevin Matocha
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+
__version__ = "0.0.0-auto.0"
25+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git"
26+
27+
# pylint: disable=unsubscriptable-object, unnecessary-pass
28+
29+
30+
class Control:
31+
"""A Control class for responsive elements, including touch response functions for displays.
32+
33+
**IMPORTANT**: The *touch_point* for all functions should be in local coordinates
34+
for this item. That means, any widget should adjust the touchpoint for self.x and
35+
self.y before passing the touchpoint to this set of Control functions.
36+
37+
The Control class uses a state variable **touch_boundary** [x, y, width, height]
38+
that defines the rectangular boundary for touch inputs. The **touch_boundary**
39+
is used by the `contains` function to check when touches are within the Control's
40+
boundary. Note: These **touch_boundary** dimensions are in the Control's local
41+
pixel coordinates. The **x** and **y** values define the upper left corner of the
42+
**touch_boundary**. The **touch_boundary** value should be updated by the sublcass
43+
definiton.
44+
45+
"""
46+
47+
def __init__(
48+
self,
49+
):
50+
self.touch_boundary = (
51+
None # `self.touch_boundary` should be updated by the subclass
52+
)
53+
# Tuple of [x, y, width, height]: [int, int, int, int] all in pixel units
54+
# where x,y define the upper left corner
55+
# and width and height define the size of the `touch_boundary`
56+
57+
def contains(self, touch_point):
58+
"""Checks if the Control was touched. Returns True if the touch_point is within the
59+
Control's touch_boundary.
60+
61+
:param touch_point: x,y location of the screen, converted to local coordinates.
62+
:type touch_point: Tuple[x,y]
63+
:return: Boolean
64+
65+
"""
66+
67+
# Note: if a widget's `scale` property is > 1, be sure to update the
68+
# `touch_boundary` dimensions to accommodate the `scale` factor
69+
if (self.touch_boundary is not None) and (
70+
(
71+
self.touch_boundary[0]
72+
<= touch_point[0]
73+
<= (self.touch_boundary[0] + self.touch_boundary[2])
74+
)
75+
and (
76+
self.touch_boundary[1]
77+
<= touch_point[1]
78+
<= (self.touch_boundary[1] + self.touch_boundary[3])
79+
)
80+
):
81+
return True
82+
return False
83+
84+
# place holder touch_handler response functions
85+
def selected(self, touch_point):
86+
"""Response function when Control is selected. Should be overridden by subclass.
87+
88+
:param touch_point: x,y location of the screen, converted to local coordinates.
89+
:type touch_point: Tuple[x,y]
90+
:return: None
91+
92+
"""
93+
pass
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# SPDX-FileCopyrightText: 2021 Kevin Matocha, Tim Cocks
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
`widget`
7+
================================================================================
8+
9+
CircuitPython GUI Widget Class for visual elements
10+
11+
* Author(s): Kevin Matocha
12+
13+
Implementation Notes
14+
--------------------
15+
16+
**Hardware:**
17+
18+
**Software and Dependencies:**
19+
20+
* Adafruit CircuitPython firmware for the supported boards:
21+
https://github.com/adafruit/circuitpython/releases
22+
23+
"""
24+
25+
import displayio
26+
27+
__version__ = "0.0.0-auto.0"
28+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_Layout.git"
29+
30+
# pylint: disable=too-many-arguments
31+
32+
33+
class Widget(displayio.Group):
34+
"""
35+
A Widget class definition for graphical display elements. The Widget handles
36+
the sizing and positioning of the widget.
37+
38+
:param int x: pixel position
39+
:param int y: pixel position
40+
:param int width: width of the widget in pixels, set to None to auto-size relative to
41+
the height
42+
:param int height: height of the widget in pixels
43+
:param anchor_point: (X,Y) values from 0.0 to 1.0 to define the anchor point relative to the
44+
widget bounding box
45+
:type anchor_point: Tuple[float,float]
46+
:param int anchored_position: (x,y) pixel value for the location of the anchor_point
47+
:type anchored_position: Tuple[int, int]
48+
49+
50+
.. figure:: gui_layout_coordinates.png
51+
:scale: 50 %
52+
:alt: Diagram of layout coordinates
53+
54+
Diagram showing the global and local coordinate systems and the Widget's
55+
associated class variables.
56+
57+
**Widget Class Positioning: Display vs. Local Coordinates**
58+
59+
The Widget class is used to define the position and size of the graphical elements
60+
that define the widget.
61+
The Widget is a subclass of `displayio.Group` and inherits the positioning elements
62+
of `displayio.Group`, including *x*, *y* (in pixels). If the Widget is directly added
63+
to the display, then the *.x* and *.y* positions refer to the pixel position on
64+
the display. (Note: If the Widget is actually held within another Group, then the *.x*
65+
and *.y* of the widget are in that Group's local coordinate system.)
66+
67+
This Widget coordinate system is illustrated in the diagram above, showing the
68+
coordinate systems of a sliding switch widget. The display's origin (x=0, y=0)
69+
is at the upper left corner of the display. In this example the display size is
70+
320 x 240 pixels, so the display's bottom right corner is at display coordinates
71+
(x=320, y=240). The upper left corner of the widget is labeled notionally as
72+
*widget.x* and *widget.y* is set at the display pixel position of (x=100, y=50).
73+
74+
75+
**Local Coordinates: bounding_box**
76+
77+
Other parameters defined in the Widget class use a "local" coordinate system, as
78+
indicated by the red text in the display. These include `bounding_box` and
79+
*touch_boundary*. The `bounding_box` defines the origin of a Widget is at the upper
80+
left corner of the key graphical element of the widget and is set to (0,0) in
81+
widget local coordinates. The `width` and `height` of the `bounding_box` are
82+
defined as the pixel distances that make a mininum box that contains the key
83+
graphical elements of the widget. In the case of this example, the width is
84+
100 pixels and the height is 40 pixels. (Note: If a label is included for a
85+
widget, the label should not be included in the `bounding_box`.)
86+
87+
**Local Coordinates: touch_boundary (inherited from `Control` class)**
88+
This example of a sliding switch reacts to touch using the addition of
89+
inheritance from the `Control` class, so additional dimensional parameters are
90+
included for that class definition. Similar to the definition of
91+
the `bounding_box`, the *touch_boundary* is also defined using the widget's
92+
local coordinate system.
93+
94+
As shown in the diagram, we see that the *touch_boundary* is larger than the
95+
`bounding_box`. The *touch_boundary* should likely be larger than the
96+
`bounding_box` since finger touches are not precise. The use of additional
97+
space around the widget ensures that the widget reacts when the touch is close
98+
enough. In the case of this example, the switch widget provides a *touch_padding*
99+
option to define additional space around the `bounding_box` where touches are
100+
accepted (with the `Control.contains()` function). Looking at the example, we
101+
see that the upper left corner of the *touch_boundary* is (x=-10, y=-10) in widget
102+
local coordinates. This means that the accepted touch boundary starts at 10 pixels
103+
up and 10 pixels left of the upper left corner of the widget. The *touch_boundary*
104+
is 120 pixels wide and 60 pixels high. This confirms that a 10 pixel *touch_padding*
105+
was used, giving additional 10 pixels around the `bounding_box`. Note: If you are
106+
building your own new widgets, the *touch_boundary* tuple can be adjusted directly to
107+
meet whatever needs your widget needs. The *touch_boundary* is used in the
108+
`Control.contains()` function to determine when the Control-type widget was touched.
109+
110+
Note: If a widget does not need to respond to touches (for example a display of a
111+
value), then it should not inherit the `Control` class, and thus will not have a
112+
*touch_boundary*.
113+
114+
**Positioning on the screen: Using x and y or anchor_point and anchored_position**
115+
116+
The Widget class has several options for setting the widget position on the screen.
117+
In the simplest case, you can define the widget's *.x* and *.y* properties to set
118+
the position. (**Reminder**: If your widget is directly shown by the display using
119+
*display.show(my_widget)*), then the *.x* and *.y* positions will be in the display's
120+
coordinate system. But if your widget is held inside of another Group, then its
121+
coordinates will be in that Group's coordinate system.)
122+
123+
The Widget class definition also allows for relative positioning on the screen using
124+
the combination of `anchor_point` and `anchored_position`. This method is useful
125+
when you want your widget to be centered or aligned along one of its edges.
126+
127+
A good example of the use of `anchor_point` and `anchored_position` is in the
128+
`Adafruit "Candy Hearts" learn guide
129+
<https://learn.adafruit.com/circuit-python-tft-gizmo-candy-hearts/how-it-works>`_
130+
related to text positioning.
131+
132+
The `anchor_point` is a Tuple (float, float) that corresponds to the fractional
133+
position of the size of the widget. The upper left corner being
134+
`anchor_point` =(0.0, 0.0) and the lower right corner being `anchor_point` =(1.0, 1.0).
135+
The center of the widget is then `anchor_point` =(0.5, 0.5), halfway along the
136+
x-size and halfway along the y-size. One more example, the center of the bottom
137+
edge is (0.5, 1.0), halfway along the x-size and all the way of the y-size.
138+
139+
Once you define the `anchor_point`, you can now set the `anchored_position`. The
140+
`anchored_position` is the pixel dimension location where you want to put the
141+
`anchor_point`. To learn from example, let's say I want to place my widget so
142+
its bottom right corner is at the bottom right of my display (assume 320 x 240
143+
pixel size display).
144+
145+
First, I want to define the widget reference point to be the bottom right corner of
146+
my widget, so I'll set `anchor_point` =(1.0,1.0). Next, I want that anchor point
147+
on the widget to be placed at the bottom right corner of my display, so I'll set
148+
`anchored_position` =(320,240). In essence, the `anchor_point` is defining the
149+
reference ("anchor") point on the widget (but in relative widget-sized dimensions
150+
using x,y floats between 0.0 and 1.0) and then places that `anchor_point` at the
151+
pixel location specified as the `anchored_position` in pixel dimensions
152+
(x, y are in pixel units on the display).
153+
154+
The reason for using `anchor_point` is so that you
155+
don't need to know the width or height of the widget in advance, you can use
156+
`anchor_point` and it will always adjust for the widget's height and width to
157+
set the position at the `anchored_position` pixel position.
158+
159+
In summary:
160+
- `anchor_point` is x,y tuple (floats) of the relative size of the widget. Upper left
161+
corner is (0.0, 0.0) and lower right is (1.0, 1.0).
162+
- `anchored_position` is in x,y tuple (ints) pixel coordinates where the `anchor_point`
163+
will be placed.
164+
165+
"""
166+
167+
def __init__(
168+
self,
169+
x=0,
170+
y=0,
171+
scale=1,
172+
width=None,
173+
height=None,
174+
anchor_point=None,
175+
anchored_position=None,
176+
**kwargs,
177+
):
178+
179+
super().__init__(x=x, y=y, scale=scale, **kwargs)
180+
# send x,y and scale to Group
181+
# **kwargs should include `max_size`from the subclass implementation
182+
# to define how many graphical elements will be held in the Group that
183+
# makes up this widget
184+
#
185+
# If scale is set > 1, will need to update the Control `touch_boundary`
186+
# to accommodate the larger scale
187+
188+
self._width = width
189+
self._height = height
190+
self._anchor_point = anchor_point
191+
self._anchored_position = anchored_position
192+
193+
# self._bounding_box: pixel extent of the widget [x0, y0, width, height]
194+
# The bounding box should be updated based on the specifics of the widget
195+
if (width is not None) and (height is not None):
196+
self._bounding_box = [0, 0, width, height]
197+
else:
198+
self._bounding_box = [0, 0, 0, 0]
199+
200+
self._update_position()
201+
202+
def resize(self, new_width, new_height):
203+
"""Resizes the widget dimensions (for use with automated layout functions).
204+
205+
**IMPORTANT:** The `resize` function should be overridden by the subclass definition.
206+
207+
The width and height are provided together so the subclass `resize`
208+
function can apply any constraints that require consideration of both width
209+
and height (such as maintaining a Widget's preferred aspect ratio). The Widget should
210+
be resized to the maximum size that can fit within the dimensions defined by
211+
the requested *new_width* and *new_height*. After resizing, the Widget's
212+
`bounding_box` should also be updated.
213+
214+
:param int new_width: target maximum width (in pixels)
215+
:param int new_height: target maximum height (in pixels)
216+
:return: None
217+
218+
"""
219+
self._width = new_width
220+
self._height = new_height
221+
222+
self._bounding_box[2] = new_width
223+
self._bounding_box[3] = new_height
224+
225+
def _update_position(self):
226+
"""
227+
Widget class function for updating the widget's *x* and *y* position based
228+
upon the `anchor_point` and `anchored_position` values. The subclass should
229+
call `_update_position` after the widget is resized.
230+
231+
:return: None
232+
"""
233+
234+
if (self._anchor_point is not None) and (self._anchored_position is not None):
235+
self.x = (
236+
self._anchored_position[0]
237+
- int(self._anchor_point[0] * self._bounding_box[2])
238+
- self._bounding_box[0]
239+
)
240+
self.y = (
241+
self._anchored_position[1]
242+
- int(self._anchor_point[1] * self._bounding_box[3])
243+
- self._bounding_box[1]
244+
)
245+
246+
@property
247+
def width(self):
248+
"""The widget width, in pixels. (getter only)
249+
250+
:return: int
251+
"""
252+
return self._width
253+
254+
@property
255+
def height(self):
256+
"""The widget height, in pixels. (getter only)
257+
258+
:return: int
259+
"""
260+
return self._height
261+
262+
@property
263+
def bounding_box(self):
264+
"""The boundary of the widget. [x, y, width, height] in Widget's local
265+
coordinates (in pixels). (getter only)
266+
267+
:return: Tuple[int, int, int, int]"""
268+
return self._bounding_box
269+
270+
@property
271+
def anchor_point(self):
272+
"""The anchor point for positioning the widget, works in concert
273+
with `anchored_position` The relative (X,Y) position of the widget where the
274+
anchored_position is placed. For example (0.0, 0.0) is the Widget's upper left corner,
275+
(0.5, 0.5) is the Widget's center point, and (1.0, 1.0) is the Widget's lower right corner.
276+
277+
:param anchor_point: In relative units of the Widget size.
278+
:type anchor_point: Tuple[float, float]"""
279+
return self._anchor_point
280+
281+
@anchor_point.setter
282+
def anchor_point(self, new_anchor_point):
283+
self._anchor_point = new_anchor_point
284+
self._update_position()
285+
286+
@property
287+
def anchored_position(self):
288+
"""The anchored position (in pixels) for positioning the widget, works in concert
289+
with `anchor_point`. The `anchored_position` is the x,y pixel position
290+
for the placement of the Widget's `anchor_point`.
291+
292+
:param anchored_position: The (x,y) pixel position for the anchored_position (in pixels).
293+
:type anchored_position: Tuple[int, int]
294+
295+
"""
296+
return self._anchored_position
297+
298+
@anchored_position.setter
299+
def anchored_position(self, new_anchored_position):
300+
self._anchored_position = new_anchored_position
301+
self._update_position()

0 commit comments

Comments
 (0)