import math import pygame import json from typing import Self from typing import Tuple class Position: def __init__(self, x = 0, y = 0): self.x = x self.y = y def __add__(self, position: Self) -> Self: return Position(self.x + position.x, self.y + position.y) def __sub__(self, position: Self) -> Self: return Position(self.x - position.x, self.y - position.y) def __mul__(self, position: Self) -> Self: return Position(self.x * position.x, self.y * position.y) def __str__(self): return 'Position(%d, %d)' % (self.x, self.y) def as_tuple(self) -> Tuple: return (self.x, self.y) def get_distance(self, position: Self) -> Self: delta = position - self return math.sqrt(math.pow(delta.x, 2) + math.pow(delta.y, 2)) def get_duplicate(self) -> Self: return Position(self.x, self.y) @staticmethod def from_tuple(tuple) -> Self: return Position(tuple[0], tuple[1]) class Color: def __init__(self, red, green, blue): self.red = red self.green = green self.blue = blue def as_tuple(self) -> Tuple: return (self.red, self.green, self.blue) class Dimensions: def __init__(self, width, height): self.width = width self.height = height def __add__(self, dimensions: Self): return Dimensions(self.width + dimensions.width, self.height + dimensions.height) def as_tuple(self): return self.width, self.height @staticmethod def from_tuple(tuple: Tuple): return Dimensions(tuple[0], tuple[1]) class Drawable: def __init__(self, dimensions = Dimensions(0, 0)): self.surface = pygame.Surface(dimensions.as_tuple(), pygame.SRCALPHA) def draw_inside(self, drawable: Self, position: Position): self.surface.blit(drawable.surface, position.as_tuple()) def clear(self, color = Color(0, 0, 0)): if color is None: self.surface.fill(pygame.Color(0, 0, 0, 0)) return self.surface.fill(color.as_tuple()) def get_image(self): image = Image() image.surface = self.surface return image def get_dimensions(self) -> Dimensions: return Dimensions.from_tuple(self.surface.get_size()) def set_transparency(self, transparency: float): self.surface.set_alpha(255 - 255 * transparency) class Image(Drawable): def __init__(self, path = None): Drawable.__init__(self) if path is not None: self.surface = pygame.image.load(path) class Canvas(Drawable): def __init__(self, size: Dimensions): Drawable.__init__(self, size) class Screen: def __init__(self, title: str, resolution: Dimensions): self.resolution = resolution self.screen = pygame.display.set_mode(resolution.as_tuple(), pygame.NOFRAME | pygame.FULLSCREEN | pygame.DOUBLEBUF) pygame.display.set_caption(title) def get_canvas(self): canvas = Canvas(self.resolution) canvas.surface = pygame.display.get_surface() return canvas def update(self) -> None: pygame.dispay.flip() @staticmethod def get_screen_resolutions(): resolutions = [] for mode in pygame.display.list_modes(): resolutions.append(Dimensions.from_tuple(mode)) return resolutions class Visualization: def __init__(self, canvas: Canvas, position: Position): self.canvas = canvas self.position = position self.on_hover = lambda : None def draw(self, canvas: Canvas) -> None: canvas.draw_inside(self.canvas, self.position) def set_transparency(self, transparency: float): self.canvas.set_transparency(transparency) def get_dimensions(self) -> Dimensions: return self.canvas.get_dimensions() def is_inside(self, position: Position) -> bool: if position.x < self.position.x: return False if position.y < self.position.y: return False dimensions = self.canvas.get_dimensions() if position.x > self.position.x + dimensions.width: return False if position.y > self.position.y + dimensions.height: return False return True class Clickable(Visualization): def __init__(self, surface: Canvas, position: Position): Visualization.__init__(self, surface, position) self.on_click = lambda : None def click(self) -> None: self.on_click() class Dragable(Clickable): def __init__(self, canvas: Canvas, position: Position): Clickable.__init__(self, canvas, position) self.on_drag = lambda: None self.on_drop = lambda dropable: None self.on_fail = lambda drag: None def drag(self) -> None: self.on_drag() def drop(self, dropable: 'Dropable') -> None: self.on_drop(dropable) def fail(self, drag: 'Drag'): self.on_fail(drag) class Container(Visualization): def __init__(self, position: Position, dimensions: Dimensions): Visualization.__init__(self, Canvas(dimensions), position) self.background_image = None self.background_color = Color(0, 0, 0) self.visualizations = [] self.clickables = [] self.dropables = [] def set_background_image(self, image: Image) -> None: self.background_image = image def render(self) -> None: self.canvas.clear(self.background_color) if self.background_image is not None: self.canvas.draw_inside(self.background_image, Dimensions(0, 0)) for visualization in self.visualizations: visualization.draw(self.canvas) def add_visualization(self, visualization: Visualization) -> None: self.visualizations.append(visualization) def add_clickable(self, clickable: Clickable) -> None: self.add_visualization(clickable) self.clickables.append(clickable) def add_dropable(self, dropable: 'Dropable'): self.add_visualization(dropable) self.dropables.append(dropable) class Dropable(Container): def __init__(self, position: Position, dimensions: Dimensions): Container.__init__(self, position, dimensions) self.on_drop = lambda dragable: None def drop(self, dragable: Dragable) -> None: self.on_drop(dragable) class Text(Visualization, Drawable): def __init__(self, text: str, size: int = 12, font_family: str = 'freesansbold.ttf', color: Color = Color(255, 255, 255)): font = pygame.font.Font(font_family, size) self.surface = font.render(text, True, color.as_tuple()) Visualization.__init__( self, Canvas(Dimensions.from_tuple(self.surface.get_size())), Position(0, 0) ) self.canvas.surface = self.surface class Drag: def __init__(self, dragable: Dragable, cursor_offset: Position): self.dragable = dragable self.origin = dragable.position.get_duplicate() self.cursor_offset = cursor_offset class UI: def __init__(self, title: str, resolution: Dimensions): pygame.init() self.is_running = True self.clock = pygame.time.Clock() self.screen = Screen(title, resolution) self.canvas = self.screen.get_canvas() self.drag = None self.containers = [] def init(self) -> None: pass def update(self) -> None: self.canvas.clear() for container in self.containers: container.render() container.draw(self.canvas) pygame.display.update() def add_container(self, container: Container) -> None: self.containers.append(container) def exit(self) -> None: self.is_running = False def main(self) -> None: while self.is_running: event = pygame.event.wait() if event.type == pygame.QUIT: self.exit() elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.exit() elif event.type == pygame.MOUSEBUTTONDOWN: self._handle_click(event) elif event.type == pygame.MOUSEBUTTONUP: self._handle_drop(event) elif event.type == pygame.MOUSEMOTION: self._handle_mouse_motion(event) self.update() pygame.quit() def _handle_click(self, event: pygame.event.Event) -> None: clickables_clicked = self._get_clickables_clicked(event) if len(clickables_clicked) == 0: return clickable = clickables_clicked[-1] if self._is_in_filter(clickable, [Dragable]): self.drag = Drag( clickable, Position.from_tuple(event.pos) - clickable.position ) clickable.click() def _handle_drag(self, event: pygame.event.Event) -> None: position_cursor = Position.from_tuple(event.pos) position = position_cursor - self.drag.cursor_offset self.drag.dragable.position = position self.drag.dragable.drag() def _handle_drop(self, event: pygame.event.Event) -> None: if self.drag is None: return position_cursor = Position.from_tuple(event.pos) dropables_hovered = self._get_visualizations_hovered(position_cursor, [Dropable]) if len(dropables_hovered) == 0: self.drag.dragable.fail(self.drag) self.drag.dragable.position = self.drag.origin else: dropables_hovered[-1].drop(self.drag.dragable) self.drag.dragable.drop(dropables_hovered[-1]) self.drag = None def _handle_mouse_motion(self, event: pygame.event.Event) -> None: if type(self.drag) is Drag: self._handle_drag(event) def _get_visualizations_hovered(self, cursor_position: Position, filter: list = []) -> list[Visualization]: containers_hovered = [] visualizations_hovered = [] for container in self.containers: if not container.is_inside(cursor_position): continue containers_hovered.append(container) for container_hovered in containers_hovered: for visualization in container_hovered.visualizations: if not self._is_in_filter(visualization, filter): continue if visualization.is_inside(cursor_position): visualizations_hovered.append(visualization) return visualizations_hovered def _get_clickables_clicked(self, event: pygame.event.Event) -> list[Clickable]: clickables = self._get_visualizations_hovered( Position.from_tuple(event.pos), [Clickable, Dragable] ) print(clickables) return clickables def _is_in_filter(self, visualization: Visualization, filter: list[Visualization] = []) -> bool: if len(filter) == 0: return True for f in filter: for parent in visualization.__class__.mro(): if parent is f: return True return False class ListItem(Dragable): def __init__(self, canvas: Canvas): Dragable.__init__(self, canvas, Position(0, 0)) class ItemList(Dropable): def __init__(self, position: Position, dimensions: Dimensions): Dropable.__init__(self, position, dimensions) def add_item(self, item: ListItem) -> None: self.add_dropable(item) class ResourceContainer: def __init__(self): self.images = {} def add_image(self, name, url) -> None: self.images[name] = Image(url) def get_image(self, name) -> Image: return self._get_resource(self.images, name) def _get_resource(self, resources: list, name: str): try: return resources[name] except KeyError: raise Exception('Resource "%s" not found!' % (name)) @staticmethod def from_json_file(json_file) -> Self: with open(json_file, 'r') as file: data = json.loads(file.read()) resources = ResourceContainer() for image in data['images']: resources.add_image(image, data['images'][image]) return resources