Lightningbeam/src-tauri/src/lib.rs

486 lines
16 KiB
Rust

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<render_window::RenderWindowHandle>,
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<Mutex<frame_streamer::FrameStreamer>>>,
) -> 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<Mutex<RenderWindowState>>>,
) -> 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<Mutex<RenderWindowState>>>,
) -> 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<Mutex<RenderWindowState>>>,
) -> 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<Mutex<RenderWindowState>>>,
) -> 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<Mutex<RenderWindowState>>>,
) -> 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<Mutex<RenderWindowState>>>,
) -> 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<String>) {
let state = app.state::<Mutex<AppState>>();
// 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<PathBuf>) {
// -- 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::<Vec<_>>()
.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::<Vec<_>>();
tauri::async_runtime::spawn(async move {
for path in files {
create_window(app.clone(), Some(path)).await;
}
});
}
},
);
}