use crate::ecs::window::resources::{SecondaryWindowInput, SecondaryWindowState};
use crate::ecs::world::World;
use crate::run::State;
use std::collections::HashMap;
use std::sync::Arc;
use winit::event::{DeviceEvent, WindowEvent};
#[cfg(feature = "egui")]
mod egui_support;
pub struct SecondaryWindowInfo {
pub handle: Arc<winit::window::Window>,
pub index: usize,
#[cfg(feature = "egui")]
pub egui_state: Option<egui_winit::State>,
}
pub struct MultiWindowContext {
pub state: Box<dyn State>,
pub world: World,
pub renderer: Option<Box<dyn crate::ecs::world::Render>>,
primary_window_id: Option<winit::window::WindowId>,
secondary_windows: HashMap<winit::window::WindowId, SecondaryWindowInfo>,
focused_window: Option<winit::window::WindowId>,
next_secondary_index: usize,
#[cfg(not(target_arch = "wasm32"))]
pub snapshot_path: Option<std::path::PathBuf>,
#[cfg(not(target_arch = "wasm32"))]
pub snapshot_queued: bool,
}
impl MultiWindowContext {
pub fn new(state: Box<dyn State>, world: World) -> Self {
Self {
state,
world,
renderer: None,
primary_window_id: None,
secondary_windows: HashMap::new(),
focused_window: None,
next_secondary_index: 1,
#[cfg(not(target_arch = "wasm32"))]
snapshot_path: std::env::var("NIGHTSHADE_SNAPSHOT_PATH")
.ok()
.map(std::path::PathBuf::from),
#[cfg(not(target_arch = "wasm32"))]
snapshot_queued: false,
}
}
fn is_primary_window(&self, window_id: winit::window::WindowId) -> bool {
self.primary_window_id == Some(window_id)
}
fn process_pending_spawns(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let pending: Vec<_> = self
.world
.resources
.secondary_windows
.pending_spawns
.drain(..)
.collect();
for request in pending {
let attributes = winit::window::Window::default_attributes()
.with_title(&request.title)
.with_inner_size(winit::dpi::LogicalSize::new(request.width, request.height));
let Ok(window) = event_loop.create_window(attributes) else {
continue;
};
let window_handle = Arc::new(window);
let index = self.next_secondary_index;
self.next_secondary_index += 1;
if let Some(renderer) = &mut self.renderer
&& let Err(error) = renderer.create_secondary_surface(
index,
window_handle.clone(),
request.width.max(1),
request.height.max(1),
)
{
tracing::error!("Failed to create secondary surface {index}: {error}");
continue;
}
#[cfg(feature = "egui")]
let egui_state = if request.egui_enabled {
egui_support::create_secondary_egui_state(&mut self.renderer, index, &window_handle)
} else {
None
};
let winit_id = window_handle.id();
self.secondary_windows.insert(
winit_id,
SecondaryWindowInfo {
handle: window_handle,
index,
#[cfg(feature = "egui")]
egui_state,
},
);
self.world
.resources
.secondary_windows
.states
.push(SecondaryWindowState {
index,
title: request.title,
size: (request.width, request.height),
is_focused: false,
input: SecondaryWindowInput::default(),
close_requested: false,
egui_enabled: request.egui_enabled,
});
}
}
fn process_close_requests(&mut self) {
let closed_indices: Vec<usize> = self
.world
.resources
.secondary_windows
.states
.iter()
.filter(|window| window.close_requested)
.map(|window| window.index)
.collect();
for index in &closed_indices {
if let Some(renderer) = &mut self.renderer {
renderer.remove_secondary_surface(*index);
}
self.secondary_windows
.retain(|_, info| info.index != *index);
}
self.world
.resources
.secondary_windows
.states
.retain(|window| !window.close_requested);
}
fn handle_secondary_window_event(
&mut self,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
let Some(info) = self.secondary_windows.get_mut(&window_id) else {
return;
};
let index = info.index;
#[cfg(feature = "egui")]
if let Some(egui_state) = info.egui_state.as_mut() {
let _ = egui_state.on_window_event(&info.handle, &event);
}
match event {
WindowEvent::Resized(winit::dpi::PhysicalSize { width, height })
if width > 0 && height > 0 =>
{
if let Some(renderer) = &mut self.renderer
&& let Err(error) = renderer.resize_secondary_surface(index, width, height)
{
tracing::error!("Failed to resize secondary surface {index}: {error}");
}
if let Some(state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
state.size = (width, height);
}
}
WindowEvent::CloseRequested => {
if let Some(state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
state.close_requested = true;
}
}
WindowEvent::Focused(focused) => {
if focused {
self.focused_window = Some(window_id);
self.world.resources.secondary_windows.focused_index = Some(index);
self.world.resources.window.is_focused = false;
} else if self.focused_window == Some(window_id) {
self.focused_window = None;
self.world.resources.secondary_windows.focused_index = None;
}
if let Some(state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
state.is_focused = focused;
}
}
WindowEvent::CursorMoved { position, .. } => {
if let Some(state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
let new_pos = nalgebra_glm::Vec2::new(position.x as f32, position.y as f32);
state.input.mouse_position_delta = new_pos - state.input.mouse_position;
state.input.mouse_position = new_pos;
state
.input
.mouse_state
.set(crate::ecs::input::resources::MouseState::MOVED, true);
}
}
WindowEvent::MouseInput { button, state, .. } => {
if let Some(window_state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
let pressed = state == winit::event::ElementState::Pressed;
let released = state == winit::event::ElementState::Released;
use crate::ecs::input::resources::MouseState;
match button {
winit::event::MouseButton::Left => {
let was_clicked = window_state
.input
.mouse_state
.contains(MouseState::LEFT_CLICKED);
window_state
.input
.mouse_state
.set(MouseState::LEFT_CLICKED, pressed);
if pressed && !was_clicked {
window_state
.input
.mouse_state
.set(MouseState::LEFT_JUST_PRESSED, true);
}
if released && was_clicked {
window_state
.input
.mouse_state
.set(MouseState::LEFT_JUST_RELEASED, true);
}
}
winit::event::MouseButton::Middle => {
let was_clicked = window_state
.input
.mouse_state
.contains(MouseState::MIDDLE_CLICKED);
window_state
.input
.mouse_state
.set(MouseState::MIDDLE_CLICKED, pressed);
if pressed && !was_clicked {
window_state
.input
.mouse_state
.set(MouseState::MIDDLE_JUST_PRESSED, true);
}
if released && was_clicked {
window_state
.input
.mouse_state
.set(MouseState::MIDDLE_JUST_RELEASED, true);
}
}
winit::event::MouseButton::Right => {
let was_clicked = window_state
.input
.mouse_state
.contains(MouseState::RIGHT_CLICKED);
window_state
.input
.mouse_state
.set(MouseState::RIGHT_CLICKED, pressed);
if pressed && !was_clicked {
window_state
.input
.mouse_state
.set(MouseState::RIGHT_JUST_PRESSED, true);
}
if released && was_clicked {
window_state
.input
.mouse_state
.set(MouseState::RIGHT_JUST_RELEASED, true);
}
}
_ => {}
}
}
}
WindowEvent::KeyboardInput {
event:
winit::event::KeyEvent {
physical_key: winit::keyboard::PhysicalKey::Code(key_code),
state,
ref text,
..
},
..
} => {
if let Some(window_state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
*window_state
.input
.keyboard_keystates
.entry(key_code)
.or_insert(state) = state;
let pressed = state == winit::event::ElementState::Pressed;
window_state.input.frame_keys.push((key_code, pressed));
if pressed && let Some(text) = text {
for character in text.chars() {
window_state.input.frame_chars.push(character);
}
}
}
}
WindowEvent::MouseWheel { delta, .. } => {
if let Some(window_state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
let (horizontal_delta, vertical_delta) = match delta {
winit::event::MouseScrollDelta::LineDelta(horizontal, vertical) => {
(horizontal, vertical)
}
winit::event::MouseScrollDelta::PixelDelta(position) => {
(position.x as f32 / 120.0, position.y as f32 / 120.0)
}
};
window_state.input.mouse_wheel_delta =
nalgebra_glm::Vec2::new(horizontal_delta, vertical_delta);
window_state
.input
.mouse_state
.set(crate::ecs::input::resources::MouseState::SCROLLED, true);
}
}
_ => {}
}
}
}
impl winit::application::ApplicationHandler for MultiWindowContext {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
#[allow(unused_mut)]
let mut attributes = winit::window::Window::default_attributes();
#[cfg(not(target_arch = "wasm32"))]
{
attributes = attributes.with_title(self.state.title());
#[cfg(feature = "assets")]
if let Some(icon_bytes) = self.state.icon_bytes()
&& let Ok(icon_image) = image::load_from_memory(icon_bytes)
{
let icon_rgba = icon_image.to_rgba8();
let (width, height) = icon_rgba.dimensions();
if let Ok(icon) =
winit::window::Icon::from_rgba(icon_rgba.into_raw(), width, height)
{
attributes = attributes.with_window_icon(Some(icon));
}
}
}
let Ok(window) = event_loop.create_window(attributes) else {
return;
};
let window_handle = Arc::new(window);
self.primary_window_id = Some(window_handle.id());
self.focused_window = Some(window_handle.id());
self.world.resources.window.cached_scale_factor = window_handle.scale_factor() as f32;
self.world.resources.window.handle = Some(window_handle.clone());
self.world.resources.window.window_index = 0;
self.world.resources.window.is_focused = true;
#[cfg(not(target_arch = "wasm32"))]
let dimension = window_handle.inner_size();
#[cfg(all(not(target_arch = "wasm32"), feature = "async_runtime"))]
{
let runtime = tokio::runtime::Runtime::new().unwrap();
match runtime.block_on(crate::render::core::create_renderer(
window_handle.clone(),
dimension.width,
dimension.height,
)) {
Ok(mut renderer) => {
if let Err(error) = renderer.configure_with_state(self.state.as_mut()) {
tracing::error!("Failed to configure renderer with state: {error}");
}
self.renderer = Some(renderer);
}
Err(error) => {
tracing::error!("Failed to create renderer: {error}");
}
}
}
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async_runtime")))]
{
match pollster::block_on(crate::render::core::create_renderer(
window_handle.clone(),
dimension.width,
dimension.height,
)) {
Ok(mut renderer) => {
if let Err(error) = renderer.configure_with_state(self.state.as_mut()) {
tracing::error!("Failed to configure renderer with state: {error}");
}
self.renderer = Some(renderer);
}
Err(error) => {
tracing::error!("Failed to create renderer: {error}");
}
}
}
#[cfg(not(target_arch = "wasm32"))]
{
#[cfg(feature = "egui")]
crate::run::initialize_user_interface(&mut self.world);
self.world.resources.frame_schedule = crate::schedule::build_default_frame_schedule();
self.state.initialize(&mut self.world);
crate::ecs::camera::systems::update_camera_aspect_ratios(&mut self.world);
if self.world.resources.graphics.use_fullscreen
&& let Some(window_handle) = &self.world.resources.window.handle
{
window_handle.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
}
if let Some(window_handle) = &self.world.resources.window.handle {
window_handle.set_cursor_visible(self.world.resources.graphics.show_cursor);
}
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
if self.is_primary_window(window_id) {
if self.world.resources.window.should_exit
|| matches!(event, WindowEvent::CloseRequested)
{
event_loop.exit();
return;
}
match &event {
WindowEvent::Focused(focused) => {
self.world.resources.window.is_focused = *focused;
if *focused {
self.focused_window = Some(window_id);
self.world.resources.secondary_windows.focused_index = None;
}
}
WindowEvent::ScaleFactorChanged { .. } => {
if let Some(handle) = self.world.resources.window.handle.as_ref() {
self.world.resources.window.cached_scale_factor =
handle.scale_factor() as f32;
}
#[cfg(feature = "egui")]
crate::run::update_egui_scale_factor(&mut self.world);
}
WindowEvent::Resized(winit::dpi::PhysicalSize { width, height })
if *width > 0 && *height > 0 =>
{
self.world.resources.window.cached_viewport_size = Some((*width, *height));
if let Some(handle) = self.world.resources.window.handle.as_ref() {
self.world.resources.window.cached_scale_factor =
handle.scale_factor() as f32;
}
if let Some(renderer) = &mut self.renderer
&& let Err(error) = renderer.resize_surface(*width, *height)
{
tracing::error!("Failed to resize surface: {error}");
}
#[cfg(feature = "egui")]
crate::run::update_egui_scale_factor(&mut self.world);
}
_ => {}
}
if let Some(new_state) =
crate::run::step(&mut self.world, &mut self.state, &mut self.renderer, &event)
{
self.state = new_state;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = &self.snapshot_path {
let uptime = self.world.resources.window.timing.uptime_milliseconds;
if !self.snapshot_queued && uptime >= 4000 {
crate::ecs::world::commands::capture_screenshot_to_path(
&mut self.world,
path.clone(),
);
self.snapshot_queued = true;
} else if self.snapshot_queued && uptime >= 5000 {
self.world.resources.window.should_exit = true;
}
}
crate::run::request_redraw_system(&mut self.world);
} else {
self.handle_secondary_window_event(window_id, event);
}
}
fn device_event(
&mut self,
_event_loop: &winit::event_loop::ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: DeviceEvent,
) {
if let DeviceEvent::MouseMotion { delta } = event
&& let Some(focused_id) = self.focused_window
{
if self.is_primary_window(focused_id) {
let mouse = &mut self.world.resources.input.mouse;
mouse.raw_mouse_delta.x += delta.0 as f32;
mouse.raw_mouse_delta.y += delta.1 as f32;
} else if let Some(info) = self.secondary_windows.get(&focused_id) {
let index = info.index;
if let Some(window_state) = self
.world
.resources
.secondary_windows
.states
.iter_mut()
.find(|w| w.index == index)
{
window_state.input.raw_mouse_delta.x += delta.0 as f32;
window_state.input.raw_mouse_delta.y += delta.1 as f32;
}
}
}
}
fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
self.process_pending_spawns(event_loop);
self.process_close_requests();
#[cfg(feature = "egui")]
self.process_secondary_egui();
for state in &mut self.world.resources.secondary_windows.states {
state.input.frame_keys.clear();
state.input.frame_chars.clear();
}
crate::run::request_redraw_system(&mut self.world);
}
fn exiting(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}
}