Coverage for topdownengine/ui.py: 0%

86 statements  

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

1from __future__ import annotations 

2import pygame as pg 

3from collections.abc import Callable 

4from .font import Font 

5from .visual_utils import VisualUtils 

6 

7class UIContainer: 

8 """Class to store a collection of UI elements. 

9  

10 Attributes: 

11 elements (set[BaseUIElement]): The set of all elements in this container. This is a managed property. 

12 """ 

13 

14 def __init__(self): 

15 "Initialize the UIContainer." 

16 self._elements = set() 

17 

18 @property 

19 def elements(self) -> set[BaseUIElement]: 

20 "The set of all elements in this container. This is a managed property." 

21 return self._elements 

22 

23 def add_ui_element(self, element: BaseUIElement) -> None: 

24 """Add a UI element to this container. 

25  

26 Args: 

27 element (BaseUIElement): The element to add. 

28 

29 Raises: 

30 TypeError: If the element is not an instance of a subclass of BaseUIElement. 

31 """ 

32 if not isinstance(element, BaseUIElement): 

33 raise TypeError("Elements must be subclasses of the BaseUIElement class.") 

34 

35 self._elements.add(element) 

36 element._containers.add(self) 

37 

38 def remove_ui_element(self, element: BaseUIElement) -> None: 

39 """Remove a UI element from this container. 

40  

41 Args: 

42 element (BaseUIElement): The element to remove. 

43 """ 

44 self._elements.remove(element) 

45 element._containers.remove(self) 

46 

47 def remove_all_ui_elements(self) -> None: 

48 "Remove all UI elements from this container." 

49 for element in self.elements.copy(): 

50 self.remove_ui_element(element) 

51 

52 def handle_event(self, event: pg.Event) -> None: 

53 """Handle a single event for all elements in this container. 

54  

55 Args: 

56 event (pygame.Event): The event to handle. 

57 """ 

58 for e in self.elements.copy(): 

59 e.handle_event(event) 

60 

61 def update(self, dt: float) -> None: 

62 """Update all elements in this container. 

63  

64 Args: 

65 dt (float): The deltatime. 

66 """ 

67 for e in self.elements.copy(): 

68 e.update(dt) 

69 

70 def render(self, surface: pg.Surface) -> None: 

71 """Render all elementsto a given surface. 

72  

73 Args: 

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

75 """ 

76 for e in self.elements: 

77 surface.blit(e.image, e.rect) 

78 

79class BaseUIElement: 

80 """Base class for UI elements. 

81  

82 Attributes: 

83 containers (set[UIContainer]): The set of all containers that contain this element. This is a managed property. 

84 image (pygame.Surface): The surface of the element. This is a managed property. 

85 """ 

86 

87 def __init__(self, position: pg.typing.Point, align: str="center", image: pg.Surface=None): 

88 """Handle a single event for all elements in this container. 

89  

90 Args: 

91 position (pygame.typing.Point): The position of the element. 

92 align (str): The alignment of the element. 

93 image (pygame.Surface, optional): The image to use for the element. 

94 """ 

95 self._containers = set() 

96 self._image = image 

97 if self._image is None: 

98 self._image = pg.Surface((1,1), pg.SRCALPHA) 

99 self.rect = self._image.get_rect(**{align: position}) 

100 self.align = align 

101 

102 @property 

103 def containers(self) -> set[UIContainer]: 

104 "The set of all containers that contain this element. This is a managed property." 

105 return self._containers 

106 

107 @property 

108 def image(self) -> pg.Surface: 

109 "The surface of the element. This is a managed property." 

110 return self._image 

111 

112 @image.setter 

113 def image(self, new_image: pg.Surface): 

114 self._image = new_image 

115 self.rect = new_image.get_rect(**{self.align: getattr(self.rect, self.align)}) 

116 

117 def add_container(self, container: UIContainer) -> None: 

118 """Add this UI element to a container. 

119  

120 Args: 

121 container (UIContainer): The container to add. 

122 

123 Raises: 

124 TypeError: If the container is not an instance of UIContainer. 

125 """ 

126 

127 if not isinstance(container, UIContainer): 

128 raise TypeError("Containers must be instances of UIContainer.") 

129 

130 self._containers.add(container) 

131 container._elements.add(self) 

132 

133 def remove_container(self, container: UIContainer) -> None: 

134 """Remove this UI element from a container. 

135  

136 Args: 

137 container (UIContainer): The container to remove from. 

138 """ 

139 self._containers.remove(container) 

140 container._elements.remove(self) 

141 

142 def remove_from_all_containers(self) -> None: 

143 "Remove this UI element from all containers." 

144 for container in self.containers.copy(): 

145 self.remove_container(container) 

146 

147 def handle_event(self, event: pg.Event) -> None: 

148 """Handle a single event. 

149  

150 Args: 

151 event (pygame.Event): The event to handle. 

152 """ 

153 pass 

154 

155 def update(self, dt: float) -> None: 

156 """Update this element. 

157  

158 Args: 

159 dt (float): The deltatime. 

160 """ 

161 pass 

162 

163class Button(BaseUIElement): 

164 def __init__(self, position: pg.typing.Point, align: str="center", image: pg.Surface=None, on_click: Callable[[], None]=None, hover_highlight_strength: int=100): 

165 super().__init__(position, align, image) 

166 self.on_click = on_click 

167 self.hover_highlight_strength = hover_highlight_strength 

168 self._enable_hover = False 

169 

170 def _get_image(self) -> pg.Surface: 

171 if self._enable_hover and self.is_mouse_over(): 

172 return VisualUtils.make_img_white(self._image, self.hover_highlight_strength) 

173 

174 return self._image 

175 

176 image = property(fget=_get_image, fset=BaseUIElement.image.fset) 

177 

178 def is_mouse_over(self) -> bool: 

179 "Is the mouse over this button?" 

180 return self.rect.collidepoint(pg.mouse.get_pos()) 

181 

182 def handle_event(self, event: pg.Event) -> None: 

183 if event.type == pg.MOUSEBUTTONUP and self.is_mouse_over() and self.on_click is not None: 

184 self.on_click() 

185 

186 def update(self, dt: float) -> None: 

187 self._enable_hover = True 

188 

189class Text(BaseUIElement): 

190 def __init__(self, position: pg.typing.Point, font: Font, size: int, text: str, color: pg.typing.ColorLike, align: str="center"): 

191 image = font._render(size, text, color) 

192 super().__init__(position, align, image)