Coverage for topdownengine/ui.py: 0%
86 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
1from __future__ import annotations
2import pygame as pg
3from collections.abc import Callable
4from .font import Font
5from .visual_utils import VisualUtils
7class UIContainer:
8 """Class to store a collection of UI elements.
10 Attributes:
11 elements (set[BaseUIElement]): The set of all elements in this container. This is a managed property.
12 """
14 def __init__(self):
15 "Initialize the UIContainer."
16 self._elements = set()
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
23 def add_ui_element(self, element: BaseUIElement) -> None:
24 """Add a UI element to this container.
26 Args:
27 element (BaseUIElement): The element to add.
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.")
35 self._elements.add(element)
36 element._containers.add(self)
38 def remove_ui_element(self, element: BaseUIElement) -> None:
39 """Remove a UI element from this container.
41 Args:
42 element (BaseUIElement): The element to remove.
43 """
44 self._elements.remove(element)
45 element._containers.remove(self)
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)
52 def handle_event(self, event: pg.Event) -> None:
53 """Handle a single event for all elements in this container.
55 Args:
56 event (pygame.Event): The event to handle.
57 """
58 for e in self.elements.copy():
59 e.handle_event(event)
61 def update(self, dt: float) -> None:
62 """Update all elements in this container.
64 Args:
65 dt (float): The deltatime.
66 """
67 for e in self.elements.copy():
68 e.update(dt)
70 def render(self, surface: pg.Surface) -> None:
71 """Render all elementsto a given surface.
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)
79class BaseUIElement:
80 """Base class for UI elements.
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 """
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.
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
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
107 @property
108 def image(self) -> pg.Surface:
109 "The surface of the element. This is a managed property."
110 return self._image
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)})
117 def add_container(self, container: UIContainer) -> None:
118 """Add this UI element to a container.
120 Args:
121 container (UIContainer): The container to add.
123 Raises:
124 TypeError: If the container is not an instance of UIContainer.
125 """
127 if not isinstance(container, UIContainer):
128 raise TypeError("Containers must be instances of UIContainer.")
130 self._containers.add(container)
131 container._elements.add(self)
133 def remove_container(self, container: UIContainer) -> None:
134 """Remove this UI element from a container.
136 Args:
137 container (UIContainer): The container to remove from.
138 """
139 self._containers.remove(container)
140 container._elements.remove(self)
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)
147 def handle_event(self, event: pg.Event) -> None:
148 """Handle a single event.
150 Args:
151 event (pygame.Event): The event to handle.
152 """
153 pass
155 def update(self, dt: float) -> None:
156 """Update this element.
158 Args:
159 dt (float): The deltatime.
160 """
161 pass
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
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)
174 return self._image
176 image = property(fget=_get_image, fset=BaseUIElement.image.fset)
178 def is_mouse_over(self) -> bool:
179 "Is the mouse over this button?"
180 return self.rect.collidepoint(pg.mouse.get_pos())
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()
186 def update(self, dt: float) -> None:
187 self._enable_hover = True
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)