Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): frontend prototype using egui #3

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
2,858 changes: 2,669 additions & 189 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["loki-client", "loki-server", "loki-shared", "lokui", "protolok"]
members = ["loki-client", "loki-frontend-prototype", "loki-server", "loki-shared", "lokui", "protolok"]
7 changes: 7 additions & 0 deletions loki-frontend-prototype/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "loki-frontend-prototype"
version = "0.1.0"
edition = "2021"

[dependencies]
eframe = "0.21.3"
660 changes: 660 additions & 0 deletions loki-frontend-prototype/LICENSE.md

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions loki-frontend-prototype/src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use std::collections::BTreeMap;

use eframe::egui;

use crate::{
id::{IdGenerator, UserId},
model::{Guild, User},
ui::views::main::MainView,
};

pub struct App {
view: View,
state: State,
}

impl App {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let mut id_gen = IdGenerator::new();

let current_user_id: UserId = id_gen.generate();

let mut state = State {
current_user: current_user_id,
guilds: vec![],
users: BTreeMap::new(),
id_gen,
};
state.users.insert(
current_user_id,
User {
id: current_user_id,
username: "John Doe".to_string(),
},
);
state.users.insert(
UserId(0),
User {
id: UserId(0),
username: "Invalid User".into(),
},
);

Self {
view: View::Main(MainView::default()),
state,
}
}
}

impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
match &mut self.view {
View::Main(view) => view.ui(ui, &mut self.state),
};
});
}
}

enum View {
Main(MainView),
}

pub struct State {
pub current_user: UserId,
pub guilds: Vec<Guild>,
pub users: BTreeMap<UserId, User>,
pub id_gen: IdGenerator,
}
67 changes: 67 additions & 0 deletions loki-frontend-prototype/src/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::{fmt::Display, time::SystemTime};

/// The
const EPOCH: SystemTime = SystemTime::UNIX_EPOCH;

pub trait Id {
fn value(&self) -> u64;
}

macro_rules! id {
($($name:ident,)*) => {
$(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct $name(pub u64);

impl Id for $name {
fn value(&self) -> u64 {
self.0
}
}

impl From<u64> for $name {
fn from(value: u64) -> Self {
Self(value)
}
}

impl Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
)*
};
}

id!(ChannelId, GuildId, MessageId, UserId,);

impl UserId {
pub const INVALID: UserId = UserId(0);
}

pub struct IdGenerator {
counter: u16,
}

impl IdGenerator {
pub fn new() -> Self {
Self { counter: 0 }
}

pub fn generate<T: Id + From<u64>>(&mut self) -> T {
self.counter = self.counter.overflowing_add(1).0;

// Using `SystemTime` is not ideal for us because it's not monotonic,
// meaning that if we use `now()` twice, the second time might return
// an earlier value than the first time. The chances of that causing
// problems here are unlikely though, so it's fine.
let time = (SystemTime::now().duration_since(EPOCH))
.unwrap()
.as_millis();

let value = ((time as u64) << 22) + (self.counter & 0b111111111111) as u64;

T::from(value)
}
}
14 changes: 14 additions & 0 deletions loki-frontend-prototype/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mod app;
mod id;
mod model;
mod ui;

pub fn run() {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"Loki (Prototype)",
native_options,
Box::new(|cc| Box::new(app::App::new(cc))),
)
.expect("Failed to create window");
}
3 changes: 3 additions & 0 deletions loki-frontend-prototype/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
loki_frontend_prototype::run();
}
38 changes: 38 additions & 0 deletions loki-frontend-prototype/src/model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::id::{ChannelId, GuildId, IdGenerator, MessageId, UserId};

pub struct Guild {
pub id: GuildId,
pub name: String,
pub channels: Vec<Channel>,
}

impl Guild {
pub fn new(id_gen: &mut IdGenerator, name: String) -> Self {
Self {
id: id_gen.generate(),
name,
channels: vec![Channel {
id: id_gen.generate(),
name: "general".into(),
messages: vec![],
}],
}
}
}

pub struct Channel {
pub id: ChannelId,
pub name: String,
pub messages: Vec<Message>,
}

pub struct Message {
pub id: MessageId,
pub author: UserId,
pub contents: String,
}

