Lev
3 years ago
7 changed files with 331 additions and 0 deletions
Binary file not shown.
@ -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 |
@ -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) |
@ -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 |
@ -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() |
@ -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() |
@ -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 |
Loading…
Reference in new issue