Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d7092f8
Add Self annotation to gui.rect.move
pushfoo Apr 13, 2024
04bf83a
Skip dot reference in gui.Rect.moveby unpacking
pushfoo Apr 13, 2024
8ae84b2
Add return annotations on gui.Rect.properties
pushfoo Apr 13, 2024
5991832
Annotate gui.Rect.resize argument types
pushfoo Apr 13, 2024
4bc9d55
Add gui.Rect.resize docstring annotations
pushfoo Apr 13, 2024
e7dade9
Document gui.Rect.size
pushfoo Apr 13, 2024
6555bac
Revert mention of pixels in gui.Rect.resize
pushfoo Apr 13, 2024
44aeae7
Add docstrings for edge properties with mention of axis alignment
pushfoo Apr 13, 2024
234d968
Use float values to make type checkers happy
pushfoo Apr 13, 2024
148d90e
Add arcade.types.AsFloat alias
pushfoo Apr 13, 2024
ad97d08
Use AsFloat in gui.Rect.move signature
pushfoo Apr 13, 2024
6b180a4
Annotate gui.Rect.collide_with_point
pushfoo Apr 13, 2024
f4f0f2d
Add docstring for gui.Rect.collide_with_point
pushfoo Apr 13, 2024
dedc2f8
Expand docstring for gui.Rect.scale
pushfoo Apr 13, 2024
2f3038b
Make gui.Rect.scale's rounding adjustable
pushfoo Apr 13, 2024
3286ff2
use AsFloat in gui.Rect.align_right
pushfoo Apr 13, 2024
28eea58
Type hint gui.Rect.align_center
pushfoo Apr 13, 2024
c780755
Use AsFloat in align_center_x and align_center_y
pushfoo Apr 13, 2024
87b7d7f
Type hint gui.Rect.min_size
pushfoo Apr 13, 2024
2cd24b3
Type hint gui.Rect.max_size
pushfoo Apr 13, 2024
325d5fc
Skip dot access in gui.Rect.max_size
pushfoo Apr 13, 2024
d7cc800
Add return annotation on gui.Rect.union
pushfoo Apr 13, 2024
9381981
Add tests for gui.rect.Scale
pushfoo Apr 13, 2024
81bb266
Leave off Self magic for now
pushfoo Apr 13, 2024
e2670b1
Fix Callable first argument for now
pushfoo Apr 13, 2024
573b39b
Make UIDropDown's do_layout None-aware
pushfoo Apr 13, 2024
23d4431
Remove unused import
pushfoo Apr 13, 2024
14dfe26
Make LAYOUT_OFFSET a float to implicit type convert to make pyright h…
pushfoo Apr 13, 2024
293b78c
Fix copy and paste issue in rect unit tests
pushfoo Apr 13, 2024
991d6cf
Add float conversion to make pyright happy
pushfoo Apr 13, 2024
44b527a
Convert to int when interfacing with IncrementalTextLayout
pushfoo Apr 13, 2024
4a9c130
Round to int locally on UITextArea
pushfoo Apr 13, 2024
7871104
Return floats from arcade.Text lrbt props
pushfoo Apr 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 104 additions & 35 deletions arcade/gui/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from abc import ABC
from math import floor
from random import randint
from typing import (
NamedTuple,
Expand All @@ -12,6 +13,7 @@
Tuple,
List,
Dict,
Callable
)

from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED
Expand All @@ -31,7 +33,7 @@
from arcade.gui.nine_patch import NinePatchTexture
from arcade.gui.property import Property, bind, ListProperty
from arcade.gui.surface import Surface
from arcade.types import RGBA255, Color
from arcade.types import RGBA255, Color, Point, AsFloat

if TYPE_CHECKING:
from arcade.gui.ui_manager import UIManager
Expand All @@ -52,66 +54,125 @@ class Rect(NamedTuple):
width: float
height: float

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

def collide_with_point(self, x, y):
def collide_with_point(self, x: AsFloat, y: AsFloat) -> bool:
"""Return true if ``x`` and ``y`` are within this rect.

This check is inclusive. Values on the :py:attr:`.left`,
:py:attr:`.right`, :py:attr:`.top`, and :py:attr:`.bottom`
edges will be counted as inside the rect.

.. code-block:: python

>>> bounds = Rect(0.0, 0.0, 5.0, 5.0)
>>> bounds.collide_with_point(0.0, 0.0)
True
>>> bounds.collide_with_point(5.0, 5.0)
True

:param x: The x value to check as inside the rect.
:param y: The y value to check as inside the rect.
"""
left, bottom, width, height = self
return left <= x <= left + width and bottom <= y <= bottom + height

