lemur/src/window/game.rs

386 lines
14 KiB
Rust
Raw Normal View History

2024-12-10 04:18:42 +00:00
use std::sync::mpsc;
2024-11-28 15:27:18 +00:00
use crate::{
app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, SimId},
2024-11-26 05:38:03 +00:00
};
use egui::{
2024-12-10 04:18:42 +00:00
ecolor::HexColor, menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame,
Layout, Response, Sense, TopBottomPanel, Ui, Vec2, ViewportBuilder, ViewportCommand,
ViewportId, WidgetText, Window,
2024-11-26 05:38:03 +00:00
};
2024-12-10 04:18:42 +00:00
use egui_toast::{Toast, Toasts};
2024-11-28 15:27:18 +00:00
use winit::event_loop::EventLoopProxy;
2024-11-26 05:38:03 +00:00
2024-12-01 04:14:01 +00:00
use super::{
game_screen::{DisplayMode, GameScreen},
AppWindow,
};
2024-11-26 05:38:03 +00:00
2024-12-01 05:45:13 +00:00
const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
2024-11-26 05:38:03 +00:00
pub struct GameWindow {
client: EmulatorClient,
2024-11-28 15:27:18 +00:00
proxy: EventLoopProxy<UserEvent>,
2024-11-26 05:38:03 +00:00
sim_id: SimId,
2024-12-01 04:14:01 +00:00
display_mode: DisplayMode,
2024-12-01 05:45:13 +00:00
colors: [Color32; 2],
2024-11-26 05:38:03 +00:00
screen: Option<GameScreen>,
2024-12-10 04:18:42 +00:00
messages: Option<mpsc::Receiver<Toast>>,
2024-12-04 01:34:13 +00:00
color_picker: Option<ColorPickerState>,
2024-11-26 05:38:03 +00:00
}
impl GameWindow {
2024-11-28 15:27:18 +00:00
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>, sim_id: SimId) -> Self {
2024-11-26 05:38:03 +00:00
Self {
client,
2024-11-28 15:27:18 +00:00
proxy,
2024-11-26 05:38:03 +00:00
sim_id,
2024-12-01 04:14:01 +00:00
display_mode: DisplayMode::Anaglyph,
2024-12-01 05:45:13 +00:00
colors: COLOR_PRESETS[0],
2024-11-26 05:38:03 +00:00
screen: None,
2024-12-10 04:18:42 +00:00
messages: None,
2024-12-04 01:34:13 +00:00
color_picker: None,
2024-11-26 05:38:03 +00:00
}
}
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
ui.menu_button("ROM", |ui| {
if ui.button("Open ROM").clicked() {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(SimId::Player1, path));
}
ui.close_menu();
}
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(ViewportCommand::Close);
}
});
ui.menu_button("Emulation", |ui| {
let has_game = self.client.has_game(self.sim_id);
if self.client.is_running(self.sim_id) {
if ui.add_enabled(has_game, Button::new("Pause")).clicked() {
self.client.send_command(EmulatorCommand::Pause);
ui.close_menu();
}
} else if ui.add_enabled(has_game, Button::new("Resume")).clicked() {
self.client.send_command(EmulatorCommand::Resume);
ui.close_menu();
}
if ui.add_enabled(has_game, Button::new("Reset")).clicked() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
}
});
ui.menu_button("Video", |ui| {
2024-12-01 04:14:01 +00:00
ui.menu_button("Screen Size", |ui| {
let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap());
let current_dims = current_dims.max - current_dims.min;
2024-11-26 05:38:03 +00:00
2024-12-01 04:14:01 +00:00
for scale in 1..=4 {
let label = format!("x{scale}");
let scale = scale as f32;
2024-12-01 20:52:11 +00:00
let dims = {
let Vec2 { x, y } = self.display_mode.proportions();
Vec2::new(x * scale, y * scale + 22.0)
};
2024-12-01 04:14:01 +00:00
if ui
.selectable_button((current_dims - dims).length() < 1.0, label)
.clicked()
{
ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims));
ui.close_menu();
}
}
});
ui.menu_button("Display Mode", |ui| {
2024-12-01 20:52:11 +00:00
let old_proportions = self.display_mode.proportions();
2024-11-26 05:38:03 +00:00
if ui
2024-12-01 04:14:01 +00:00
.selectable_option(&mut self.display_mode, DisplayMode::Anaglyph, "Anaglyph")
2024-11-26 05:38:03 +00:00
.clicked()
{
ui.close_menu();
}
2024-12-01 04:14:01 +00:00
if ui
.selectable_option(&mut self.display_mode, DisplayMode::LeftEye, "Left Eye")
.clicked()
{
ui.close_menu();
}
if ui
.selectable_option(&mut self.display_mode, DisplayMode::RightEye, "Right Eye")
.clicked()
{
ui.close_menu();
}
2024-12-01 20:52:11 +00:00
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 scale = new_proportions / old_proportions;
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));
}
2024-12-01 04:14:01 +00:00
});
2024-12-01 05:45:13 +00:00
ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.colors = preset;
2024-12-04 01:34:13 +00:00
ui.close_menu();
2024-12-01 05:45:13 +00:00
}
}
2024-12-04 01:34:13 +00:00
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
if ui.button("Custom").clicked() {
let color_str = |color: Color32| {
format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b())
};
let is_running = self.client.is_running(self.sim_id);
if is_running {
self.client.send_command(EmulatorCommand::Pause);
}
self.color_picker = Some(ColorPickerState {
color_codes: [color_str(self.colors[0]), color_str(self.colors[1])],
just_opened: true,
unpause_on_close: is_running,
});
ui.close_menu();
}
});
2024-12-01 05:45:13 +00:00
});
2024-11-26 05:38:03 +00:00
});
ui.menu_button("Audio", |ui| {
let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
if ui.selectable_button(p1_enabled, "Player 1").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
ui.close_menu();
}
if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
ui.close_menu();
}
});
ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() {
2024-11-28 15:27:18 +00:00
self.proxy.send_event(UserEvent::OpenInput).unwrap();
2024-11-26 05:38:03 +00:00
ui.close_menu();
}
});
ui.menu_button("Multiplayer", |ui| {
if self.sim_id == SimId::Player1
&& !self.client.has_player_2()
&& ui.button("Open Player 2").clicked()
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
2024-11-28 15:27:18 +00:00
self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
2024-11-26 05:38:03 +00:00
ui.close_menu();
}
if self.client.has_player_2() {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
ui.close_menu();
}
if !linked && ui.button("Link").clicked() {
self.client.send_command(EmulatorCommand::Link);
ui.close_menu();
}
}
});
}
2024-12-04 01:34:13 +00:00
fn show_color_picker(&mut self, ui: &mut Ui) {
let Some(state) = self.color_picker.as_mut() else {
return;
};
let open = ui
.horizontal(|ui| {
let left_color = ui.color_picker(&mut self.colors[0], &mut state.color_codes[0]);
if state.just_opened {
left_color.request_focus();
state.just_opened = false;
}
let right_color = ui.color_picker(&mut self.colors[1], &mut state.color_codes[1]);
left_color.has_focus() || right_color.has_focus()
})
.inner;
if !open {
if state.unpause_on_close {
self.client.send_command(EmulatorCommand::Resume);
}
self.color_picker = None;
}
}
2024-11-26 05:38:03 +00:00
}
impl AppWindow for GameWindow {
fn viewport_id(&self) -> ViewportId {
match self.sim_id {
SimId::Player1 => ViewportId::ROOT,
SimId::Player2 => ViewportId::from_hash_of("Player2"),
}
}
fn initial_viewport(&self) -> ViewportBuilder {
2024-12-01 20:52:11 +00:00
let dimensions = self.display_mode.proportions() + Vec2::new(0.0, 22.0);
2024-11-26 05:38:03 +00:00
ViewportBuilder::default()
2024-12-08 19:49:54 +00:00
.with_title("Lemur")
2024-12-01 20:52:11 +00:00
.with_inner_size(dimensions)
2024-11-26 05:38:03 +00:00
}
fn show(&mut self, ctx: &Context) {
2024-12-10 04:18:42 +00:00
let mut toasts = Toasts::new()
.anchor(Align2::LEFT_BOTTOM, (10.0, 10.0))
.direction(Direction::BottomUp);
if let Some(messages) = self.messages.as_mut() {
while let Ok(toast) = messages.try_recv() {
toasts.add(toast);
}
}
2024-11-26 05:38:03 +00:00
TopBottomPanel::top("menubar")
2024-11-28 17:39:08 +00:00
.exact_height(22.0)
2024-11-26 05:38:03 +00:00
.show(ctx, |ui| {
menu::bar(ui, |ui| {
self.show_menu(ctx, ui);
});
});
2024-12-04 01:34:13 +00:00
if self.color_picker.is_some() {
Window::new("Color Picker")
.title_bar(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ctx, |ui| {
self.show_color_picker(ui);
});
}
2024-11-28 17:39:08 +00:00
let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show(ctx, |ui| {
2024-12-01 04:14:01 +00:00
if let Some(screen) = self.screen.as_mut() {
2024-12-01 05:45:13 +00:00
screen.update(self.display_mode, self.colors);
2024-11-26 05:38:03 +00:00
ui.add(screen);
}
});
2024-12-10 04:18:42 +00:00
toasts.show(ctx);
2024-11-26 05:38:03 +00:00
}
2024-11-28 15:27:18 +00:00
fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
let (screen, sink) = GameScreen::init(render_state);
2024-12-10 04:18:42 +00:00
let (message_sink, message_source) = mpsc::channel();
self.client.send_command(EmulatorCommand::ConnectToSim(
self.sim_id,
sink,
message_sink,
));
self.screen = Some(screen);
self.messages = Some(message_source);
2024-11-28 15:27:18 +00:00
}
fn on_destroy(&mut self) {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
}
2024-11-26 05:38:03 +00:00
}
trait UiExt {
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
2024-12-01 04:14:01 +00:00
fn selectable_option<T: Eq>(
&mut self,
current_value: &mut T,
selected_value: T,
text: impl Into<WidgetText>,
) -> Response {
let response = self.selectable_button(*current_value == selected_value, text);
if response.clicked() {
*current_value = selected_value;
}
response
}
2024-12-01 05:45:13 +00:00
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
2024-12-04 01:34:13 +00:00
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
2024-11-26 05:38:03 +00:00
}
impl UiExt for Ui {
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
let mut selected = selected;
self.checkbox(&mut selected, text)
}
2024-12-01 05:45:13 +00:00
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response {
let button_size = Vec2::new(60.0, 20.0);
let (rect, response) = self.allocate_at_least(button_size, Sense::click());
let center_x = rect.center().x;
let left_rect = rect.with_max_x(center_x);
self.painter().rect_filled(left_rect, 0.0, left);
let right_rect = rect.with_min_x(center_x);
self.painter().rect_filled(right_rect, 0.0, right);
let style = self.style().interact(&response);
self.painter().rect_stroke(rect, 0.0, style.fg_stroke);
response
}
2024-12-04 01:34:13 +00:00
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response {
self.allocate_ui_with_layout(
Vec2::new(100.0, 130.0),
Layout::top_down_justified(egui::Align::Center),
|ui| {
let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover());
ui.painter().rect_filled(rect, 0.0, *color);
let resp = ui.text_edit_singleline(hex);
if resp.changed() {
if let Ok(new_color) = HexColor::from_str_without_hash(hex) {
*color = new_color.color();
}
}
resp
},
)
.inner
}
}
struct ColorPickerState {
color_codes: [String; 2],
just_opened: bool,
unpause_on_close: bool,
2024-11-26 05:38:03 +00:00
}