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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Fixed `InputManager.parse()` raising `AttributeError`, so a saved input configuration couldn't be loaded.

### Breaking Changes
- The default font is now Liberation Sans, which comes with Arcade, instead of whichever of Calibri and Arial the computer has ([#1135](https://github.com/pythonarcade/arcade/issues/1135)). Text looks the same on every computer. It applies to `Text`, `draw_text()`, `create_text_sprite()`, `TextPool`, and the GUI's `UILabel`, `UITextArea`, `UIInputText` and buttons (which still use Kenney Future first if you load the Kenney fonts). Liberation Sans has the same letter widths as Arial, so text on Windows, which used Calibri, is about 10% wider. For the old look, pass `font_name=("calibri", "arial")`.
- The Liberation fonts load the first time one is used, so `font_name="Liberation Mono"` works without `load_liberation_fonts()`.
- `UITextArea` and `UIInputText` accept font file paths and resource handles for `font_name`, like `Text`.
- `arcade.load_tilemap()` and `TileMap` now give every layer's sprite list a spatial hash by default (`use_spatial_hash=True`) ([#2663](https://github.com/pythonarcade/arcade/issues/2663)). Forgetting it on a wall layer made collision checks against it much slower: 100 bullets checked against 2,000 wall tiles took 48 ms per frame without it and 0.6 ms with it. A hash makes moving a layer's sprites slower, so for a layer whose sprites move a lot, such as moving platforms, turn it off with `layer_options={"Moving Platforms": {"use_spatial_hash": False}}`, or for the whole map with `use_spatial_hash=False`.
- `layer_options` now warns about option names it doesn't know, which it used to ignore. The performance tips page named the option `"use_spatial_hashing"`; it's `"use_spatial_hash"`.
- `Sprite`, `BasicSprite`, `SpriteSolidColor`, `SpriteCircle`, `TextureAnimationSprite` and `AnimatedWalkingSprite` raise `TypeError` for keyword arguments they don't accept, instead of silently ignoring them. For example, `Sprite(path, hit_box_algorithm=...)` used to do nothing; set the hit box algorithm when loading the texture instead. `Sprite` now takes `visible` as a named argument.
Expand Down
2 changes: 1 addition & 1 deletion arcade/gui/experimental/typed_text_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(
width: float = 100,
height: float = 24,
text: str = "",
font_name=("Arial",),
font_name=("Liberation Sans", "arial"),
font_size: float = 12,
text_color: RGBOrA255 = BLACK,
error_color: RGBOrA255 = RED,
Expand Down
4 changes: 2 additions & 2 deletions arcade/gui/widgets/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class UITextureButtonStyle(UIStyleBase):
"""

font_size: int = 12
font_name: FontNameOrNames = ("Kenney Future", "arial", "calibri")
font_name: FontNameOrNames = ("Kenney Future", "Liberation Sans", "arial")
font_color: RGBA255 = uicolor.WHITE


Expand Down Expand Up @@ -219,7 +219,7 @@ class UIFlatButtonStyle(UIStyleBase):
"""

font_size: int = 12
font_name: FontNameOrNames = ("Kenney Future", "arial", "calibri")
font_name: FontNameOrNames = ("Kenney Future", "Liberation Sans", "arial")
font_color: RGBA255 = color.WHITE
bg: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE
border: RGBA255 | None = None
Expand Down
23 changes: 17 additions & 6 deletions arcade/gui/widgets/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
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, _to_text_color, _to_weight
from arcade.text import (
FontNameOrNames,
_attempt_font_name_resolution,
_to_text_color,
_to_weight,
)
from arcade.types import LBWH, RGBA255, Color, RGBOrA255


Expand Down Expand Up @@ -92,7 +97,7 @@ def __init__(
y: float = 0,
width: float | None = None,
height: float | None = None,
font_name=("calibri", "arial"),
font_name=("Liberation Sans", "arial"),
font_size: float = 12,
text_color: RGBOrA255 = arcade.color.WHITE,
bold: str | bool = False,
Expand Down Expand Up @@ -521,7 +526,7 @@ def __init__(
width: float = 100,
height: float = 25, # required height for font size 12 + border width 1
text: str = "",
font_name=("Arial",),
font_name=("Liberation Sans", "arial"),
font_size: float = 12,
text_color: RGBOrA255 = arcade.color.WHITE,
multiline=False,
Expand Down Expand Up @@ -567,7 +572,12 @@ def __init__(
self.doc.set_style(
0,
len(text),
dict(font_name=font_name, font_size=font_size, color=self._text_color),
dict(
# Loads font files and bundled fonts, as arcade.Text does
font_name=_attempt_font_name_resolution(font_name),
font_size=font_size,
color=self._text_color,
),
)

self.layout = pyglet.text.layout.IncrementalTextLayout(
Expand Down Expand Up @@ -839,7 +849,7 @@ def __init__(
width: float = 400,
height: float = 40,
text: str = "",
font_name=("arial", "calibri"),
font_name=("Liberation Sans", "arial"),
font_size: float = 12,
bold=False,
italic=False,
Expand Down Expand Up @@ -869,7 +879,8 @@ def __init__(

# pyglet 3 reads "weight" and "style", not "bold" and "italic"
text_style = dict(
font_name=font_name,
# Loads font files and bundled fonts, as arcade.Text does
font_name=_attempt_font_name_resolution(font_name),
font_size=font_size,
color=Color.from_iterable(text_color),
weight=_to_weight(bold),
Expand Down
5 changes: 4 additions & 1 deletion arcade/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,13 @@ def load_liberation_fonts() -> None:
For best performance, you may want to switch to
:py:class:`arcade.load_font` before release.

The Liberation fonts are proven, permissively-licensed fonts.[
The Liberation fonts are proven, permissively-licensed fonts.
For previews and additional information, please see
:ref:`resources-fonts-liberation`.

Liberation Sans is Arcade's default font, and each family is loaded
the first time it's used. Call this to load them all ahead of time.

.. list-table:: ``font_name`` values for :py:class:`arcade.Text`
:header-rows: 1

Expand Down
34 changes: 28 additions & 6 deletions arcade/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,32 @@ def _font_file_family(font_name: str) -> str | None:
return _load_font_file(path)


# Font families bundled with Arcade that are loaded the first time they're
# asked for, by their file name prefix
_BUNDLED_FAMILIES = {
"liberation sans": "Liberation_Sans",
"liberation serif": "Liberation_Serif",
"liberation mono": "Liberation_Mono",
}


def _load_bundled_family(font_name: str) -> None:
"""If a font name is a family bundled with Arcade, load all its styles."""
prefix = _BUNDLED_FAMILIES.get(font_name.strip().lower())
if prefix is None:
return
for style in ("Regular", "Bold", "Italic", "BoldItalic"):
# Each file is only loaded once
_load_font_file(resolve(f":system:fonts/ttf/Liberation/{prefix}_{style}.ttf"))


def _attempt_font_name_resolution(font_name: FontNameOrNames) -> str:
"""Resolve a font name, path, or list of them to the name of one font.

Paths to font files are loaded and replaced by the font's family
name. The first name pyglet finds is returned, or pyglet's default
font if it finds none.
name, and the Liberation fonts bundled with Arcade are loaded when
they're asked for. The first name pyglet finds is returned, or
pyglet's default font if it finds none.

Args:
font_name: A font name, path to a font file, or a tuple or list
Expand All @@ -149,6 +169,8 @@ def _attempt_font_name_resolution(font_name: FontNameOrNames) -> str:
raise ValueError(f"Couldn't find a font for {font_name!r}")

names = [_font_file_family(font) or font for font in font_list]
for name in names:
_load_bundled_family(name)
return pyglet.font.load(names).name


Expand Down Expand Up @@ -321,7 +343,7 @@ def __init__(
font_size: float = 12,
width: int | None = None,
align: str = "left",
font_name: FontNameOrNames = ("calibri", "arial"),
font_name: FontNameOrNames = ("Liberation Sans", "arial"),
bold: bool | str = False,
italic: bool | str = False,
anchor_x: str = "left",
Expand Down Expand Up @@ -942,7 +964,7 @@ def on_draw(self):
``anchor_x``).
"""

def __init__(self, font_name: FontNameOrNames = ("calibri", "arial"), **defaults):
def __init__(self, font_name: FontNameOrNames = ("Liberation Sans", "arial"), **defaults):
self._font_name = font_name
self._defaults = defaults
self._cache: dict[str, Text] = {}
Expand Down Expand Up @@ -1058,7 +1080,7 @@ def create_text_sprite(
font_size: float = 12.0,
width: int | None = None,
align: str = "left",
font_name: FontNameOrNames = ("calibri", "arial"),
font_name: FontNameOrNames = ("Liberation Sans", "arial"),
bold: bool | str = False,
italic: bool = False,
anchor_x: str = "left",
Expand Down Expand Up @@ -1227,7 +1249,7 @@ def draw_text(
font_size: float = 12.0,
width: int | None = None,
align: str = "left",
font_name: FontNameOrNames = ("calibri", "arial"),
font_name: FontNameOrNames = ("Liberation Sans", "arial"),
bold: bool | str = False,
italic: bool = False,
anchor_x: str = "left",
Expand Down
8 changes: 4 additions & 4 deletions doc/_includes/resources_Liberation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Arcade also includes the Liberation font family. This trio is designed and
licensed specifically to be a portable, drop-in set of substitutes for Times, Arial,
and Courier fonts. It uses the proven, commercial-friendly `SIL Open Font License`_.

To use these fonts, you may use either approach:

* load files for specific variants via :py:func:`arcade.load_font`
* load all variants at once with :py:func:`arcade.resources.load_liberation_fonts`.
Liberation Sans is Arcade's default font. Each family is loaded the first
time you use it, so you can use ``font_name="Liberation Mono"`` without
loading anything. To load them ahead of time, for example during a loading
screen, use :py:func:`arcade.resources.load_liberation_fonts`.
4 changes: 2 additions & 2 deletions doc/programming_guide/gui/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ Styling options are shown in the table below.
|``font_name`` |Font name or family for the button text. If a tuple is |
| |supplied then Arcade will attempt to load all of the fonts, |
| |prioritizing the first one. Defaults to |
| |``("calibri", "arial")``. |
| |``("Kenney Future", "Liberation Sans", "arial")``. |
+----------------+------------------------------------------------------------+
|``font_color`` |Font color for the button text (foreground). Defaults to |
| |white for normal, hover, and disabled states. Defaults to |
Expand Down Expand Up @@ -372,7 +372,7 @@ flat buttons.
|``font_name`` |Font name or family for the button text. If a tuple is |
| |supplied then Arcade will attempt to load all of the fonts, |
| |prioritizing the first one. Defaults to |
| |``("calibri", "arial")``. |
| |``("Kenney Future", "Liberation Sans", "arial")``. |
+----------------+------------------------------------------------------------+
|``font_color`` |Font color for the button text (foreground). Defaults to |
| |white for normal, hover, and disabled states. Defaults to |
Expand Down
12 changes: 8 additions & 4 deletions doc/programming_guide/gui/style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,23 @@ of the class.
"normal": UIStyle(),
"hover": UIStyle(
font_size=12,
font_name=("calibri", "arial"),
font_name=("Kenney Future", "Liberation Sans", "arial"),
font_color=arcade.color.WHITE,
bg=(21, 19, 21, 255),
border=(77, 81, 87, 255),
border_width=2,
),
"press": UIStyle(
font_size=12,
font_name=("calibri", "arial"),
font_name=("Kenney Future", "Liberation Sans", "arial"),
font_color=arcade.color.BLACK,
bg=arcade.color.WHITE,
border=arcade.color.WHITE,
border_width=2,
),
"disabled": UIStyle(
font_size=12,
font_name=("calibri", "arial"),
font_name=("Kenney Future", "Liberation Sans", "arial"),
font_color=arcade.color.WHITE,
bg=arcade.color.GRAY,
border=None,
Expand All @@ -113,13 +113,17 @@ For the UIFlatButton the supported attributes are:
Name Type Default value Description
================ =================== ======================== ==================================
``font_size`` ``int`` 12 Size of the text on the button
``font_name`` ``FontNameOrNames`` ``("calibri", "arial")`` Font of the text
``font_name`` ``FontNameOrNames`` See below Font of the text
``font_color`` ``RGBA255`` ``arcade.color.WHITE`` Color of text
``bg`` ``RGBA255`` ``(21, 19, 21, 255)`` Background color
``border`` ``Optional`` ``None`` Border color
``border_width`` ``int`` 0 Border width
================ =================== ======================== ==================================

The default ``font_name`` is ``("Kenney Future", "Liberation Sans", "arial")``. Kenney Future
is used once you load the Kenney fonts with :py:func:`arcade.resources.load_kenney_fonts`, and
Liberation Sans otherwise.

The style attribute is a dictionary, which maps states such as ``normal``, ``hover``, ``press``,
and ``disabled`` to an instance of the class's ``UIStyle``.

Expand Down
9 changes: 5 additions & 4 deletions doc/programming_guide/text.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ Fonts
* **The name of a font** installed on the computer, such as ``"arial"``.
* **A tuple or list of names.** The first one that's found is used,
which helps when different computers have different fonts. The default
is ``("calibri", "arial")``.
is ``("Liberation Sans", "arial")``. Liberation Sans comes with Arcade,
so text looks the same on every computer.
* **The path to a font file**, such as ``"fonts/MyFont.ttf"``, or a
:ref:`resource handle <resource_handles>`, such as
``":resources:fonts/ttf/MyFont.ttf"``. The file is loaded the first
Expand All @@ -200,13 +201,13 @@ viewer shows it.
Fonts that come with Arcade
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Arcade includes the Kenney and Liberation fonts. They aren't loaded
automatically, so load them before using them:
Arcade includes the Kenney and Liberation fonts. The Liberation fonts
are loaded the first time you use one. Load the Kenney fonts before
using them:

.. code-block:: python

arcade.resources.load_kenney_fonts()
arcade.resources.load_liberation_fonts()

text = arcade.Text("Hello", 10, 10, font_name="Kenney Future")

Expand Down
6 changes: 4 additions & 2 deletions tests/unit/gui/test_uitextarea_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ def test_html_text_keeps_its_styles(window):
area = UITextArea(
text='<b>bold</b> plain <font color="#ff0000">red</font>',
document_mode="HTML",
font_name=("Arial",),
# A bundled font, so the name is the same on every computer
font_name=("Liberation Serif",),
font_size=20,
text_color=BLUE,
)
assert area.doc.get_style("weight", 0) == "bold"
assert area.doc.get_style("weight", 6) == "normal"
# The arguments replace the HTML decoder's own defaults
assert area.doc.get_style("font_name", 6) == ("Arial",)
# Resolved to the name of the font that was found, as arcade.Text does
assert area.doc.get_style("font_name", 6) == "Liberation Serif"
assert area.doc.get_style("font_size", 6) == 20
assert area.doc.get_style("color", 6) == BLUE
# A color set in the HTML wins
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/text/test_default_font.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Liberation Sans is the default font, loaded the first time it's used (#1135)."""

import arcade
from arcade.gui import UIInputText, UILabel, UITextArea


def test_text_default_font(window):
text = arcade.Text("Hello", 0, 0)
# The name of the font that was found
assert text.font_name == "Liberation Sans"


def test_default_font_bold_and_italic(window):
font = arcade.Text("Hello", 0, 0, bold=True, italic=True)._label.document.get_font()
assert font.name == "Liberation Sans"
assert font.weight == "bold"
assert font.style == "italic"


def test_create_text_sprite_default_font(window):
sprite = arcade.create_text_sprite("Hello")
assert sprite.width > 0


def test_liberation_families_load_when_used(window, monkeypatch):
# tests/conftest.py loads the Liberation fonts for every test, so record
# which files using a name asks for instead
loaded = []
real_load = arcade.text._load_font_file

def record(path):
loaded.append(path.name)
return real_load(path)

monkeypatch.setattr(arcade.text, "_load_font_file", record)
arcade.Text("x", 0, 0, font_name="liberation mono")
assert loaded == [
f"Liberation_Mono_{style}.ttf" for style in ("Regular", "Bold", "Italic", "BoldItalic")
]

loaded.clear()
arcade.Text("x", 0, 0, font_name="Arial")
assert loaded == []


def test_gui_text_widgets_default_font(window):
assert UILabel(text="x").font_name == "Liberation Sans"
assert UITextArea(text="x").doc.get_style("font_name", 0) == "Liberation Sans"
assert UIInputText(text="x").doc.get_style("font_name", 0) == "Liberation Sans"


def test_gui_text_widgets_take_font_file_paths(window):
path = ":resources:fonts/ttf/Kenney/Kenney_Pixel.ttf"
assert UITextArea(text="x", font_name=path).doc.get_style("font_name", 0) == "Kenney Pixel"
assert UIInputText(text="x", font_name=path).doc.get_style("font_name", 0) == "Kenney Pixel"
Loading