#![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() } }