use rfd::AsyncFileDialog; use std::fs; use std::path::PathBuf; use std::sync::mpsc::Receiver; use std::sync::mpsc::Sender; use std::sync::mpsc::SyncSender; use std::thread; use std::time::Duration; use std::time::Instant; use winit::application::ApplicationHandler; use winit::dpi::PhysicalSize; use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; use winit::event_loop::ControlFlow; use winit::window::WindowId; use crate::cef; use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; use crate::event::{AppEvent, AppEventScheduler}; use crate::persist::PersistentData; use crate::render::{RenderError, RenderState}; use crate::window::Window; use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { render_state: Option, wgpu_context: WgpuContext, window: Option, window_scale: f64, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, desktop_wrapper: DesktopWrapper, cef_context: Box, cef_schedule: Option, cef_view_info_sender: Sender, last_ui_update: Instant, avg_frame_time: f32, start_render_sender: SyncSender<()>, web_communication_initialized: bool, web_communication_startup_buffer: Vec>, persistent_data: PersistentData, launch_documents: Vec, } impl App { pub(crate) fn init() { Window::init(); } pub(crate) fn new( cef_context: Box, cef_view_info_sender: Sender, wgpu_context: WgpuContext, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, launch_documents: Vec, ) -> Self { let ctrlc_app_event_scheduler = app_event_scheduler.clone(); ctrlc::set_handler(move || { tracing::info!("Termination signal received, exiting..."); ctrlc_app_event_scheduler.schedule(AppEvent::CloseWindow); }) .expect("Error setting Ctrl-C handler"); let rendering_app_event_scheduler = app_event_scheduler.clone(); let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1); std::thread::spawn(move || { loop { let result = futures::executor::block_on(DesktopWrapper::execute_node_graph()); rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result)); let _ = start_render_receiver.recv(); } }); let mut persistent_data = PersistentData::default(); persistent_data.load_from_disk(); Self { render_state: None, wgpu_context, window: None, window_scale: 1., app_event_receiver, app_event_scheduler, desktop_wrapper: DesktopWrapper::new(), last_ui_update: Instant::now(), cef_context, cef_schedule: Some(Instant::now()), cef_view_info_sender, avg_frame_time: 0., start_render_sender, web_communication_initialized: false, web_communication_startup_buffer: Vec::new(), persistent_data, launch_documents, } } fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage, responses: &mut Vec) { match message { DesktopFrontendMessage::ToWeb(messages) => { let Some(bytes) = serialize_frontend_messages(messages) else { tracing::error!("Failed to serialize frontend messages"); return; }; self.send_or_queue_web_message(bytes); } DesktopFrontendMessage::OpenFileDialog { title, filters, context } => { let app_event_scheduler = self.app_event_scheduler.clone(); let _ = thread::spawn(move || { let mut dialog = AsyncFileDialog::new().set_title(title); for filter in filters { dialog = dialog.add_filter(filter.name, &filter.extensions); } let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) }; if let Some(path) = futures::executor::block_on(show_dialog) && let Ok(content) = fs::read(&path) { let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context }; app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); } }); } DesktopFrontendMessage::SaveFileDialog { title, default_filename, default_folder, filters, context, } => { let app_event_scheduler = self.app_event_scheduler.clone(); let _ = thread::spawn(move || { let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename); if let Some(folder) = default_folder { dialog = dialog.set_directory(folder); } for filter in filters { dialog = dialog.add_filter(filter.name, &filter.extensions); } let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) }; if let Some(path) = futures::executor::block_on(show_dialog) { let message = DesktopWrapperMessage::SaveFileDialogResult { path, context }; app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); } }); } DesktopFrontendMessage::WriteFile { path, content } => { if let Err(e) = fs::write(&path, content) { tracing::error!("Failed to write file {}: {}", path.display(), e); } } DesktopFrontendMessage::OpenUrl(url) => { let _ = thread::spawn(move || { if let Err(e) = open::that(&url) { tracing::error!("Failed to open URL: {}: {}", url, e); } }); } DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => { if let Some(render_state) = &mut self.render_state && let Some(window) = &self.window { let window_size = window.surface_size(); let viewport_offset_x = x / window_size.width as f64; let viewport_offset_y = y / window_size.height as f64; render_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]); let viewport_scale_x = if width != 0.0 { window_size.width as f64 / width } else { 1.0 }; let viewport_scale_y = if height != 0.0 { window_size.height as f64 / height } else { 1.0 }; render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]); } } DesktopFrontendMessage::UpdateOverlays(scene) => { if let Some(render_state) = &mut self.render_state { render_state.set_overlays_scene(scene); } } DesktopFrontendMessage::PersistenceWriteDocument { id, document } => { self.persistent_data.write_document(id, document); } DesktopFrontendMessage::PersistenceDeleteDocument { id } => { self.persistent_data.delete_document(&id); } DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => { self.persistent_data.set_current_document(id); } DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => { self.persistent_data.set_document_order(ids); } DesktopFrontendMessage::PersistenceWritePreferences { preferences } => { self.persistent_data.write_preferences(preferences); } DesktopFrontendMessage::PersistenceLoadPreferences => { let preferences = self.persistent_data.load_preferences(); let message = DesktopWrapperMessage::LoadPreferences { preferences }; responses.push(message); } DesktopFrontendMessage::PersistenceLoadCurrentDocument => { if let Some((id, document)) = self.persistent_data.current_document() { let message = DesktopWrapperMessage::LoadDocument { id, document, to_front: false, select_after_open: true, }; responses.push(message); } } DesktopFrontendMessage::PersistenceLoadRemainingDocuments => { for (id, document) in self.persistent_data.documents_before_current().into_iter().rev() { let message = DesktopWrapperMessage::LoadDocument { id, document, to_front: true, select_after_open: false, }; responses.push(message); } for (id, document) in self.persistent_data.documents_after_current() { let message = DesktopWrapperMessage::LoadDocument { id, document, to_front: false, select_after_open: false, }; responses.push(message); } if let Some(id) = self.persistent_data.current_document_id() { let message = DesktopWrapperMessage::SelectDocument { id }; responses.push(message); } } DesktopFrontendMessage::OpenLaunchDocuments => { if self.launch_documents.is_empty() { return; } let app_event_scheduler = self.app_event_scheduler.clone(); let launch_documents = std::mem::take(&mut self.launch_documents); let _ = thread::spawn(move || { for path in launch_documents { tracing::info!("Opening file from command line: {}", path.display()); if let Ok(content) = fs::read(&path) { let message = DesktopWrapperMessage::OpenFile { path, content }; app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); } else { tracing::error!("Failed to read file: {}", path.display()); } } }); } DesktopFrontendMessage::UpdateMenu { entries } => { if let Some(window) = &self.window { window.update_menu(entries); } } DesktopFrontendMessage::WindowClose => { self.app_event_scheduler.schedule(AppEvent::CloseWindow); } DesktopFrontendMessage::WindowMinimize => { if let Some(window) = &self.window { window.minimize(); } } DesktopFrontendMessage::WindowMaximize => { if let Some(window) = &self.window { window.toggle_maximize(); } } DesktopFrontendMessage::WindowDrag => { if let Some(window) = &self.window { window.start_drag(); } } DesktopFrontendMessage::WindowHide => { if let Some(window) = &self.window { window.hide(); } } DesktopFrontendMessage::WindowHideOthers => { if let Some(window) = &self.window { window.hide_others(); } } DesktopFrontendMessage::WindowShowAll => { if let Some(window) = &self.window { window.show_all(); } } } } fn handle_desktop_frontend_messages(&mut self, messages: Vec) { let mut responses = Vec::new(); for message in messages { self.handle_desktop_frontend_message(message, &mut responses); } for message in responses { self.dispatch_desktop_wrapper_message(message); } } fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) { let responses = self.desktop_wrapper.dispatch(message); self.handle_desktop_frontend_messages(responses); } fn send_or_queue_web_message(&mut self, message: Vec) { if self.web_communication_initialized { self.cef_context.send_web_message(message); } else { self.web_communication_startup_buffer.push(message); } } fn user_event(&mut self, event_loop: &dyn ActiveEventLoop, event: AppEvent) { match event { AppEvent::WebCommunicationInitialized => { self.web_communication_initialized = true; for message in self.web_communication_startup_buffer.drain(..) { self.cef_context.send_web_message(message); } } AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), AppEvent::NodeGraphExecutionResult(result) => match result { NodeGraphExecutionResult::HasRun(texture) => { self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation); if let Some(texture) = texture && let Some(render_state) = self.render_state.as_mut() && let Some(window) = self.window.as_ref() { render_state.bind_viewport_texture(texture); window.request_redraw(); } } NodeGraphExecutionResult::NotRun => {} }, AppEvent::UiUpdate(texture) => { if let Some(render_state) = self.render_state.as_mut() { render_state.bind_ui_texture(texture); let elapsed = self.last_ui_update.elapsed().as_secs_f32(); self.last_ui_update = Instant::now(); if elapsed < 0.5 { self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.; } } if let Some(window) = &self.window { window.request_redraw(); } } AppEvent::ScheduleBrowserWork(instant) => { if instant <= Instant::now() { self.cef_context.work(); } else { self.cef_schedule = Some(instant); } } AppEvent::CursorChange(cursor) => { if let Some(window) = &mut self.window { window.set_cursor(event_loop, cursor); } } AppEvent::CloseWindow => { // TODO: Implement graceful shutdown tracing::info!("Exiting main event loop"); event_loop.exit(); } #[cfg(target_os = "macos")] AppEvent::MenuEvent { id } => { self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id }); } } } } impl ApplicationHandler for App { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { let window = Window::new(event_loop, self.app_event_scheduler.clone()); self.window_scale = window.scale_factor(); let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(self.window_scale)); // Ensures the CEF texture does not remain at 1x1 pixels until the window is resized by the user // Affects only some Mac devices (issue found on 2023 M2 Mac Mini). let PhysicalSize { width, height } = window.surface_size(); let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { width, height }); self.cef_context.notify_view_info_changed(); self.window = Some(window); let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone()); self.render_state = Some(render_state); self.desktop_wrapper.init(self.wgpu_context.clone()); #[cfg(target_os = "windows")] let platform = Platform::Windows; #[cfg(target_os = "macos")] let platform = Platform::Mac; #[cfg(target_os = "linux")] let platform = Platform::Linux; self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::UpdatePlatform(platform)); } fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) { while let Ok(event) = self.app_event_receiver.try_recv() { self.user_event(event_loop, event); } } fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) { self.cef_context.handle_window_event(&event); match event { WindowEvent::CloseRequested => { self.app_event_scheduler.schedule(AppEvent::CloseWindow); } WindowEvent::SurfaceResized(PhysicalSize { width, height }) => { let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { width, height }); self.cef_context.notify_view_info_changed(); if let Some(render_state) = &mut self.render_state { render_state.resize(width, height); } if let Some(window) = &self.window { let maximized = window.is_maximized(); self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized })); window.request_redraw(); } } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { self.window_scale = scale_factor; let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(self.window_scale)); self.cef_context.notify_view_info_changed(); } WindowEvent::RedrawRequested => { let Some(render_state) = &mut self.render_state else { return }; if let Some(window) = &self.window { let size = window.surface_size(); render_state.resize(size.width, size.height); match render_state.render(window) { Ok(_) => {} Err(RenderError::OutdatedUITextureError) => { self.cef_context.notify_view_info_changed(); } Err(RenderError::SurfaceError(wgpu::SurfaceError::Lost)) => { tracing::warn!("lost surface"); } Err(RenderError::SurfaceError(wgpu::SurfaceError::OutOfMemory)) => { tracing::error!("GPU out of memory"); event_loop.exit(); } Err(RenderError::SurfaceError(e)) => tracing::error!("Render error: {:?}", e), } let _ = self.start_render_sender.try_send(()); } } WindowEvent::DragDropped { paths, .. } => { for path in paths { match fs::read(&path) { Ok(content) => { let message = DesktopWrapperMessage::OpenFile { path, content }; self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); } Err(e) => { tracing::error!("Failed to read dropped file {}: {}", path.display(), e); return; } }; } } _ => {} } // Notify cef of possible input events self.cef_context.work(); } fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) { // Set a timeout in case we miss any cef schedule requests let timeout = Instant::now() + Duration::from_millis(10); let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout)); if let Some(schedule) = self.cef_schedule && schedule < Instant::now() { self.cef_schedule = None; // Poll cef message loop multiple times to avoid message loop starvation for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS { self.cef_context.work(); } } if let Some(window) = &self.window.as_ref() { window.request_redraw(); } event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until)); } }