def scale(self, scale: float) -> "Rect":
"""Returns a new rect with scale applied"""
def scale(
self,
scale: float,
rounding: Optional[Callable[..., float]] = floor
) -> "Rect":
"""Return a new rect scaled relative to the origin.

By default, the new rect's values are rounded down to whole
values. You can alter this by passing a different rounding
behavior:

* Pass ``None`` to skip rounding
* Pass a function which takes a number and returns a float
to choose rounding behavior.

:param scale: A scale factor.
:param rounding: ``None`` or a callable specifying how to
round the scaled values.
"""
x, y, width, height = self
if rounding is not None:
return Rect(
rounding(x * scale),
rounding(y * scale),
rounding(width * scale),
rounding(height * scale),
)
return Rect(
int(self.x * scale),
int(self.y * scale),
int(self.width * scale),
int(self.height * scale),
x * scale,
y * scale,
width * scale,
height * scale,
)

def resize(self, width=None, height=None):
"""
Returns a rect with changed width and height.
def resize(
self,
width: float | None = None,
height: float | None = None
) -> "Rect":
"""Return a rect with a new width or height but same lower left.

Fix x and y coordinate.
:param width: A width for the new rectangle.
:param height: A height for the new rectangle.
"""
width = width if width is not None else self.width
height = height if height is not None else self.height
return Rect(self.x, self.y, width, height)

@property
def size(self):
def size(self) -> Tuple[float, float]:
"""Read-only pixel size of the rect.

Since these rects are immutable, use helper instance methods to
get updated rects. For example, :py:meth:`.resize` may be what
you're looking for.
"""
return self.width, self.height

@property
def left(self):
def left(self) -> float:
"""The left edge on the X axis."""
return self.x

@property
def right(self):
def right(self) -> float:
"""The right edge on the X axis."""
return self.x + self.width

@property
def bottom(self):
def bottom(self) -> float:
"""The bottom edge on the Y axis."""
return self.y

@property
def top(self):
def top(self) -> float:
"""The top edge on the Y axis."""
return self.y + self.height

@property
def center_x(self):
def center_x(self) -> float:
return self.x + self.width / 2

@property
def center_y(self):
def center_y(self) -> float:
return self.y + self.height / 2

@property
def center(self):
def center(self) -> Point:
return self.center_x, self.center_y

@property
def position(self):
def position(self) -> Point:
"""Bottom left coordinates"""
return self.left, self.bottom

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

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

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

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

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

def min_size(self, width=None, height=None):
def min_size(
self,
width: Optional[AsFloat] = None,
height: Optional[AsFloat] = None
) -> "Rect":
"""
Sets the size to at least the given min values.
"""
Expand All @@ -162,19 +227,23 @@ def min_size(self, width=None, height=None):
max(height or 0.0, self.height),
)

def max_size(self, width: Optional[float] = None, height: Optional[float] = None):
def max_size(
self,
width: Optional[AsFloat] = None,
height: Optional[AsFloat] = None
) -> "Rect":
"""
Limits the size to the given max values.
"""
w, h = self.size
if width:
w = min(width, self.width)
if height:
h = min(height, self.height)
x, y, w, h = self
if width is not None:
w = min(width, w)
if height is not None:
h = min(height, h)

return Rect(self.x, self.y, w, h)
return Rect(x, y, w, h)

def union(self, rect: "Rect"):
def union(self, rect: "Rect") -> "Rect":
"""
Returns a new Rect that is the union of this rect and another.
The union is the smallest rectangle that contains theses two rectangles.
Expand Down
7 changes: 6 additions & 1 deletion arcade/gui/widgets/dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,13 @@ def do_layout(self):
self._default_button.rect = self.rect

# resize layout to contain widgets
overlay = self._overlay
rect = overlay.rect
if overlay.size_hint_min is not None:
rect = rect.resize(*overlay.size_hint_min)

