Skip to content
84 changes: 84 additions & 0 deletions HI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import arcade, pyglet

def _attempt_font_name_resolution(font_name):
"""
Attempt to resolve a tuple of font names.

Preserves the original logic of this section, even though it
doesn't seem to make sense entirely. Comments are an attempt
to make sense of the original code.

If it can't resolve a definite path, it will return the original
argument for pyglet to attempt to resolve. This is consistent with
the original behavior of this code before it was encapsulated.

:param Union[str, Tuple[str, ...]] font_name:
:return: Either a resolved path or the original tuple
"""
if font_name:

# ensure
if isinstance(font_name, str):
font_list: Tuple[str, ...] = (font_name,)
elif isinstance(font_name, tuple):
font_list = font_name
else:
raise TypeError("font_name parameter must be a string, or a tuple of strings that specify a font name.")

for font in font_list:
try:
path = resolve(font)
# print(f"Font path: {path=}")

# found a font successfully!
return path.name

except FileNotFoundError:
pass

# failed to find it ourselves, hope pyglet can make sense of it
return font_name


class UsageAttempt(arcade.Window):

def __init__(self, width: int = 320, height: int = 240):
super().__init__(width=width, height=height)

self.sprites = arcade.SpriteList()
text_sprite = arcade.create_text_sprite(
"First line\nsecond line",
multiline=True,
width=100,
)
text_sprite.position = self.width // 2, self.height // 2
self.sprites.append(text_sprite)

self._label = pyglet.text.Label(
text="First line\nsecond line\nTHIRD Line",
x = 200,
y = 200,
font_name="Arial",
font_size=12,
anchor_x="left",
width=100,
align="baseline",
bold=True,
italic=True,
multiline=True,
)

def on_draw(self):
self.clear()
self.sprites.draw()

window = arcade.get_window()
with window.ctx.pyglet_rendering():
self._label.draw()




if __name__ == "__main__":
w = UsageAttempt()
arcade.run()
2 changes: 2 additions & 0 deletions arcade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def configure_logging(level: Optional[int] = None):
from .text import (
draw_text,
load_font,
create_text_texture,
create_text_sprite,
Text,
)
Expand Down Expand Up @@ -331,6 +332,7 @@ def configure_logging(level: Optional[int] = None):
'get_sprites_at_point',
'SpatialHash',
'get_timings',
'create_text_texture',
'create_text_sprite',
'clear_timings',
'get_window',
Expand Down
123 changes: 94 additions & 29 deletions arcade/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from arcade.types import Color, Point, RGBA255
from arcade.resources import resolve
from arcade.utils import PerformanceWarning, warning

from arcade.color import TRANSPARENT_BLACK
from warnings import warn

def load_font(path: Union[str, Path]) -> None:
"""
Expand Down Expand Up @@ -81,7 +82,6 @@ def _attempt_font_name_resolution(font_name: FontNameOrNames) -> FontNameOrNames

def _draw_pyglet_label(label: pyglet.text.Label) -> None:
"""

Helper for drawing pyglet labels with rotation within arcade.

Originally part of draw_text in this module, now abstracted and improved
Expand Down Expand Up @@ -569,28 +569,24 @@ def position(self, point: Point):
self._label.position = *point, self._label.z


