diff --git a/HI.py b/HI.py new file mode 100644 index 0000000000..6e8f435961 --- /dev/null +++ b/HI.py @@ -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() \ No newline at end of file diff --git a/arcade/__init__.py b/arcade/__init__.py index 19adea6503..7c0b328b9e 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -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, ) @@ -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', diff --git a/arcade/text.py b/arcade/text.py index 5db3ffbc2a..a6f11d50bb 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -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: """ @@ -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 @@ -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 @@ -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, ) diff --git a/tests/unit/text/test_text.py b/tests/unit/text/test_text.py index 3924b5ec27..3c502481aa 100644 --- a/tests/unit/text/test_text.py +++ b/tests/unit/text/test_text.py @@ -153,6 +153,3 @@ def new_text(*args, **kwargs) -> None: window.flip() - -# def test_create_text_sprite(window): -# pass diff --git a/tests/unit/text/test_text_sprite.py b/tests/unit/text/test_text_sprite.py index 04fea24564..e93be9f5f9 100644 --- a/tests/unit/text/test_text_sprite.py +++ b/tests/unit/text/test_text_sprite.py @@ -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)