Skip to content

Commit 044716f

Browse files
committed
feat: add mixer, timeline, track list, and transport panels
- Implemented MixerPanel for channel strips with faders, meters, and controls for mute, solo, and arm. - Created TimelinePanel for the main arrangement view, including clips, playhead, and grid. - Developed TrackListPanel for track headers with controls for volume, pan, mute, and solo. - Introduced TransportPanel for playback controls, BPM editing, and time display.
1 parent 0e81cdb commit 044716f

File tree

6 files changed

+1827
-0
lines changed

6 files changed

+1827
-0
lines changed

apps/egui_ui/src/panels/browser.rs

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
//! Browser Panel
2+
//!
3+
//! File browser for audio files and VST plugins
4+
5+
use eframe::egui::{self, Color32, RichText, Sense, Vec2};
6+
use rysyn_ffi_bridge::{Command, StateSnapshot};
7+
use std::path::PathBuf;
8+
9+
#[derive(Default, PartialEq)]
10+
pub enum BrowserTab {
11+
#[default]
12+
Files,
13+
Plugins,
14+
Presets,
15+
}
16+
17+
#[derive(Default)]
18+
pub struct BrowserPanel {
19+
current_tab: BrowserTab,
20+
current_path: PathBuf,
21+
search_query: String,
22+
selected_item: Option<String>,
23+
}
24+
25+
impl BrowserPanel {
26+
pub fn show(
27+
&mut self,
28+
ui: &mut egui::Ui,
29+
state: &StateSnapshot,
30+
mut send_cmd: impl FnMut(Command),
31+
) {
32+
// Tab bar
33+
ui.horizontal(|ui| {
34+
ui.selectable_value(&mut self.current_tab, BrowserTab::Files, "Files");
35+
ui.selectable_value(&mut self.current_tab, BrowserTab::Plugins, "Plugins");
36+
ui.selectable_value(&mut self.current_tab, BrowserTab::Presets, "Presets");
37+
});
38+
39+
ui.separator();
40+
41+
// Search bar
42+
ui.horizontal(|ui| {
43+
ui.label("🔍");
44+
ui.add(
45+
egui::TextEdit::singleline(&mut self.search_query)
46+
.hint_text("Search...")
47+
.desired_width(ui.available_width())
48+
);
49+
});
50+
51+
ui.add_space(4.0);
52+
53+
match self.current_tab {
54+
BrowserTab::Files => self.show_files_tab(ui, state, &mut send_cmd),
55+
BrowserTab::Plugins => self.show_plugins_tab(ui, state, &mut send_cmd),
56+
BrowserTab::Presets => self.show_presets_tab(ui, state, &mut send_cmd),
57+
}
58+
}
59+
60+
fn show_files_tab(
61+
&mut self,
62+
ui: &mut egui::Ui,
63+
_state: &StateSnapshot,
64+
send_cmd: &mut impl FnMut(Command),
65+
) {
66+
// Path breadcrumb
67+
ui.horizontal(|ui| {
68+
if ui.small_button("🏠").clicked() {
69+
if let Some(home) = dirs::home_dir() {
70+
self.current_path = home;
71+
}
72+
}
73+
74+
if ui.small_button("⬆").clicked() {
75+
if let Some(parent) = self.current_path.parent() {
76+
self.current_path = parent.to_path_buf();
77+
}
78+
}
79+
80+
ui.label(
81+
RichText::new(self.current_path.display().to_string())
82+
.size(10.0)
83+
.color(Color32::GRAY)
84+
);
85+
});
86+
87+
ui.separator();
88+
89+
// File list
90+
egui::ScrollArea::vertical()
91+
.auto_shrink([false, false])
92+
.show(ui, |ui| {
93+
if let Ok(entries) = std::fs::read_dir(&self.current_path) {
94+
let mut items: Vec<_> = entries.filter_map(|e| e.ok()).collect();
95+
items.sort_by(|a, b| {
96+
let a_dir = a.path().is_dir();
97+
let b_dir = b.path().is_dir();
98+
match (a_dir, b_dir) {
99+
(true, false) => std::cmp::Ordering::Less,
100+
(false, true) => std::cmp::Ordering::Greater,
101+
_ => a.file_name().cmp(&b.file_name()),
102+
}
103+
});
104+
105+
for entry in items {
106+
let path = entry.path();
107+
let name = path.file_name()
108+
.map(|n| n.to_string_lossy().to_string())
109+
.unwrap_or_default();
110+
111+
// Filter by search
112+
if !self.search_query.is_empty()
113+
&& !name.to_lowercase().contains(&self.search_query.to_lowercase()) {
114+
continue;
115+
}
116+
117+
let is_dir = path.is_dir();
118+
let is_audio = is_audio_file(&path);
119+
120+
// Skip non-audio files (but show directories)
121+
if !is_dir && !is_audio && !self.search_query.is_empty() {
122+
continue;
123+
}
124+
125+
let icon = if is_dir {
126+
"📁"
127+
} else if is_audio {
128+
"🎵"
129+
} else {
130+
"📄"
131+
};
132+
133+
let is_selected = self.selected_item.as_ref() == Some(&name);
134+
135+
let response = ui.add(
136+
egui::SelectableLabel::new(
137+
is_selected,
138+
format!("{} {}", icon, name)
139+
)
140+
);
141+
142+
if response.clicked() {
143+
self.selected_item = Some(name.clone());
144+
}
145+
146+
if response.double_clicked() {
147+
if is_dir {
148+
self.current_path = path.clone();
149+
self.selected_item = None;
150+
} else if is_audio {
151+
// Import audio file
152+
send_cmd(Command::ImportAudioFile {
153+
path: path.to_string_lossy().to_string()
154+
});
155+
}
156+
}
157+
158+
// Drag to timeline
159+
if is_audio {
160+
response.on_hover_text("Drag to timeline or double-click to import");
161+
}
162+
}
163+
} else {
164+
ui.label(
165+
RichText::new("Cannot read directory")
166+
.color(Color32::from_rgb(255, 100, 100))
167+
);
168+
}
169+
});
170+
}
171+
172+
fn show_plugins_tab(
173+
&mut self,
174+
ui: &mut egui::Ui,
175+
state: &StateSnapshot,
176+
send_cmd: &mut impl FnMut(Command),
177+
) {
178+
// Plugin categories
179+
ui.horizontal(|ui| {
180+
ui.label("Category:");
181+
ui.selectable_label(true, "All");
182+
ui.selectable_label(false, "Instruments");
183+
ui.selectable_label(false, "Effects");
184+
});
185+
186+
ui.separator();
187+
188+
// Rescan button
189+
ui.horizontal(|ui| {
190+
if ui.button("🔄 Rescan Plugins").clicked() {
191+
send_cmd(Command::RescanPlugins);
192+
}
193+
});
194+
195+
ui.add_space(4.0);
196+
197+
// Plugin list
198+
egui::ScrollArea::vertical()
199+
.auto_shrink([false, false])
200+
.show(ui, |ui| {
201+
for plugin in &state.available_plugins {
202+
// Filter by search
203+
if !self.search_query.is_empty()
204+
&& !plugin.name.to_lowercase().contains(&self.search_query.to_lowercase()) {
205+
continue;
206+
}
207+
208+
let icon = if plugin.is_instrument { "🎹" } else { "🎛" };
209+
let format_text = match plugin.format.as_str() {
210+
"VST3" => "VST3",
211+
"AU" => "AU",
212+
"CLAP" => "CLAP",
213+
_ => "?",
214+
};
215+
216+
let is_selected = self.selected_item.as_ref() == Some(&plugin.id);
217+
218+
let response = ui.add(
219+
egui::SelectableLabel::new(
220+
is_selected,
221+
format!("{} {} [{}]", icon, &plugin.name, format_text)
222+
)
223+
);
224+
225+
if response.clicked() {
226+
self.selected_item = Some(plugin.id.clone());
227+
}
228+
229+
if response.double_clicked() {
230+
// Load plugin on selected track
231+
send_cmd(Command::LoadPlugin {
232+
track_id: state.tracks.first().map(|t| t.id).unwrap_or(0),
233+
plugin_id: plugin.id.clone(),
234+
slot: None,
235+
});
236+
}
237+
238+
response.on_hover_ui(|ui| {
239+
ui.label(&plugin.name);
240+
ui.label(format!("Manufacturer: {}", &plugin.manufacturer));
241+
ui.label(format!("Format: {}", format_text));
242+
ui.label(format!("Path: {}", &plugin.path));
243+
});
244+
}
245+
246+
if state.available_plugins.is_empty() {
247+
ui.label(
248+
RichText::new("No plugins found. Click 'Rescan Plugins'.")
249+
.color(Color32::GRAY)
250+
);
251+
}
252+
});
253+
}
254+
255+
fn show_presets_tab(
256+
&mut self,
257+
ui: &mut egui::Ui,
258+
_state: &StateSnapshot,
259+
_send_cmd: &mut impl FnMut(Command),
260+
) {
261+
ui.label(
262+
RichText::new("Presets browser coming soon...")
263+
.color(Color32::GRAY)
264+
);
265+
266+
// Placeholder for preset browser
267+
// Would show:
268+
// - Factory presets
269+
// - User presets
270+
// - Preset categories
271+
// - Favorites
272+
}
273+
}
274+
275+
fn is_audio_file(path: &std::path::Path) -> bool {
276+
let extensions = ["wav", "mp3", "flac", "aiff", "aif", "ogg", "m4a"];
277+
278+
path.extension()
279+
.and_then(|ext| ext.to_str())
280+
.map(|ext| extensions.contains(&ext.to_lowercase().as_str()))
281+
.unwrap_or(false)
282+
}

0 commit comments

Comments
 (0)