use std::{path::PathBuf, sync::{Arc, Mutex}}; use log::{trace, info, debug, warn, error}; use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder}; mod audio; mod video; mod frame_streamer; mod renderer; mod render_window; #[derive(Default)] struct AppState { counter: u32, } struct RenderWindowState { handle: Option, canvas_offset: (i32, i32), // Canvas position relative to window canvas_size: (u32, u32), } // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } #[tauri::command] fn trace(msg: String) { trace!("{}",msg); } #[tauri::command] fn info(msg: String) { info!("{}",msg); } #[tauri::command] fn debug(msg: String) { debug!("{}",msg); } #[tauri::command] fn warn(msg: String) { warn!("{}",msg); } #[tauri::command] fn error(msg: String) { error!("{}",msg); } #[tauri::command] fn get_frame_streamer_port( frame_streamer: tauri::State<'_, Arc>>, ) -> u16 { let streamer = frame_streamer.lock().unwrap(); streamer.port() } // Render window commands #[tauri::command] fn render_window_create( x: i32, y: i32, width: u32, height: u32, canvas_offset_x: i32, canvas_offset_y: i32, app: tauri::AppHandle, state: tauri::State<'_, Arc>>, ) -> Result<(), String> { let mut render_state = state.lock().unwrap(); if render_state.handle.is_some() { return Err("Render window already exists".to_string()); } let handle = render_window::spawn_render_window(x, y, width, height)?; render_state.handle = Some(handle); render_state.canvas_offset = (canvas_offset_x, canvas_offset_y); render_state.canvas_size = (width, height); // Start a background thread to poll main window position let state_clone = state.inner().clone(); let app_clone = app.clone(); std::thread::spawn(move || { let mut last_pos: Option<(i32, i32)> = None; loop { std::thread::sleep(std::time::Duration::from_millis(50)); if let Some(main_window) = app_clone.get_webview_window("main") { if let Ok(pos) = main_window.outer_position() { let current_pos = (pos.x, pos.y); // Only update if position actually changed if last_pos != Some(current_pos) { eprintln!("[WindowSync] Main window position: {:?}", current_pos); let render_state = state_clone.lock().unwrap(); if let Some(handle) = &render_state.handle { let new_x = pos.x + render_state.canvas_offset.0; let new_y = pos.y + render_state.canvas_offset.1; handle.set_position(new_x, new_y); last_pos = Some(current_pos); } else { break; // No handle, exit thread } } } else { break; // Window closed, exit thread } } else { break; // Main window gone, exit thread } } }); Ok(()) } #[tauri::command] fn render_window_update_gradient( top_r: f32, top_g: f32, top_b: f32, top_a: f32, bottom_r: f32, bottom_g: f32, bottom_b: f32, bottom_a: f32, state: tauri::State<'_, Arc>>, ) -> Result<(), String> { let render_state = state.lock().unwrap(); if let Some(handle) = &render_state.handle { handle.update_gradient( [top_r, top_g, top_b, top_a], [bottom_r, bottom_g, bottom_b, bottom_a], ); Ok(()) } else { Err("Render window not created".to_string()) } } #[tauri::command] fn render_window_set_position( x: i32, y: i32, state: tauri::State<'_, Arc>>, ) -> Result<(), String> { let render_state = state.lock().unwrap(); if let Some(handle) = &render_state.handle { handle.set_position(x, y); Ok(()) } else { Err("Render window not created".to_string()) } } #[tauri::command] fn render_window_sync_position( app: tauri::AppHandle, state: tauri::State<'_, Arc>>, ) -> Result<(), String> { let render_state = state.lock().unwrap(); if let Some(main_window) = app.get_webview_window("main") { if let Ok(pos) = main_window.outer_position() { if let Some(handle) = &render_state.handle { let new_x = pos.x + render_state.canvas_offset.0; let new_y = pos.y + render_state.canvas_offset.1; eprintln!("[Manual Sync] Updating to ({}, {})", new_x, new_y); handle.set_position(new_x, new_y); Ok(()) } else { Err("Render window not created".to_string()) } } else { Err("Could not get window position".to_string()) } } else { Err("Main window not found".to_string()) } } #[tauri::command] fn render_window_set_size( width: u32, height: u32, state: tauri::State<'_, Arc>>, ) -> Result<(), String> { let render_state = state.lock().unwrap(); if let Some(handle) = &render_state.handle { handle.set_size(width, height); Ok(()) } else { Err("Render window not created".to_string()) } } #[tauri::command] fn render_window_close( state: tauri::State<'_, Arc>>, ) -> Result<(), String> { let mut render_state = state.lock().unwrap(); if let Some(handle) = render_state.handle.take() { handle.close(); Ok(()) } else { Err("Render window not created".to_string()) } } use tauri::PhysicalSize; #[tauri::command] async fn create_window(app: tauri::AppHandle, path: Option) { let state = app.state::>(); // Lock the mutex to get mutable access: let mut state = state.lock().unwrap(); // Increment the counter and generate a unique window label let window_label = format!("window{}", state.counter); state.counter += 1; // Build the new window with the unique label let webview_window = WebviewWindowBuilder::new(&app, &window_label, WebviewUrl::App("index.html".into())) .title("Lightningbeam") .build() .unwrap(); // Get the current monitor's screen size from the new window if let Ok(Some(monitor)) = webview_window.current_monitor() { let screen_size = monitor.size(); // Get the size of the monitor let width = 4096; let height = 4096; // Set the window size to be the smaller of the specified size or the screen size let new_width = width.min(screen_size.width as u32); let new_height = height.min(screen_size.height as u32 - 100); // Set the size using PhysicalSize webview_window.set_size(tauri::Size::Physical(PhysicalSize::new(new_width, new_height))) .expect("Failed to set window size"); } else { eprintln!("Could not detect the current monitor."); } // Set the opened file if provided if let Some(val) = path { // Pass path data to the window via JavaScript webview_window.eval(&format!("window.openedFiles = [\"{val}\"]")).unwrap(); // Set the window title if provided webview_window.set_title(&val).expect("Failed to set window title"); } } fn handle_file_associations(app: AppHandle, files: Vec) { // -- Scope handling start -- // You can remove this block if you only want to know about the paths, but not actually "use" them in the frontend. // This requires the `fs` tauri plugin and is required to make the plugin's frontend work: use tauri_plugin_fs::FsExt; let fs_scope = app.fs_scope(); // This is for the `asset:` protocol to work: let asset_protocol_scope = app.asset_protocol_scope(); for file in &files { // This requires the `fs` plugin: let _ = fs_scope.allow_file(file); // This is for the `asset:` protocol: let _ = asset_protocol_scope.allow_file(file); } // -- Scope handling end -- let files = files .into_iter() .map(|f| { let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash format!("\"{file}\"",) // wrap in quotes for JS array }) .collect::>() .join(","); warn!("{}",files); let window = app.get_webview_window("main").unwrap(); window.eval(&format!("window.openedFiles = [{files}]")).unwrap(); } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Initialize env_logger with Error level only env_logger::Builder::from_default_env() .filter_level(log::LevelFilter::Error) .init(); // Initialize WebSocket frame streamer let frame_streamer = frame_streamer::FrameStreamer::new() .expect("Failed to start frame streamer"); eprintln!("[App] Frame streamer started on port {}", frame_streamer.port()); tauri::Builder::default() .manage(Mutex::new(AppState::default())) .manage(Arc::new(Mutex::new(audio::AudioState::default()))) .manage(Arc::new(Mutex::new(video::VideoState::default()))) .manage(Arc::new(Mutex::new(frame_streamer))) .manage(Arc::new(Mutex::new(RenderWindowState { handle: None, canvas_offset: (0, 0), canvas_size: (0, 0), }))) .setup(|app| { #[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS { let mut files = Vec::new(); // NOTICE: `args` may include URL protocol (`your-app-protocol://`) // or arguments (`--`) if your app supports them. // files may also be passed as `file://path/to/file` for maybe_file in std::env::args().skip(1) { // skip flags like -f or --flag if maybe_file.starts_with('-') { continue; } // handle `file://` path urls and skip other urls if let Ok(url) = Url::parse(&maybe_file) { // if let Ok(url) = url::Url::parse(&maybe_file) { if let Ok(path) = url.to_file_path() { files.push(path); } } else { files.push(PathBuf::from(maybe_file)) } } handle_file_associations(app.handle().clone(), files); } #[cfg(debug_assertions)] // only include this code on debug builds { let window = app.get_webview_window("main").unwrap(); window.open_devtools(); window.close_devtools(); } Ok(()) }) // .plugin( // tauri_plugin_log::Builder::new() // .filter(|metadata| { // // ONLY allow Error-level logs, block everything else // metadata.level() == log::Level::Error // }) // .timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal) // .format(|out, message, record| { // let date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); // out.finish(format_args!( // "{}[{}] {}", // date, // record.level(), // message // )) // }) // .targets([ // Target::new(TargetKind::Stdout), // // LogDir locations: // // Linux: /home/user/.local/share/org.lightningbeam.core/logs // // macOS: /Users/user/Library/Logs/org.lightningbeam.core/logs // // Windows: C:\Users\user\AppData\Local\org.lightningbeam.core\logs // Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }), // Target::new(TargetKind::Webview), // ]) // .build() // ) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ greet, trace, debug, info, warn, error, create_window, get_frame_streamer_port, render_window_create, render_window_update_gradient, render_window_set_position, render_window_set_size, render_window_sync_position, render_window_close, audio::audio_init, audio::audio_reset, audio::audio_play, audio::audio_stop, audio::audio_seek, audio::audio_test_beep, audio::audio_set_track_parameter, audio::audio_create_track, audio::audio_load_file, audio::audio_add_clip, audio::audio_move_clip, audio::audio_trim_clip, audio::audio_start_recording, audio::audio_stop_recording, audio::audio_pause_recording, audio::audio_resume_recording, audio::audio_start_midi_recording, audio::audio_stop_midi_recording, audio::audio_create_midi_clip, audio::audio_add_midi_note, audio::audio_load_midi_file, audio::audio_get_midi_clip_data, audio::audio_update_midi_clip_notes, audio::audio_send_midi_note_on, audio::audio_send_midi_note_off, audio::audio_set_active_midi_track, audio::audio_get_pool_file_info, audio::audio_get_pool_waveform, audio::graph_add_node, audio::graph_add_node_to_template, audio::graph_remove_node, audio::graph_connect, audio::graph_connect_in_template, audio::graph_disconnect, audio::graph_set_parameter, audio::graph_set_output_node, audio::graph_save_preset, audio::graph_load_preset, audio::graph_list_presets, audio::graph_delete_preset, audio::graph_get_state, audio::graph_get_template_state, audio::sampler_load_sample, audio::multi_sampler_add_layer, audio::multi_sampler_get_layers, audio::multi_sampler_update_layer, audio::multi_sampler_remove_layer, audio::get_oscilloscope_data, audio::automation_add_keyframe, audio::automation_remove_keyframe, audio::automation_get_keyframes, audio::automation_set_name, audio::automation_get_name, audio::audio_serialize_pool, audio::audio_load_pool, audio::audio_resolve_missing_file, audio::audio_serialize_track_graph, audio::audio_load_track_graph, video::video_load_file, video::video_stream_frame, video::video_set_cache_size, video::video_get_pool_info, ]) // .manage(window_counter) .build(tauri::generate_context!()) .expect("error while running tauri application") .run( #[allow(unused_variables)] |app, event| { #[cfg(any(target_os = "macos", target_os = "ios"))] if let tauri::RunEvent::Opened { urls } = event { let app = app.clone(); let files = urls .into_iter() .filter_map(|url| url.to_file_path().ok()) .map(|f| { let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash format!("\"{file}\"",) // wrap in quotes for JS array }) .collect::>(); tauri::async_runtime::spawn(async move { for path in files { create_window(app.clone(), Some(path)).await; } }); } }, ); }