|
| 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