From 6c10112a16e5becf3913af64f5d01fbcdb791401 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 16 Feb 2026 10:05:39 -0500 Subject: [PATCH] Fix build on Windows --- daw-backend/Cargo.toml | 2 +- daw-backend/src/lib.rs | 76 +++--- lightningbeam-ui/Cargo.lock | 222 ++++++++-------- .../lightningbeam-editor/Cargo.toml | 2 +- .../lightningbeam-editor/build.rs | 53 +++- .../lightningbeam-editor/src/main.rs | 240 +++++++----------- 6 files changed, 305 insertions(+), 290 deletions(-) diff --git a/daw-backend/Cargo.toml b/daw-backend/Cargo.toml index e7bfb33..8242b60 100644 --- a/daw-backend/Cargo.toml +++ b/daw-backend/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -cpal = "0.15" +cpal = "0.17" symphonia = { version = "0.5", features = ["all"] } rtrb = "0.3" midly = "0.5" diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index 008a35f..bb7c664 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -67,14 +67,12 @@ impl AudioSystem { .ok_or("No output device available")?; let default_output_config = output_device.default_output_config().map_err(|e| e.to_string())?; - let sample_rate = default_output_config.sample_rate().0; + let sample_rate = default_output_config.sample_rate(); let channels = default_output_config.channels() as u32; let debug_audio = std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1"); - if debug_audio { - eprintln!("[AUDIO DEBUG] Device: {:?}", output_device.name()); - eprintln!("[AUDIO DEBUG] Default config: {:?}", default_output_config); - eprintln!("[AUDIO DEBUG] Default buffer size: {:?}", default_output_config.buffer_size()); - } + + eprintln!("[AUDIO] Device: {:?}, format={:?}, rate={}, channels={}", + output_device.name().unwrap_or_default(), default_output_config.sample_format(), sample_rate, channels); // Create queues let (command_tx, command_rx) = rtrb::RingBuffer::new(512); // Larger buffer for MIDI + UI commands @@ -107,36 +105,23 @@ impl AudioSystem { } } - // Build output stream with configurable buffer size - let mut output_config: cpal::StreamConfig = default_output_config.clone().into(); + // Build output stream + let mut output_config: cpal::StreamConfig = default_output_config.into(); - // Set the requested buffer size - output_config.buffer_size = cpal::BufferSize::Fixed(buffer_size); + // WASAPI shared mode on Windows does not support fixed buffer sizes. + // Use the device default on Windows; honor the requested size on other platforms. + if cfg!(target_os = "windows") { + output_config.buffer_size = cpal::BufferSize::Default; + } else { + output_config.buffer_size = cpal::BufferSize::Fixed(buffer_size); + } let mut output_buffer = vec![0.0f32; 16384]; - if debug_audio { - eprintln!("[AUDIO DEBUG] Output config: sr={} Hz, ch={}, buf={:?}", - output_config.sample_rate.0, output_config.channels, output_config.buffer_size); - if let cpal::BufferSize::Fixed(size) = output_config.buffer_size { - let latency_ms = (size as f64 / output_config.sample_rate.0 as f64) * 1000.0; - eprintln!("[AUDIO DEBUG] Expected latency: {:.2} ms", latency_ms); - } - } - - let mut callback_log_count: u32 = 0; - let cb_debug = debug_audio; let output_stream = output_device .build_output_stream( &output_config, move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - if cb_debug && callback_log_count < 10 { - let frames = data.len() / output_config.channels as usize; - let latency_ms = (frames as f64 / output_config.sample_rate.0 as f64) * 1000.0; - eprintln!("[AUDIO CB #{}] {} samples ({} frames, {:.2} ms)", - callback_log_count, data.len(), frames, latency_ms); - callback_log_count += 1; - } let buf = &mut output_buffer[..data.len()]; buf.fill(0.0); engine.process(buf); @@ -145,7 +130,7 @@ impl AudioSystem { |err| eprintln!("Output stream error: {}", err), None, ) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("Failed to build output stream: {e:?}"))?; // Get input device let input_device = match host.default_input_device() { @@ -170,13 +155,10 @@ impl AudioSystem { } }; - // Get input config matching output sample rate and channels if possible + // Get input config - use the input device's own default config let input_config = match input_device.default_input_config() { Ok(config) => { - let mut cfg: cpal::StreamConfig = config.into(); - // Try to match output sample rate and channels - cfg.sample_rate = cpal::SampleRate(sample_rate); - cfg.channels = channels as u16; + let cfg: cpal::StreamConfig = config.into(); cfg } Err(e) => { @@ -193,25 +175,41 @@ impl AudioSystem { stream: output_stream, sample_rate, channels, - event_rx: None, // No event receiver when audio device unavailable + event_rx: None, }); } }; // Build input stream that feeds into the ringbuffer - let input_stream = input_device + let input_stream = match input_device .build_input_stream( &input_config, move |data: &[f32], _: &cpal::InputCallbackInfo| { - // Push input samples to ringbuffer for recording for &sample in data { let _ = input_tx.push(sample); } }, |err| eprintln!("Input stream error: {}", err), None, - ) - .map_err(|e| e.to_string())?; + ) { + Ok(stream) => stream, + Err(e) => { + eprintln!("Warning: Could not build input stream: {}, recording will be disabled", e); + output_stream.play().map_err(|e| e.to_string())?; + + if let Some(emitter) = event_emitter { + Self::spawn_emitter_thread(event_rx, emitter); + } + + return Ok(Self { + controller, + stream: output_stream, + sample_rate, + channels, + event_rx: None, + }); + } + }; // Start both streams output_stream.play().map_err(|e| e.to_string())?; diff --git a/lightningbeam-ui/Cargo.lock b/lightningbeam-ui/Cargo.lock index c154343..a1aeb75 100644 --- a/lightningbeam-ui/Cargo.lock +++ b/lightningbeam-ui/Cargo.lock @@ -23,6 +23,10 @@ name = "accesskit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" +dependencies = [ + "enumn", + "serde", +] [[package]] name = "accesskit_atspi_common" @@ -145,6 +149,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -187,9 +192,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" dependencies = [ "alsa-sys", "bitflags 2.10.0", @@ -221,9 +226,9 @@ dependencies = [ "jni-sys", "libc", "log", - "ndk 0.9.0", + "ndk", "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "thiserror 1.0.69", ] @@ -1289,22 +1294,16 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.11.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] @@ -1329,25 +1328,32 @@ dependencies = [ [[package]] name = "cpal" -version = "0.15.3" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb" dependencies = [ - "alsa 0.9.1", - "core-foundation-sys", + "alsa 0.10.0", "coreaudio-rs", "dasp_sample", "jni", "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2 0.6.3", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.54.0", + "windows 0.61.3", ] [[package]] @@ -1798,6 +1804,7 @@ checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" dependencies = [ "bytemuck", "emath", + "serde", ] [[package]] @@ -1851,6 +1858,8 @@ dependencies = [ "log", "nohash-hasher", "profiling", + "ron", + "serde", "smallvec", "unicode-segmentation", ] @@ -1943,9 +1952,9 @@ dependencies = [ [[package]] name = "egui_node_graph2" version = "0.7.0" -source = "git+https://github.com/PVDoriginal/egui_node_graph2#a25a90822d8f9c956e729f3907aad98f59fa46bc" dependencies = [ "egui", + "serde", "slotmap", "smallvec", "thiserror 1.0.69", @@ -1964,6 +1973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -2022,6 +2032,17 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "epaint" version = "0.33.3" @@ -2038,6 +2059,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "profiling", + "serde", ] [[package]] @@ -3411,6 +3433,7 @@ dependencies = [ name = "lightningbeam-core" version = "0.1.0" dependencies = [ + "arboard", "base64 0.21.7", "bytemuck", "chrono", @@ -3593,9 +3616,9 @@ dependencies = [ [[package]] name = "mach2" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" dependencies = [ "libc", ] @@ -3811,20 +3834,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - [[package]] name = "ndk" version = "0.9.0" @@ -3834,7 +3843,7 @@ dependencies = [ "bitflags 2.10.0", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -3846,15 +3855,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -4092,6 +4092,31 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.10.0", + "libc", + "objc2 0.6.3", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -4116,6 +4141,29 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.3", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -4135,7 +4183,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.3", ] @@ -4311,29 +4361,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -5293,6 +5320,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ron" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +dependencies = [ + "base64 0.22.1", + "bitflags 2.10.0", + "serde", + "serde_derive", + "unicode-ident", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -5629,6 +5669,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ + "serde", "version_check", ] @@ -7008,7 +7049,7 @@ dependencies = [ "log", "metal", "naga", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "objc", "once_cell", "ordered-float", @@ -7088,16 +7129,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -7130,16 +7161,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" @@ -7243,15 +7264,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.2.0" @@ -7626,7 +7638,7 @@ dependencies = [ "js-sys", "libc", "memmap2", - "ndk 0.9.0", + "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", diff --git a/lightningbeam-ui/lightningbeam-editor/Cargo.toml b/lightningbeam-ui/lightningbeam-editor/Cargo.toml index 2e94dcf..5c94b34 100644 --- a/lightningbeam-ui/lightningbeam-editor/Cargo.toml +++ b/lightningbeam-ui/lightningbeam-editor/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" lightningbeam-core = { path = "../lightningbeam-core" } daw-backend = { path = "../../daw-backend" } rtrb = "0.3" -cpal = "0.15" +cpal = "0.17" ffmpeg-next = { version = "8.0", features = ["static"] } # UI Framework diff --git a/lightningbeam-ui/lightningbeam-editor/build.rs b/lightningbeam-ui/lightningbeam-editor/build.rs index b27a3cd..bad38b7 100644 --- a/lightningbeam-ui/lightningbeam-editor/build.rs +++ b/lightningbeam-ui/lightningbeam-editor/build.rs @@ -3,8 +3,13 @@ use std::fs; use std::path::PathBuf; fn main() { - // Only bundle libs on Linux - if env::var("CARGO_CFG_TARGET_OS").unwrap() != "linux" { + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + + if target_os == "windows" { + bundle_windows_dlls(); + } + + if target_os != "linux" { return; } @@ -79,6 +84,50 @@ fn main() { println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display()); } +fn bundle_windows_dlls() { + let ffmpeg_dir = match env::var("FFMPEG_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => return, + }; + + let bin_dir = ffmpeg_dir.join("bin"); + if !bin_dir.exists() { + println!("cargo:warning=FFMPEG_DIR/bin not found, skipping DLL bundling"); + return; + } + + let out_dir = env::var("OUT_DIR").unwrap(); + let target_dir = PathBuf::from(&out_dir) + .parent().unwrap() + .parent().unwrap() + .parent().unwrap() + .to_path_buf(); + + let dlls = [ + "avcodec-62.dll", + "avdevice-62.dll", + "avfilter-11.dll", + "avformat-62.dll", + "avutil-60.dll", + "swresample-6.dll", + "swscale-9.dll", + ]; + + for dll in &dlls { + let src = bin_dir.join(dll); + let dst = target_dir.join(dll); + if src.exists() { + if let Err(e) = fs::copy(&src, &dst) { + println!("cargo:warning=Failed to copy {}: {}", dll, e); + } + } else { + println!("cargo:warning=FFmpeg DLL not found: {}", src.display()); + } + } + + println!("cargo:warning=Bundled FFmpeg DLLs to {}", target_dir.display()); +} + fn copy_library(lib_name: &str, search_paths: &[&str], lib_dir: &PathBuf) { let mut copied = false; diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index d18e4f9..b286dbe 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -183,64 +183,90 @@ enum SplitPreviewMode { }, } +/// Rasterize an embedded SVG and upload it as an egui texture +fn rasterize_svg(svg_data: &[u8], name: &str, render_size: u32, ctx: &egui::Context) -> Option { + let tree = resvg::usvg::Tree::from_data(svg_data, &resvg::usvg::Options::default()).ok()?; + let pixmap_size = tree.size().to_int_size(); + let scale_x = render_size as f32 / pixmap_size.width() as f32; + let scale_y = render_size as f32 / pixmap_size.height() as f32; + let scale = scale_x.min(scale_y); + + let final_size = resvg::usvg::Size::from_wh( + pixmap_size.width() as f32 * scale, + pixmap_size.height() as f32 * scale, + ).unwrap_or(resvg::usvg::Size::from_wh(render_size as f32, render_size as f32).unwrap()); + + let mut pixmap = resvg::tiny_skia::Pixmap::new( + final_size.width() as u32, + final_size.height() as u32, + )?; + let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + let rgba_data = pixmap.data(); + let size = [pixmap.width() as usize, pixmap.height() as usize]; + let color_image = egui::ColorImage::from_rgba_unmultiplied(size, rgba_data); + Some(ctx.load_texture(name, color_image, egui::TextureOptions::LINEAR)) +} + +/// Embedded pane icon SVGs +mod pane_icons { + pub static STAGE: &[u8] = include_bytes!("../../../src/assets/stage.svg"); + pub static TIMELINE: &[u8] = include_bytes!("../../../src/assets/timeline.svg"); + pub static TOOLBAR: &[u8] = include_bytes!("../../../src/assets/toolbar.svg"); + pub static INFOPANEL: &[u8] = include_bytes!("../../../src/assets/infopanel.svg"); + pub static PIANO_ROLL: &[u8] = include_bytes!("../../../src/assets/piano-roll.svg"); + pub static PIANO: &[u8] = include_bytes!("../../../src/assets/piano.svg"); + pub static NODE_EDITOR: &[u8] = include_bytes!("../../../src/assets/node-editor.svg"); +} + +/// Embedded tool icon SVGs +mod tool_icons { + pub static SELECT: &[u8] = include_bytes!("../../../src/assets/select.svg"); + pub static DRAW: &[u8] = include_bytes!("../../../src/assets/draw.svg"); + pub static TRANSFORM: &[u8] = include_bytes!("../../../src/assets/transform.svg"); + pub static RECTANGLE: &[u8] = include_bytes!("../../../src/assets/rectangle.svg"); + pub static ELLIPSE: &[u8] = include_bytes!("../../../src/assets/ellipse.svg"); + pub static PAINT_BUCKET: &[u8] = include_bytes!("../../../src/assets/paint_bucket.svg"); + pub static EYEDROPPER: &[u8] = include_bytes!("../../../src/assets/eyedropper.svg"); + pub static LINE: &[u8] = include_bytes!("../../../src/assets/line.svg"); + pub static POLYGON: &[u8] = include_bytes!("../../../src/assets/polygon.svg"); + pub static BEZIER_EDIT: &[u8] = include_bytes!("../../../src/assets/bezier_edit.svg"); + pub static TEXT: &[u8] = include_bytes!("../../../src/assets/text.svg"); +} + +/// Embedded focus icon SVGs +mod focus_icons { + pub static ANIMATION: &[u8] = include_bytes!("../../../src/assets/focus-animation.svg"); + pub static MUSIC: &[u8] = include_bytes!("../../../src/assets/focus-music.svg"); + pub static VIDEO: &[u8] = include_bytes!("../../../src/assets/focus-video.svg"); +} + /// Icon cache for pane type icons struct IconCache { icons: HashMap, - assets_path: std::path::PathBuf, } impl IconCache { fn new() -> Self { - let assets_path = std::path::PathBuf::from( - std::env::var("HOME").unwrap_or_else(|_| "/home/skyler".to_string()) - ).join("Dev/Lightningbeam-2/src/assets"); - Self { icons: HashMap::new(), - assets_path, } } fn get_or_load(&mut self, pane_type: PaneType, ctx: &egui::Context) -> Option<&egui::TextureHandle> { if !self.icons.contains_key(&pane_type) { - // Load SVG and rasterize using resvg - let icon_path = self.assets_path.join(pane_type.icon_file()); - if let Ok(svg_data) = std::fs::read(&icon_path) { - // Rasterize at reasonable size for pane icons - let render_size = 64; - - if let Ok(tree) = resvg::usvg::Tree::from_data(&svg_data, &resvg::usvg::Options::default()) { - let pixmap_size = tree.size().to_int_size(); - let scale_x = render_size as f32 / pixmap_size.width() as f32; - let scale_y = render_size as f32 / pixmap_size.height() as f32; - let scale = scale_x.min(scale_y); - - let final_size = resvg::usvg::Size::from_wh( - pixmap_size.width() as f32 * scale, - pixmap_size.height() as f32 * scale, - ).unwrap_or(resvg::usvg::Size::from_wh(render_size as f32, render_size as f32).unwrap()); - - if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new( - final_size.width() as u32, - final_size.height() as u32, - ) { - let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); - resvg::render(&tree, transform, &mut pixmap.as_mut()); - - // Convert RGBA8 to egui ColorImage - let rgba_data = pixmap.data(); - let size = [pixmap.width() as usize, pixmap.height() as usize]; - let color_image = egui::ColorImage::from_rgba_unmultiplied(size, rgba_data); - - // Upload to GPU - let texture = ctx.load_texture( - pane_type.icon_file(), - color_image, - egui::TextureOptions::LINEAR, - ); - self.icons.insert(pane_type, texture); - } - } + let svg_data = match pane_type { + PaneType::Stage | PaneType::Outliner | PaneType::PresetBrowser | PaneType::AssetLibrary => pane_icons::STAGE, + PaneType::Timeline => pane_icons::TIMELINE, + PaneType::Toolbar => pane_icons::TOOLBAR, + PaneType::Infopanel => pane_icons::INFOPANEL, + PaneType::PianoRoll => pane_icons::PIANO_ROLL, + PaneType::VirtualPiano => pane_icons::PIANO, + PaneType::NodeEditor | PaneType::ShaderEditor => pane_icons::NODE_EDITOR, + }; + if let Some(texture) = rasterize_svg(svg_data, pane_type.icon_file(), 64, ctx) { + self.icons.insert(pane_type, texture); } } self.icons.get(&pane_type) @@ -250,61 +276,32 @@ impl IconCache { /// Icon cache for tool icons struct ToolIconCache { icons: HashMap, - assets_path: std::path::PathBuf, } impl ToolIconCache { fn new() -> Self { - let assets_path = std::path::PathBuf::from( - std::env::var("HOME").unwrap_or_else(|_| "/home/skyler".to_string()) - ).join("Dev/Lightningbeam-2/src/assets"); - Self { icons: HashMap::new(), - assets_path, } } fn get_or_load(&mut self, tool: Tool, ctx: &egui::Context) -> Option<&egui::TextureHandle> { if !self.icons.contains_key(&tool) { - // Load SVG and rasterize at high resolution using resvg - let icon_path = self.assets_path.join(tool.icon_file()); - if let Ok(svg_data) = std::fs::read(&icon_path) { - // Rasterize at 3x size for crisp display (180px for 60px display) - let render_size = 180; - - if let Ok(tree) = resvg::usvg::Tree::from_data(&svg_data, &resvg::usvg::Options::default()) { - let pixmap_size = tree.size().to_int_size(); - let scale_x = render_size as f32 / pixmap_size.width() as f32; - let scale_y = render_size as f32 / pixmap_size.height() as f32; - let scale = scale_x.min(scale_y); - - let final_size = resvg::usvg::Size::from_wh( - pixmap_size.width() as f32 * scale, - pixmap_size.height() as f32 * scale, - ).unwrap_or(resvg::usvg::Size::from_wh(render_size as f32, render_size as f32).unwrap()); - - if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new( - final_size.width() as u32, - final_size.height() as u32, - ) { - let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); - resvg::render(&tree, transform, &mut pixmap.as_mut()); - - // Convert RGBA8 to egui ColorImage - let rgba_data = pixmap.data(); - let size = [pixmap.width() as usize, pixmap.height() as usize]; - let color_image = egui::ColorImage::from_rgba_unmultiplied(size, rgba_data); - - // Upload to GPU - let texture = ctx.load_texture( - tool.icon_file(), - color_image, - egui::TextureOptions::LINEAR, - ); - self.icons.insert(tool, texture); - } - } + let svg_data = match tool { + Tool::Select => tool_icons::SELECT, + Tool::Draw => tool_icons::DRAW, + Tool::Transform => tool_icons::TRANSFORM, + Tool::Rectangle => tool_icons::RECTANGLE, + Tool::Ellipse => tool_icons::ELLIPSE, + Tool::PaintBucket => tool_icons::PAINT_BUCKET, + Tool::Eyedropper => tool_icons::EYEDROPPER, + Tool::Line => tool_icons::LINE, + Tool::Polygon => tool_icons::POLYGON, + Tool::BezierEdit => tool_icons::BEZIER_EDIT, + Tool::Text => tool_icons::TEXT, + }; + if let Some(texture) = rasterize_svg(svg_data, tool.icon_file(), 180, ctx) { + self.icons.insert(tool, texture); } } self.icons.get(&tool) @@ -314,74 +311,33 @@ impl ToolIconCache { /// Icon cache for focus card icons (start screen) struct FocusIconCache { icons: HashMap, - assets_path: std::path::PathBuf, } impl FocusIconCache { fn new() -> Self { - let assets_path = std::path::PathBuf::from( - std::env::var("HOME").unwrap_or_else(|_| "/home/skyler".to_string()) - ).join("Dev/Lightningbeam-2/src/assets"); - Self { icons: HashMap::new(), - assets_path, } } fn get_or_load(&mut self, icon: FocusIcon, icon_color: egui::Color32, ctx: &egui::Context) -> Option<&egui::TextureHandle> { if !self.icons.contains_key(&icon) { - // Determine which SVG file to load - let svg_filename = match icon { - FocusIcon::Animation => "focus-animation.svg", - FocusIcon::Music => "focus-music.svg", - FocusIcon::Video => "focus-video.svg", + let (svg_bytes, svg_filename) = match icon { + FocusIcon::Animation => (focus_icons::ANIMATION, "focus-animation.svg"), + FocusIcon::Music => (focus_icons::MUSIC, "focus-music.svg"), + FocusIcon::Video => (focus_icons::VIDEO, "focus-video.svg"), }; - let icon_path = self.assets_path.join(svg_filename); - if let Ok(svg_data) = std::fs::read_to_string(&icon_path) { - // Replace currentColor with the actual color - let color_hex = format!( - "#{:02x}{:02x}{:02x}", - icon_color.r(), icon_color.g(), icon_color.b() - ); - let svg_with_color = svg_data.replace("currentColor", &color_hex); + // Replace currentColor with the actual color + let svg_data = String::from_utf8_lossy(svg_bytes); + let color_hex = format!( + "#{:02x}{:02x}{:02x}", + icon_color.r(), icon_color.g(), icon_color.b() + ); + let svg_with_color = svg_data.replace("currentColor", &color_hex); - // Rasterize at 2x size for crisp display - let render_size = 120; - - if let Ok(tree) = resvg::usvg::Tree::from_data(svg_with_color.as_bytes(), &resvg::usvg::Options::default()) { - let pixmap_size = tree.size().to_int_size(); - let scale_x = render_size as f32 / pixmap_size.width() as f32; - let scale_y = render_size as f32 / pixmap_size.height() as f32; - let scale = scale_x.min(scale_y); - - let final_size = resvg::usvg::Size::from_wh( - pixmap_size.width() as f32 * scale, - pixmap_size.height() as f32 * scale, - ).unwrap_or(resvg::usvg::Size::from_wh(render_size as f32, render_size as f32).unwrap()); - - if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new( - final_size.width() as u32, - final_size.height() as u32, - ) { - let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); - resvg::render(&tree, transform, &mut pixmap.as_mut()); - - // Convert RGBA8 to egui ColorImage - let rgba_data = pixmap.data(); - let size = [pixmap.width() as usize, pixmap.height() as usize]; - let color_image = egui::ColorImage::from_rgba_unmultiplied(size, rgba_data); - - // Upload to GPU - let texture = ctx.load_texture( - svg_filename, - color_image, - egui::TextureOptions::LINEAR, - ); - self.icons.insert(icon, texture); - } - } + if let Some(texture) = rasterize_svg(svg_with_color.as_bytes(), svg_filename, 120, ctx) { + self.icons.insert(icon, texture); } } self.icons.get(&icon)