This commit is contained in:
Tiago Batista Cardoso
2025-11-04 23:26:45 +01:00
commit d4126675bd
36 changed files with 5446 additions and 0 deletions

4510
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "enxos"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
dirs = "6.0.0"
ggez = "0.9.3"
hex = "0.4.3"
once_cell = "1.21.3"
rand = "0.9.2"
reqwest = { version = "0.12.24", features = ["json"] }
rust-embed = "8.7.2"
serde = "1.0.228"
serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["full"] }

BIN
assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
assets/button_bg_32x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

BIN
assets/button_bg_50x50.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

BIN
assets/button_bg_65x30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

BIN
assets/close_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

BIN
assets/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

BIN
assets/dialog_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Binary file not shown.

BIN
assets/games_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

BIN
assets/games_icon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

BIN
assets/minimize_button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

BIN
assets/network.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/no.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

BIN
assets/settings_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

BIN
assets/start_button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/start_menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

154
src/main.rs Normal file
View File

@@ -0,0 +1,154 @@
mod net;
mod ui;
use crate::{
net::networkmanager::NETWORK_MANAGER,
ui::{
elements::{
button::{Button, START_BUTTON},
start_menu::StartMenu,
},
utils::assets::*,
windows::window_manager::{self, WindowManager},
},
};
use std::{env, path};
use ggez::{
Context, GameError, GameResult, event,
graphics::{self, Color, DrawParam, FontData, Image},
input::mouse::{self},
mint::Point2,
};
pub struct State {
cursor: Image,
bg: Image,
network: Image,
network_no: Image,
window_manager: WindowManager,
start_button: Button,
mouse_pos: Point2<f32>,
start_menu: StartMenu,
}
impl State {
pub fn new(ctx: &mut Context) -> GameResult<State> {
let bg = Image::from_bytes(ctx, &get_asset("bg.png").expect("Couldn't load bg.png"))?;
let cursor = Image::from_bytes(
ctx,
&get_asset("cursor.png").expect("Couldn't load cursor.png"),
)?;
let network = Image::from_bytes(
ctx,
&get_asset("network.png").expect("Couldn't load network.png"),
)?;
let network_no =
Image::from_bytes(ctx, &get_asset("no.png").expect("Couldn't load no.png"))?;
let start_button =
Button::new(START_BUTTON, ctx, None, None).expect("Error initializing start button")?;
let start_menu = StartMenu::new(ctx)?;
let window_manager = WindowManager::new();
Ok(State {
cursor,
bg,
network,
network_no,
window_manager,
start_button,
mouse_pos: Point2::from([0.0, 0.0]),
start_menu,
})
}
}
impl event::EventHandler<GameError> for State {
fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
self.mouse_pos = mouse::MouseContext::position(&ctx.mouse);
if ctx.mouse.button_just_pressed(mouse::MouseButton::Left) {
let res = self.start_button.update(&ctx.mouse);
if res {
self.start_menu.toggle();
} else {
if self.start_menu.visible() && !self.start_menu.click_in_bounds(self.mouse_pos) {
self.start_menu.toggle();
}
}
}
if self.start_menu.visible() {
self.start_menu.update(ctx, &mut self.window_manager);
}
self.window_manager.update(ctx);
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
let mut canvas = graphics::Canvas::from_frame(ctx, Color::from([0.0, 0.0, 0.0, 1.0]));
canvas.draw(&self.bg, DrawParam::default());
self.start_menu.draw(&mut canvas);
self.window_manager.draw(&mut canvas);
self.start_button.draw(&mut canvas);
let network_drawparam = DrawParam::default().dest([750.0, 565.0]);
canvas.draw(&self.network, network_drawparam);
if !NETWORK_MANAGER.is_online() {
canvas.draw(&self.network_no, network_drawparam);
}
// Cursor is drawn last
canvas.draw(
&self.cursor,
DrawParam::default().dest([self.mouse_pos.x, self.mouse_pos.y]),
);
Ok(canvas.finish(ctx)?)
}
}
fn main() -> GameResult {
//unsafe { env::set_var("RUST_BACKTRACE", "1") };
let resource_path = if env::var_os("CARGO_MANIFEST_DIR").is_some() {
path::PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap())
} else {
path::PathBuf::from(".")
};
let (mut ctx, event_loop) = ggez::ContextBuilder::new("enxos", "enx")
.window_setup(ggez::conf::WindowSetup::default().title("enxos"))
.window_mode(
ggez::conf::WindowMode::default()
.dimensions(800.0, 600.0)
.resizable(false),
)
.add_resource_path(resource_path)
.build()?;
//mouse::set_cursor_grabbed(&mut ctx, true)?;
mouse::set_cursor_hidden(&mut ctx, true);
let msw98reg = FontData::from_vec(
get_asset_vec("fonts/MSW98UI-Regular.ttf").expect("Couldn't load font."),
)?;
let msw98bold =
FontData::from_vec(get_asset_vec("fonts/MSW98UI-Bold.ttf").expect("Couldn't load font."))?;
ctx.gfx.add_font("msw98-bold", msw98bold);
ctx.gfx.add_font("msw98-reg", msw98reg);
let state = State::new(&mut ctx)?;
event::run(ctx, event_loop, state);
}

