--- /dev/null
+import pygame
+
+
+class EventMethodDispatcher:
+ def handle_event(self, ev):
+ method_name = f"handle_{pygame.event.event_name(ev.type).lower()}"
+ if hasattr(self, method_name):
+ getattr(self, method_name)(ev)
+
+
+class UIParent(EventMethodDispatcher):
+ BACKGROUND_COLOR: pygame.Color
+
+ def __init__(self, surf, font=None):
+ super().__init__()
+ self.surf = surf
+ self.font = font
+ self.running = True
+ self.dirty = False
+ self.clock = pygame.time.Clock()
+ self.children = []
+
+ def handle_quit(self, _):
+ self.running = False
+
+ def handle_windowexposed(self, _):
+ self.dirty = True
+
+ handle_activeevent = handle_windowexposed
+
+ def handle_keydown(self, ev):
+ if ev.key == pygame.K_ESCAPE:
+ self.running = False
+ return
+
+ def handle_event(self, ev):
+ super().handle_event(ev)
+ if not self.running:
+ return
+ for child in self.children:
+ child.handle_event(ev)
+ if not self.running:
+ break
+
+ def update(self):
+ for child in self.children:
+ child.update()
+
+ def draw(self):
+ if hasattr(self, "BACKGROUND_COLOR"):
+ self.surf.fill(self.BACKGROUND_COLOR)
+ for child in self.children:
+ child.draw()
+
+ def run(self):
+ while True:
+ for ev in pygame.event.get():
+ self.handle_event(ev)
+ if not self.running:
+ return
+ self.update()
+ if self.dirty:
+ self.draw()
+ pygame.display.update()
+ self.dirty = False
+ self.clock.tick(60)
+
+
+class UIChild(EventMethodDispatcher):
+ parent: "UIParent"
+
+ def __init__(self, parent):
+ self.parent = parent
+
+ @property
+ def dirty(self):
+ return self.parent.dirty
+
+ @dirty.setter
+ def dirty(self, value):
+ self.parent.dirty = value
+
+ @property
+ def font(self):
+ return self.parent.font
+
+ @property
+ def surf(self):
+ return self.parent.surf
+
+ def draw(self):
+ pass
+
+ def update(self):
+ pass
+
+
+class Button(UIChild):
+ def __init__(self, parent, rect, label, callback, is_active=None):
+ super().__init__(parent)
+ self.rect = rect
+ self.label = label
+ self.callback = callback
+ self.is_active = is_active
+ self.pushed = False
+
+ def draw(self):
+ if not self.pushed:
+ label_color = (
+ "lime" if callable(self.is_active) and self.is_active() else "gray"
+ )
+ frame_color = label_color
+ else:
+ pygame.draw.rect(self.parent.surf, "darkgray", self.rect)
+ frame_color = "lightgray"
+ label_color = "black"
+ label = self.label
+ if callable(label):
+ label = label()
+ fs = self.parent.font.render(label, True, label_color)
+ pygame.draw.rect(self.parent.surf, frame_color, self.rect, 8)
+ fs_size = fs.get_size()
+ center = self.rect.center
+ self.parent.surf.blit(
+ fs, (center[0] - fs_size[0] // 2, center[1] - fs_size[1] // 2)
+ )
+
+ def handle_mousebuttondown(self, ev):
+ if ev.button == 1 and self.rect.collidepoint(ev.pos):
+ self.pushed = True
+ self.parent.dirty = True
+
+ def handle_mousebuttonup(self, ev):
+ if ev.button == 1 and self.pushed and self.rect.collidepoint(ev.pos):
+ self.pushed = False
+ self.callback()
+ self.parent.dirty = True
+
+ def handle_mousemotion(self, ev):
+ if not self.pushed:
+ return
+ if ev.buttons[0] and not self.rect.collidepoint(ev.pos):
+ self.pushed = False
+ self.parent.dirty = True