Compare commits

..

2 Commits

Author SHA1 Message Date
Tarasov Aleksandr 78960cdc10 Refactor draw_files and draw_tree_node to improve maintainability and readability (#108)
- Extracted search field rendering to `draw_files_search_field`
- Extracted list rendering to `draw_files_list`
- Split `draw_tree_node` file and directory branch logic to `draw_tree_node_file` and `draw_tree_node_dir`

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-17 17:07:32 +03:00
Tarasov Aleksandr 0439cf815e perf: eliminate redundant PathBuf clone in GUI directory list (#110)
This commit optimizes the GUI render loop in `src/gui/draw.rs` during the rendering of the drag and drop directory list. Previously, `self.app_state.dirs.clone()` was cloning the entire vector of `PathBuf`s on every frame, which caused unnecessary allocations.

Now, `std::mem::take` temporarily removes the list of directories from `app_state.dirs` inside `show_vec`, and items are passed by reference rather than being cloned (`let path = item;` instead of `item.clone()`). Finally, the original list is restored into `app_state.dirs`. To ensure the state doesn't mutate or invalidate when `self.open_dir(&path)` is clicked, this logic has been deferred to run after the `app_state` vector is restored.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-17 17:07:04 +03:00
+256 -225
View File
@@ -673,10 +673,11 @@ impl SoundpadGui {
ScrollArea::vertical().id_salt(0).show(ui, |ui| { ScrollArea::vertical().id_salt(0).show(ui, |ui| {
ui.set_min_width(area_size.x); ui.set_min_width(area_size.x);
let mut dirs = self.app_state.dirs.clone(); let mut dirs = std::mem::take(&mut self.app_state.dirs);
let mut dir_to_open = None;
dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| { dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| {
let path = item.clone(); let path = item;
ui.horizontal(|ui| { ui.horizontal(|ui| {
handle.ui(ui, |ui| { handle.ui(ui, |ui| {
ui.label(ICON_DRAG_INDICATOR.codepoint); ui.label(ICON_DRAG_INDICATOR.codepoint);
@@ -688,7 +689,7 @@ impl SoundpadGui {
let mut dir_button_text = RichText::new(name.clone()); let mut dir_button_text = RichText::new(name.clone());
if let Some(current_dir) = &self.app_state.current_dir if let Some(current_dir) = &self.app_state.current_dir
&& current_dir.eq(&path) && current_dir.eq(&*path)
{ {
dir_button_text = dir_button_text.color(Color32::WHITE); dir_button_text = dir_button_text.color(Color32::WHITE);
} }
@@ -698,7 +699,7 @@ impl SoundpadGui {
let dir_button_response = ui.add(dir_button); let dir_button_response = ui.add(dir_button);
if dir_button_response.clicked() { if dir_button_response.clicked() {
self.open_dir(&path); dir_to_open = Some(path.clone());
} }
let delete_dir_button = Button::new(ICON_DELETE).frame(false); let delete_dir_button = Button::new(ICON_DELETE).frame(false);
@@ -718,7 +719,7 @@ impl SoundpadGui {
)) ))
.clicked() .clicked()
{ {
self.open_dir(&path); dir_to_open = Some(path.clone());
} }
if ui if ui
@@ -750,6 +751,10 @@ impl SoundpadGui {
}); });
self.app_state.dirs = dirs; self.app_state.dirs = dirs;
if let Some(path) = dir_to_open {
self.open_dir(&path);
}
ui.horizontal(|ui| { ui.horizontal(|ui| {
let add_dirs_button = Button::new(ICON_ADD).frame(false); let add_dirs_button = Button::new(ICON_ADD).frame(false);
let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button); let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button);
@@ -769,56 +774,259 @@ impl SoundpadGui {
}); });
} }
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) { fn draw_files_search_field(&mut self, ui: &mut Ui) {
ui.vertical(|ui| { ui.horizontal(|ui| {
ui.horizontal(|ui| { let search_field_response = ui.add_sized(
let search_field_response = ui.add_sized( [ui.available_width(), 22.0],
[ui.available_width(), 22.0], TextEdit::singleline(&mut self.app_state.search_query)
TextEdit::singleline(&mut self.app_state.search_query) .hint_text(t!("gui.search_placeholder")),
.hint_text(t!("gui.search_placeholder")), );
);
if self.app_state.force_focus_search { if self.app_state.force_focus_search {
search_field_response.request_focus(); search_field_response.request_focus();
self.app_state.force_focus_search = false; self.app_state.force_focus_search = false;
}
self.app_state.search_field_id = Some(search_field_response.id);
});
}
fn draw_files_list(&mut self, ui: &mut Ui, area_size: Vec2) {
ScrollArea::vertical().id_salt(1).show(ui, |ui| {
ui.set_min_width(area_size.x);
ui.set_min_height(area_size.y);
ui.vertical(|ui| {
let mut actions = Vec::new();
let files = self.get_filtered_files();
for entry_path in files {
Self::draw_tree_node(
ui,
entry_path,
&mut self.app_state,
&self.audio_player_state,
&mut actions,
);
} }
self.app_state.search_field_id = Some(search_field_response.id); for action in actions {
}); match action {
FileAction::Play(path, concurrent) => self.play_file(&path, concurrent),
ui.separator(); FileAction::StopAndPlay(id, path, concurrent) => {
self.stop(Some(id));
ScrollArea::vertical().id_salt(1).show(ui, |ui| { self.play_file(&path, concurrent);
ui.set_min_width(area_size.x); }
ui.set_min_height(area_size.y); FileAction::AssignHotkey(path) => {
self.app_state.assigning_hotkey_for_file = Some(path);
ui.vertical(|ui| { self.app_state.hotkey_capture_active = true;
let mut actions = Vec::new(); }
let files = self.get_filtered_files();
for entry_path in files {
Self::draw_tree_node(
ui,
entry_path,
&mut self.app_state,
&self.audio_player_state,
&mut actions,
);
} }
}
});
});
}
for action in actions { fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
match action { ui.vertical(|ui| {
FileAction::Play(path, concurrent) => self.play_file(&path, concurrent), self.draw_files_search_field(ui);
FileAction::StopAndPlay(id, path, concurrent) => { ui.separator();
self.stop(Some(id)); self.draw_files_list(ui, area_size);
self.play_file(&path, concurrent); });
} }
FileAction::AssignHotkey(path) => {
self.app_state.assigning_hotkey_for_file = Some(path); fn draw_tree_node_dir(
self.app_state.hotkey_capture_active = true; ui: &mut Ui,
path: std::path::PathBuf,
app_state: &mut AppState,
audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>,
) {
let dir_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
CollapsingHeader::new(dir_name)
.id_salt(&path)
.show(ui, |ui| {
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
cached.clone()
} else {
let mut read = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.filter_map(|e| e.ok()) {
read.push(entry.path());
}
}
read.sort_by(|a, b| {
let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir();
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
a.cmp(b)
}
});
app_state.dir_cache.insert(path.clone(), read.clone());
read
};
let search_query = app_state.search_query.to_lowercase();
let search_query = search_query.trim();
for child in children {
if !child.is_dir() {
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
continue;
}
if !search_query.is_empty() {
let file_name = child
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) {
continue;
} }
} }
} }
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
}
});
}
fn draw_tree_node_file(
ui: &mut Ui,
path: std::path::PathBuf,
app_state: &mut AppState,
audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>,
) {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
ui.horizontal(|ui| {
// Hotkey badge
let mut hotkey_badge = None;
for slot in &app_state.hotkey_config.slots {
if slot.action.name == "play"
&& let Some(file_path_str) = slot.action.args.get("file_path")
&& Path::new(file_path_str) == path
{
if let Some(chord) = &slot.key_chord {
hotkey_badge = Some(format!("[{}]", chord));
} else {
hotkey_badge = Some(format!("[{}]", slot.slot));
}
break;
}
}
if let Some(badge) = &hotkey_badge {
ui.label(
RichText::new(badge)
.small()
.monospace()
.color(Color32::from_rgb(100, 200, 100)),
);
}
let file_button_text = RichText::new(&file_name);
let file_button = Button::new(file_button_text).frame(false).truncate();
let file_button_response = ui.add(file_button);
if file_button_response.clicked() {
ui.input(|i| {
if i.modifiers.ctrl {
actions.push(FileAction::Play(path.clone(), true));
} else if i.modifiers.shift
&& let Some(last_track) = audio_player_state.tracks.last()
{
actions.push(FileAction::StopAndPlay(
last_track.id,
path.clone(),
true,
));
} else {
actions.push(FileAction::Play(path.clone(), false));
}
}); });
}
// Context menu
file_button_response.context_menu(|ui| {
if ui
.button(format!(
"{} {}",
ICON_BOLT.codepoint,
t!("gui.context.files.play_solo")
))
.clicked()
{
actions.push(FileAction::Play(path.clone(), false));
}
if ui
.button(format!(
"{} {}",
ICON_ADD.codepoint,
t!("gui.context.files.add_new")
))
.clicked()
{
actions.push(FileAction::Play(path.clone(), true));
}
if ui
.button(format!(
"{} {}",
ICON_SWAP_HORIZ.codepoint,
t!("gui.context.files.replace_last")
))
.clicked()
&& let Some(last_track) = audio_player_state.tracks.last()
{
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
}
ui.separator();
if ui
.button(format!(
"{} {}",
ICON_OPEN_IN_BROWSER.codepoint,
t!("gui.context.files.show_in_fm")
))
.clicked()
&& let Err(e) = opener::reveal(&path)
{
eprintln!("Failed to open file manager: {}", e);
}
ui.separator();
if ui
.button(format!(
"{} {}",
ICON_KEYBOARD.codepoint,
t!("gui.context.files.asign_hotkey")
))
.clicked()
{
actions.push(FileAction::AssignHotkey(path.clone()));
ui.close();
}
}); });
}); });
} }
@@ -831,186 +1039,9 @@ impl SoundpadGui {
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
) { ) {
if path.is_dir() { if path.is_dir() {
let dir_name = path Self::draw_tree_node_dir(ui, path, app_state, audio_player_state, actions);
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
CollapsingHeader::new(dir_name)
.id_salt(&path)
.show(ui, |ui| {
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
cached.clone()
} else {
let mut read = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.filter_map(|e| e.ok()) {
read.push(entry.path());
}
}
read.sort_by(|a, b| {
let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir();
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
a.cmp(b)
}
});
app_state.dir_cache.insert(path.clone(), read.clone());
read
};
let search_query = app_state.search_query.to_lowercase();
let search_query = search_query.trim();
for child in children {
if !child.is_dir() {
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
continue;
}
if !search_query.is_empty() {
let file_name = child
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) {
continue;
}
}
}
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
}
});
} else { } else {
let file_name = path Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
ui.horizontal(|ui| {
// Hotkey badge
let mut hotkey_badge = None;
for slot in &app_state.hotkey_config.slots {
if slot.action.name == "play"
&& let Some(file_path_str) = slot.action.args.get("file_path")
&& Path::new(file_path_str) == path
{
if let Some(chord) = &slot.key_chord {
hotkey_badge = Some(format!("[{}]", chord));
} else {
hotkey_badge = Some(format!("[{}]", slot.slot));
}
break;
}
}
if let Some(badge) = &hotkey_badge {
ui.label(
RichText::new(badge)
.small()
.monospace()
.color(Color32::from_rgb(100, 200, 100)),
);
}
let file_button_text = RichText::new(&file_name);
let file_button = Button::new(file_button_text).frame(false).truncate();
let file_button_response = ui.add(file_button);
if file_button_response.clicked() {
ui.input(|i| {
if i.modifiers.ctrl {
actions.push(FileAction::Play(path.clone(), true));
} else if i.modifiers.shift
&& let Some(last_track) = audio_player_state.tracks.last()
{
actions.push(FileAction::StopAndPlay(
last_track.id,
path.clone(),
true,
));
} else {
actions.push(FileAction::Play(path.clone(), false));
}
});
}
// Context menu
file_button_response.context_menu(|ui| {
if ui
.button(format!(
"{} {}",
ICON_BOLT.codepoint,
t!("gui.context.files.play_solo")
))
.clicked()
{
actions.push(FileAction::Play(path.clone(), false));
}
if ui
.button(format!(
"{} {}",
ICON_ADD.codepoint,
t!("gui.context.files.add_new")
))
.clicked()
{
actions.push(FileAction::Play(path.clone(), true));
}
if ui
.button(format!(
"{} {}",
ICON_SWAP_HORIZ.codepoint,
t!("gui.context.files.replace_last")
))
.clicked()
&& let Some(last_track) = audio_player_state.tracks.last()
{
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
}
ui.separator();
if ui
.button(format!(
"{} {}",
ICON_OPEN_IN_BROWSER.codepoint,
t!("gui.context.files.show_in_fm")
))
.clicked()
&& let Err(e) = opener::reveal(&path)
{
eprintln!("Failed to open file manager: {}", e);
}
ui.separator();
if ui
.button(format!(
"{} {}",
ICON_KEYBOARD.codepoint,
t!("gui.context.files.asign_hotkey")
))
.clicked()
{
actions.push(FileAction::AssignHotkey(path.clone()));
ui.close();
}
});
});
} }
} }