2024-12-08 19:39:40 +00:00
|
|
|
use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration};
|
2024-11-28 15:27:18 +00:00
|
|
|
|
|
|
|
use egui::{
|
|
|
|
ahash::{HashMap, HashMapExt},
|
2024-12-08 21:00:55 +00:00
|
|
|
Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder,
|
|
|
|
ViewportCommand, ViewportId, ViewportInfo,
|
2024-11-28 15:27:18 +00:00
|
|
|
};
|
|
|
|
use gilrs::{EventType, Gilrs};
|
|
|
|
use winit::{
|
|
|
|
application::ApplicationHandler,
|
2024-11-29 23:54:26 +00:00
|
|
|
event::WindowEvent,
|
2024-11-28 15:27:18 +00:00
|
|
|
event_loop::{ActiveEventLoop, EventLoopProxy},
|
|
|
|
window::Window,
|
|
|
|
};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
controller::ControllerManager,
|
2024-12-08 19:39:40 +00:00
|
|
|
emulator::{EmulatorClient, EmulatorCommand, SimId},
|
2024-11-28 15:27:18 +00:00
|
|
|
input::MappingProvider,
|
|
|
|
window::{AppWindow, GameWindow, InputWindow},
|
|
|
|
};
|
|
|
|
|
2024-12-08 21:00:55 +00:00
|
|
|
fn load_icon() -> anyhow::Result<IconData> {
|
|
|
|
let bytes = include_bytes!("../assets/lemur-256x256.png");
|
|
|
|
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?;
|
|
|
|
let rgba = img.into_rgba8();
|
|
|
|
Ok(IconData {
|
|
|
|
width: rgba.width(),
|
|
|
|
height: rgba.height(),
|
|
|
|
rgba: rgba.into_vec(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-28 15:27:18 +00:00
|
|
|
pub struct Application {
|
2024-12-08 21:00:55 +00:00
|
|
|
icon: Option<Arc<IconData>>,
|
2024-11-28 15:27:18 +00:00
|
|
|
client: EmulatorClient,
|
|
|
|
proxy: EventLoopProxy<UserEvent>,
|
|
|
|
mappings: MappingProvider,
|
|
|
|
controllers: ControllerManager,
|
|
|
|
viewports: HashMap<ViewportId, Viewport>,
|
|
|
|
focused: Option<ViewportId>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Application {
|
|
|
|
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
|
2024-12-08 21:00:55 +00:00
|
|
|
let icon = load_icon().ok().map(Arc::new);
|
2024-11-28 15:27:18 +00:00
|
|
|
let mappings = MappingProvider::new();
|
|
|
|
let controllers = ControllerManager::new(client.clone(), &mappings);
|
|
|
|
{
|
|
|
|
let mappings = mappings.clone();
|
|
|
|
let proxy = proxy.clone();
|
|
|
|
thread::spawn(|| process_gamepad_input(mappings, proxy));
|
|
|
|
}
|
|
|
|
Self {
|
2024-12-08 21:00:55 +00:00
|
|
|
icon,
|
2024-11-28 15:27:18 +00:00
|
|
|
client,
|
|
|
|
proxy,
|
|
|
|
mappings,
|
|
|
|
controllers,
|
|
|
|
viewports: HashMap::new(),
|
|
|
|
focused: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
|
|
|
|
let viewport_id = window.viewport_id();
|
|
|
|
if self.viewports.contains_key(&viewport_id) {
|
|
|
|
return;
|
|
|
|
}
|
2024-12-08 21:00:55 +00:00
|
|
|
self.viewports.insert(
|
|
|
|
viewport_id,
|
|
|
|
Viewport::new(event_loop, self.icon.clone(), window),
|
|
|
|
);
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ApplicationHandler<UserEvent> for Application {
|
|
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
|
|
let app = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player1);
|
2024-12-08 21:00:55 +00:00
|
|
|
let wrapper = Viewport::new(event_loop, self.icon.clone(), Box::new(app));
|
2024-11-28 15:27:18 +00:00
|
|
|
self.focused = Some(wrapper.id());
|
|
|
|
self.viewports.insert(wrapper.id(), wrapper);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn window_event(
|
|
|
|
&mut self,
|
|
|
|
event_loop: &ActiveEventLoop,
|
|
|
|
window_id: winit::window::WindowId,
|
|
|
|
event: WindowEvent,
|
|
|
|
) {
|
|
|
|
let Some(viewport) = self
|
|
|
|
.viewports
|
|
|
|
.values_mut()
|
|
|
|
.find(|v| v.window.id() == window_id)
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
let viewport_id = viewport.id();
|
|
|
|
match &event {
|
|
|
|
WindowEvent::KeyboardInput { event, .. } => {
|
|
|
|
self.controllers.handle_key_event(event);
|
2024-11-29 23:54:26 +00:00
|
|
|
viewport.app.handle_key_event(event);
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
WindowEvent::Focused(new_focused) => {
|
|
|
|
self.focused = new_focused.then_some(viewport_id);
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
2024-11-28 17:39:08 +00:00
|
|
|
let mut queue_redraw = false;
|
|
|
|
let mut inactive_viewports = HashSet::new();
|
2024-11-28 15:27:18 +00:00
|
|
|
match viewport.on_window_event(event) {
|
|
|
|
Some(Action::Redraw) => {
|
|
|
|
for viewport in self.viewports.values_mut() {
|
2024-11-28 17:39:08 +00:00
|
|
|
match viewport.redraw(event_loop) {
|
|
|
|
Some(Action::Redraw) => {
|
|
|
|
queue_redraw = true;
|
|
|
|
}
|
|
|
|
Some(Action::Close) => {
|
|
|
|
inactive_viewports.insert(viewport.id());
|
|
|
|
}
|
|
|
|
None => {}
|
|
|
|
}
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(Action::Close) => {
|
2024-11-28 17:39:08 +00:00
|
|
|
inactive_viewports.insert(viewport_id);
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
None => {}
|
|
|
|
}
|
2024-11-28 17:39:08 +00:00
|
|
|
self.viewports
|
|
|
|
.retain(|k, _| !inactive_viewports.contains(k));
|
|
|
|
match self.viewports.get(&ViewportId::ROOT) {
|
|
|
|
Some(viewport) => {
|
|
|
|
if queue_redraw {
|
|
|
|
viewport.window.request_redraw();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => event_loop.exit(),
|
|
|
|
}
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn device_event(
|
|
|
|
&mut self,
|
|
|
|
_event_loop: &ActiveEventLoop,
|
|
|
|
_device_id: winit::event::DeviceId,
|
|
|
|
event: winit::event::DeviceEvent,
|
|
|
|
) {
|
|
|
|
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
|
|
|
let Some(viewport) = self
|
|
|
|
.focused
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|id| self.viewports.get_mut(id))
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
viewport.state.on_mouse_motion(delta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
|
|
|
match event {
|
2024-11-29 23:54:26 +00:00
|
|
|
UserEvent::GamepadEvent(event) => {
|
|
|
|
self.controllers.handle_gamepad_event(&event);
|
|
|
|
let Some(viewport) = self
|
|
|
|
.focused
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|id| self.viewports.get_mut(id))
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
viewport.app.handle_gamepad_event(&event);
|
|
|
|
}
|
2024-11-28 15:27:18 +00:00
|
|
|
UserEvent::OpenInput => {
|
|
|
|
let input = InputWindow::new(self.mappings.clone());
|
|
|
|
self.open(event_loop, Box::new(input));
|
|
|
|
}
|
|
|
|
UserEvent::OpenPlayer2 => {
|
|
|
|
let p2 = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player2);
|
|
|
|
self.open(event_loop, Box::new(p2));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
|
|
|
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
|
|
|
|
viewport.window.request_redraw();
|
|
|
|
}
|
|
|
|
}
|
2024-12-08 19:39:40 +00:00
|
|
|
|
|
|
|
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
|
|
|
|
let (sender, receiver) = oneshot::channel();
|
|
|
|
if self.client.send_command(EmulatorCommand::Exit(sender)) {
|
|
|
|
if let Err(err) = receiver.recv_timeout(Duration::from_secs(5)) {
|
|
|
|
eprintln!("could not gracefully exit: {}", err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct Viewport {
|
|
|
|
painter: egui_wgpu::winit::Painter,
|
|
|
|
ctx: Context,
|
|
|
|
info: ViewportInfo,
|
|
|
|
commands: Vec<ViewportCommand>,
|
|
|
|
builder: ViewportBuilder,
|
|
|
|
window: Arc<Window>,
|
|
|
|
state: egui_winit::State,
|
|
|
|
app: Box<dyn AppWindow>,
|
|
|
|
}
|
|
|
|
impl Viewport {
|
2024-12-08 21:00:55 +00:00
|
|
|
pub fn new(
|
|
|
|
event_loop: &ActiveEventLoop,
|
|
|
|
icon: Option<Arc<IconData>>,
|
|
|
|
mut app: Box<dyn AppWindow>,
|
|
|
|
) -> Self {
|
2024-11-28 15:27:18 +00:00
|
|
|
let mut painter = egui_wgpu::winit::Painter::new(
|
2024-12-01 02:56:33 +00:00
|
|
|
egui_wgpu::WgpuConfiguration {
|
|
|
|
present_mode: wgpu::PresentMode::AutoNoVsync,
|
|
|
|
..egui_wgpu::WgpuConfiguration::default()
|
|
|
|
},
|
2024-11-28 15:27:18 +00:00
|
|
|
1,
|
|
|
|
None,
|
|
|
|
false,
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
|
|
|
|
let ctx = Context::default();
|
2024-12-01 04:14:01 +00:00
|
|
|
let mut fonts = FontDefinitions::default();
|
2024-11-28 17:39:08 +00:00
|
|
|
fonts.font_data.insert(
|
|
|
|
"Selawik".into(),
|
|
|
|
FontData::from_static(include_bytes!("../assets/selawik.ttf")),
|
|
|
|
);
|
|
|
|
fonts
|
|
|
|
.families
|
|
|
|
.get_mut(&FontFamily::Proportional)
|
|
|
|
.unwrap()
|
|
|
|
.insert(0, "Selawik".into());
|
|
|
|
ctx.set_fonts(fonts);
|
2024-11-28 15:27:18 +00:00
|
|
|
ctx.style_mut(|s| {
|
|
|
|
s.wrap_mode = Some(TextWrapMode::Extend);
|
|
|
|
s.visuals.menu_rounding = Default::default();
|
|
|
|
});
|
|
|
|
|
|
|
|
let mut info = ViewportInfo::default();
|
2024-12-08 21:00:55 +00:00
|
|
|
let mut builder = app.initial_viewport();
|
|
|
|
if let Some(icon) = icon {
|
|
|
|
builder = builder.with_icon(icon);
|
|
|
|
}
|
2024-11-28 15:27:18 +00:00
|
|
|
let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter);
|
|
|
|
egui_winit::update_viewport_info(&mut info, &ctx, &window, true);
|
|
|
|
|
|
|
|
app.on_init(painter.render_state().as_ref().unwrap());
|
|
|
|
Self {
|
|
|
|
painter,
|
|
|
|
ctx,
|
|
|
|
info,
|
|
|
|
commands: vec![],
|
|
|
|
builder,
|
|
|
|
window,
|
|
|
|
state,
|
|
|
|
app,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn id(&self) -> ViewportId {
|
|
|
|
self.app.viewport_id()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn on_window_event(&mut self, event: WindowEvent) -> Option<Action> {
|
|
|
|
let response = self.state.on_window_event(&self.window, &event);
|
|
|
|
egui_winit::update_viewport_info(
|
|
|
|
&mut self.info,
|
|
|
|
self.state.egui_ctx(),
|
|
|
|
&self.window,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
|
|
|
|
match event {
|
|
|
|
WindowEvent::RedrawRequested => Some(Action::Redraw),
|
|
|
|
WindowEvent::CloseRequested => Some(Action::Close),
|
|
|
|
WindowEvent::Resized(size) => {
|
|
|
|
let (Some(width), Some(height)) =
|
|
|
|
(NonZero::new(size.width), NonZero::new(size.height))
|
|
|
|
else {
|
|
|
|
return None;
|
|
|
|
};
|
|
|
|
self.painter
|
|
|
|
.on_window_resized(ViewportId::ROOT, width, height);
|
|
|
|
None
|
|
|
|
}
|
|
|
|
_ if response.repaint => Some(Action::Redraw),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option<Action> {
|
|
|
|
let mut input = self.state.take_egui_input(&self.window);
|
|
|
|
input.viewports = std::iter::once((ViewportId::ROOT, self.info.clone())).collect();
|
|
|
|
let mut output = self.ctx.run(input, |ctx| {
|
|
|
|
self.app.show(ctx);
|
|
|
|
});
|
|
|
|
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
|
|
|
|
self.painter.paint_and_update_textures(
|
|
|
|
ViewportId::ROOT,
|
|
|
|
output.pixels_per_point,
|
|
|
|
[0.0, 0.0, 0.0, 0.0],
|
|
|
|
&clipped_primitives,
|
|
|
|
&output.textures_delta,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
|
|
|
|
self.state
|
|
|
|
.handle_platform_output(&self.window, output.platform_output);
|
|
|
|
|
2024-11-28 17:39:08 +00:00
|
|
|
let Some(mut viewport_output) = output.viewport_output.remove(&ViewportId::ROOT) else {
|
2024-11-28 15:27:18 +00:00
|
|
|
return Some(Action::Close);
|
|
|
|
};
|
|
|
|
|
|
|
|
let (mut deferred_commands, recreate) = self.builder.patch(viewport_output.builder);
|
|
|
|
if recreate {
|
|
|
|
let (window, state) =
|
|
|
|
create_window_and_state(&self.ctx, event_loop, &self.builder, &mut self.painter);
|
|
|
|
egui_winit::update_viewport_info(&mut self.info, &self.ctx, &window, true);
|
|
|
|
self.window = window;
|
|
|
|
self.state = state;
|
|
|
|
}
|
|
|
|
self.commands.append(&mut deferred_commands);
|
2024-11-28 17:39:08 +00:00
|
|
|
self.commands.append(&mut viewport_output.commands);
|
2024-11-28 15:27:18 +00:00
|
|
|
egui_winit::process_viewport_commands(
|
|
|
|
&self.ctx,
|
|
|
|
&mut self.info,
|
|
|
|
std::mem::take(&mut self.commands),
|
|
|
|
&self.window,
|
|
|
|
&mut HashSet::default(),
|
|
|
|
);
|
|
|
|
|
|
|
|
if self.info.close_requested() {
|
|
|
|
Some(Action::Close)
|
|
|
|
} else {
|
|
|
|
Some(Action::Redraw)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for Viewport {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
self.app.on_destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum UserEvent {
|
|
|
|
GamepadEvent(gilrs::Event),
|
|
|
|
OpenInput,
|
|
|
|
OpenPlayer2,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub enum Action {
|
|
|
|
Redraw,
|
|
|
|
Close,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn create_window_and_state(
|
|
|
|
ctx: &Context,
|
|
|
|
event_loop: &ActiveEventLoop,
|
|
|
|
builder: &ViewportBuilder,
|
|
|
|
painter: &mut egui_wgpu::winit::Painter,
|
|
|
|
) -> (Arc<Window>, egui_winit::State) {
|
|
|
|
pollster::block_on(painter.set_window(ViewportId::ROOT, None)).unwrap();
|
|
|
|
let window = Arc::new(egui_winit::create_window(ctx, event_loop, builder).unwrap());
|
|
|
|
pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone()))).unwrap();
|
|
|
|
let state = egui_winit::State::new(
|
|
|
|
ctx.clone(),
|
|
|
|
ViewportId::ROOT,
|
|
|
|
event_loop,
|
|
|
|
Some(window.scale_factor() as f32),
|
|
|
|
event_loop.system_theme(),
|
|
|
|
painter.max_texture_side(),
|
|
|
|
);
|
|
|
|
(window, state)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {
|
|
|
|
let Ok(mut gilrs) = Gilrs::new() else {
|
|
|
|
eprintln!("could not connect gamepad listener");
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
while let Some(event) = gilrs.next_event_blocking(None) {
|
|
|
|
if event.event == EventType::Connected {
|
|
|
|
let Some(gamepad) = gilrs.connected_gamepad(event.id) else {
|
|
|
|
continue;
|
|
|
|
};
|
2024-11-29 19:36:46 +00:00
|
|
|
mappings.handle_gamepad_connect(&gamepad);
|
|
|
|
}
|
|
|
|
if event.event == EventType::Disconnected {
|
|
|
|
mappings.handle_gamepad_disconnect(event.id);
|
2024-11-28 15:27:18 +00:00
|
|
|
}
|
|
|
|
if proxy.send_event(UserEvent::GamepadEvent(event)).is_err() {
|
|
|
|
// main thread has closed! we done
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|