def create_text_sprite(
def create_text_texture(
text: str,
color: RGBA255 = arcade.color.WHITE,
font_size: float = 12,
width: int = 0,
width: int = 100,
align: str = "left",
font_name: FontNameOrNames = ("calibri", "arial"),
bold: bool = False,
italic: bool = False,
anchor_x: str = "left",
multiline: bool = False,
texture_atlas: Optional[arcade.TextureAtlas] = None,
) -> arcade.Sprite:
texture_atlas: Optional[arcade.TextureAtlas] = None):
"""
Creates a sprite containing text based off of :py:class:`~arcade.Text`.
Creates a texture containing text based off of :py:class:`~pyglet.text.Label`.

Internally this creates a Text object and an empty texture. It then uses either the
provided texture atlas, or gets the default one, and draws the Text object into the
texture atlas.

It then creates a sprite referencing the newly created texture, and positions it
accordingly, and that is final result that is returned from the function.
Internally this creates a :py:class:`~pyglet.text.Label` object and an empty texture. It then uses either the
provided texture atlas, or gets the default one, and draws the `~pyglet.text.Label` object into the
texture atlas. Then it returns the texture.

If you are providing a custom texture atlas, something important to keep in mind is
that the resulting Sprite can only be added to SpriteLists which use that atlas. If
Expand All @@ -611,40 +607,109 @@ def create_text_sprite(
:param Optional[arcade.TextureAtlas] texture_atlas: The texture atlas to use for the
newly created texture. The default global atlas will be used if this is None.
"""
text_object = Text(
text,
start_x=0,
start_y=0,
color=color,

if align != "center" and align != "left" and align != "right":
raise ValueError("The 'align' parameter must be equal to 'left', 'right', or 'center'.")

adjusted_font = _attempt_font_name_resolution(font_name)
_label = pyglet.text.Label(
text=text,
font_name=adjusted_font,
font_size=font_size,
anchor_x=anchor_x,
color=Color.from_iterable(color),
width=width,
align=align,
font_name=font_name,
bold=bold,
italic=italic,
anchor_x=anchor_x,
anchor_y="baseline",
multiline=multiline,
)
lines = _label._get_lines()
left = _label._get_left()
right = left + _label.content_width
top = _label._get_top(lines)
bottom = _label._get_bottom(lines)

size = (
int(text_object.right - text_object.left),
int(text_object.top - text_object.bottom),
int(right - left),
int(top - bottom),
)
text_object.y = -text_object.bottom
texture = arcade.Texture.create_empty(text, size)

if not _label.content_width or not _label.content_height:
warn("Either width or height is 0")
return arcade.Texture.create_empty(text, (0, 0))

texture = arcade.Texture.create_empty(text, size)
if not texture_atlas:
texture_atlas = arcade.get_window().ctx.default_atlas

texture_atlas.add(texture)
with texture_atlas.render_into(texture) as fbo:
fbo.clear((0, 0, 0, 255))
text_object.draw()
fbo.clear(TRANSPARENT_BLACK)
_draw_pyglet_label(_label)
return texture


def create_text_sprite(
text: str,
color: RGBA255 = arcade.color.WHITE,
font_size: float = 12,
width: int = 100,
align: str = "left",
font_name: FontNameOrNames = ("calibri", "arial"),
bold: bool = False,
italic: bool = False,
anchor_x: str = "left",
multiline: bool = False,
texture_atlas: Optional[arcade.TextureAtlas] = None,
) -> arcade.Sprite:
"""
Creates a sprite containing text based off of :py:func:`create_text_texture`.

Internally this calls the create_text_texture function and gives it the relevant information.
When it is done, the create_text_texture function returns a texture.

The create_text_sprite then creates a sprite referencing the newly created texture, and positions it
accordingly, and that is final result that is returned from the function.

If you are providing a custom texture atlas, something important to keep in mind is
that the resulting Sprite can only be added to SpriteLists which use that atlas. If
it is added to a SpriteList which uses a different atlas, you will likely just see
a black box drawn in its place.

:param str text: Initial text to display. Can be an empty string
:param RGBA255 color: Color of the text as a tuple or list of 3 (RGB) or 4 (RGBA) integers
:param float font_size: Size of the text in points
:param float width: A width limit in pixels
:param str align: Horizontal alignment; values other than "left" require width to be set
:param FontNameOrNames font_name: A font name, path to a font file, or list of names
:param bool bold: Whether to draw the text as bold
:param bool italic: Whether to draw the text as italic
:param str anchor_x: How to calculate the anchor point's x coordinate.
Options: "left", "center", or "right"
:param bool multiline: Requires width to be set; enables word wrap rather than clipping
:param Optional[arcade.TextureAtlas] texture_atlas: The texture atlas to use for the
newly created texture. The default global atlas will be used if this is None.
"""

texture = create_text_texture(text,
color = color,
font_size = font_size,
width = width,
align = align,
font_name = font_name,
bold = bold,
italic = italic,
anchor_x = anchor_x,
multiline = multiline,
texture_atlas = texture_atlas
)

size = texture._size
return arcade.Sprite(
texture,
center_x=text_object.right - (size[0] / 2),
center_y=text_object.top,
center_x=size[0]/2,
center_y=size[1]/2,
)


Expand Down
3 changes: 0 additions & 3 deletions tests/unit/text/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,3 @@ def new_text(*args, **kwargs) -> None:

window.flip()


# def test_create_text_sprite(window):
# pass
9 changes: 7 additions & 2 deletions tests/unit/text/test_text_sprite.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import pytest
import arcade

def test_text_texture(window):
texture = arcade.create_text_texture("Hello World")
assert isinstance(texture, arcade.Texture)
assert texture.width == pytest.approx(80, rel=10)
assert texture.height == pytest.approx(20, rel=5)

def test_create(window):
def test_text_sprite(window):
sprite = arcade.create_text_sprite("Hello World")
assert isinstance(sprite, arcade.Sprite)
assert sprite.width == pytest.approx(75, rel=10)
assert sprite.width == pytest.approx(80, rel=10)
assert sprite.height == pytest.approx(20, rel=5)