Coverage for topdownengine/font.py: 15%

47 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-24 02:27 +0000

1import pygame as pg 

2 

3class Font: 

4 """A class built on top of pygame.Font that allows for caching of different sizes, prebuilt word wrap, and other features. 

5  

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 """ 

9 

10 def __init__(self, path: str): 

11 """Initialize the Font object. 

12  

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 """ 

16 

17 self.path = path 

18 self._sizes = {} 

19 

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) 

29 

30 return self._sizes[size] 

31 

32 def wrap(self, line: str, size: int, max_width: int) -> list[str]: 

33 """Break a single string into multiple lines based on width. 

34  

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. 

39  

40 Returns: 

41 list[str]: The list of wrapped lines. 

42 """ 

43 lines = [] 

44 current_line = "" 

45 fnt = self._get_size(size) 

46 

47 # Split by spaces 

48 words = line.split(" ") 

49 

50 for word in words: 

51 if not word: 

52 continue 

53 

54 # Determine the line to test size with 

55 test_line = f"{current_line} {word}" if current_line else word 

56 

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 = "" 

65 

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 

79 

80 # Add the final leftover line 

81 if current_line: 

82 lines.append(current_line) 

83 

84 return lines 

85 

86 def _render(self, size: int, text: str, color: pg.typing.ColorLike): 

87 return self._get_size(size).render(text, True, color) 

88 

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. 

91  

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". 

100  

101 Raises: 

102 ValueError: If an invalid align argument is passed into the method. 

103 """ 

104 surf = self._render(size, text, color) 

105 

106 try: 

107 rect = surf.get_rect(**{align: (x, y)}) 

108 except TypeError: 

109 raise ValueError(f"Invalid align value of {align}") 

110 

111 surface.blit(surf, rect)