436 lines
10 KiB
Python
Executable File
436 lines
10 KiB
Python
Executable File
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
|