diff --git a/src/config.rs b/src/config.rs index a84a4d2..b993933 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use std::fs::File; use std::{env, io}; +use log::info; use std::io::Write; const CONFIGURATION_FILE_NAME: &str = "vpxtool.cfg"; @@ -38,14 +39,95 @@ pub struct ResolvedConfig { pub editor: Option, } +/// FullScreen = 0 +/// PlayfieldFullScreen = 0 +/// WindowPosX = +/// PlayfieldWindowPosX = +/// WindowPosY = +/// PlayfieldWindowPosY = +/// Width = 540 +/// PlayfieldWidth = 540 +/// Height = 960 +/// PlayfieldHeight = 960 +/// +/// Note: For macOS with hidpi screen this these are logical sizes/locations, not pixel sizes +pub(crate) struct PlayfieldInfo { + pub(crate) fullscreen: bool, + pub(crate) x: Option, + pub(crate) y: Option, + pub(crate) width: Option, + pub(crate) height: Option, +} + +pub(crate) struct VPinballConfig { + ini: ini::Ini, +} + +impl VPinballConfig { + pub fn read(ini_path: &Path) -> Result { + info!("Reading vpinball ini file: {:?}", ini_path); + let ini = ini::Ini::load_from_file(ini_path)?; + Ok(VPinballConfig { ini }) + } + + pub fn get_pinmame_path(&self) -> Option { + if let Some(standalone_section) = self.ini.section(Some("Standalone")) { + standalone_section.get("PinMAMEPath").map(|s| s.to_string()) + } else { + None + } + } + + pub fn get_playfield_info(&self) -> Option { + if let Some(standalone_section) = self.ini.section(Some("Player")) { + // get all the values from PlayfieldXXX and fall back to the normal values + let fullscreen = match standalone_section.get("PlayfieldFullScreen") { + Some(value) => value == "1", + None => match standalone_section.get("FullScreen") { + Some(value) => value == "1", + None => true, // not sure if this is the correct default value for every os + }, + }; + let x = standalone_section + .get("PlayfieldWndX") + .or_else(|| standalone_section.get("WindowPosX")) + .and_then(|s| s.parse::().ok()); + + let y = standalone_section + .get("PlayfieldWndY") + .or_else(|| standalone_section.get("WindowPosY")) + .and_then(|s| s.parse::().ok()); + + let width = standalone_section + .get("PlayfieldWidth") + .or_else(|| standalone_section.get("Width")) + .and_then(|s| s.parse::().ok()); + + let height = standalone_section + .get("PlayfieldHeight") + .or_else(|| standalone_section.get("Height")) + .and_then(|s| s.parse::().ok()); + + Some(PlayfieldInfo { + fullscreen, + x, + y, + width, + height, + }) + } else { + None + } + } +} + impl ResolvedConfig { pub fn global_pinmame_folder(&self) -> PathBuf { // first we try to read the ini file let ini_file = self.vpinball_ini_file(); if ini_file.exists() { - let ini = ini::Ini::load_from_file(ini_file).unwrap(); - let standalone_section = ini.section(Some("Standalone")).unwrap(); - if let Some(value) = standalone_section.get("PinMAMEPath") { + let vpinball_config = VPinballConfig::read(&ini_file).unwrap(); + if let Some(value) = vpinball_config.get_pinmame_path() { // if the path exists we return it let path = PathBuf::from(value); if path.exists() { diff --git a/src/guifrontend.rs b/src/guifrontend.rs index 859fcd6..b2d1f86 100644 --- a/src/guifrontend.rs +++ b/src/guifrontend.rs @@ -4,20 +4,19 @@ use bevy::sprite::{MaterialMesh2dBundle, Mesh2dHandle}; // use bevy_asset_loader::prelude::*; // use bevy_egui::{egui, EguiContexts, EguiPlugin}; // use image::ImageReader; -use std::collections::HashSet; -use std::{ - io, - path::{Path, PathBuf}, - process::{exit, ExitStatus}, -}; - -use crate::config::ResolvedConfig; +use crate::config::{ResolvedConfig, VPinballConfig}; use crate::indexer; use crate::indexer::IndexedTable; use bevy::window::*; use colored::Colorize; use console::Emoji; use is_executable::IsExecutable; +use std::collections::HashSet; +use std::{ + io, + path::{Path, PathBuf}, + process::{exit, ExitStatus}, +}; // enum Orientation { // Horizontal, @@ -49,6 +48,11 @@ pub struct Config { pub config: ResolvedConfig, } +#[derive(Resource)] +pub struct VpxConfig { + pub config: VPinballConfig, +} + #[derive(Resource)] pub struct VpxTables { pub indexed_tables: Vec, @@ -60,6 +64,36 @@ pub struct InfoBox { // info_string: String, } +fn correct_mac_window_size( + mut window_query: Query<&mut Window, With>, + vpx_config: Res, +) { + // only on macOS + // #[cfg(target_os = "macos")] is annoying because it causes clippy to complain about dead code + if cfg!(target_os = "macos") { + let mut window = window_query.single_mut(); + if window.resolution.scale_factor() != 1.0 { + info!( + "Resizing window for macOS with scale factor {}", + window.resolution.scale_factor(), + ); + let vpinball_config = &vpx_config.config; + if let Some(playfield) = vpinball_config.get_playfield_info() { + if let (Some(logical_x), Some(logical_y)) = (playfield.x, playfield.y) { + // For macOS with scales factor > 1 this is not correct but we don't know the scale + // factor before the window is created. + let physical_x = logical_x as f32 * window.resolution.scale_factor(); + let physical_y = logical_y as f32 * window.resolution.scale_factor(); + // this will apply the width as if it was set in logical pixels + window.position = + WindowPosition::At(IVec2::new(physical_x as i32, physical_y as i32)); + window.set_changed(); + } + } + } + } +} + fn create_wheel( mut commands: Commands, asset_server: Res, @@ -381,7 +415,7 @@ fn gui_update( tx: Res, config: Res, ) { - let mut window = window_query.single_mut(); + let window = window_query.single_mut(); let width = window.width(); let height = window.height(); @@ -553,7 +587,7 @@ fn gui_update( wheel.launch_path.clone().into_os_string().to_string_lossy() ); println!("Hide window"); - window.visible = false; + //window.visible = false; let tx = tx.clone(); let path = wheel.launch_path.clone(); @@ -592,23 +626,54 @@ pub fn guifrontend( // viewport: egui::ViewportBuilder::default().with_inner_size([400.0, 800.0]), // ..Default::default() // }; + + let vpinball_ini_path = config.vpinball_ini_file(); + let vpinball_config = VPinballConfig::read(&vpinball_ini_path).unwrap(); + let mut position = WindowPosition::default(); + let mut mode = WindowMode::Fullscreen; + let mut resolution = WindowResolution::default(); + if let Some(playfield) = vpinball_config.get_playfield_info() { + if let (Some(x), Some(y)) = (playfield.x, playfield.y) { + // For macOS with scale factor > 1 this is not correct but we don't know the scale + // factor before the window is created. We will correct the position later using the + // system "correct_mac_window_size". + let physical_x = x as i32; + let physical_y = y as i32; + position = WindowPosition::At(IVec2::new(physical_x, physical_y)); + } + if let (Some(width), Some(height)) = (playfield.width, playfield.height) { + resolution = WindowResolution::new(width as f32, height as f32); + } + mode = if playfield.fullscreen { + WindowMode::Fullscreen + } else { + WindowMode::Windowed + }; + } + App::new() .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "VPXTOOL".to_string(), - window_level: WindowLevel::AlwaysOnTop, + // window_level: WindowLevel::AlwaysOnTop, + resolution, + mode, + position, ..Default::default() }), ..Default::default() })) .insert_resource(Config { config }) + .insert_resource(VpxConfig { + config: vpinball_config, + }) .insert_resource(VpxTables { indexed_tables: vpx_files_with_tableinfo, }) .insert_resource(ClearColor(Color::srgb(0.1, 0.1, 0.1))) // .insert_resource(ClearColor(Color::srgb(0.9, 0.3, 0.6))) .add_event::() - .add_systems(Startup, setup) + .add_systems(Startup, (correct_mac_window_size, setup)) .add_systems(Startup, (create_wheel, create_flippers)) .add_systems(Startup, play_background_audio) .add_systems(Update, gui_update) @@ -687,7 +752,7 @@ fn read_stream( println!("Showing window"); window.visible = true; // bring window to front - window.window_level = WindowLevel::AlwaysOnTop; + // window.window_level = WindowLevel::AlwaysOnTop; // request focus window.focused = true; events.send(StreamEvent(from_stream));