diff --git a/client/Cargo.toml b/client/Cargo.toml index ab7eb0b..63dce5f 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,4 +6,12 @@ edition = "2021" [dependencies] realm_auth = { path = "../auth" } realm_server = { path = "../server" } -realm_shared = { path = "../shared" } \ No newline at end of file +realm_shared = { path = "../shared" } +egui = "0.28" +eframe = { version = "0.28", default-features = false, features = [ + "default_fonts", # Embed the default egui fonts. + "glow", # Use the glow rendering backend. Alternative: "wgpu". + "persistence", # Enable restoring app state when restarting the app. +] } +serde = { version = "1", features = ["derive"] } +env_logger = "0.11.5" \ No newline at end of file diff --git a/client/assets/icon-256.png b/client/assets/icon-256.png new file mode 100644 index 0000000..ae72287 Binary files /dev/null and b/client/assets/icon-256.png differ diff --git a/client/src/app.rs b/client/src/app.rs new file mode 100644 index 0000000..00e88eb --- /dev/null +++ b/client/src/app.rs @@ -0,0 +1,66 @@ +use crate::ui::panels; + +/// We derive Deserialize/Serialize so we can persist app state on shutdown. +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(default)] // if we add new fields, give them default values when deserializing old state +pub struct TemplateApp { + // Example stuff: + pub label: String, + pub selected: bool, + pub selected_serverid: Option, + pub selected_roomid: Option, + + #[serde(skip)] // This how you opt-out of serialization of a field + pub value: f32, +} + +impl Default for TemplateApp { + fn default() -> Self { + Self { + // Example stuff: + label: "Hello World!".to_owned(), + selected: false, + selected_serverid: None, + selected_roomid: None, + value: 2.7, + } + } +} + +impl TemplateApp { + /// Called once before the first frame. + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + // This is also where you can customize the look and feel of egui using + // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. + + // Load previous app state (if any). + // Note that you must enable the `persistence` feature for this to work. + if let Some(storage) = cc.storage { + return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); + } + + Default::default() + } +} + +impl eframe::App for TemplateApp { + /// Called each time the UI needs repainting, which may be many times per second. + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. + // For inspiration and more examples, go to https://emilk.github.io/egui + + // File -> Quit + panels::top_panel(ctx); + + panels::servers(self, ctx); + + panels::rooms(self, ctx); + + panels::messages(self, ctx) + } + + /// Called by the frame work to save state before shutdown. + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value(storage, eframe::APP_KEY, self); + } +} \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index d1b8c2a..3c75466 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,2 +1,4 @@ pub mod client; -pub mod types; \ No newline at end of file +pub mod types; +pub mod app; +pub mod ui; \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index e7a11a9..464d2a9 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,3 +1,20 @@ -fn main() { - println!("Hello, world!"); +fn main() -> eframe::Result { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([720.0, 500.0]) + .with_min_inner_size([500.0, 300.0]) + .with_icon( + // NOTE: Adding an icon is optional + eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..]) + .expect("Failed to load icon"), + ), + ..Default::default() + }; + eframe::run_native( + "Realm", + native_options, + Box::new(|cc| Ok(Box::new(realm_client::app::TemplateApp::new(cc)))), + ) } diff --git a/client/src/ui/mod.rs b/client/src/ui/mod.rs new file mode 100644 index 0000000..25966b0 --- /dev/null +++ b/client/src/ui/mod.rs @@ -0,0 +1 @@ +pub mod panels; \ No newline at end of file diff --git a/client/src/ui/panels.rs b/client/src/ui/panels.rs new file mode 100644 index 0000000..d0af77b --- /dev/null +++ b/client/src/ui/panels.rs @@ -0,0 +1,64 @@ +use egui::{Context, SelectableLabel}; +use crate::app::TemplateApp; + +pub fn top_panel(ctx: &Context) { + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + // The top panel is often a good place for a menu bar: + + egui::menu::bar(ui, |ui| { + // NOTE: no File->Quit on web pages! + let is_web = cfg!(target_arch = "wasm32"); + if !is_web { + ui.menu_button("File", |ui| { + if ui.button("Quit").clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + ui.add_space(16.0); + } + + egui::widgets::global_dark_light_mode_buttons(ui); + }); + }); +} + +pub fn servers(app: &mut TemplateApp, ctx: &Context) { + egui::SidePanel::left("servers").show(ctx, |ui| { + ui.heading("Servers"); + ui.separator(); + + if ui.add(SelectableLabel::new(app.selected, "server")).clicked() { + app.selected = !app.selected; + } + }); +} + +pub fn rooms(app: &mut TemplateApp, ctx: &Context) { + egui::SidePanel::left("rooms").show(ctx, |ui| { + ui.heading("Rooms"); + ui.separator(); + + if ui.add(SelectableLabel::new(app.selected, "room")).clicked() { + app.selected = !app.selected; + } + }); +} + +pub fn messages(app: &mut TemplateApp, ctx: &Context) { + egui::CentralPanel::default().show(ctx, |ui| { + // The central panel the region left after adding TopPanel's and SidePanel's + ui.heading("eframe template"); + + ui.horizontal(|ui| { + ui.label("Write something: "); + ui.text_edit_singleline(&mut app.label); + }); + + ui.add(egui::Slider::new(&mut app.value, 0.0..=10.0).text("value")); + if ui.button("Increment").clicked() { + app.value += 1.0; + } + + ui.separator(); + }); +} \ No newline at end of file