self._overlay.rect = (
self._overlay.rect.resize(*self._overlay.size_hint_min)
rect
.align_top(self.bottom - 2)
.align_left(self._default_button.left)
)
Expand Down
20 changes: 11 additions & 9 deletions arcade/gui/widgets/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def __init__(
)

self.layout = pyglet.text.layout.IncrementalTextLayout(
self.doc, width - self.LAYOUT_OFFSET, height, multiline=multiline
self.doc, int(width - self.LAYOUT_OFFSET), int(height), multiline=multiline
)
self.layout.x += self.LAYOUT_OFFSET
self.caret = Caret(self.layout, color=Color.from_iterable(caret_color))
Expand Down Expand Up @@ -431,8 +431,8 @@ def _update_layout(self):

if layout_size != self.content_size:
layout.begin_update()
layout.width = self.content_width - self.LAYOUT_OFFSET
layout.height = self.content_height
layout.width = int(self.content_width - self.LAYOUT_OFFSET)
layout.height = int(self.content_height)
layout.end_update()

@property
Expand Down Expand Up @@ -522,8 +522,8 @@ def __init__(

self.layout = pyglet.text.layout.ScrollableTextLayout(
self.doc,
width=self.content_width,
height=self.content_height,
width=int(self.content_width),
height=int(self.content_height),
multiline=multiline,
)

Expand Down Expand Up @@ -552,12 +552,14 @@ def text(self, value):
def _update_layout(self):
# Update Pyglet layout size
layout = self.layout
layout_size = layout.width, layout.height

if layout_size != self.content_size:
# Convert from local float coords to ints to avoid jitter
# since pyglet imposes int-only coordinates as of pyglet 2.0
content_width, content_height = map(int, self.content_size)
if content_width != layout.width or content_height != layout.height:
layout.begin_update()
layout.width = self.content_width
layout.height = self.content_height
layout.width = content_width
layout.height = content_height
layout.end_update()

def do_render(self, surface: Surface):
Expand Down
8 changes: 4 additions & 4 deletions arcade/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,28 +442,28 @@ def content_height(self) -> int:
return self._label.content_height

@property
def left(self) -> int:
def left(self) -> float:
"""
Pixel location of the left content border.
"""
return self._label.left

@property
def right(self) -> int:
def right(self) -> float:
"""
Pixel location of the right content border.
"""
return self._label.right

@property
def top(self) -> int:
def top(self) -> float:
"""
Pixel location of the top content border.
"""
return self._label.top

@property
def bottom(self) -> int:
def bottom(self) -> float:
"""
Pixel location of the bottom content border.
"""
Expand Down
5 changes: 5 additions & 0 deletions arcade/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
if TYPE_CHECKING:
from arcade.texture import Texture


#: 1. Makes pyright happier while also telling readers
#: 2. Tells readers we're converting any ints to floats
AsFloat = Union[float, int]

MAX_UINT24 = 0xFFFFFF
MAX_UINT32 = 0xFFFFFFFF

Expand Down
29 changes: 29 additions & 0 deletions tests/unit/gui/test_rect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from math import ceil

from arcade.gui.widgets import Rect


Expand Down Expand Up @@ -175,3 +177,30 @@ def test_collide_with_point():
assert rect.collide_with_point(50, 50)
assert rect.collide_with_point(100, 100)
assert not rect.collide_with_point(150, 150)


def test_rect_scale():
rect = Rect(0, 0, 95, 99)

# Default rounding rounds down
assert rect.scale(0.9) == (0,0, 85, 89)

# Passing in a rounding technique works too
assert rect.scale(0.9, rounding=ceil) == (0, 0, 86, 90)

# Passing in None applies no rounding
rect_100 = Rect(100,100,100,100)
rect_100_scaled = rect_100.scale(0.1234, None)
assert rect_100_scaled == (12.34, 12.34, 12.34, 12.34)
assert rect_100_scaled.x == 12.34
assert rect_100_scaled.y == 12.34
assert rect_100_scaled.width == 12.34
assert rect_100_scaled.height == 12.34

# Passing in None via rounding keyword applies no rounding
rect_100_scaled = rect_100.scale(0.1234, rounding=None)
assert rect_100_scaled == (12.34, 12.34, 12.34, 12.34)
assert rect_100_scaled.x == 12.34
assert rect_100_scaled.y == 12.34
assert rect_100_scaled.width == 12.34
assert rect_100_scaled.height == 12.34