Coverage for topdownengine/font.py: 15%
47 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 02:27 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 02:27 +0000
1import pygame as pg
3class Font:
4 """A class built on top of pygame.Font that allows for caching of different sizes, prebuilt word wrap, and other features.
6 Attributes:
7 path (str): The path to load from. If there is no font at that path, it will load a system font of that name.
8 """
10 def __init__(self, path: str):
11 """Initialize the Font object.
13 Args:
14 path (str): The path to load from. If there is no font at that path, it will load a system font of that name.
15 """
17 self.path = path
18 self._sizes = {}
20 def _get_size(self, size: int) -> pg.Font:
21 if not size in self._sizes:
22 try:
23 # Attempt to load it as a custom font file
24 self._sizes[size] = pg.font.Font(self.path, size)
25 except (FileNotFoundError, pg.error):
26 # Try to get it as a system font if it doesn't exist
27 # If it's not a system font, pygame-ce uses a default font.
28 self._sizes[size] = pg.font.SysFont(self.path, size)
30 return self._sizes[size]
32 def wrap(self, line: str, size: int, max_width: int) -> list[str]:
33 """Break a single string into multiple lines based on width.
35 Args:
36 line (str): The string to wrap.
37 size (int): The fontsize to use for calculations.
38 max_width (int): The maximum width for each wrapped line.
40 Returns:
41 list[str]: The list of wrapped lines.
42 """
43 lines = []
44 current_line = ""
45 fnt = self._get_size(size)
47 # Split by spaces
48 words = line.split(" ")
50 for word in words:
51 if not word:
52 continue
54 # Determine the line to test size with
55 test_line = f"{current_line} {word}" if current_line else word
57 # If it fits, add it to the current line
58 if fnt.size(test_line)[0] <= max_width:
59 current_line = test_line
60 else:
61 # The current line is full, so append it and make a new line.
62 if current_line:
63 lines.append(current_line)
64 current_line = ""
66 # Check if the word itself is wider than max_width
67 if fnt.size(word)[0] > max_width:
68 # Split by character for oversized words
69 temp_word = ""
70 for char in word:
71 if fnt.size(temp_word + char)[0] <= max_width:
72 temp_word += char
73 else:
74 lines.append(temp_word)
75 temp_word = char
76 current_line = temp_word
77 else:
78 current_line = word
80 # Add the final leftover line
81 if current_line:
82 lines.append(current_line)
84 return lines
86 def _render(self, size: int, text: str, color: pg.typing.ColorLike):
87 return self._get_size(size).render(text, True, color)
89 def draw_text(self, text: str, x: int, y: int, size: int, surface: pg.Surface, color: pg.typing.ColorLike, align: str="center") -> None:
90 """Draws text to a surface.
92 Args:
93 text (str): The text to render to the surface.
94 x (int): The x-position.
95 y (int): The y-position.
96 size (int): The font size to use.
97 surface (pygame.Surface): The surface to render to.
98 color (pygame.typing.ColorLike): The color to use.
99 align (str): The alignment to use. Defaults to "center".
101 Raises:
102 ValueError: If an invalid align argument is passed into the method.
103 """
104 surf = self._render(size, text, color)
106 try:
107 rect = surf.get_rect(**{align: (x, y)})
108 except TypeError:
109 raise ValueError(f"Invalid align value of {align}")
111 surface.blit(surf, rect)