Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Text sprites went blank when the texture atlas rebuilt itself, which happens when it fills up after some textures were freed. The atlas redraws textures from their images, and a text sprite's image was empty. `sprite.texture.image` now also contains the text.
- Fixed an `AttributeError` being printed as "Exception ignored in `__del__`" when creating an OpenGL texture, framebuffer, buffer, program, compute shader, texture array, or vertex array failed, for example with an invalid number of texture components.
- Fixed `arcade.draw_text()` cache problems: a multiline call and an otherwise equal single-line call shared one label and drew wrong, and animating `rotation` or `font_size` added a cached label every frame, forever. The cache now holds at most 256 labels, forgetting the least recently used.
- Fixed `arcade.Text` style properties:
- Setting `italic` after creating the text had no visible effect. `"oblique"` is now also accepted.
- `bold` strings such as `"light"` or `"semibold"` were all treated as bold, including `"normal"`. They now set that font weight, and the `bold` property returns the weight name for weights other than bold and normal.
- A `pyglet.text.LinearGradient` color raised `TypeError` in `Text`, `draw_text()`, `create_text_sprite()`, and `UILabel.update_font()`. Gradients now work, and the `color` property returns the gradient.
- The `Text` constructor ignored every error while creating the label, so bad arguments such as an unknown keyword or an invalid `font_name` only raised later, the first time the text was used. Only the "no window yet" case is deferred now.

### New Features
- Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check.
Expand Down
24 changes: 14 additions & 10 deletions arcade/gui/widgets/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pyglet
from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED
from pyglet.text.caret import Caret
from pyglet.text import LinearGradient
from pyglet.text.document import AbstractDocument
from typing_extensions import override

Expand All @@ -30,7 +31,7 @@
from arcade.gui.surface import Surface
from arcade.gui.widgets import UIInteractiveWidget, UIWidget
from arcade.gui.widgets.layout import UIAnchorLayout
from arcade.text import FontNameOrNames
from arcade.text import FontNameOrNames, _to_text_color
from arcade.types import LBWH, RGBA255, Color, RGBOrA255


Expand Down Expand Up @@ -216,7 +217,7 @@ def font_size(self) -> float:
return self._label.font_size

@property
def font_color(self) -> Color:
def font_color(self) -> Color | LinearGradient:
"""Font color of the label. Use :py:meth:`~arcade.gui.UILabel.update_font` to change."""
return self._label.color

Expand Down Expand Up @@ -262,9 +263,9 @@ def update_font(
self,
font_name: FontNameOrNames | None = None,
font_size: float | None = None,
font_color: Color | None = None,
font_color: RGBOrA255 | LinearGradient | None = None,
bold: bool | str | None = None,
italic: bool | None = None,
italic: bool | str | None = None,
):
"""Update font of the label.

Expand All @@ -273,20 +274,23 @@ def update_font(
beginning of the tuple and keep trying to load fonts until
success.
font_size: Font size of font.
font_color: Color of the text.
bold: May be any value in :py:obj:`pyglet.text.Weight`,
font_color: Color of the text, or a
:py:class:`pyglet.text.LinearGradient`.
bold: May be any value in :py:class:`pyglet.enums.Weight`,
``True`` (converts to ``"bold"``), or ``False``
(converts to ``"regular"``).
(converts to ``"normal"``).
italic: If enabled, the label's text will be in an *italic*
style. May also be ``"oblique"``.
"""
font_name = font_name or self._requested_font_name
font_size = font_size or self._label.font_size
font_color = font_color or self._label.color
if font_color is None:
font_color = self._label.color
font_bold = bold if bold is not None else self._label.bold
font_italic = italic if italic is not None else self._label.italic

# ensure type of font_color, label will allways be a color
font_color = Color.from_iterable(font_color)
# The label holds a Color or a gradient, so compare with the same type
font_color = _to_text_color(font_color)

# Check if values actually changed, if then update and trigger render.
# The label holds the resolved font name (e.g. "arial" for
Expand Down
133 changes: 101 additions & 32 deletions arcade/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import math
from enum import Enum
from pathlib import Path
from typing import Any
from uuid import uuid4
Expand All @@ -13,9 +14,10 @@
# Pyright can't figure out the dynamic backend imports in pyglet.graphics
# right now. Maybe can fix in future Pyglet version
from pyglet.graphics import Batch, Group # type: ignore
from pyglet.text import LinearGradient

import arcade
from arcade.exceptions import PerformanceWarning, warning
from arcade.exceptions import NoArcadeWindowError, PerformanceWarning, warning
from arcade.resources import resolve
from arcade.texture_atlas import TextureAtlasBase
from arcade.types import Color, Point, RGBOrA255
Expand Down Expand Up @@ -108,6 +110,49 @@ def _draw_pyglet_label(label: pyglet.text.Label) -> None:
label.draw()


