Coverage for topdownengine/font.py: 53%

55 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-13 14:20 +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 get_max_size_for_text_in_rect(self, text: str, target_rect: pg.Rect): 

33 """Finds the largest font size where the text fits inside a target_rect. Uses the height as the maximum logical starting size. 

34  

35 Args: 

36 text (str): The text to scale. 

37 target_rect (pygame.Rect): The Rect object to scale to. 

38  

39 Returns: 

40 int: The largest possible font size where the text fits in the target_rect. 

41 """ 

42 start_size = max(2, target_rect.height) 

43 

44 for size in range(start_size, 1, -1): 

45 rendered = self._render(size, text, "black") 

46 text_w, text_h = rendered.get_bounding_rect().size 

47 

48 if text_w <= target_rect.width and text_h <= target_rect.height: 

49 return size 

50 

51 return 2 

52 

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

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

55  

56 Args: 

57 line (str): The string to wrap. 

58 size (int): The fontsize to use for calculations. 

59 max_width (int): The maximum width for each wrapped line. 

60  

61 Returns: 

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

63 """ 

64 lines = [] 

65 current_line = "" 

66 fnt = self._get_size(size) 

67 

68 # Split by spaces 

69 words = line.split(" ") 

70 

71 for word in words: 

72 if not word: 

73 continue 

74 

75 # Determine the line to test size with 

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

77 

78 # If it fits, add it to the current line 

79 if fnt.size(test_line)[0] <= max_width: 

80 current_line = test_line 

81 else: 

82 # The current line is full, so append it and make a new line. 

83 if current_line: 

84 lines.append(current_line) 

85 current_line = "" 

86 

87 # Check if the word itself is wider than max_width 

88 if fnt.size(word)[0] > max_width: 

89 # Split by character for oversized words 

90 temp_word = "" 

91 for char in word: 

92 if fnt.size(temp_word + char)[0] <= max_width: 

93 temp_word += char 

94 else: 

95 lines.append(temp_word) 

96 temp_word = char 

97 current_line = temp_word 

98 else: 

99 current_line = word 

100 

101 # Add the final leftover line 

102 if current_line: 

103 lines.append(current_line) 

104 

105 return lines 

106 

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

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

109 

110 def draw_text(self, text: str, x: int, y: int, size: int, surface: pg.Surface, color: pg.typing.ColorLike, align: str="center") -> None: 

111 """Draws text to a surface. 

112  

113 Args: 

114 text (str): The text to render to the surface. 

115 x (int): The x-position. 

116 y (int): The y-position. 

117 size (int): The font size to use. 

118 surface (pygame.Surface): The surface to render to. 

119 color (pygame.typing.ColorLike): The color to use. 

120 align (str): The alignment to use. Defaults to "center". 

121  

122 Raises: 

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

124 """ 

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

126 

127 try: 

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

129 except AttributeError: 

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

131 

132 surface.blit(surf, rect)