diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 12cb42f..ebc62f8 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -16,7 +16,12 @@ This guide provides detailed instructions for building Lightningbeam on differen ```bash # Clone the repository git clone https://github.com/skykooler/lightningbeam.git -cd lightningbeam/lightningbeam-ui +cd lightningbeam + +# Initialize submodules (including nested ones required by nam-ffi) +git submodule update --init --recursive + +cd lightningbeam-ui # Build and run cargo build @@ -270,6 +275,23 @@ cargo build -p lightningbeam-core ## Troubleshooting +### Submodule / CMake Issues + +#### "does not contain a CMakeLists.txt file" (RTNeural or math_approx) + +**Cause**: The `vendor/NeuralAudio` submodule has its own nested submodules (`deps/RTNeural`, `deps/math_approx`) that weren't initialized. A plain `git submodule update --init` only initializes top-level submodules. + +**Solution**: Use `--recursive` to initialize all nested submodules: +```bash +git submodule update --init --recursive +``` + +Or, if the top-level submodule is already checked out: +```bash +cd vendor/NeuralAudio +git submodule update --init +``` + ### Audio Issues #### "ALSA lib cannot find card" or similar errors diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 7f7d37c..80819ac 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -5943,13 +5943,128 @@ fn render_pane( } }); + // Secondary tab selector for music/instrument panes + let secondary_tab_types = [ + PaneType::VirtualPiano, + PaneType::PianoRoll, + PaneType::NodeEditor, + ]; + let show_secondary_tabs = pane_type + .map(|pt| secondary_tab_types.contains(&pt)) + .unwrap_or(false); + + let tab_size = 24.0; + let secondary_selector_extra_width = if show_secondary_tabs { + 8.0 + 3.0 * tab_size + 8.0 + } else { + 0.0 + }; + + if show_secondary_tabs { + let n = secondary_tab_types.len(); + let selector_start_x = icon_button_rect.max.x + 8.0; + let corner_r = 4.0_f32; + let selector_rect = egui::Rect::from_min_size( + egui::pos2(selector_start_x, header_rect.min.y + icon_padding), + egui::vec2(n as f32 * tab_size, tab_size), + ); + + // Shared background + ui.painter().rect_filled( + selector_rect, + corner_r, + egui::Color32::from_rgba_premultiplied(50, 50, 50, 200), + ); + + for (i, &tab_type) in secondary_tab_types.iter().enumerate() { + let tab_x = selector_start_x + i as f32 * tab_size; + let tab_rect = egui::Rect::from_min_size( + egui::pos2(tab_x, header_rect.min.y + icon_padding), + egui::vec2(tab_size, tab_size), + ); + + let is_active = pane_type == Some(tab_type); + + // Active tab highlight with per-corner rounding + if is_active { + let cr = corner_r as u8; + let rounding = egui::Rounding { + nw: if i == 0 { cr } else { 0 }, + sw: if i == 0 { cr } else { 0 }, + ne: if i == n - 1 { cr } else { 0 }, + se: if i == n - 1 { cr } else { 0 }, + }; + ui.painter().rect_filled( + tab_rect, + rounding, + egui::Color32::from_rgba_premultiplied(60, 90, 150, 230), + ); + } + + // Divider lines between tabs + if i > 0 { + let divider_color = if is_active || pane_type == Some(secondary_tab_types[i - 1]) { + egui::Color32::from_rgba_premultiplied(80, 110, 170, 180) + } else { + egui::Color32::from_gray(70) + }; + ui.painter().vline( + tab_x, + tab_rect.y_range(), + egui::Stroke::new(1.0, divider_color), + ); + } + + // Icon + if let Some(icon) = ctx.icon_cache.get_or_load(tab_type, ui.ctx()) { + let icon_texture_id = icon.id(); + ui.painter().image( + icon_texture_id, + tab_rect.shrink(3.0), + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + } + + // Interaction + let tab_id = ui.id().with(("secondary_tab", path, i)); + let tab_response = ui.interact(tab_rect, tab_id, egui::Sense::click()); + + if tab_response.hovered() && !is_active { + ui.painter().rect_filled( + tab_rect, + egui::Rounding { + nw: if i == 0 { corner_r as u8 } else { 0 }, + sw: if i == 0 { corner_r as u8 } else { 0 }, + ne: if i == n - 1 { corner_r as u8 } else { 0 }, + se: if i == n - 1 { corner_r as u8 } else { 0 }, + }, + egui::Color32::from_rgba_premultiplied(70, 70, 70, 180), + ); + } + + if tab_response.clicked() { + *pane_name = tab_type.to_name().to_string(); + } + } + + // Outer border + ui.painter().rect_stroke( + selector_rect, + corner_r, + egui::Stroke::new(1.0, egui::Color32::from_gray(80)), + egui::StrokeKind::Middle, + ); + } + // Draw pane title in header let title_text = if let Some(pane_type) = pane_type { pane_type.display_name() } else { pane_name.as_str() }; - let title_pos = header_rect.min + egui::vec2(icon_padding * 2.0 + icon_size + 8.0, header_height / 2.0); + let title_x_start = icon_padding * 2.0 + icon_size + 8.0 + secondary_selector_extra_width; + let title_pos = header_rect.min + egui::vec2(title_x_start, header_height / 2.0); ui.painter().text( title_pos, egui::Align2::LEFT_CENTER, @@ -5961,8 +6076,8 @@ fn render_pane( // Create header controls area (positioned after title) let title_width = 150.0; // Approximate width for title let header_controls_rect = egui::Rect::from_min_size( - header_rect.min + egui::vec2(icon_padding * 2.0 + icon_size + 8.0 + title_width, 0.0), - egui::vec2(header_rect.width() - (icon_padding * 2.0 + icon_size + 8.0 + title_width), header_height), + header_rect.min + egui::vec2(title_x_start + title_width, 0.0), + egui::vec2(header_rect.width() - (title_x_start + title_width), header_height), ); // Render pane-specific header controls (if pane has them)