Save/load more config

This commit is contained in:
Simon Gellis 2024-12-14 23:08:44 -05:00
parent 6e2d70abd7
commit e31269368d
5 changed files with 148 additions and 52 deletions

27
Cargo.lock generated
View File

@ -18,6 +18,16 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "accesskit"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99b76d84ee70e30a4a7e39ab9018e2b17a6a09e31084176cc7c0b2dec036ba45"
dependencies = [
"enumn",
"serde",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.0" version = "2.0.0"
@ -33,6 +43,7 @@ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom",
"once_cell", "once_cell",
"serde",
"version_check", "version_check",
"zerocopy", "zerocopy",
] ]
@ -923,6 +934,7 @@ checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"emath", "emath",
"serde",
] ]
[[package]] [[package]]
@ -931,11 +943,13 @@ version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974"
dependencies = [ dependencies = [
"accesskit",
"ahash", "ahash",
"emath", "emath",
"epaint", "epaint",
"log", "log",
"nohash-hasher", "nohash-hasher",
"serde",
] ]
[[package]] [[package]]
@ -1009,6 +1023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"serde",
] ]
[[package]] [[package]]
@ -1059,6 +1074,17 @@ dependencies = [
"syn 2.0.90", "syn 2.0.90",
] ]
[[package]]
name = "enumn"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]] [[package]]
name = "epaint" name = "epaint"
version = "0.29.1" version = "0.29.1"
@ -1074,6 +1100,7 @@ dependencies = [
"log", "log",
"nohash-hasher", "nohash-hasher",
"parking_lot", "parking_lot",
"serde",
] ]
[[package]] [[package]]

View File

@ -10,7 +10,7 @@ bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" } cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "5" directories = "5"
egui = "0.29" egui = { version = "0.29", features = ["serde"] }
egui_extras = "0.29" egui_extras = "0.29"
egui-toast = "0.15" egui-toast = "0.15"
egui-winit = "0.29" egui-winit = "0.29"

View File

@ -38,6 +38,7 @@ pub struct Application {
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider, mappings: MappingProvider,
controllers: ControllerManager, controllers: ControllerManager,
persistence: Persistence,
viewports: HashMap<ViewportId, Viewport>, viewports: HashMap<ViewportId, Viewport>,
focused: Option<ViewportId>, focused: Option<ViewportId>,
} }
@ -46,7 +47,7 @@ impl Application {
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self { pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
let persistence = Persistence::new(); let persistence = Persistence::new();
let mappings = MappingProvider::new(persistence); let mappings = MappingProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings); let controllers = ControllerManager::new(client.clone(), &mappings);
{ {
let mappings = mappings.clone(); let mappings = mappings.clone();
@ -59,6 +60,7 @@ impl Application {
proxy, proxy,
mappings, mappings,
controllers, controllers,
persistence,
viewports: HashMap::new(), viewports: HashMap::new(),
focused: None, focused: None,
} }
@ -78,7 +80,12 @@ impl Application {
impl ApplicationHandler<UserEvent> for Application { impl ApplicationHandler<UserEvent> for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let app = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player1); let app = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
SimId::Player1,
);
let wrapper = Viewport::new(event_loop, self.icon.clone(), Box::new(app)); let wrapper = Viewport::new(event_loop, self.icon.clone(), Box::new(app));
self.focused = Some(wrapper.id()); self.focused = Some(wrapper.id());
self.viewports.insert(wrapper.id(), wrapper); self.viewports.insert(wrapper.id(), wrapper);
@ -177,7 +184,12 @@ impl ApplicationHandler<UserEvent> for Application {
self.open(event_loop, Box::new(input)); self.open(event_loop, Box::new(input));
} }
UserEvent::OpenPlayer2 => { UserEvent::OpenPlayer2 => {
let p2 = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player2); let p2 = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
SimId::Player2,
);
self.open(event_loop, Box::new(p2)); self.open(event_loop, Box::new(p2));
} }
} }

View File

