Skip to content

Commit 47fd3cb

Browse files
authored
Eliminate pyright errors (#2062)
* Add Self annotation to gui.rect.move * Skip dot reference in gui.Rect.moveby unpacking * Add return annotations on gui.Rect.properties * Annotate gui.Rect.resize argument types * Add gui.Rect.resize docstring annotations * Document gui.Rect.size * Revert mention of pixels in gui.Rect.resize * Add docstrings for edge properties with mention of axis alignment * Use float values to make type checkers happy * Add arcade.types.AsFloat alias * Use AsFloat in gui.Rect.move signature * Annotate gui.Rect.collide_with_point * Add docstring for gui.Rect.collide_with_point * Expand docstring for gui.Rect.scale * Make gui.Rect.scale's rounding adjustable * use AsFloat in gui.Rect.align_right * Type hint gui.Rect.align_center * Use AsFloat in align_center_x and align_center_y * Type hint gui.Rect.min_size * Type hint gui.Rect.max_size * Skip dot access in gui.Rect.max_size * Add return annotation on gui.Rect.union * Add tests for gui.rect.Scale * Leave off Self magic for now * Fix Callable first argument for now * Make UIDropDown's do_layout None-aware * Remove unused import * Make LAYOUT_OFFSET a float to implicit type convert to make pyright happy * Fix copy and paste issue in rect unit tests * Add float conversion to make pyright happy * Convert to int when interfacing with IncrementalTextLayout * Round to int locally on UITextArea * Return floats from arcade.Text lrbt props
1 parent 2671261 commit 47fd3cb

File tree

6 files changed

+159
-49
lines changed

6 files changed

+159
-49
lines changed

arcade/gui/widgets/__init__.py

Lines changed: 104 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from abc import ABC
4+
from math import floor
45
from random import randint
56
from typing import (
67
NamedTuple,
@@ -12,6 +13,7 @@
1213
Tuple,
1314
List,
1415
Dict,
16+
Callable
1517
)
1618

1719
from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED
@@ -31,7 +33,7 @@
3133
from arcade.gui.nine_patch import NinePatchTexture
3234
from arcade.gui.property import Property, bind, ListProperty
3335
from arcade.gui.surface import Surface
34-
from arcade.types import RGBA255, Color
36+
from arcade.types import RGBA255, Color, Point, AsFloat
3537

3638
if TYPE_CHECKING:
3739
from arcade.gui.ui_manager import UIManager
@@ -52,66 +54,125 @@ class Rect(NamedTuple):
5254
width: float
5355
height: float
5456

55-
def move(self, dx: float = 0, dy: float = 0):
57+
def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> "Rect":
5658
"""Returns new Rect which is moved by dx and dy"""
57-
return Rect(self.x + dx, self.y + dy, self.width, self.height)
59+
x, y, width, height = self
60+
return Rect(x + dx, y + dy, width, height)
5861

59-
def collide_with_point(self, x, y):
62+
def collide_with_point(self, x: AsFloat, y: AsFloat) -> bool:
63+
"""Return true if ``x`` and ``y`` are within this rect.
64+
65+
This check is inclusive. Values on the :py:attr:`.left`,
66+
:py:attr:`.right`, :py:attr:`.top`, and :py:attr:`.bottom`
67+
edges will be counted as inside the rect.
68+
69+
.. code-block:: python
70+
71+
>>> bounds = Rect(0.0, 0.0, 5.0, 5.0)
72+
>>> bounds.collide_with_point(0.0, 0.0)
73+
True
74+
>>> bounds.collide_with_point(5.0, 5.0)
75+
True
76+
77+
:param x: The x value to check as inside the rect.
78+
:param y: The y value to check as inside the rect.
79+
"""
6080
left, bottom, width, height = self
6181
return left <= x <= left + width and bottom <= y <= bottom + height
6282

63-
def scale(self, scale: float) -> "Rect":
64-
"""Returns a new rect with scale applied"""
83+
def scale(
84+
self,
85+
scale: float,
86+
rounding: Optional[Callable[..., float]] = floor
87+
) -> "Rect":
88+
"""Return a new rect scaled relative to the origin.
89+
90+
By default, the new rect's values are rounded down to whole
91+
values. You can alter this by passing a different rounding
92+
behavior:
93+
94+
* Pass ``None`` to skip rounding
95+
* Pass a function which takes a number and returns a float
96+
to choose rounding behavior.
97+
98+
:param scale: A scale factor.
99+
:param rounding: ``None`` or a callable specifying how to
100+
round the scaled values.
101+
"""
102+
x, y, width, height = self
103+
if rounding is not None:
104+
return Rect(
105+
rounding(x * scale),
106+
rounding(y * scale),
107+
rounding(width * scale),
108+
rounding(height * scale),
109+
)
65110
return Rect(
66-
int(self.x * scale),
67-
int(self.y * scale),
68-
int(self.width * scale),
69-
int(self.height * scale),
111+
x * scale,
112+
y * scale,
113+
width * scale,
114+
height * scale,
70115
)
71116

72-
def resize(self, width=None, height=None):
73-
"""
74-
Returns a rect with changed width and height.
117+
def resize(
118+
self,
119+
width: float | None = None,
120+
height: float | None = None
121+
) -> "Rect":
122+
"""Return a rect with a new width or height but same lower left.
123+
75124
Fix x and y coordinate.
125+
:param width: A width for the new rectangle.
126+
:param height: A height for the new rectangle.
76127
"""
77128
width = width if width is not None else self.width
78129
height = height if height is not None else self.height
79130
return Rect(self.x, self.y, width, height)
80131

81132
@property
82-
def size(self):
133+
def size(self) -> Tuple[float, float]:
134+
"""Read-only pixel size of the rect.
135+
136+
Since these rects are immutable, use helper instance methods to
137+
get updated rects. For example, :py:meth:`.resize` may be what
138+
you're looking for.
139+
"""
83140
return self.width, self.height
84141

85142
@property
86-
def left(self):
143+
def left(self) -> float:
144+
"""The left edge on the X axis."""
87145
return self.x
88146

89147
@property
90-
def right(self):
148+
def right(self) -> float:
149+
"""The right edge on the X axis."""
91150
return self.x + self.width
92151

93152
@property
94-
def bottom(self):
153+
def bottom(self) -> float:
154+
"""The bottom edge on the Y axis."""
95155
return self.y
96156

97157
@property
98-
def top(self):
158+
def top(self) -> float:
159+
"""The top edge on the Y axis."""
99160
return self.y + self.height
100161

101162
@property
102-
def center_x(self):
163+
def center_x(self) -> float:
103164
return self.x + self.width / 2
104165

105166
@property
106-
def center_y(self):
167+
def center_y(self) -> float:
107168
return self.y + self.height / 2
108169

109170
@property
110-
def center(self):
171+
def center(self) -> Point:
111172
return self.center_x, self.center_y
112173

113174
@property
114-
def position(self):
175+
def position(self) -> Point:
115176
"""Bottom left coordinates"""
116177
return self.left, self.bottom
117178

@@ -130,28 +191,32 @@ def align_left(self, value: float) -> "Rect":
130191
diff_x = value - self.left
131192
return self.move(dx=diff_x)
132193

133-
def align_right(self, value: float) -> "Rect":
194+
def align_right(self, value: AsFloat) -> "Rect":
134195
"""Returns new Rect, which is aligned to the right"""
135196
diff_x = value - self.right
136197
return self.move(dx=diff_x)
137198

138-
def align_center(self, center_x, center_y):
199+
def align_center(self, center_x: AsFloat, center_y: AsFloat) -> "Rect":
139200
"""Returns new Rect, which is aligned to the center x and y"""
140201
diff_x = center_x - self.center_x
141202
diff_y = center_y - self.center_y
142203
return self.move(dx=diff_x, dy=diff_y)
143204

144-
def align_center_x(self, value: float) -> "Rect":
205+
def align_center_x(self, value: AsFloat) -> "Rect":
145206
"""Returns new Rect, which is aligned to the center_x"""
146207
diff_x = value - self.center_x
147208
return self.move(dx=diff_x)
148209

149-
def align_center_y(self, value: float) -> "Rect":
210+
def align_center_y(self, value: AsFloat) -> "Rect":
150211
"""Returns new Rect, which is aligned to the center_y"""
151212
diff_y = value - self.center_y
152213
return self.move(dy=diff_y)
153214

154-
def min_size(self, width=None, height=None):
215+
def min_size(
216+
self,
217+
width: Optional[AsFloat] = None,
218+
height: Optional[AsFloat] = None
219+
) -> "Rect":
155220
"""
156221
Sets the size to at least the given min values.
157222
"""
@@ -162,19 +227,23 @@ def min_size(self, width=None, height=None):
162227
max(height or 0.0, self.height),
163228
)
164229

165-
def max_size(self, width: Optional[float] = None, height: Optional[float] = None):
230+
def max_size(
231+
self,
232+
width: Optional[AsFloat] = None,
233+
height: Optional[AsFloat] = None
234+
) -> "Rect":
166235
"""
167236
Limits the size to the given max values.
168237
"""
169-
w, h = self.size
170-
if width:
171-
w = min(width, self.width)
172-
if height:
173-
h = min(height, self.height)
238+
x, y, w, h = self
239+
if width is not None:
240+
w = min(width, w)
241+
if height is not None:
242+
h = min(height, h)
174243

175-
return Rect(self.x, self.y, w, h)
244+
return Rect(x, y, w, h)
176245

177-
def union(self, rect: "Rect"):
246+
def union(self, rect: "Rect") -> "Rect":
178247
"""
179248
Returns a new Rect that is the union of this rect and another.
180249
The union is the smallest rectangle that contains theses two rectangles.

arcade/gui/widgets/dropdown.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,13 @@ def do_layout(self):
176176
self._default_button.rect = self.rect
177177

178178
# resize layout to contain widgets
179+
overlay = self._overlay
180+
rect = overlay.rect
181+
if overlay.size_hint_min is not None:
182+
rect = rect.resize(*overlay.size_hint_min)
183+
179184
self._overlay.rect = (
180-
self._overlay.rect.resize(*self._overlay.size_hint_min)
185+
rect
181186
.align_top(self.bottom - 2)
182187
.align_left(self._default_button.left)
183188
)

arcade/gui/widgets/text.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def __init__(
352352
)
353353

354354
self.layout = pyglet.text.layout.IncrementalTextLayout(
355-
self.doc, width - self.LAYOUT_OFFSET, height, multiline=multiline
355+
self.doc, int(width - self.LAYOUT_OFFSET), int(height), multiline=multiline
356356
)
357357
self.layout.x += self.LAYOUT_OFFSET
358358
self.caret = Caret(self.layout, color=Color.from_iterable(caret_color))
@@ -432,8 +432,8 @@ def _update_layout(self):
432432

433433
if layout_size != self.content_size:
434434
layout.begin_update()
435-
layout.width = self.content_width - self.LAYOUT_OFFSET
436-
layout.height = self.content_height
435+
layout.width = int(self.content_width - self.LAYOUT_OFFSET)
436+
layout.height = int(self.content_height)
437437
layout.end_update()
438438

439439
@property
@@ -523,8 +523,8 @@ def __init__(
523523

524524
self.layout = pyglet.text.layout.ScrollableTextLayout(
525525
self.doc,
526-
width=self.content_width,
527-
height=self.content_height,
526+
width=int(self.content_width),
527+
height=int(self.content_height),
528528
multiline=multiline,
529529
)
530530

@@ -553,12 +553,14 @@ def text(self, value):
553553
def _update_layout(self):
554554
# Update Pyglet layout size
555555
layout = self.layout
556-
layout_size = layout.width, layout.height
557556

558-
if layout_size != self.content_size:
557+
# Convert from local float coords to ints to avoid jitter
558+
# since pyglet imposes int-only coordinates as of pyglet 2.0
559+
content_width, content_height = map(int, self.content_size)
560+
if content_width != layout.width or content_height != layout.height:
559561
layout.begin_update()
560-
layout.width = self.content_width
561-
layout.height = self.content_height
562+
layout.width = content_width
563+
layout.height = content_height
562564
layout.end_update()
563565

564566
def do_render(self, surface: Surface):

arcade/text.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,28 +442,28 @@ def content_height(self) -> int:
442442
return self._label.content_height
443443

444444
@property
445-
def left(self) -> int:
445+
def left(self) -> float:
446446
"""
447447
Pixel location of the left content border.
448448
"""
449449
return self._label.left
450450

451451
@property
452-
def right(self) -> int:
452+
def right(self) -> float:
453453
"""
454454
Pixel location of the right content border.
455455
"""
456456
return self._label.right
457457

458458
@property
459-
def top(self) -> int:
459+
def top(self) -> float:
460460
"""
461461
Pixel location of the top content border.
462462
"""
463463
return self._label.top
464464

465465
@property
466-
def bottom(self) -> int:
466+
def bottom(self) -> float:
467467
"""
468468
Pixel location of the bottom content border.
469469
"""

arcade/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
if TYPE_CHECKING:
3535
from arcade.texture import Texture
3636

37+
38+
#: 1. Makes pyright happier while also telling readers
39+
#: 2. Tells readers we're converting any ints to floats
40+
AsFloat = Union[float, int]
41+
3742
MAX_UINT24 = 0xFFFFFF
3843
MAX_UINT32 = 0xFFFFFFFF
3944

tests/unit/gui/test_rect.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from math import ceil
2+
13
from arcade.gui.widgets import Rect
24

35

@@ -175,3 +177,30 @@ def test_collide_with_point():
175177
assert rect.collide_with_point(50, 50)
176178
assert rect.collide_with_point(100, 100)
177179
assert not rect.collide_with_point(150, 150)
180+
181+
182+
def test_rect_scale():
183+
rect = Rect(0, 0, 95, 99)
184+
185+
# Default rounding rounds down
186+
assert rect.scale(0.9) == (0,0, 85, 89)
187+
188+
# Passing in a rounding technique works too
189+
assert rect.scale(0.9, rounding=ceil) == (0, 0, 86, 90)
190+
191+
# Passing in None applies no rounding
192+
rect_100 = Rect(100,100,100,100)
193+
rect_100_scaled = rect_100.scale(0.1234, None)
194+
assert rect_100_scaled == (12.34, 12.34, 12.34, 12.34)
195+
assert rect_100_scaled.x == 12.34
196+
assert rect_100_scaled.y == 12.34
197+
assert rect_100_scaled.width == 12.34
198+
assert rect_100_scaled.height == 12.34
199+
200+
# Passing in None via rounding keyword applies no rounding
201+
rect_100_scaled = rect_100.scale(0.1234, rounding=None)
202+
assert rect_100_scaled == (12.34, 12.34, 12.34, 12.34)
203+
assert rect_100_scaled.x == 12.34
204+
assert rect_100_scaled.y == 12.34
205+
assert rect_100_scaled.width == 12.34
206+
assert rect_100_scaled.height == 12.34

0 commit comments

Comments
 (0)