def _to_text_color(color: RGBOrA255 | LinearGradient) -> Color | LinearGradient:
"""Convert a text color to a Color, passing gradients through unchanged."""
if isinstance(color, LinearGradient):
return color
return Color.from_iterable(color)


def _to_weight(bold: bool | str) -> str:
"""Convert a ``bold`` value to a pyglet font weight name."""
if isinstance(bold, Enum):
return bold.value
if isinstance(bold, str) and bold:
return bold
return (Weight.BOLD if bold else Weight.NORMAL).value


def _from_weight(weight: Any) -> bool | str:
"""Convert a pyglet font weight back to a ``bold`` value."""
if weight == Weight.BOLD.value:
return True
if weight is None or weight == Weight.NORMAL.value:
return False
return str(weight)


def _to_style(italic: bool | str) -> str:
"""Convert an ``italic`` value to a pyglet font style name."""
if isinstance(italic, Enum):
return italic.value
if isinstance(italic, str) and italic:
return italic
return (Style.ITALIC if italic else Style.NORMAL).value


def _from_style(style: Any) -> bool | str:
"""Convert a pyglet font style back to an ``italic`` value."""
if style == Style.ITALIC.value:
return True
if style is None or style == Style.NORMAL.value:
return False
return str(style)


class Text:
"""
An object-oriented way to draw text to the screen.
Expand Down Expand Up @@ -155,7 +200,8 @@ class Text:
y: y position to align the text's anchor point with
z: z position to align the text's anchor point with
color: Color of the text as an RGBA tuple or a
:py:class:`~arcade.types.Color` instance.
:py:class:`~arcade.types.Color` instance, or a
:py:class:`pyglet.text.LinearGradient` for a left-to-right gradient.
font_size: Size of the text in points
width: A width limit in pixels
align: Horizontal alignment; values other than "left" require width to be set.
Expand Down Expand Up @@ -211,13 +257,13 @@ def __init__(
text: str,
x: float,
y: float,
color: RGBOrA255 = arcade.color.WHITE,
color: RGBOrA255 | LinearGradient = arcade.color.WHITE,
font_size: float = 12,
width: int | None = None,
align: str = "left",
font_name: FontNameOrNames = ("calibri", "arial"),
bold: bool | str = False,
italic: bool = False,
italic: bool | str = False,
anchor_x: str = "left",
anchor_y: str = "baseline",
multiline: bool = False,
Expand All @@ -231,13 +277,13 @@ def __init__(
text=text,
x=x,
y=y,
color=Color.from_iterable(color),
color=_to_text_color(color),
font_size=font_size,
width=width,
align=align,
font_name=font_name,
weight=Weight.BOLD if bold else Weight.NORMAL,
style=Style.ITALIC if italic else Style.NORMAL,
weight=_to_weight(bold),
style=_to_style(italic),
anchor_x=anchor_x,
anchor_y=anchor_y,
multiline=multiline,
Expand All @@ -260,7 +306,8 @@ def __init__(
self._initialized = False
try:
self._init_deferred()
except Exception:
except NoArcadeWindowError:
# No window yet, so create the label when it's first used
pass

@property
Expand Down Expand Up @@ -456,13 +503,21 @@ def rotation(self, rotation: float):
self.label.rotation = rotation

@property
def color(self) -> Color:
"""Get or set the text color for the label."""
return Color.from_iterable(self.label.color)
def color(self) -> Color | LinearGradient:
"""
Get or set the text color for the label.

This is a :py:class:`~arcade.types.Color`, or a
:py:class:`pyglet.text.LinearGradient` if one was set.
"""
color = self.label.color
if isinstance(color, LinearGradient):
return color
return Color.from_iterable(color)

@color.setter
def color(self, color: RGBOrA255):
self.label.color = Color.from_iterable(color)
def color(self, color: RGBOrA255 | LinearGradient):
self.label.color = _to_text_color(color)

@property
def width(self) -> int | None:
Expand Down Expand Up @@ -563,29 +618,41 @@ def bold(self) -> bool | str:
"""
Get or set bold state of the label.

The supported values include:

* ``"black"``
* ``"bold" (same as ``True``)
* ``"semibold"``
* ``"semilight"``
* ``"light"``
``True`` is the same as ``"bold"``, and ``False`` the same as
``"normal"``. Other values are font weight names from
:py:class:`pyglet.enums.Weight`, such as ``"thin"``,
``"light"``, ``"medium"``, ``"semibold"``, ``"extrabold"``
or ``"black"``. Not every font has every weight.

