#![cfg_attr(windows, windows_subsystem = "windows")]
//! Desktop frontend for zagan-bet.
//!
//! Phase 4: the egui shell over the shared `ClientCore`, with the transport on
//! a worker thread. The same data directory and flags as the terminal client.
use zagan_bet::client::paths::{default_data_dir, parse_args};
use zagan_bet_gui::app::{Startup, ZaganApp};
fn main() -> eframe::Result {
let args = match parse_args(std::env::args().skip(1)) {
Ok(args) => args,
Err(message) => {
eprintln!("{message}");
std::process::exit(2);
}
};
if args.help {
let usage = "usage: zagan-bet-gui [-d
] [--unsafe]
-d, --data-dir where to keep client.db (default: XDG data dir)
--unsafe skip certificate verification
-h, --help show this message";
#[cfg(windows)]
show_dialog(usage, false);
#[cfg(not(windows))]
println!("{usage}");
return Ok(());
}
let data_dir = args.data_dir.unwrap_or_else(default_data_dir);
if let Err(error) = std::fs::create_dir_all(&data_dir) {
eprintln!("Cannot create {}: {error}", data_dir.display());
std::process::exit(1);
}
let startup = Startup {
data_dir,
unsafe_mode: args.unsafe_mode,
};
let result = eframe::run_native(
"Zagan-Bet",
native_options(),
Box::new(move |cc| Ok(Box::new(ZaganApp::new(cc, startup)))),
);
#[cfg(windows)]
if let Err(error) = &result {
// The binary is a GUI-subsystem app, so this is the only way a startup
// failure reaches the player.
show_dialog(&format!("Zagan-Bet could not start.\n\n{error}"), true);
}
result
}
/// A native Windows message box, for help and for startup failures (there is
/// no console attached).
#[cfg(windows)]
fn show_dialog(message: &str, error: bool) {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::WindowsAndMessaging::{
MB_ICONERROR, MB_ICONINFORMATION, MessageBoxW,
};
let wide = |text: &str| -> Vec {
OsStr::new(text)
.encode_wide()
.chain(std::iter::once(0))
.collect()
};
let text = wide(message);
let title = wide("Zagan-Bet");
let icon = if error {
MB_ICONERROR
} else {
MB_ICONINFORMATION
};
// SAFETY: both strings are NUL-terminated and outlive the call.
unsafe {
MessageBoxW(std::ptr::null_mut(), text.as_ptr(), title.as_ptr(), icon);
}
}
fn viewport() -> eframe::egui::ViewportBuilder {
eframe::egui::ViewportBuilder::default()
.with_title("Zagan-Bet")
.with_app_id("zagan-bet")
.with_inner_size([1440.0, 900.0])
.with_min_inner_size([1100.0, 720.0])
}
/// Windows renders through Direct3D 12. egui's default adapter choice prefers
/// hardware; this selector also accepts WARP, the software rasterizer, so a
/// Remote Desktop or VM session with no OpenGL driver still runs the app.
#[cfg(windows)]
fn native_options() -> eframe::NativeOptions {
use std::sync::Arc;
use eframe::egui_wgpu::{WgpuConfiguration, WgpuSetup};
use eframe::wgpu;
let mut wgpu_options = WgpuConfiguration::default();
if let WgpuSetup::CreateNew(setup) = &mut wgpu_options.wgpu_setup {
setup.instance_descriptor.backends = wgpu::Backends::DX12;
setup.native_adapter_selector = Some(Arc::new(|adapters, surface| {
let usable = |adapter: &wgpu::Adapter| {
surface.is_none_or(|surface| adapter.is_surface_supported(surface))
};
for kind in [
wgpu::DeviceType::DiscreteGpu,
wgpu::DeviceType::IntegratedGpu,
wgpu::DeviceType::VirtualGpu,
wgpu::DeviceType::Cpu,
wgpu::DeviceType::Other,
] {
if let Some(adapter) = adapters
.iter()
.find(|adapter| adapter.get_info().device_type == kind && usable(adapter))
{
return Ok(adapter.clone());
}
}
Err("no Direct3D 12 adapter (hardware or WARP) is available".to_string())
}));
}
eframe::NativeOptions {
viewport: viewport(),
renderer: eframe::Renderer::Wgpu,
wgpu_options,
..Default::default()
}
}
#[cfg(not(windows))]
fn native_options() -> eframe::NativeOptions {
eframe::NativeOptions {
viewport: viewport(),
renderer: eframe::Renderer::Glow,
..Default::default()
}
}