-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathlabel.py
executable file
·418 lines (351 loc) · 16.4 KB
/
label.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_display_text.label`
====================================================
Displays text labels using CircuitPython's displayio.
* Author(s): Scott Shawcroft
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
"""
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
from displayio import Bitmap, Palette, TileGrid
from adafruit_display_text import LabelBase
try:
from typing import Optional, Tuple
from fontio import FontProtocol
except ImportError:
pass
class Label(LabelBase):
"""A label displaying a string of text. The origin point set by ``x`` and ``y``
properties will be the left edge of the bounding box, and in the center of a M
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
it will try to have it be center-left as close as possible.
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
Must include a capital M for measuring character size.
:type font: ~fontio.FontProtocol
:param str text: Text to display
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
for transparent
:param float line_spacing: Line spacing of text to display
:param bool background_tight: Set `True` only if you want background box to tightly
surround text. When set to 'True' Padding parameters will be ignored.
:param int padding_top: Additional pixels added to background bounding box at top.
This parameter could be negative indicating additional pixels subtracted from the
background bounding box.
:param int padding_bottom: Additional pixels added to background bounding box at bottom.
This parameter could be negative indicating additional pixels subtracted from the
background bounding box.
:param int padding_left: Additional pixels added to background bounding box at left.
This parameter could be negative indicating additional pixels subtracted from the
background bounding box.
:param int padding_right: Additional pixels added to background bounding box at right.
This parameter could be negative indicating additional pixels subtracted from the
background bounding box.
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
Tuple with decimal percentage of width and height.
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
containing x,y pixel coordinates.
:param int scale: Integer value of the pixel scaling
:param bool base_alignment: when True allows to align text label to the baseline.
This is helpful when two or more labels need to be aligned to the same baseline
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
tab character
:param str label_direction: string defining the label text orientation. There are 5
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
``TTB``-Top-To-Bottom ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``"""
def __init__(self, font: FontProtocol, **kwargs) -> None:
self._background_palette = Palette(1)
self._added_background_tilegrid = False
super().__init__(font, **kwargs)
text = self._replace_tabs(self._text)
self._width = len(text)
self._height = self._font.get_bounding_box()[1]
# Create the two-color text palette
self._palette[0] = 0
self._palette.make_transparent(0)
if text is not None:
self._reset_text(str(text))
def _create_background_box(self, lines: int, y_offset: int) -> TileGrid:
"""Private Class function to create a background_box
:param lines: int number of lines
:param y_offset: int y pixel bottom coordinate for the background_box"""
left = self._bounding_box[0]
if self._background_tight: # draw a tight bounding box
box_width = self._bounding_box[2]
box_height = self._bounding_box[3]
x_box_offset = 0
y_box_offset = self._bounding_box[1]
else: # draw a "loose" bounding box to include any ascenders/descenders.
ascent, descent = self._ascent, self._descent
if self._label_direction in {"DWR", "UPR"}:
box_height = self._bounding_box[3] + self._padding_right + self._padding_left
x_box_offset = -self._padding_left
box_width = (
(ascent + descent)
+ int((lines - 1) * self._width * self._line_spacing)
+ self._padding_top
+ self._padding_bottom
)
elif self._label_direction == "TTB":
box_height = self._bounding_box[3] + self._padding_top + self._padding_bottom
x_box_offset = -self._padding_left
box_width = (
(ascent + descent)
+ int((lines - 1) * self._height * self._line_spacing)
+ self._padding_right
+ self._padding_left
)
else:
box_width = self._bounding_box[2] + self._padding_left + self._padding_right
x_box_offset = -self._padding_left
box_height = (
(ascent + descent)
+ int((lines - 1) * self._height * self._line_spacing)
+ self._padding_top
+ self._padding_bottom
)
if self._label_direction == "DWR":
padding_to_use = self._padding_bottom
elif self._label_direction == "TTB":
padding_to_use = self._padding_top
y_offset = 0
ascent = 0
else:
padding_to_use = self._padding_top
if self._base_alignment:
y_box_offset = -ascent - padding_to_use
else:
y_box_offset = -ascent + y_offset - padding_to_use
box_width = max(0, box_width) # remove any negative values
box_height = max(0, box_height) # remove any negative values
if self._label_direction == "UPR":
movx = y_box_offset
movy = -box_height - x_box_offset
elif self._label_direction == "DWR":
movx = y_box_offset
movy = x_box_offset
elif self._label_direction == "TTB":
movx = x_box_offset
movy = y_box_offset
else:
movx = left + x_box_offset
movy = y_box_offset
background_bitmap = Bitmap(box_width, box_height, 1)
tile_grid = TileGrid(
background_bitmap,
pixel_shader=self._background_palette,
x=movx,
y=movy,
)
return tile_grid
def _set_background_color(self, new_color: Optional[int]) -> None:
"""Private class function that allows updating the font box background color
:param int new_color: Color as an RGB hex number, setting to None makes it transparent
"""
if new_color is None:
self._background_palette.make_transparent(0)
if self._added_background_tilegrid:
self._local_group.pop(0)
self._added_background_tilegrid = False
else:
self._background_palette.make_opaque(0)
self._background_palette[0] = new_color
self._background_color = new_color
lines = self._text.rstrip("\n").count("\n") + 1
y_offset = self._ascent // 2
if self._bounding_box is None:
# Still in initialization
return
if not self._added_background_tilegrid: # no bitmap is in the self Group
# add bitmap if text is present and bitmap sizes > 0 pixels
if (
(len(self._text) > 0)
and (self._bounding_box[2] + self._padding_left + self._padding_right > 0)
and (self._bounding_box[3] + self._padding_top + self._padding_bottom > 0)
):
self._local_group.insert(0, self._create_background_box(lines, y_offset))
self._added_background_tilegrid = True
elif (
(len(self._text) > 0)
and (self._bounding_box[2] + self._padding_left + self._padding_right > 0)
and (self._bounding_box[3] + self._padding_top + self._padding_bottom > 0)
):
self._local_group[0] = self._create_background_box(lines, self._y_offset)
else: # delete the existing bitmap
self._local_group.pop(0)
self._added_background_tilegrid = False
def _update_text(self, new_text: str) -> None:
x = 0
y = 0
if self._added_background_tilegrid:
i = 1
else:
i = 0
tilegrid_count = i
if self._base_alignment:
self._y_offset = 0
else:
self._y_offset = self._ascent // 2
if self._label_direction == "RTL":
left = top = bottom = 0
right = None
elif self._label_direction == "LTR":
right = top = bottom = 0
left = None
else:
top = right = left = 0
bottom = 0
for character in new_text:
if character == "\n":
y += int(self._height * self._line_spacing)
x = 0
continue
glyph = self._font.get_glyph(ord(character))
if not glyph:
continue
position_x, position_y = 0, 0
if self._label_direction in {"LTR", "RTL"}:
bottom = max(bottom, y - glyph.dy + self._y_offset)
if y == 0: # first line, find the Ascender height
top = min(top, -glyph.height - glyph.dy + self._y_offset)
position_y = y - glyph.height - glyph.dy + self._y_offset
if self._label_direction == "LTR":
right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx)
if x == 0:
if left is None:
left = 0
else:
left = min(left, glyph.dx)
position_x = x + glyph.dx
else:
left = max(left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx)
if x == 0:
if right is None:
right = 0
else:
right = max(right, glyph.dx)
position_x = x - glyph.width
elif self._label_direction == "TTB":
if x == 0:
if left is None:
left = 0
else:
left = min(left, glyph.dx)
if y == 0:
top = min(top, -glyph.dy)
bottom = max(bottom, y + glyph.height, y + glyph.height + glyph.dy)
right = max(right, x + glyph.width + glyph.dx, x + glyph.shift_x + glyph.dx)
position_y = y + glyph.dy
position_x = x - glyph.width // 2 + self._y_offset
elif self._label_direction == "UPR":
if x == 0:
if bottom is None:
bottom = -glyph.dx
if y == 0: # first line, find the Ascender height
bottom = min(bottom, -glyph.dy)
left = min(left, x - glyph.height + self._y_offset)
top = min(top, y - glyph.width - glyph.dx, y - glyph.shift_x)
right = max(right, x + glyph.height, x + glyph.height - glyph.dy)
position_y = y - glyph.width - glyph.dx
position_x = x - glyph.height - glyph.dy + self._y_offset
elif self._label_direction == "DWR":
if y == 0:
if top is None:
top = -glyph.dx
top = min(top, -glyph.dx)
if x == 0:
left = min(left, -glyph.dy)
left = min(left, x, x - glyph.dy - self._y_offset)
bottom = max(bottom, y + glyph.width + glyph.dx, y + glyph.shift_x)
right = max(right, x + glyph.height)
position_y = y + glyph.dx
position_x = x + glyph.dy - self._y_offset
if glyph.width > 0 and glyph.height > 0:
face = TileGrid(
glyph.bitmap,
pixel_shader=self._palette,
default_tile=glyph.tile_index,
tile_width=glyph.width,
tile_height=glyph.height,
x=position_x,
y=position_y,
)
if self._label_direction == "UPR":
face.transpose_xy = True
face.flip_x = True
if self._label_direction == "DWR":
face.transpose_xy = True
face.flip_y = True
if tilegrid_count < len(self._local_group):
self._local_group[tilegrid_count] = face
else:
self._local_group.append(face)
tilegrid_count += 1
if self._label_direction == "RTL":
x = x - glyph.shift_x
if self._label_direction == "TTB":
if glyph.height < 2:
y = y + glyph.shift_x
else:
y = y + glyph.height + 1
if self._label_direction == "UPR":
y = y - glyph.shift_x
if self._label_direction == "DWR":
y = y + glyph.shift_x
if self._label_direction == "LTR":
x = x + glyph.shift_x
i += 1
if self._label_direction == "LTR" and left is None:
left = 0
if self._label_direction == "RTL" and right is None:
right = 0
if self._label_direction == "TTB" and top is None:
top = 0
while len(self._local_group) > tilegrid_count: # i:
self._local_group.pop()
if self._label_direction == "RTL":
# type-checkers think left can be None
self._bounding_box = (-left, top, left - right, bottom - top)
if self._label_direction == "TTB":
self._bounding_box = (left, top, right - left, bottom - top)
if self._label_direction == "UPR":
self._bounding_box = (left, top, right, bottom - top)
if self._label_direction == "DWR":
self._bounding_box = (left, top, right, bottom - top)
if self._label_direction == "LTR":
self._bounding_box = (left, top, right - left, bottom - top)
self._text = new_text
if self._background_color is not None:
self._set_background_color(self._background_color)
def _reset_text(self, new_text: str) -> None:
current_anchored_position = self.anchored_position
self._update_text(str(self._replace_tabs(new_text)))
self.anchored_position = current_anchored_position
def _set_font(self, new_font: FontProtocol) -> None:
old_text = self._text
current_anchored_position = self.anchored_position
self._text = ""
self._font = new_font
self._height = self._font.get_bounding_box()[1]
self._update_text(str(old_text))
self.anchored_position = current_anchored_position
def _set_line_spacing(self, new_line_spacing: float) -> None:
self._line_spacing = new_line_spacing
self.text = self._text # redraw the box
def _set_text(self, new_text: str, scale: int) -> None:
self._reset_text(new_text)
def _set_label_direction(self, new_label_direction: str) -> None:
self._label_direction = new_label_direction
self._update_text(str(self._text))
def _get_valid_label_directions(self) -> Tuple[str, ...]:
return "LTR", "RTL", "UPR", "DWR", "TTB"