Returns ``True`` for bold, ``False`` for normal, and the weight
name for any other weight.
"""
return self.label.weight == Weight.BOLD
return _from_weight(self.label.weight)

@bold.setter
def bold(self, bold: bool | str):
self.label.weight = Weight.BOLD if bold else Weight.NORMAL
self.label.weight = _to_weight(bold)

@property
def italic(self) -> bool | str:
"""Get or set the italic state of the label."""
return self.label.italic
"""
Get or set the italic state of the label.

``True`` is the same as ``"italic"``, and ``False`` the same as
``"normal"``. ``"oblique"`` is also accepted.

Returns ``True`` for italic, ``False`` for normal, and the style
name for any other style.
"""
return _from_style(self.label.document.get_style("style"))

@italic.setter
def italic(self, italic: bool | str):
self.label.italic = italic
# Set the "style" document style, which pyglet uses to pick the
# font. pyglet's own Label.italic sets an "italic" style, which
# doesn't change the font.
label = self.label
label.document.set_style(0, len(label.document.text), {"style": _to_style(italic)})

@property
def multiline(self) -> bool:
Expand Down Expand Up @@ -751,7 +818,7 @@ def draw(
text: str,
x: float,
y: float,
color: RGBOrA255 = arcade.color.WHITE,
color: RGBOrA255 | LinearGradient = arcade.color.WHITE,
font_size: float = 12,
**kwargs,
) -> Text:
Expand Down Expand Up @@ -786,7 +853,7 @@ def get(
text: str,
x: float,
y: float,
color: RGBOrA255 = arcade.color.WHITE,
color: RGBOrA255 | LinearGradient = arcade.color.WHITE,
font_size: float = 12,
**kwargs,
) -> Text:
Expand Down Expand Up @@ -852,7 +919,7 @@ def remove(self, key: str) -> None:

def create_text_sprite(
text: str,
color: RGBOrA255 = arcade.color.WHITE,
color: RGBOrA255 | LinearGradient = arcade.color.WHITE,
font_size: float = 12.0,
width: int | None = None,
align: str = "left",
Expand Down Expand Up @@ -882,7 +949,8 @@ def create_text_sprite(
Args:
text: Initial text to display. Can be an empty string
color: Color of the text as an RGBA tuple or a
:py:class:`~arcade.types.Color` instance.
:py:class:`~arcade.types.Color` instance, or a
:py:class:`pyglet.text.LinearGradient` for a left-to-right gradient.
font_size: Size of the text in points
width: A width limit in pixels
align: Horizontal alignment; values other than "left" require width to be set.
Expand Down Expand Up @@ -1000,7 +1068,7 @@ def draw_text(
text: Any,
x: float,
y: float,
color: RGBOrA255 = arcade.color.WHITE,
color: RGBOrA255 | LinearGradient = arcade.color.WHITE,
font_size: float = 12.0,
width: int | None = None,
align: str = "left",
Expand Down Expand Up @@ -1041,7 +1109,8 @@ def draw_text(
y: y position to align the text's anchor point with
z: z position to align the text's anchor point with
color: Color of the text as an RGBA tuple or a
:py:class:`~arcade.types.Color` instance.
:py:class:`~arcade.types.Color` instance, or a
:py:class:`pyglet.text.LinearGradient` for a left-to-right gradient.
font_size: Size of the text in points
width: A width limit in pixels
align: Horizontal alignment; values other than "left" require width to be set.
Expand Down Expand Up @@ -1185,7 +1254,7 @@ def draw_text(
f"but got {width!r}."
)

color = Color.from_iterable(color)
color = _to_text_color(color)
text = str(text)
# Reuse labels, keyed by the settings that are expensive to change and
# the text. Position, color, and rotation are cheap to update.
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/gui/test_uilabel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import Mock

from pyglet.math import Vec2
from pyglet.text import LinearGradient

from arcade.gui import UILabel
from arcade.types import Color, LBWH
Expand Down Expand Up @@ -259,3 +260,26 @@ def test_fit_content_uses_adaptive_multiline_width(ui):
# check size_hint_min updated
assert label.size_hint_min[0] > shm_w
assert label.size_hint_min[1] < shm_h


def test_update_font_italic_bold_and_gradient(window):
label = UILabel(text="Example", font_name="Liberation Sans")
gradient = LinearGradient((255, 0, 0, 255), (0, 0, 255, 255))

label.update_font(italic=True, bold="light", font_color=gradient)

assert label.italic is True
assert label.bold == "light"
assert label.font_color == gradient
assert label._label.label.document.get_style("style") == "italic"

# Re-applying the same values is not a change
label._requires_render = False
label.update_font(italic=True, bold="light", font_color=gradient)
assert label._requires_render is False

# Plain colors still become a Color
label.update_font(italic=False, bold=False, font_color=(0, 255, 0))
assert label.italic is False
assert label.bold is False
assert label.font_color == Color(0, 255, 0, 255)
Loading
Loading