use core::time::Duration;
use std::future::Future;
use std::path::PathBuf;
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::{CursorGrabMode, Window, WindowAttributes};
use crate::platform::Platform;
use crate::{Config, Error};
pub use std::time::Instant;
pub(crate) const PLATFORM: Platform = Platform::Desktop;
pub(crate) const AFTER_LOSS: &str =
"Start the game again; if it happens again, update the graphics driver.";
pub(crate) const AFTER_OUT_OF_MEMORY: &str =
"Start the game again with other programs that use the graphics chip closed.";
pub(crate) fn install_diagnostics() {
let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.filter_module("calloop", log::LevelFilter::Error)
.try_init();
}
pub(crate) fn spawn(future: impl Future<Output = ()> + 'static) {
pollster::block_on(future);
}
pub(crate) async fn read(source: &str) -> Result<Vec<u8>, Error> {
std::fs::read(source)
.map_err(|error| Error::msg(format!("the asset source `{source}` did not open: {error}")))
}
pub(crate) fn window_attributes(config: &Config) -> Result<WindowAttributes, Error> {
let size = config.size();
Ok(Window::default_attributes()
.with_title(config.title())
.with_inner_size(LogicalSize::new(size.x, size.y)))
}
pub(crate) fn run_app(event_loop: EventLoop<()>, mut app: impl ApplicationHandler + 'static) {
if let Err(error) = event_loop.run_app(&mut app) {
report_fatal(&Error::msg(format!("the event loop stopped: {error}")));
}
}
pub(crate) async fn adapter_facts() -> Option<String> {
None
}
pub(crate) fn report_fatal(error: &Error) {
eprintln!("mirage-engine: {error}");
}
pub(crate) fn close(_window: &Window, _config: &Config) {}
pub(crate) struct PointerHold {
set: bool,
held: bool,
}
impl PointerHold {
pub(crate) fn released() -> Self {
Self {
set: false,
held: false,
}
}
pub(crate) fn set(&mut self, window: &Window, held: bool) -> bool {
if core::mem::replace(&mut self.set, held) == held {
return self.held;
}
self.held = held
&& (grab(window, CursorGrabMode::Locked) || grab(window, CursorGrabMode::Confined));
if !self.held {
grab(window, CursorGrabMode::None);
}
self.held
}
pub(crate) fn see_gesture(&mut self, _window: &Window) {}
pub(crate) fn see_focus_change(&mut self) {
self.set = false;
self.held = false;
}
}
fn grab(window: &Window, mode: CursorGrabMode) -> bool {
window
.set_cursor_grab(mode)
.inspect_err(|error| log::debug!("mirage-engine left the pointer as it was: {error}"))
.is_ok()
}
pub(crate) fn store_read(name: &str) -> Option<String> {
read_text(store_path(name)?)
}
fn read_text(path: PathBuf) -> Option<String> {
std::fs::read_to_string(&path)
.inspect_err(|error| {
log::debug!(
"mirage-engine read nothing from {}: {error}",
path.display()
);
})
.ok()
}
pub(crate) fn store_write(name: &str, text: &str) {
let Some(path) = store_path(name) else {
return;
};
let Some(folder) = path.parent() else {
return;
};
if let Err(error) = std::fs::create_dir_all(folder).and_then(|()| std::fs::write(&path, text)) {
log::debug!("mirage-engine kept nothing in {}: {error}", path.display());
}
}
#[cfg(target_os = "linux")]
pub(crate) fn double_click_interval() -> Option<Duration> {
let settings = |file: &str| settings_dir().map(|dir| dir.join(file)).and_then(read_text);
let gtk = |version: &str| settings(&format!("{version}/settings.ini"));
let kde = || settings("kdeglobals");
let x = || {
home()
.map(|home| home.join(".Xresources"))
.and_then(read_text)
};
gtk("gtk-4.0")
.and_then(|text| Settings(&text).gtk())
.or_else(|| gtk("gtk-3.0").and_then(|text| Settings(&text).gtk()))
.or_else(|| kde().and_then(|text| Settings(&text).kde()))
.or_else(|| x().and_then(|text| Settings(&text).xresources()))
}
#[cfg(windows)]
pub(crate) fn double_click_interval() -> Option<Duration> {
let milliseconds =
unsafe { windows_sys::Win32::UI::Input::KeyboardAndMouse::GetDoubleClickTime() };
Some(Duration::from_millis(u64::from(milliseconds)))
}
#[cfg(not(any(target_os = "linux", windows)))]
pub(crate) fn double_click_interval() -> Option<Duration> {
None
}
#[cfg(target_os = "linux")]
struct Settings<'a>(&'a str);
#[cfg(target_os = "linux")]
impl Settings<'_> {
fn gtk(&self) -> Option<Duration> {
self.keyed("gtk-double-click-time", None)
}
fn kde(&self) -> Option<Duration> {
self.keyed("DoubleClickInterval", Some("[KDE]"))
}
fn xresources(&self) -> Option<Duration> {
self.0
.lines()
.filter_map(|line| line.split_once(':'))
.rfind(|(name, _)| matches!(name.trim(), "*.multiClickTime" | "*multiClickTime"))
.and_then(|(_, value)| Self::milliseconds(value))
}
fn keyed(&self, key: &str, section: Option<&str>) -> Option<Duration> {
let mut inside = section.is_none();
for line in self.0.lines().map(str::trim) {
if line.starts_with('[') {
inside = section.is_none_or(|named| named == line);
} else if inside
&& let Some((name, value)) = line.split_once('=')
&& name.trim() == key
{
return Self::milliseconds(value);
}
}
None
}
fn milliseconds(text: &str) -> Option<Duration> {
text.trim().parse().ok().map(Duration::from_millis)
}
}
fn store_path(name: &str) -> Option<PathBuf> {
Some(
settings_dir()?
.join("mirage-engine")
.join(format!("{name}.txt")),
)
}
#[cfg(target_os = "windows")]
fn settings_dir() -> Option<PathBuf> {
std::env::var_os("APPDATA").map(PathBuf::from)
}
#[cfg(target_os = "macos")]
fn settings_dir() -> Option<PathBuf> {
home().map(|home| home.join("Library").join("Application Support"))
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
fn settings_dir() -> Option<PathBuf> {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.or_else(|| home().map(|home| home.join(".config")))
}
#[cfg(not(target_os = "windows"))]
fn home() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[test]
fn each_kind_of_settings_file_states_its_own_interval() {
assert_eq!(
Settings("[Settings]\ngtk-double-click-time=250\n").gtk(),
Some(Duration::from_millis(250))
);
assert_eq!(
Settings("[General]\nDoubleClickInterval=999\n[KDE]\nDoubleClickInterval = 250\n")
.kde(),
Some(Duration::from_millis(250)),
"under its own section, never another's"
);
assert_eq!(
Settings("! what a machine is set to\n*.multiClickTime: 250\n").xresources(),
Some(Duration::from_millis(250))
);
assert_eq!(
Settings("*multiClickTime: 250\n").xresources(),
Some(Duration::from_millis(250)),
"spelled either way"
);
assert_eq!(
Settings("*.multiClickTime: 250\n*.multiClickTime: 300\n").xresources(),
Some(Duration::from_millis(300)),
"and the last line is the one that counts"
);
}
#[test]
fn a_settings_file_stating_no_interval_states_nothing() {
assert_eq!(Settings("").gtk(), None, "nothing at all");
assert_eq!(
Settings("[Settings]\ngtk-font-name=Sans 10\n").gtk(),
None,
"other settings"
);
assert_eq!(
Settings("[General]\nDoubleClickInterval=250\n").kde(),
None,
"the key outside its own section"
);
assert_eq!(
Settings("*.multiClickTime: soon\n").xresources(),
None,
"and a value that is no whole number of milliseconds"
);
}
}