diff --git a/degeon_py/CRC35.otf b/degeon_py/CRC35.otf new file mode 100755 index 0000000..9ec5817 Binary files /dev/null and b/degeon_py/CRC35.otf differ diff --git a/degeon_py/active_chat.py b/degeon_py/active_chat.py new file mode 100644 index 0000000..7d82732 --- /dev/null +++ b/degeon_py/active_chat.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import typing +from dataclasses import dataclass +import pygame + +from config import HEIGHT, WIDTH, CHAT_SELECTOR_WIDTH, DARKER_BLUE, CHAT_PREVIEW_HEIGHT, BLUE, WHITE, font, DARK_BLUE, \ + MESSAGE_HEIGHT +import degeon_core as dc + +from message import Message + + +@dataclass +class ActiveChat: + """ + The widget with the current chat + + Attributes: + :param chat (dc.Chat): the chat (Rust Chat type) + :param height (int): the height of the active chat widget + :param width (int): its width + :param delta_x (int): distance from the left edge of the application to the left edge of the screen + :param header_height (int): height of the header (the title) + """ + chat: dc.Chat + height: int = HEIGHT + width: int = WIDTH - CHAT_SELECTOR_WIDTH - 10 + delta_x: int = CHAT_SELECTOR_WIDTH + 10 + header_height: int = int(CHAT_PREVIEW_HEIGHT * 1.5) + + @classmethod + def new(cls, chat: dc.Chat, **kwargs) -> ActiveChat: + """ + Create a new `Chat` from a rust Chat object + :param chat: rusty chat + :param kwargs: optional other paraeters + :return: the `Chat` + """ + return cls(chat=chat, **kwargs) + + def get_messages(self) -> typing.Iterable[Message]: + """ + Get an iterator over all messages in this chat in the backwards order + This function creates a python `message.Message` object from rust instances + :return: an iterator of `message.Message` objects + """ + for msg in reversed(self.chat.messages): + yield Message(text=msg.get_content_py().text, is_from_me=False) + + def get_header(self) -> pygame.Surface: + """ + Render a pygame surface with the header. + The header is (for now) just a name of the user written on a background + + :return: the header + """ + surface: pygame.Surface = pygame.Surface((self.width, self.header_height)) + surface.fill(DARK_BLUE) + name_surface: pygame.Surface = font.render(self.chat.profile.name, True, WHITE) + surface.blit(name_surface, (20, 20)) + return surface + + def render(self) -> pygame.Surface: + """ + Creates a pygame surface and draws the chat on it + :return: the surface with the chat on it + """ + surface: pygame.Surface = pygame.Surface((self.width, self.height)) + surface.fill(DARKER_BLUE) + + # Render messages + # This is the y0 for the last message + last_message_y = self.height - 60 + for i, message in zip(range(30), self.get_messages()): + msg_surface = message.render() + surface.blit(msg_surface, (0, last_message_y - (MESSAGE_HEIGHT + 30) * (i + 1))) + # Render header + header = self.get_header() + surface.blit(header, (0, 10)) + return surface + + def process_event(self, event: pygame.event.Event): + """ + Process a click: select the necessary chat if this click is in the widget + :param event: a pygame event + """ + pass # todo diff --git a/degeon_py/chat_selector.py b/degeon_py/chat_selector.py new file mode 100644 index 0000000..78fc73a --- /dev/null +++ b/degeon_py/chat_selector.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import typing +from dataclasses import dataclass, field + +import pygame + +from config import CHAT_SELECTOR_WIDTH, HEIGHT, DARK_BLUE, WHITE, GREY, BLUE, font, CHAT_PREVIEW_HEIGHT, MEDIUM_BLUE + + +@dataclass +class ChatSelector: + """ + The widget with the list of chats. + It's a dataclass, it should be initialized using the `from_chats` classmethod + + Attributes: + chats (List[dc.Chat]): list of all chats, where each chat is a native Rust struct Chat + active_chat (int): the index of the current selected chat + hovered_chat (int or None): the index of the current hovered chat + width (int): the width of this widget + height (int): the height of this widget + chat_height (int): height of one chat + """ + chats: typing.List['dc.Chat'] = field(default_factory=list) + active_chat: int = -1 + hovered_chat: typing.Optional[int] = None + width: int = CHAT_SELECTOR_WIDTH + height: int = HEIGHT + chat_height: int = CHAT_PREVIEW_HEIGHT + + def render(self) -> pygame.Surface: + """ + Creates a pygame surface and draws the list of chats on it + :return: the surface with the chat selector + """ + surface: pygame.Surface = pygame.Surface((self.width, self.height)) + surface.fill(GREY) + for i, chat in enumerate(self.chats): + bg_color, text_color = DARK_BLUE, WHITE + if i == self.hovered_chat: + bg_color = MEDIUM_BLUE + if i == self.active_chat: + bg_color = BLUE + title_surface: pygame.Surface = font.render(chat.profile.name, True, text_color) + pygame.draw.rect(surface, bg_color, (3, i * self.chat_height + 1, self.width - 6, self.chat_height - 2)) + surface.blit(title_surface, (7, i * self.chat_height + 10)) + return surface + + def process_event(self, event: pygame.event.Event): + """ + Process a click: select the necessary chat if this click is in the widget + :param event: a pygame event + """ + if event.type == pygame.MOUSEBUTTONUP and event.pos[0] < self.width: + self.active_chat = event.pos[1] // self.chat_height + self.hovered_chat = None + if event.type == pygame.MOUSEMOTION: + if 0 < event.pos[0] < self.width \ + and 0 < event.pos[1] < min(self.height, len(self.chats) * self.chat_height) - 2: + self.hovered_chat = event.pos[1] // self.chat_height + else: + self.hovered_chat = None + + @classmethod + def from_chats(cls, chats: typing.List['dc.Chat'], **kwargs) -> ChatSelector: + return cls(chats, **kwargs) \ No newline at end of file diff --git a/degeon_py/config.py b/degeon_py/config.py new file mode 100644 index 0000000..48e871e --- /dev/null +++ b/degeon_py/config.py @@ -0,0 +1,31 @@ +import pygame + + +pygame.init() + +# Fontss +font = pygame.font.Font('CRC35.otf', 25) +font_large = pygame.font.Font('CRC35.otf', 35) + +# Colors used in the app +RED = 0xFF0000 +BLUE = 0x0000FF +YELLOW = 0xFFC91F +GREEN = 0x00FF00 +MAGENTA = 0xFF03B8 +CYAN = 0x00FFCC +BLACK = 0x000 +WHITE = 0xFFFFFF +MEDIUM_BLUE = 0x2f2f4e +DARK_BLUE = 0x282e46 +DARKER_BLUE = 0x202033 +GREY = 0x383e4F + +# Geometrical parameters +WIDTH = 1000 +HEIGHT = 800 +CHAT_PREVIEW_HEIGHT = 80 +CHAT_SELECTOR_WIDTH = WIDTH // 3 +MESSAGE_HEIGHT = 60 + +FPS = 30 diff --git a/degeon_py/degeon.py b/degeon_py/degeon.py new file mode 100644 index 0000000..4d3aa9c --- /dev/null +++ b/degeon_py/degeon.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +import typing +import pygame + +from chat_selector import ChatSelector +from active_chat import ActiveChat +from config import FPS, DARKER_BLUE, font, WHITE, WIDTH, CHAT_SELECTOR_WIDTH, HEIGHT + +import degeon_core as dc + + +@dataclass +class Degeon: + """ + The main class with everything connected to the app: the data, the + + Attributes + """ + core: 'dc.Degeon' + chat_selector: ChatSelector + active_chat: typing.Optional[ActiveChat] = None + has_profile_popup_opened: bool = False + has_no_peers_popup: bool = False + clock: pygame.time.Clock = field(default_factory=pygame.time.Clock) + fps: int = FPS + + @classmethod + def new(cls) -> Degeon: + """ + Create a new default instance with settings from file + :return: a Degeon instance + """ + core: dc.Degeon = dc.new_degeon() + chat_selector = ChatSelector() + return cls(core=core, chat_selector=chat_selector) + + def render(self, screen: pygame.Surface): + """ + Render everything on the screen + :param screen: the main screen + """ + chats_surface = self.chat_selector.render() + screen.blit(chats_surface, (0, 0)) + if self.active_chat is not None: + active_chat_surface = self.active_chat.render() + screen.blit(active_chat_surface, (self.active_chat.delta_x, 0)) + else: + text_surface: pygame.Surface = font.render('<- Select chat in the menu', True, WHITE) + screen.blit(text_surface, + (round(WIDTH / 2 + CHAT_SELECTOR_WIDTH / 2 - text_surface.get_width() / 2), HEIGHT // 2)) + + def process_core_messages(self): + """ + Do all the necessary Rust work + """ + pass # todo + + def tick(self): + """ + Handle incoming messages, update chats, create no_peers popup if necessary + """ + # process events in core + self.process_core_messages() + self.chat_selector.chats = self.core.chats + if 0 <= self.chat_selector.active_chat < len(self.chat_selector.chats): + self.active_chat = ActiveChat.new(self.chat_selector.chats[self.chat_selector.active_chat]) + else: + self.active_chat = None + + def process_event(self, event: pygame.event.Event): + """ + Process an event + :param event: pygame event + """ + self.chat_selector.process_event(event) + + def main_loop(self, screen: pygame.Surface): + """ + Drawing everything and handling events + """ + while True: + screen.fill(DARKER_BLUE) + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + self.process_event(event) + self.tick() + self.render(screen) + self.clock.tick(self.fps) + pygame.display.update() diff --git a/degeon_py/main.py b/degeon_py/main.py new file mode 100644 index 0000000..ab0fcaa --- /dev/null +++ b/degeon_py/main.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import pygame +from degeon import Degeon +from config import WIDTH, HEIGHT + + +deg = Degeon.new() +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +deg.main_loop(screen) +pygame.quit() diff --git a/degeon_py/message.py b/degeon_py/message.py new file mode 100644 index 0000000..b3ba57a --- /dev/null +++ b/degeon_py/message.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import pygame + +from config import WIDTH, CHAT_SELECTOR_WIDTH, MESSAGE_HEIGHT, font, BLUE, DARK_BLUE, WHITE, DARKER_BLUE + + +@dataclass +class Message: + """ + The message (for now, it consists only of text) + + Attributes: + :param text (str): the message text + :param is_from_me (bool): False if the message is not from the current user + :param chat_width (int): the width of the active chat widget + """ + text: str + is_from_me: bool + chat_width: int = WIDTH - CHAT_SELECTOR_WIDTH - 10 + height: int = MESSAGE_HEIGHT + + def render(self) -> pygame.Surface: + """ + Creates a surface with a rectangle and the message text written on it + :return: the surface with rendered message + """ + surface = pygame.Surface((self.chat_width, self.height)) + surface.fill(DARKER_BLUE) + bg_color = BLUE * self.is_from_me + DARK_BLUE * (not self.is_from_me) + text_surface: pygame.Surface = font.render(self.text, True, WHITE) + padding = 5 + # Size of the scaled text surface + blit_height = self.height - padding * 2 + blit_width = round(text_surface.get_width() * blit_height / text_surface.get_height()) + x = 0 if not self.is_from_me else self.chat_width - blit_width - padding * 2 + pygame.draw.rect(surface, bg_color, (x, 0, blit_width + padding * 2, self.height)) + text_surface = pygame.transform.smoothscale(text_surface, (blit_width, blit_height)) + surface.blit(text_surface, (x + padding, padding)) + return surface