Compare commits

...

2 Commits

Author SHA1 Message Date
Skyler Lehmkuhl 2a94ac0f69 Merge branch 'rust-ui' of https://git.skyler.io/skyler/Lightningbeam into rust-ui 2026-02-16 10:06:00 -05:00
Skyler Lehmkuhl 6c10112a16 Fix build on Windows 2026-02-16 10:05:39 -05:00
6 changed files with 268 additions and 289 deletions

View File

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
cpal = "0.15" cpal = "0.17"
symphonia = { version = "0.5", features = ["all"] } symphonia = { version = "0.5", features = ["all"] }
rtrb = "0.3" rtrb = "0.3"
midly = "0.5" midly = "0.5"

View File

@ -67,14 +67,12 @@ impl AudioSystem {
.ok_or("No output device available")?; .ok_or("No output device available")?;
let default_output_config = output_device.default_output_config().map_err(|e| e.to_string())?; 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 channels = default_output_config.channels() as u32;
let debug_audio = std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1"); 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] Device: {:?}, format={:?}, rate={}, channels={}",
eprintln!("[AUDIO DEBUG] Default config: {:?}", default_output_config); output_device.name().unwrap_or_default(), default_output_config.sample_format(), sample_rate, channels);
eprintln!("[AUDIO DEBUG] Default buffer size: {:?}", default_output_config.buffer_size());
}
// Create queues // Create queues
let (command_tx, command_rx) = rtrb::RingBuffer::new(512); // Larger buffer for MIDI + UI commands 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 // Build output stream
let mut output_config: cpal::StreamConfig = default_output_config.clone().into(); let mut output_config: cpal::StreamConfig = default_output_config.into();
// Set the requested buffer size // WASAPI shared mode on Windows does not support fixed buffer sizes.
output_config.buffer_size = cpal::BufferSize::Fixed(buffer_size); // 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]; 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 let output_stream = output_device
.build_output_stream( .build_output_stream(
&output_config, &output_config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { 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()]; let buf = &mut output_buffer[..data.len()];
buf.fill(0.0); buf.fill(0.0);
engine.process(buf); engine.process(buf);
@ -145,7 +130,7 @@ impl AudioSystem {
|err| eprintln!("Output stream error: {}", err), |err| eprintln!("Output stream error: {}", err),
None, None,
) )
.map_err(|e| e.to_string())?; .map_err(|e| format!("Failed to build output stream: {e:?}"))?;
// Get input device // Get input device
let input_device = match host.default_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() { let input_config = match input_device.default_input_config() {
Ok(config) => { Ok(config) => {
let mut cfg: cpal::StreamConfig = config.into(); let 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;
cfg cfg
} }
Err(e) => { Err(e) => {
@ -193,25 +175,41 @@ impl AudioSystem {
stream: output_stream, stream: output_stream,
sample_rate, sample_rate,
channels, channels,
event_rx: None, // No event receiver when audio device unavailable event_rx: None,
}); });
} }
}; };
// Build input stream that feeds into the ringbuffer // Build input stream that feeds into the ringbuffer
let input_stream = input_device let input_stream = match input_device
.build_input_stream( .build_input_stream(
&input_config, &input_config,
move |data: &[f32], _: &cpal::InputCallbackInfo| { move |data: &[f32], _: &cpal::InputCallbackInfo| {
// Push input samples to ringbuffer for recording
for &sample in data { for &sample in data {
let _ = input_tx.push(sample); let _ = input_tx.push(sample);
} }
}, },
|err| eprintln!("Input stream error: {}", err), |err| eprintln!("Input stream error: {}", err),
None, 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 // Start both streams
output_stream.play().map_err(|e| e.to_string())?; output_stream.play().map_err(|e| e.to_string())?;

View File

@ -192,9 +192,9 @@ dependencies = [
[[package]] [[package]]
name = "alsa" name = "alsa"
version = "0.9.1" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3"
dependencies = [ dependencies = [
"alsa-sys", "alsa-sys",
"bitflags 2.10.0", "bitflags 2.10.0",
@ -226,9 +226,9 @@ dependencies = [
"jni-sys", "jni-sys",
"libc", "libc",
"log", "log",
"ndk 0.9.0", "ndk",
"ndk-context", "ndk-context",
"ndk-sys 0.6.0+11769913", "ndk-sys",
"num_enum", "num_enum",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@ -1294,22 +1294,16 @@ dependencies = [
[[package]] [[package]]
name = "coreaudio-rs" name = "coreaudio-rs"
version = "0.11.3" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation-sys", "libc",
"coreaudio-sys", "objc2-audio-toolbox",
] "objc2-core-audio",
"objc2-core-audio-types",
[[package]] "objc2-core-foundation",
name = "coreaudio-sys"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6"
dependencies = [
"bindgen",
] ]
[[package]] [[package]]
@ -1334,25 +1328,32 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.15.3" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
dependencies = [ dependencies = [
"alsa 0.9.1", "alsa 0.10.0",
"core-foundation-sys",
"coreaudio-rs", "coreaudio-rs",
"dasp_sample", "dasp_sample",
"jni", "jni",
"js-sys", "js-sys",
"libc", "libc",
"mach2", "mach2",
"ndk 0.8.0", "ndk",
"ndk-context", "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",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"windows 0.54.0", "windows 0.61.3",
] ]
[[package]] [[package]]
@ -3595,9 +3596,9 @@ dependencies = [
[[package]] [[package]]
name = "mach2" name = "mach2"
version = "0.4.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -3813,20 +3814,6 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@ -3836,7 +3823,7 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"jni-sys", "jni-sys",
"log", "log",
"ndk-sys 0.6.0+11769913", "ndk-sys",
"num_enum", "num_enum",
"raw-window-handle", "raw-window-handle",
"thiserror 1.0.69", "thiserror 1.0.69",
@ -3848,15 +3835,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 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]] [[package]]
name = "ndk-sys" name = "ndk-sys"
version = "0.6.0+11769913" version = "0.6.0+11769913"
@ -4094,6 +4072,31 @@ dependencies = [
"objc2-foundation 0.3.2", "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]] [[package]]
name = "objc2-cloud-kit" name = "objc2-cloud-kit"
version = "0.2.2" version = "0.2.2"
@ -4118,6 +4121,29 @@ dependencies = [
"objc2-foundation 0.2.2", "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]] [[package]]
name = "objc2-core-data" name = "objc2-core-data"
version = "0.2.2" version = "0.2.2"
@ -4137,7 +4163,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.6.2",
"dispatch2", "dispatch2",
"libc",
"objc2 0.6.3", "objc2 0.6.3",
] ]
@ -4313,29 +4341,6 @@ dependencies = [
"objc2-foundation 0.2.2", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@ -7024,7 +7029,7 @@ dependencies = [
"log", "log",
"metal", "metal",
"naga", "naga",
"ndk-sys 0.6.0+11769913", "ndk-sys",
"objc", "objc",
"once_cell", "once_cell",
"ordered-float", "ordered-float",
@ -7104,16 +7109,6 @@ dependencies = [
"windows_x86_64_msvc 0.42.2", "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]] [[package]]
name = "windows" name = "windows"
version = "0.58.0" version = "0.58.0"
@ -7146,16 +7141,6 @@ dependencies = [
"windows-core 0.61.2", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.58.0" version = "0.58.0"
@ -7259,15 +7244,6 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.2.0" version = "0.2.0"
@ -7642,7 +7618,7 @@ dependencies = [
"js-sys", "js-sys",
"libc", "libc",
"memmap2", "memmap2",
"ndk 0.9.0", "ndk",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",

View File

@ -7,7 +7,7 @@ edition = "2021"
lightningbeam-core = { path = "../lightningbeam-core" } lightningbeam-core = { path = "../lightningbeam-core" }
daw-backend = { path = "../../daw-backend" } daw-backend = { path = "../../daw-backend" }
rtrb = "0.3" rtrb = "0.3"
cpal = "0.15" cpal = "0.17"
ffmpeg-next = { version = "8.0", features = ["static"] } ffmpeg-next = { version = "8.0", features = ["static"] }
# UI Framework # UI Framework

View File

@ -3,8 +3,13 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
fn main() { fn main() {
// Only bundle libs on Linux let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
if env::var("CARGO_CFG_TARGET_OS").unwrap() != "linux" {
if target_os == "windows" {
bundle_windows_dlls();
}
if target_os != "linux" {
return; return;
} }
@ -79,6 +84,50 @@ fn main() {
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display()); 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) { fn copy_library(lib_name: &str, search_paths: &[&str], lib_dir: &PathBuf) {
let mut copied = false; let mut copied = false;

View File

@ -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<egui::TextureHandle> {
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 /// Icon cache for pane type icons
struct IconCache { struct IconCache {
icons: HashMap<PaneType, egui::TextureHandle>, icons: HashMap<PaneType, egui::TextureHandle>,
assets_path: std::path::PathBuf,
} }
impl IconCache { impl IconCache {
fn new() -> Self { 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 { Self {
icons: HashMap::new(), icons: HashMap::new(),
assets_path,
} }
} }
fn get_or_load(&mut self, pane_type: PaneType, ctx: &egui::Context) -> Option<&egui::TextureHandle> { fn get_or_load(&mut self, pane_type: PaneType, ctx: &egui::Context) -> Option<&egui::TextureHandle> {
if !self.icons.contains_key(&pane_type) { if !self.icons.contains_key(&pane_type) {
// Load SVG and rasterize using resvg let svg_data = match pane_type {
let icon_path = self.assets_path.join(pane_type.icon_file()); PaneType::Stage | PaneType::Outliner | PaneType::PresetBrowser | PaneType::AssetLibrary => pane_icons::STAGE,
if let Ok(svg_data) = std::fs::read(&icon_path) { PaneType::Timeline => pane_icons::TIMELINE,
// Rasterize at reasonable size for pane icons PaneType::Toolbar => pane_icons::TOOLBAR,
let render_size = 64; PaneType::Infopanel => pane_icons::INFOPANEL,
PaneType::PianoRoll => pane_icons::PIANO_ROLL,
if let Ok(tree) = resvg::usvg::Tree::from_data(&svg_data, &resvg::usvg::Options::default()) { PaneType::VirtualPiano => pane_icons::PIANO,
let pixmap_size = tree.size().to_int_size(); PaneType::NodeEditor | PaneType::ShaderEditor => pane_icons::NODE_EDITOR,
let scale_x = render_size as f32 / pixmap_size.width() as f32; };
let scale_y = render_size as f32 / pixmap_size.height() as f32; if let Some(texture) = rasterize_svg(svg_data, pane_type.icon_file(), 64, ctx) {
let scale = scale_x.min(scale_y); self.icons.insert(pane_type, texture);
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);
}
}
} }
} }
self.icons.get(&pane_type) self.icons.get(&pane_type)
@ -250,61 +276,32 @@ impl IconCache {
/// Icon cache for tool icons /// Icon cache for tool icons
struct ToolIconCache { struct ToolIconCache {
icons: HashMap<Tool, egui::TextureHandle>, icons: HashMap<Tool, egui::TextureHandle>,
assets_path: std::path::PathBuf,
} }
impl ToolIconCache { impl ToolIconCache {
fn new() -> Self { 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 { Self {
icons: HashMap::new(), icons: HashMap::new(),
assets_path,
} }
} }
fn get_or_load(&mut self, tool: Tool, ctx: &egui::Context) -> Option<&egui::TextureHandle> { fn get_or_load(&mut self, tool: Tool, ctx: &egui::Context) -> Option<&egui::TextureHandle> {
if !self.icons.contains_key(&tool) { if !self.icons.contains_key(&tool) {
// Load SVG and rasterize at high resolution using resvg let svg_data = match tool {
let icon_path = self.assets_path.join(tool.icon_file()); Tool::Select => tool_icons::SELECT,
if let Ok(svg_data) = std::fs::read(&icon_path) { Tool::Draw => tool_icons::DRAW,
// Rasterize at 3x size for crisp display (180px for 60px display) Tool::Transform => tool_icons::TRANSFORM,
let render_size = 180; Tool::Rectangle => tool_icons::RECTANGLE,
Tool::Ellipse => tool_icons::ELLIPSE,
if let Ok(tree) = resvg::usvg::Tree::from_data(&svg_data, &resvg::usvg::Options::default()) { Tool::PaintBucket => tool_icons::PAINT_BUCKET,
let pixmap_size = tree.size().to_int_size(); Tool::Eyedropper => tool_icons::EYEDROPPER,
let scale_x = render_size as f32 / pixmap_size.width() as f32; Tool::Line => tool_icons::LINE,
let scale_y = render_size as f32 / pixmap_size.height() as f32; Tool::Polygon => tool_icons::POLYGON,
let scale = scale_x.min(scale_y); Tool::BezierEdit => tool_icons::BEZIER_EDIT,
Tool::Text => tool_icons::TEXT,
let final_size = resvg::usvg::Size::from_wh( };
pixmap_size.width() as f32 * scale, if let Some(texture) = rasterize_svg(svg_data, tool.icon_file(), 180, ctx) {
pixmap_size.height() as f32 * scale, self.icons.insert(tool, texture);
).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);
}
}
} }
} }
self.icons.get(&tool) self.icons.get(&tool)
@ -314,74 +311,33 @@ impl ToolIconCache {
/// Icon cache for focus card icons (start screen) /// Icon cache for focus card icons (start screen)
struct FocusIconCache { struct FocusIconCache {
icons: HashMap<FocusIcon, egui::TextureHandle>, icons: HashMap<FocusIcon, egui::TextureHandle>,
assets_path: std::path::PathBuf,
} }
impl FocusIconCache { impl FocusIconCache {
fn new() -> Self { 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 { Self {
icons: HashMap::new(), icons: HashMap::new(),
assets_path,
} }
} }
fn get_or_load(&mut self, icon: FocusIcon, icon_color: egui::Color32, ctx: &egui::Context) -> Option<&egui::TextureHandle> { fn get_or_load(&mut self, icon: FocusIcon, icon_color: egui::Color32, ctx: &egui::Context) -> Option<&egui::TextureHandle> {
if !self.icons.contains_key(&icon) { if !self.icons.contains_key(&icon) {
// Determine which SVG file to load let (svg_bytes, svg_filename) = match icon {
let svg_filename = match icon { FocusIcon::Animation => (focus_icons::ANIMATION, "focus-animation.svg"),
FocusIcon::Animation => "focus-animation.svg", FocusIcon::Music => (focus_icons::MUSIC, "focus-music.svg"),
FocusIcon::Music => "focus-music.svg", FocusIcon::Video => (focus_icons::VIDEO, "focus-video.svg"),
FocusIcon::Video => "focus-video.svg",
}; };
let icon_path = self.assets_path.join(svg_filename); // Replace currentColor with the actual color
if let Ok(svg_data) = std::fs::read_to_string(&icon_path) { let svg_data = String::from_utf8_lossy(svg_bytes);
// Replace currentColor with the actual color let color_hex = format!(
let color_hex = format!( "#{:02x}{:02x}{:02x}",
"#{:02x}{:02x}{:02x}", icon_color.r(), icon_color.g(), icon_color.b()
icon_color.r(), icon_color.g(), icon_color.b() );
); let svg_with_color = svg_data.replace("currentColor", &color_hex);
let svg_with_color = svg_data.replace("currentColor", &color_hex);
// Rasterize at 2x size for crisp display if let Some(texture) = rasterize_svg(svg_with_color.as_bytes(), svg_filename, 120, ctx) {
let render_size = 120; self.icons.insert(icon, texture);
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);
}
}
} }
} }
self.icons.get(&icon) self.icons.get(&icon)