1
src/net/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod networkmanager;

120
src/net/networkmanager.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::{
fs::{self, File},
io::Read,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use once_cell::sync::Lazy;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::{runtime::Runtime, time};
pub(crate) static NETWORK_MANAGER: Lazy<Arc<NetworkManager>> =
Lazy::new(|| Arc::new(NetworkManager::new()));
pub(crate) static SERVER_STATUS: Lazy<Arc<AtomicBool>> =
Lazy::new(|| Arc::new(AtomicBool::new(false)));
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UserName {
pub(crate) username: String,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UID {
user_id: String,
}
pub struct NetworkManager {
client: Client,
runtime: Runtime,
}
impl NetworkManager {
fn new() -> Self {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let nwm = Self {
client: Client::new(),
runtime,
};
nwm
}
async fn fetch_user_data(&self) -> Result<UserName, Box<dyn std::error::Error>> {
let mut userid_path = dirs::home_dir().expect("Couldn't find home directory");
userid_path.push(".enxos");
fs::create_dir_all(&userid_path)?;
userid_path.push(".userid");
let user_id = if userid_path.exists() {
let mut file = File::open(&userid_path)?;
let mut user_id = String::new();
file.read_to_string(&mut user_id)?;
user_id.trim().to_string()
} else {
// no ~/.enxos/.userid folder, user is not registered, we need to register to the
// server
let response = self
.client
.post("http://localhost:8801/u/register")
.send()
.await?;
let body_bytes = response.bytes().await?;
let response: UID = serde_json::from_slice(&body_bytes).expect("json error");
fs::write(&userid_path, &response.user_id)?;
response.user_id
};
let response = self
.client
.post("http://localhost:8801/u/profile")
.json(&UID { user_id })
.send()
.await?;
let body_bytes = response.bytes().await?;
let user_profile: UserName = serde_json::from_slice(&body_bytes).expect("json error");
Ok(user_profile)
}
async fn ping(&self) -> Result<bool, Box<dyn std::error::Error>> {
let response = self.client.get("http://localhost:8801/").send().await?;
println!("response : {}", response.status().as_str());
Ok(response.status().as_str().contains("200"))
}
pub fn get_user_data(&self) -> UserName {
let user_profile = self.fetch_user_data();
SERVER_STATUS.store(true, Ordering::SeqCst);
self.runtime.block_on(async {
match user_profile.await {
Ok(up) => {
SERVER_STATUS.store(true, Ordering::SeqCst);
up
}
Err(_) => {
SERVER_STATUS.store(false, Ordering::SeqCst);
UserName {
username: "offline".to_owned(),
}
}
}
})
}
pub fn is_online(&self) -> bool {
SERVER_STATUS.load(Ordering::SeqCst)
}
}

298
src/ui/elements/button.rs Normal file
View File

@@ -0,0 +1,298 @@
use ggez::{
Context, GameResult,
graphics::{Canvas, Color, DrawParam, Image, Text, TextFragment},
input::mouse::MouseContext,
};
use crate::ui::utils::assets::*;
pub(crate) static START_BUTTON: i32 = 1000;
pub(crate) static GAMES_BUTTON: i32 = 1001;
pub(crate) static SETTINGS_BUTTON: i32 = 1002;
pub(crate) static SHUTDOWN_BUTTON: i32 = 1003;
pub(crate) static PROFILE_BUTTON: i32 = 1004;
pub(crate) static YES_BUTTON: i32 = 1005;
pub(crate) static NO_BUTTON: i32 = 1006;
pub struct Button {
x: f32,
y: f32,
w: f32,
h: f32,
bid: i32,
sprite: Image,
text: Option<Text>,
last_press: f32,
}
fn create_start_button(ctx: &mut Context) -> GameResult<Button> {
let x = 0.0;
let y = 564.0;
let w = 76.0;
let h = 32.0;
let frag = TextFragment::new("Start")
.font("msw98-bold")
.scale(15.0)
.color(Color::BLACK);
let text = Some(Text::new(frag));
let sprite = Image::from_bytes(
ctx,
&get_asset("start_button.png").expect("Couldn't load bg.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: START_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
fn create_games_button(ctx: &mut Context) -> GameResult<Button> {
let x = 6.0;
let y = 514.0;
let w = 32.0;
let h = 32.0;
let frag = TextFragment::new("Games")
.font("msw98-reg")
.scale(12.0)
.color(Color::BLACK);
let text = Some(Text::new(frag));
let sprite = Image::from_bytes(
ctx,
&get_asset("games_icon.png").expect("Couldn't load games_icon.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: GAMES_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
fn create_settings_button(ctx: &mut Context) -> GameResult<Button> {
let x = 54.0;
let y = 514.0;
let w = 32.0;
let h = 32.0;
let frag = TextFragment::new("Settings")
.font("msw98-reg")
.scale(12.0)
.color(Color::BLACK);
let text = Some(Text::new(frag));
let sprite = Image::from_bytes(
ctx,
&get_asset("settings_icon.png").expect("Couldn't load setings_icon.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: SETTINGS_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
fn create_shutdown_button(ctx: &mut Context) -> GameResult<Button> {
let x = 229.0;
let y = 520.0;
let w = 65.0;
let h = 30.0;
let frag = TextFragment::new("Shutdown")
.font("msw98-reg")
.scale(13.0)
.color(Color::BLACK);
let text = Some(Text::new(frag));
let sprite = Image::from_bytes(
ctx,
&get_asset("button_bg_65x30.png").expect("Couldn't load button_bg_65x30.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: SHUTDOWN_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
fn create_profile_button(ctx: &mut Context) -> GameResult<Button> {
let x = 6.0;
let y = 206.0;
let w = 50.0;
let h = 50.0;
let text = None;
let sprite = Image::from_bytes(
ctx,
&get_asset("button_bg_50x50.png").expect("Couldn't load button_bg_50x50.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: PROFILE_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
fn create_yes_button(ctx: &mut Context, x: f32, y: f32) -> GameResult<Button> {
let w = 32.0;
let h = 16.0;
let frag = TextFragment::new("Yes")
.font("msw98-reg")
.scale(12.0)
.color(Color::BLACK);
let text = Some(Text::new(frag));
let sprite = Image::from_bytes(
ctx,
&get_asset("button_bg_32x16.png").expect("Couldn't load button_bg_32x16.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: YES_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
fn create_no_button(ctx: &mut Context, x: f32, y: f32) -> GameResult<Button> {
let w = 32.0;
let h = 16.0;
let frag = TextFragment::new("No")
.font("msw98-reg")
.scale(12.0)
.color(Color::BLACK);
let text = Some(Text::new(frag));
let sprite = Image::from_bytes(
ctx,
&get_asset("button_bg_32x16.png").expect("Couldn't load button_bg_32x16.png"),
)?;
Ok(Button {
x,
y,
w,
h,
bid: NO_BUTTON,
sprite,
text,
last_press: 0.0,
})
}
impl Button {
pub fn new(
bid: i32,
ctx: &mut Context,
x: Option<f32>,
y: Option<f32>,
) -> Option<GameResult<Button>> {
match bid {
1000 => Some(create_start_button(ctx)),
1001 => Some(create_games_button(ctx)),
1002 => Some(create_settings_button(ctx)),
1003 => Some(create_shutdown_button(ctx)),
1004 => Some(create_profile_button(ctx)),
1005 => Some(create_yes_button(ctx, x.unwrap(), y.unwrap())),
1006 => Some(create_no_button(ctx, x.unwrap(), y.unwrap())),
_ => None,
}
}
pub fn update(&mut self, mouse: &MouseContext) -> bool {
let mouse_pos = mouse.position();
let in_bounds = mouse_pos.x >= self.x
&& mouse_pos.x < self.x + self.w
&& mouse_pos.y >= self.y
&& mouse_pos.y < self.y + self.h;
in_bounds
}
pub fn draw(&self, canvas: &mut Canvas) {
let actual_x = self.x;
let actual_y = self.y;
canvas.draw(
&self.sprite,
DrawParam::default().dest([actual_x, actual_y]),
);
match self.bid {
1000 => {
canvas.draw(
self.text.as_ref().expect("Couldn't draw Start Text"),
DrawParam::default().dest([actual_x + 35.0, actual_y + 12.0]),
);
}
1001 => {
canvas.draw(
self.text.as_ref().expect("Couldn't draw Games Text"),
DrawParam::default().dest([actual_x - 1.0, actual_y + self.h]),
);
}
1002 => {
canvas.draw(
self.text.as_ref().expect("Couldn't draw Settings Text"),
DrawParam::default().dest([actual_x - 6.0, actual_y + self.h]),
);
}
1003 => {
canvas.draw(
self.text.as_ref().expect("Couldn't draw Shutdown Text"),
DrawParam::default().dest([actual_x + 5.0, actual_y + 9.0]),
);
}
1005 => {
canvas.draw(
self.text.as_ref().expect("Couldn't draw Yes Text"),
DrawParam::default().dest([actual_x + 6.0, actual_y + 4.0]),
);
}
1006 => {
canvas.draw(
self.text.as_ref().expect("Couldn't draw No Text"),
DrawParam::default().dest([actual_x + 9.0, actual_y + 4.0]),
);
}
_ => {}
}
}
}

2
src/ui/elements/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub(crate) mod button;
pub(crate) mod start_menu;

View File

@@ -0,0 +1,127 @@
use ggez::{
Context, GameResult,
context::Has,
graphics::{Canvas, Color, DrawParam, Drawable, Image, Text, TextFragment},
input::mouse,
mint::Point2,
};
use tokio::runtime::Runtime;
use crate::net::networkmanager::{self, NETWORK_MANAGER, UserName};
use crate::ui::{
elements::button::*, utils::assets::get_asset, windows::window_manager::WindowManager,
};
pub struct StartMenu {
visible: bool,
background: Image,
games_button: Button,
settings_button: Button,
profile_button: Button,
shutdown_button: Button,
username: Text,
}
impl StartMenu {
pub fn new(ctx: &mut Context) -> GameResult<StartMenu> {
let games_button =
Button::new(GAMES_BUTTON, ctx, None, None).expect("Couldn't create games button")?;
let settings_button = Button::new(SETTINGS_BUTTON, ctx, None, None)
.expect("Couldn't create settings button")?;
let shutdown_button = Button::new(SHUTDOWN_BUTTON, ctx, None, None)
.expect("Couldn't create shutdown button")?;
let profile_button = Button::new(PROFILE_BUTTON, ctx, None, None)
.expect("Couldn't create profile button")?;
let background = Image::from_bytes(
ctx,
&get_asset("start_menu.png").expect("Couldn't load start_menu.png"),
)?;
let user_profile = NETWORK_MANAGER.get_user_data();
let frag = TextFragment::new(user_profile.username)
.font("msw98-reg")
.scale(20.0)
.color(Color::BLACK);
let username = Text::new(frag);
let visible = false;
Ok(StartMenu {
visible,
background,
games_button,
settings_button,
profile_button,
shutdown_button,
username,
})
}
pub fn toggle(&mut self) {
self.visible = !self.visible;
}
pub fn visible(&self) -> bool {
self.visible
}
pub fn click_in_bounds(&self, mouse_pos: Point2<f32>) -> bool {
mouse_pos.x >= 0.0
&& mouse_pos.x < 300.0
&& mouse_pos.y >= 200.0
&& mouse_pos.y < 200.0 + 361.0
}
fn refresh_user_profile(&mut self) {
let user_profile = NETWORK_MANAGER.get_user_data();
let frag = TextFragment::new(user_profile.username)
.font("msw98-reg")
.scale(20.0)
.color(Color::BLACK);
self.username.clear();
self.username.add(frag);
}
pub fn update(&mut self, ctx: &mut Context, wm: &mut WindowManager) {
if ctx.mouse.button_just_pressed(mouse::MouseButton::Left) {
let mut res = self.shutdown_button.update(&ctx.mouse);
if res {
let on_yes = Box::new(|ctx: &mut Context| ctx.request_quit());
wm.launch_yes_no_dialog(ctx, on_yes);
return;
}
res = self.games_button.update(&ctx.mouse);
if res {
// Launch games window
return;
}
res = self.settings_button.update(&ctx.mouse);
if res {
// Launch settings window
return;
}
res = self.profile_button.update(&ctx.mouse);
if res {
self.refresh_user_profile();
return;
}
}
}
pub fn draw(&mut self, canvas: &mut Canvas) {
if self.visible {
canvas.draw(&self.background, DrawParam::default().dest([0.0, 200.0]));
self.games_button.draw(canvas);
self.settings_button.draw(canvas);
self.profile_button.draw(canvas);
self.shutdown_button.draw(canvas);
self.username
.draw(canvas, DrawParam::default().dest([64.0, 222.5]));
}
}
}

3
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub(crate) mod elements;
pub(crate) mod utils;
pub(crate) mod windows;

13
src/ui/utils/assets.rs Normal file
View File

@@ -0,0 +1,13 @@
use std::borrow::Cow;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "assets"]
struct Asset;
pub fn get_asset(path: &str) -> Option<Cow<'static, [u8]>> {
Asset::get(path).map(|file| file.data.into_owned().into())
}
pub fn get_asset_vec(path: &str) -> Option<Vec<u8>> {
Asset::get(path).map(|file| file.data.to_vec())
}

1
src/ui/utils/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod assets;

View File

@@ -0,0 +1 @@
pub(crate) mod yes_no_dialog;

View File

@@ -0,0 +1,139 @@
use ggez::graphics::{Color, DrawParam, Image, Mesh, Rect, Text, TextFragment};
use ggez::{Context, graphics};
use crate::ui::elements::button::{Button, NO_BUTTON, YES_BUTTON};
use crate::ui::utils::assets::get_asset;
use crate::ui::windows::window::Window;
pub struct YesNoDialog {
pos: (f32, f32),
dimensions: (f32, f32), // Width, Height
background: Mesh,
title: Text,
icon: Image,
areusure: Text,
closed: bool,
on_yes: Box<dyn Fn(&mut Context)>,
yes_button: Button,
no_button: Button,
}
impl YesNoDialog {
pub fn new(
ctx: &mut Context,
pos: (f32, f32),
on_yes: Box<dyn Fn(&mut Context)>,
) -> YesNoDialog {
let mut frag = TextFragment::new("Confirm")
.font("msw98-reg")
.scale(13.0)
.color(Color::BLACK);
let title = Text::new(frag);
frag = TextFragment::new("Are you sure?")
.font("msw98-reg")
.scale(13.0)
.color(Color::BLACK);
let areusure = Text::new(frag);
let icon = Image::from_bytes(
ctx,
&get_asset("settings_icon.png").expect("Couldn't load dialog icon."),
)
.expect("Couldn't load dialog icon.");
let yes_button = Button::new(YES_BUTTON, ctx, Some(pos.0 + 60.0), Some(pos.1 + 80.0))
.expect("Couldn't build yes button")
.expect("Couldn't build yes button");
let no_button = Button::new(NO_BUTTON, ctx, Some(pos.0 + 110.0), Some(pos.1 + 80.0))
.expect("Couldn't build yes button")
.expect("Couldn't build yes button");
YesNoDialog {
pos,
dimensions: (200.0, 100.0),
background: Mesh::new_rectangle(
ctx,
graphics::DrawMode::fill(),
{
let rect_x = 0.0;
let rect_y = 0.0;
let rect_width = 200.0;
let rect_height = 100.0;
Rect::new(rect_x, rect_y, rect_width, rect_height)
},
Color::from_rgb(168, 168, 168),
)
.expect("Couldn't create background mesh for confirm dialog."),
title,
icon,
areusure,
closed: false,
on_yes,
yes_button,
no_button,
}
}
}
impl Window for YesNoDialog {
fn update(&mut self, ctx: &mut ggez::Context) {
if ctx
.mouse
.button_just_pressed(ggez::event::MouseButton::Left)
&& self.click_in_bounds(ctx.mouse.position())
{
if self.yes_button.update(&ctx.mouse) {
(self.on_yes)(ctx)
} else if self.no_button.update(&ctx.mouse) {
self.close();
}
}
}
fn draw(&self, canvas: &mut ggez::graphics::Canvas) {
canvas.draw(
&self.background,
DrawParam::default().dest([self.pos.0, self.pos.1]),
);
canvas.draw(
&self.icon,
DrawParam::default().dest([self.pos.0, self.pos.1]),
);
canvas.draw(
&self.title,
DrawParam::default().dest([self.pos.0 + 82.0, self.pos.1 + 10.0]),
);
canvas.draw(
&self.areusure,
DrawParam::default().dest([self.pos.0 + 65.0, self.pos.1 + 30.0]),
);
self.yes_button.draw(canvas);
self.no_button.draw(canvas);
}
fn click_in_bounds(&self, mouse_pos: ggez::mint::Point2<f32>) -> bool {
mouse_pos.x > self.pos.0
&& mouse_pos.x <= self.pos.0 + self.dimensions.0
&& mouse_pos.y > self.pos.1
&& mouse_pos.y <= self.pos.1 + self.dimensions.1
}
fn closed(&self) -> bool {
self.closed
}
fn close(&mut self) {
self.closed = true;
}
fn minimize(&mut self) {}
// Unfocusable
fn focused(&self) -> bool {
true
}
fn set_focused(&mut self, _value: bool) {}
}

View File

6
src/ui/windows/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
pub(crate) mod dialogs;
pub(crate) mod games;
pub(crate) mod settings;
pub(crate) mod social;
pub(crate) mod window;
pub(crate) mod window_manager;

View File

View File

12
src/ui/windows/window.rs Normal file
View File

@@ -0,0 +1,12 @@
use ggez::{Context, graphics::Canvas, mint::Point2};
pub(crate) trait Window {
fn update(&mut self, ctx: &mut Context);
fn draw(&self, canvas: &mut Canvas);
fn click_in_bounds(&self, mouse_pos: Point2<f32>) -> bool;
fn closed(&self) -> bool;
fn close(&mut self);
fn minimize(&mut self);
fn focused(&self) -> bool;
fn set_focused(&mut self, value: bool);
}

View File

@@ -0,0 +1,42 @@
use ggez::{Context, graphics::Canvas};
use crate::ui::windows::{dialogs::yes_no_dialog::YesNoDialog, window::Window};
pub struct WindowManager {
windows: Vec<Box<dyn Window>>,
}
impl WindowManager {
pub fn new() -> Self {
WindowManager {
windows: Vec::new(),
}
}
pub fn launch_yes_no_dialog(&mut self, ctx: &mut Context, on_yes: Box<dyn Fn(&mut Context)>) {
self.windows
.push(Box::new(YesNoDialog::new(ctx, (300.0, 250.0), on_yes)));
for window in &mut self.windows {
window.set_focused(false);
}
}
pub fn update(&mut self, ctx: &mut Context) {
// Remove any closed windows
self.windows.retain(|window| !window.closed());
// Update focused window
for window in &mut self.windows {
if window.focused() {
window.update(ctx);
}
}
}
pub fn draw(&self, canvas: &mut Canvas) {
for window in &self.windows {
window.draw(canvas);
}
}
}