pub struct User {
pub id: UserId,
pub username: String,
}
1 change: 1 addition & 0 deletions loki-frontend-prototype/src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod views;
81 changes: 81 additions & 0 deletions loki-frontend-prototype/src/ui/views/guild.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use eframe::egui::{
CentralPanel, Frame, Margin, RichText, ScrollArea, SidePanel, TopBottomPanel, Ui,
};

use crate::{
app::State,
id::{ChannelId, GuildId, UserId},
model::{Message, User},
};

pub struct GuildView {
id: GuildId,
channel_id: ChannelId,
message_field: String,
}

impl GuildView {
pub fn new(id: GuildId, channel_id: ChannelId) -> Self {
Self {
id,
channel_id,
message_field: String::new(),
}
}

pub fn ui(&mut self, ui: &mut Ui, state: &mut State) {
let Some(guild) = state.guilds.iter_mut().find(|guild| guild.id == self.id) else {
ui.heading("Error");
ui.label(format!("Non-existent guild ID: {}", self.id));
return;
};

SidePanel::left("guild/sidebar").show_inside(ui, |ui| {
ui.vertical(|ui| {
ui.label(RichText::new(&guild.name).strong());
ui.separator();
for channel in &guild.channels {
ui.button(format!("#{}", channel.name));
}
});
});

let Some(channel) = guild.channels.iter_mut().find(|channel| channel.id == self.channel_id) else {
ui.heading("Error");
ui.label(format!("Non-existent channel ID: {}", self.channel_id));
return;
};

TopBottomPanel::bottom("guild/channel/bottom").show_inside(ui, |ui| {
ui.text_edit_multiline(&mut self.message_field);
if ui.button("Send").clicked() {
channel.messages.push(Message {
id: state.id_gen.generate(),
author: state.current_user,
contents: self.message_field.clone(),
});
self.message_field.clear();
}
});

CentralPanel::default().show_inside(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| {
for message in &channel.messages {
Frame::none()
.inner_margin(Margin::symmetric(0.0, 4.0))
.show(ui, |ui| {
// TODO: More reusable way of displaying an invalid
// user.
let invalid_user = User {
id: UserId::INVALID,
username: "Invalid User".into(),
};
let user = state.users.get(&message.author).unwrap_or(&invalid_user);
ui.strong(&user.username);
ui.label(&message.contents);
});
}
});
});
}
}
71 changes: 71 additions & 0 deletions loki-frontend-prototype/src/ui/views/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::default::Default;

use eframe::egui::{CentralPanel, RichText, SidePanel, Ui};

use crate::{app::State, id::GuildId, model::Guild};

use super::guild::GuildView;

pub struct MainView {
view: View,
}

impl MainView {
pub fn ui(&mut self, ui: &mut Ui, state: &mut State) {
SidePanel::left("main/sidebar").show_inside(ui, |ui| {
ui.vertical(|ui| {
if ui.button("Home").clicked() {
self.view = View::Home;
}
ui.separator();
// `.iter()` avoids unnecessary mutable borrow.
for guild in state.guilds.iter() {
if ui.button(&guild.name).clicked() {
self.view = View::Guild(GuildView::new(guild.id, guild.channels[0].id));
}
}
ui.separator();
if ui.button("Add guild").clicked() {
self.view = View::AddGuild(AddGuildView::default());
}
})
});

CentralPanel::default().show_inside(ui, |ui| match &mut self.view {
View::Home => {
ui.label("This might be where you access direct messages, manage friends, that kind of stuff.");
}
View::Guild(view) => view.ui(ui, state),
View::AddGuild(view) => view.ui(ui, state),
});
}
}

impl Default for MainView {
fn default() -> Self {
Self { view: View::Home }
}
}

enum View {
Home,
Guild(GuildView),
AddGuild(AddGuildView),
}

#[derive(Default)]
struct AddGuildView {
guild_name: String,
}

impl AddGuildView {
pub fn ui(&mut self, ui: &mut Ui, state: &mut State) {
ui.heading("Create guild");
ui.text_edit_singleline(&mut self.guild_name);
if ui.button("Create").clicked() {
state
.guilds
.push(Guild::new(&mut state.id_gen, self.guild_name.clone()));
}
}
}
2 changes: 2 additions & 0 deletions loki-frontend-prototype/src/ui/views/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod guild;
pub mod main;