@ -3,6 +3,7 @@ use std::sync::mpsc;
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
persistence::Persistence,
}; };
use egui::{ use egui::{
ecolor::HexColor, menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, ecolor::HexColor, menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame,
@ -10,6 +11,7 @@ use egui::{
ViewportId, WidgetText, Window, ViewportId, WidgetText, Window,
}; };
use egui_toast::{Toast, Toasts}; use egui_toast::{Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy; use winit::event_loop::EventLoopProxy;
use super::{ use super::{
@ -35,22 +37,28 @@ const COLOR_PRESETS: [[Color32; 2]; 3] = [
pub struct GameWindow { pub struct GameWindow {
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
sim_id: SimId, sim_id: SimId,
display_mode: DisplayMode, config: GameConfig,
colors: [Color32; 2],
screen: Option<GameScreen>, screen: Option<GameScreen>,
messages: Option<mpsc::Receiver<Toast>>, messages: Option<mpsc::Receiver<Toast>>,
color_picker: Option<ColorPickerState>, color_picker: Option<ColorPickerState>,
} }
impl GameWindow { impl GameWindow {
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>, sim_id: SimId) -> Self { pub fn new(
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
sim_id: SimId,
) -> Self {
let config = load_config(&persistence, sim_id);
Self { Self {
client, client,
proxy, proxy,
persistence,
sim_id, sim_id,
display_mode: DisplayMode::Anaglyph, config,
colors: COLOR_PRESETS[0],
screen: None, screen: None,
messages: None, messages: None,
color_picker: None, color_picker: None,
@ -99,7 +107,7 @@ impl GameWindow {
let label = format!("x{scale}"); let label = format!("x{scale}");
let scale = scale as f32; let scale = scale as f32;
let dims = { let dims = {
let Vec2 { x, y } = self.display_mode.proportions(); let Vec2 { x, y } = self.config.display_mode.proportions();
Vec2::new(x * scale, y * scale + 22.0) Vec2::new(x * scale, y * scale + 22.0)
}; };
if ui if ui
@ -113,49 +121,46 @@ impl GameWindow {
}); });
ui.menu_button("Display Mode", |ui| { ui.menu_button("Display Mode", |ui| {
let old_proportions = self.display_mode.proportions(); let old_proportions = self.config.display_mode.proportions();
if ui let mut changed = false;
.selectable_option(&mut self.display_mode, DisplayMode::Anaglyph, "Anaglyph") let mut display_mode = self.config.display_mode;
.clicked() changed |= ui
{ .selectable_option(&mut display_mode, DisplayMode::Anaglyph, "Anaglyph")
ui.close_menu(); .clicked();
} changed |= ui
if ui .selectable_option(&mut display_mode, DisplayMode::LeftEye, "Left Eye")
.selectable_option(&mut self.display_mode, DisplayMode::LeftEye, "Left Eye") .clicked();
.clicked() changed |= ui
{ .selectable_option(&mut display_mode, DisplayMode::RightEye, "Right Eye")
ui.close_menu(); .clicked();
} changed |= ui
if ui .selectable_option(&mut display_mode, DisplayMode::SideBySide, "Side by Side")
.selectable_option(&mut self.display_mode, DisplayMode::RightEye, "Right Eye") .clicked();
.clicked()
{ if !changed {
ui.close_menu(); return;
}
if ui
.selectable_option(
&mut self.display_mode,
DisplayMode::SideBySide,
"Side by Side",
)
.clicked()
{
ui.close_menu();
} }
let new_proportions = self.display_mode.proportions(); let current_dims = {
let viewport = ctx.input(|i| i.viewport().inner_rect.unwrap());
viewport.max - viewport.min
};
let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions; let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) { if scale != Vec2::new(1.0, 1.0) {
let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap());
let current_dims = current_dims.max - current_dims.min;
ctx.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale)); ctx.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
} }
self.update_config(|c| {
c.display_mode = display_mode;
c.dimensions = current_dims * scale;
});
ui.close_menu();
}); });
ui.menu_button("Colors", |ui| { ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS { for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() { if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.colors = preset; self.update_config(|c| c.colors = preset);
ui.close_menu(); ui.close_menu();
} }
} }
@ -168,8 +173,12 @@ impl GameWindow {
if is_running { if is_running {
self.client.send_command(EmulatorCommand::Pause); self.client.send_command(EmulatorCommand::Pause);
} }
let color_codes = [
color_str(self.config.colors[0]),
color_str(self.config.colors[1]),
];
self.color_picker = Some(ColorPickerState { self.color_picker = Some(ColorPickerState {
color_codes: [color_str(self.colors[0]), color_str(self.colors[1])], color_codes,
just_opened: true, just_opened: true,
unpause_on_close: is_running, unpause_on_close: is_running,
}); });
@ -223,18 +232,21 @@ impl GameWindow {
} }
fn show_color_picker(&mut self, ui: &mut Ui) { fn show_color_picker(&mut self, ui: &mut Ui) {
let mut colors = self.config.colors;
let Some(state) = self.color_picker.as_mut() else { let Some(state) = self.color_picker.as_mut() else {
return; return;
}; };
let open = ui let (open, updated) = ui
.horizontal(|ui| { .horizontal(|ui| {
let left_color = ui.color_picker(&mut self.colors[0], &mut state.color_codes[0]); let left_color = ui.color_picker(&mut colors[0], &mut state.color_codes[0]);
if state.just_opened { if state.just_opened {
left_color.request_focus(); left_color.request_focus();
state.just_opened = false; state.just_opened = false;
} }
let right_color = ui.color_picker(&mut self.colors[1], &mut state.color_codes[1]); let right_color = ui.color_picker(&mut colors[1], &mut state.color_codes[1]);
left_color.has_focus() || right_color.has_focus() let open = left_color.has_focus() || right_color.has_focus();
let updated = left_color.changed() || right_color.changed();
(open, updated)
}) })
.inner; .inner;
if !open { if !open {
@ -243,6 +255,38 @@ impl GameWindow {
} }
self.color_picker = None; self.color_picker = None;
} }
if updated {
self.update_config(|c| c.colors = colors);
}
}
fn update_config(&mut self, update: impl FnOnce(&mut GameConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = self
.persistence
.save_config(config_filename(self.sim_id), &new_config);
}
self.config = new_config;
}
}
fn config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}
fn load_config(persistence: &Persistence, sim_id: SimId) -> GameConfig {
if let Ok(config) = persistence.load_config(config_filename(sim_id)) {
return config;
}
GameConfig {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
} }
} }
@ -255,13 +299,18 @@ impl AppWindow for GameWindow {
} }
fn initial_viewport(&self) -> ViewportBuilder { fn initial_viewport(&self) -> ViewportBuilder {
let dimensions = self.display_mode.proportions() + Vec2::new(0.0, 22.0);
ViewportBuilder::default() ViewportBuilder::default()
.with_title("Lemur") .with_title("Lemur")
.with_inner_size(dimensions) .with_inner_size(self.config.dimensions)
} }
fn show(&mut self, ctx: &Context) { fn show(&mut self, ctx: &Context) {
let dimensions = {
let bounds = ctx.input(|i| i.viewport().inner_rect.unwrap());
bounds.max - bounds.min
};
self.update_config(|c| c.dimensions = dimensions);
let mut toasts = Toasts::new() let mut toasts = Toasts::new()
.anchor(Align2::LEFT_BOTTOM, (10.0, 10.0)) .anchor(Align2::LEFT_BOTTOM, (10.0, 10.0))
.direction(Direction::BottomUp); .direction(Direction::BottomUp);
@ -289,7 +338,7 @@ impl AppWindow for GameWindow {
let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK); let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show(ctx, |ui| { CentralPanel::default().frame(frame).show(ctx, |ui| {
if let Some(screen) = self.screen.as_mut() { if let Some(screen) = self.screen.as_mut() {
screen.update(self.display_mode, self.colors); screen.update(self.config.display_mode, self.config.colors);
ui.add(screen); ui.add(screen);
} }
}); });
@ -383,3 +432,10 @@ struct ColorPickerState {
just_opened: bool, just_opened: bool,
unpause_on_close: bool, unpause_on_close: bool,
} }
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
struct GameConfig {
display_mode: DisplayMode,
colors: [Color32; 2],
dimensions: Vec2,
}

View File

@ -1,6 +1,7 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use egui::{Color32, Rgba, Vec2, Widget}; use egui::{Color32, Rgba, Vec2, Widget};
use serde::{Deserialize, Serialize};
use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, Buffer, RenderPipeline}; use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, Buffer, RenderPipeline};
use crate::graphics::TextureSink; use crate::graphics::TextureSink;
@ -260,7 +261,7 @@ impl Colors {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum DisplayMode { pub enum DisplayMode {
Anaglyph, Anaglyph,
LeftEye, LeftEye,