make sample load menus consistent
This commit is contained in:
parent
6bbf7d27df
commit
b2a6304771
|
|
@ -6,6 +6,7 @@ use eframe::egui;
|
||||||
use egui_node_graph2::*;
|
use egui_node_graph2::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use crate::widgets;
|
||||||
|
|
||||||
/// Signal types for audio node graph
|
/// Signal types for audio node graph
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -887,9 +888,10 @@ impl NodeDataTrait for NodeData {
|
||||||
let mut close_popup = false;
|
let mut close_popup = false;
|
||||||
egui::Popup::from_toggle_button_response(&button)
|
egui::Popup::from_toggle_button_response(&button)
|
||||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||||
|
.width(160.0)
|
||||||
.show(|ui| {
|
.show(|ui| {
|
||||||
ui.set_min_width(200.0);
|
let search_width = ui.available_width();
|
||||||
ui.text_edit_singleline(&mut user_state.sampler_search_text);
|
ui.add_sized([search_width, 0.0], egui::TextEdit::singleline(&mut user_state.sampler_search_text).hint_text("Search..."));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
let search = user_state.sampler_search_text.to_lowercase();
|
let search = user_state.sampler_search_text.to_lowercase();
|
||||||
|
|
||||||
|
|
@ -901,7 +903,7 @@ impl NodeDataTrait for NodeData {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let label = format!("📁 {} ({} clips)", folder.name, folder.clip_pool_indices.len());
|
let label = format!("📁 {} ({} clips)", folder.name, folder.clip_pool_indices.len());
|
||||||
if ui.selectable_label(false, label).clicked() {
|
if widgets::list_item(ui, false, &label) {
|
||||||
user_state.pending_sampler_load = Some(PendingSamplerLoad::MultiFromFolder {
|
user_state.pending_sampler_load = Some(PendingSamplerLoad::MultiFromFolder {
|
||||||
node_id,
|
node_id,
|
||||||
folder_id: folder.folder_id,
|
folder_id: folder.folder_id,
|
||||||
|
|
@ -916,12 +918,12 @@ impl NodeDataTrait for NodeData {
|
||||||
if is_multi {
|
if is_multi {
|
||||||
ui.label(egui::RichText::new("Audio Clips").small().weak());
|
ui.label(egui::RichText::new("Audio Clips").small().weak());
|
||||||
}
|
}
|
||||||
egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
|
let filtered_clips: Vec<&SamplerClipInfo> = user_state.available_clips.iter()
|
||||||
for clip in &user_state.available_clips {
|
.filter(|clip| search.is_empty() || clip.name.to_lowercase().contains(&search))
|
||||||
if !search.is_empty() && !clip.name.to_lowercase().contains(&search) {
|
.collect();
|
||||||
continue;
|
let items = filtered_clips.iter().map(|clip| (false, clip.name.as_str()));
|
||||||
}
|
if let Some(idx) = widgets::scrollable_list(ui, 200.0, items) {
|
||||||
if ui.selectable_label(false, &clip.name).clicked() {
|
let clip = filtered_clips[idx];
|
||||||
if is_multi {
|
if is_multi {
|
||||||
user_state.pending_sampler_load = Some(PendingSamplerLoad::MultiFromPool {
|
user_state.pending_sampler_load = Some(PendingSamplerLoad::MultiFromPool {
|
||||||
node_id,
|
node_id,
|
||||||
|
|
@ -939,8 +941,6 @@ impl NodeDataTrait for NodeData {
|
||||||
}
|
}
|
||||||
close_popup = true;
|
close_popup = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Open...").clicked() {
|
if ui.button("Open...").clicked() {
|
||||||
if is_multi {
|
if is_multi {
|
||||||
|
|
@ -971,18 +971,17 @@ impl NodeDataTrait for NodeData {
|
||||||
let mut close_root = false;
|
let mut close_root = false;
|
||||||
egui::Popup::from_toggle_button_response(&root_btn)
|
egui::Popup::from_toggle_button_response(&root_btn)
|
||||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||||
|
.width(80.0)
|
||||||
.show(|ui| {
|
.show(|ui| {
|
||||||
ui.set_min_width(120.0);
|
let notes: Vec<(u8, String)> = (24..=96).rev()
|
||||||
egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
|
.map(|n| (n, midi_note_name(n)))
|
||||||
// Show notes from C1 (24) to C7 (96)
|
.collect();
|
||||||
for note in (24..=96).rev() {
|
let items = notes.iter().map(|(n, name)| (*n == self.root_note, name.as_str()));
|
||||||
let name = midi_note_name(note);
|
if let Some(idx) = widgets::scrollable_list(ui, 200.0, items) {
|
||||||
if ui.selectable_label(note == self.root_note, &name).clicked() {
|
let (note, _) = ¬es[idx];
|
||||||
user_state.pending_root_note_changes.push((node_id, backend_node_id, note));
|
user_state.pending_root_note_changes.push((node_id, backend_node_id, *note));
|
||||||
close_root = true;
|
close_root = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
if close_root {
|
if close_root {
|
||||||
egui::Popup::close_id(ui.ctx(), root_popup_id);
|
egui::Popup::close_id(ui.ctx(), root_popup_id);
|
||||||
|
|
|
||||||
|
|
@ -587,12 +587,27 @@ impl NodeGraphPane {
|
||||||
.add_filter("Audio", &["wav", "flac", "mp3", "ogg", "aiff"])
|
.add_filter("Audio", &["wav", "flac", "mp3", "ogg", "aiff"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
{
|
{
|
||||||
let path_str = path.to_string_lossy().to_string();
|
|
||||||
let file_name = path.file_stem()
|
let file_name = path.file_stem()
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| "Sample".to_string());
|
.unwrap_or_else(|| "Sample".to_string());
|
||||||
|
|
||||||
|
// Import into audio pool + asset library, then load from pool
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
controller.sampler_load_sample(backend_track_id, backend_node_id, path_str);
|
match controller.import_audio_sync(path.to_path_buf()) {
|
||||||
|
Ok(pool_index) => {
|
||||||
|
// Add to document asset library
|
||||||
|
let metadata = daw_backend::io::read_metadata(&path).ok();
|
||||||
|
let duration = metadata.as_ref().map(|m| m.duration).unwrap_or(0.0);
|
||||||
|
let clip = lightningbeam_core::clip::AudioClip::new_sampled(&file_name, pool_index, duration);
|
||||||
|
shared.action_executor.document_mut().add_audio_clip(clip);
|
||||||
|
|
||||||
|
// Load into sampler from pool
|
||||||
|
controller.sampler_load_from_pool(backend_track_id, backend_node_id, pool_index);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to import audio '{}': {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
|
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
|
||||||
node.user_data.sample_display_name = Some(file_name);
|
node.user_data.sample_display_name = Some(file_name);
|
||||||
}
|
}
|
||||||
|
|
@ -635,17 +650,28 @@ impl NodeGraphPane {
|
||||||
.add_filter("Audio", &["wav", "flac", "mp3", "ogg", "aiff"])
|
.add_filter("Audio", &["wav", "flac", "mp3", "ogg", "aiff"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
{
|
{
|
||||||
let path_str = path.to_string_lossy().to_string();
|
|
||||||
let file_name = path.file_stem()
|
let file_name = path.file_stem()
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| "Sample".to_string());
|
.unwrap_or_else(|| "Sample".to_string());
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
|
// Import into audio pool + asset library, then load from pool
|
||||||
|
match controller.import_audio_sync(path.to_path_buf()) {
|
||||||
|
Ok(pool_index) => {
|
||||||
|
let metadata = daw_backend::io::read_metadata(&path).ok();
|
||||||
|
let duration = metadata.as_ref().map(|m| m.duration).unwrap_or(0.0);
|
||||||
|
let clip = lightningbeam_core::clip::AudioClip::new_sampled(&file_name, pool_index, duration);
|
||||||
|
shared.action_executor.document_mut().add_audio_clip(clip);
|
||||||
|
|
||||||
// Add as layer spanning full key range
|
// Add as layer spanning full key range
|
||||||
controller.multi_sampler_add_layer(
|
controller.multi_sampler_add_layer_from_pool(
|
||||||
backend_track_id, backend_node_id, path_str,
|
backend_track_id, backend_node_id, pool_index,
|
||||||
0, 127, 60, 0, 127, None, None,
|
0, 127, 60,
|
||||||
daw_backend::audio::node_graph::nodes::LoopMode::OneShot,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to import audio '{}': {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
|
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
|
||||||
node.user_data.sample_display_name = Some(file_name);
|
node.user_data.sample_display_name = Some(file_name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
//! Full-width selectable list for use inside popups and dropdowns.
|
||||||
|
//!
|
||||||
|
//! Solves the recurring issue where `selectable_label` inside `ScrollArea`
|
||||||
|
//! inside a `Popup` doesn't fill the available width, making only the text
|
||||||
|
//! portion clickable.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use egui::Ui;
|
||||||
|
|
||||||
|
/// Render a full-width selectable list item.
|
||||||
|
///
|
||||||
|
/// Unlike `ui.selectable_label()`, this allocates the full available width
|
||||||
|
/// for the clickable area, matching native menu item behavior.
|
||||||
|
pub fn list_item(ui: &mut Ui, selected: bool, label: &str) -> bool {
|
||||||
|
let desired_width = ui.available_width();
|
||||||
|
let height = ui.spacing().interact_size.y;
|
||||||
|
let (rect, response) = ui.allocate_exact_size(
|
||||||
|
egui::vec2(desired_width, height),
|
||||||
|
egui::Sense::click(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
let visuals = ui.visuals();
|
||||||
|
if selected {
|
||||||
|
ui.painter().rect_filled(rect, 2.0, visuals.selection.bg_fill);
|
||||||
|
} else if response.hovered() {
|
||||||
|
ui.painter().rect_filled(rect, 2.0, visuals.widgets.hovered.bg_fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_color = if selected {
|
||||||
|
visuals.selection.stroke.color
|
||||||
|
} else if response.hovered() {
|
||||||
|
visuals.widgets.hovered.text_color()
|
||||||
|
} else {
|
||||||
|
visuals.widgets.inactive.text_color()
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_pos = rect.min + egui::vec2(4.0, (rect.height() - 14.0) / 2.0);
|
||||||
|
ui.painter().text(
|
||||||
|
text_pos,
|
||||||
|
egui::Align2::LEFT_TOP,
|
||||||
|
label,
|
||||||
|
egui::FontId::proportional(14.0),
|
||||||
|
text_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a scrollable list of items inside a popup, ensuring full-width
|
||||||
|
/// clickable areas and proper ScrollArea sizing.
|
||||||
|
///
|
||||||
|
/// Returns the index of the clicked item, if any.
|
||||||
|
pub fn scrollable_list<'a>(
|
||||||
|
ui: &mut Ui,
|
||||||
|
max_height: f32,
|
||||||
|
items: impl Iterator<Item = (bool, &'a str)>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut clicked_index = None;
|
||||||
|
|
||||||
|
// Force the ScrollArea to use the full width set by the parent
|
||||||
|
let width = ui.available_width();
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(max_height)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.set_min_width(width);
|
||||||
|
for (i, (selected, label)) in items.enumerate() {
|
||||||
|
if list_item(ui, selected, label) {
|
||||||
|
clicked_index = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clicked_index
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
//! Reusable UI widgets for the editor
|
//! Reusable UI widgets for the editor
|
||||||
|
|
||||||
mod text_field;
|
mod text_field;
|
||||||
|
pub mod dropdown_list;
|
||||||
|
|
||||||
pub use text_field::ImeTextField;
|
pub use text_field::ImeTextField;
|
||||||
|
pub use dropdown_list::{list_item, scrollable_list};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue