Skip to content

Commit e63e02a

Browse files
committed
Fix URL button layout and add URL history
1 parent 8e3c37a commit e63e02a

5 files changed

Lines changed: 152 additions & 24 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ naga = { version = "29.0.3", features = ["wgsl-in"] }
5151
glam = "0.32.1"
5252
env_logger = "0.11.10"
5353
ureq = "3.3.0"
54+
directories = "6.0.0"
5455

5556
[build-dependencies]
5657
winres = "0.1.12"

src/engine.rs

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub enum EngineAction {
6464
CloseUrlDialog,
6565
SetUrlInput(String),
6666
LoadUrl(String),
67+
ClearFocusUrlInput,
6768
}
6869

6970
#[repr(C)]
@@ -1812,27 +1813,52 @@ impl<'a> VulkanEngine<'a> {
18121813
egui::Window::new("Open Network Stream")
18131814
.collapsible(false)
18141815
.resizable(false)
1816+
.default_width(600.0)
18151817
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
18161818
.show(ctx, |ui| {
18171819
ui.label("Enter Stream URL (e.g. .pls, .m3u, or direct stream link):");
1820+
ui.add_space(4.0);
18181821
let mut url_text = state.url_input_text.clone();
1819-
let text_resp = ui.add_sized([400.0, 30.0], egui::TextEdit::singleline(&mut url_text));
1822+
let text_resp = ui.add(
1823+
egui::TextEdit::singleline(&mut url_text)
1824+
.desired_width(f32::INFINITY)
1825+
.min_size(egui::vec2(0.0, 30.0))
1826+
);
1827+
if state.focus_url_input {
1828+
text_resp.request_focus();
1829+
engine_action = EngineAction::ClearFocusUrlInput;
1830+
}
18201831
if text_resp.changed() {
18211832
engine_action = EngineAction::SetUrlInput(url_text.clone());
18221833
}
18231834
ui.add_space(10.0);
18241835
ui.horizontal(|ui| {
1825-
ui.allocate_ui_with_layout(ui.available_size(), egui::Layout::centered_and_justified(egui::Direction::LeftToRight), |ui| {
1826-
if ui.button("Cancel").clicked() {
1827-
engine_action = EngineAction::CloseUrlDialog;
1828-
}
1829-
if ui.button("Open").clicked() || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) {
1836+
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
1837+
if ui.add_sized([80.0, 30.0], egui::Button::new("Open")).clicked() || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) {
18301838
if !url_text.is_empty() {
18311839
engine_action = EngineAction::LoadUrl(url_text);
18321840
}
18331841
}
1842+
if ui.add_sized([80.0, 30.0], egui::Button::new("Cancel")).clicked() {
1843+
engine_action = EngineAction::CloseUrlDialog;
1844+
}
18341845
});
18351846
});
1847+
1848+
if !state.url_history.is_empty() {
1849+
ui.separator();
1850+
ui.label(egui::RichText::new("Recent Streams:").strong());
1851+
egui::ScrollArea::vertical().max_height(150.0).show(ui, |ui| {
1852+
for (url, title) in &state.url_history {
1853+
if ui.add_sized(
1854+
[ui.available_width(), 30.0],
1855+
egui::Button::new(format!("{} ({})", title, url))
1856+
).clicked() {
1857+
engine_action = EngineAction::LoadUrl(url.clone());
1858+
}
1859+
}
1860+
});
1861+
}
18361862
});
18371863
}
18381864

@@ -2179,15 +2205,13 @@ impl<'a> VulkanEngine<'a> {
21792205
}
21802206

21812207
let mut append = state.append_to_playlist;
2182-
// Only update the file dialog UI when it's actually open to avoid layout overhead
2183-
if state.is_file_picker_open || *file_dialog.state() != egui_file_dialog::DialogState::Closed {
2184-
file_dialog.update_with_right_panel_ui(ctx, &mut |ui, _fd| {
2185-
ui.add_space(10.0);
2186-
ui.heading("Options");
2187-
ui.separator();
2188-
ui.checkbox(&mut append, "Add to Playlist instead of replacing");
2189-
});
2190-
}
2208+
file_dialog.update_with_right_panel_ui(ctx, &mut |ui, _fd| {
2209+
ui.add_space(10.0);
2210+
ui.heading("Options");
2211+
ui.separator();
2212+
ui.checkbox(&mut append, "Add to Playlist instead of replacing");
2213+
});
2214+
21912215
if append != state.append_to_playlist {
21922216
engine_action = EngineAction::SetAppendToPlaylist(append);
21932217
}
@@ -2388,7 +2412,6 @@ impl<'a> VulkanEngine<'a> {
23882412

23892413
egui::Area::new(egui::Id::new("splash_shortcuts_area"))
23902414
.anchor(egui::Align2::CENTER_BOTTOM, egui::vec2(0.0, -40.0))
2391-
.order(egui::Order::Foreground)
23922415
.show(ctx, |ui| {
23932416
egui::Frame::NONE
23942417
.fill(egui::Color32::from_black_alpha(200))
@@ -2595,13 +2618,12 @@ impl<'a> VulkanEngine<'a> {
25952618
}
25962619
ui.add_space(20.0);
25972620

2598-
if !is_game_mode {
25992621
egui::Frame::NONE
26002622
.shadow(egui::Shadow { offset: [0, 4], blur: 12, spread: 0, color: egui::Color32::from_black_alpha(200) })
26012623
.corner_radius(6.0)
26022624
.show(ui, |ui| {
26032625
ui.horizontal(|ui| {
2604-
let total_width = 300.0 + 20.0 + 250.0;
2626+
let total_width = 300.0 + 20.0 + 300.0;
26052627
ui.add_space((ui.available_width() - total_width) / 2.0);
26062628

26072629
let btn = egui::Button::new(
@@ -2613,7 +2635,11 @@ impl<'a> VulkanEngine<'a> {
26132635
.fill(egui::Color32::from_rgb(0, 100, 200))
26142636
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(80, 180, 255)));
26152637

2616-
if ui.add_sized([300.0, 60.0], btn).clicked() {
2638+
let resp1 = ui.add_sized([300.0, 60.0], btn);
2639+
if resp1.has_focus() {
2640+
ui.painter().rect(resp1.rect.expand(2.0), 6.0, egui::Color32::TRANSPARENT, egui::Stroke::new(3.0, egui::Color32::YELLOW), egui::StrokeKind::Outside);
2641+
}
2642+
if resp1.clicked() {
26172643
engine_action = EngineAction::OpenFile;
26182644
}
26192645

@@ -2628,14 +2654,17 @@ impl<'a> VulkanEngine<'a> {
26282654
.fill(egui::Color32::from_rgb(100, 50, 150))
26292655
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(180, 80, 255)));
26302656

2631-
if ui.add_sized([250.0, 60.0], url_btn).clicked() {
2657+
let resp2 = ui.add_sized([300.0, 60.0], url_btn);
2658+
if resp2.has_focus() {
2659+
ui.painter().rect(resp2.rect.expand(2.0), 6.0, egui::Color32::TRANSPARENT, egui::Stroke::new(3.0, egui::Color32::YELLOW), egui::StrokeKind::Outside);
2660+
}
2661+
if resp2.clicked() {
26322662
engine_action = EngineAction::OpenUrlDialog;
26332663
}
26342664
});
26352665
});
26362666

26372667
ui.add_space(30.0);
2638-
}
26392668

26402669
let mut force_stereo = state.force_stereo_downmix;
26412670
if ui.checkbox(&mut force_stereo, "Force Stereo Downmix (Fixes crackling on some devices)").changed() {

src/main.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,23 @@ fn main() -> Result<(), Box<dyn Error>> {
104104
state.playlist = args.file.clone();
105105
state.playlist_index = 0;
106106

107+
let mut history_changed = false;
108+
for path in &args.file {
109+
if path.starts_with("http") {
110+
if !state.url_history.iter().any(|(u, _)| u == path) {
111+
state.url_history.push((path.clone(), "Network Stream".to_string()));
112+
history_changed = true;
113+
}
114+
}
115+
}
116+
if history_changed {
117+
let mut out = String::new();
118+
for (u, t) in &state.url_history {
119+
out.push_str(&format!("{}|{}\n", u, t));
120+
}
121+
let _ = std::fs::write(crate::state::get_history_file_path(), out);
122+
}
123+
107124
if let Some(vis) = &args.vis {
108125
let vis_lower = vis.to_lowercase();
109126
if let Some(idx) = crate::state::VISUALIZERS.iter().position(|v| v.filename.to_lowercase().contains(&vis_lower) || v.name.to_lowercase().contains(&vis_lower)) {
@@ -796,20 +813,35 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
796813
}
797814
EngineAction::OpenUrlDialog => {
798815
state.is_url_dialog_open = true;
816+
state.focus_url_input = true;
817+
window.set_ime_allowed(true); // Hint to Wayland/SteamOS to show the virtual keyboard
818+
}
819+
EngineAction::ClearFocusUrlInput => {
820+
state.focus_url_input = false;
799821
}
800822
EngineAction::CloseUrlDialog => {
801823
state.is_url_dialog_open = false;
824+
window.set_ime_allowed(false);
802825
}
803826
EngineAction::SetUrlInput(url) => {
804827
state.url_input_text = url;
805828
}
806829
EngineAction::LoadUrl(url) => {
807830
state.is_url_dialog_open = false;
831+
window.set_ime_allowed(false);
808832
state.url_input_text.clear();
809833
state.playlist = vec![url.clone()];
810834
state.playlist_index = 0;
811-
state.load_request = Some(url);
835+
state.load_request = Some(url.clone());
812836
state.file_loaded = true;
837+
if !state.url_history.iter().any(|(u, _)| u == &url) {
838+
state.url_history.push((url.clone(), "Network Stream".to_string()));
839+
let mut out = String::new();
840+
for (u, t) in &state.url_history {
841+
out.push_str(&format!("{}|{}\n", u, t));
842+
}
843+
let _ = std::fs::write(crate::state::get_history_file_path(), out);
844+
}
813845
}
814846
EngineAction::None => {}
815847
}
@@ -880,7 +912,10 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
880912
}
881913
},
882914
Event::AboutToWait => {
883-
let is_dialog_open = *file_dialog.state() != egui_file_dialog::DialogState::Closed;
915+
let is_dialog_open = *file_dialog.state() != egui_file_dialog::DialogState::Closed || {
916+
let state = app_state.lock().unwrap();
917+
state.is_url_dialog_open
918+
};
884919

885920
{
886921
let mut state = app_state.lock().unwrap();
@@ -973,8 +1008,41 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
9731008
gilrs::Button::DPadRight => { push_key(egui::Key::ArrowRight); continue; }
9741009
gilrs::Button::South => { push_key(egui::Key::Enter); continue; }
9751010
gilrs::Button::East => { push_key(egui::Key::Escape); continue; }
1011+
gilrs::Button::RightTrigger => {
1012+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE });
1013+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: false, repeat: false, modifiers: egui::Modifiers::NONE });
1014+
continue;
1015+
}
1016+
gilrs::Button::LeftTrigger => {
1017+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::SHIFT });
1018+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: false, repeat: false, modifiers: egui::Modifiers::SHIFT });
1019+
continue;
1020+
}
9761021
_ => {}
9771022
}
1023+
} else {
1024+
let is_splash = {
1025+
let state = app_state.lock().unwrap();
1026+
!state.file_loaded
1027+
};
1028+
if is_splash {
1029+
let mut state = app_state.lock().unwrap();
1030+
let mut push_tab = |shift: bool| {
1031+
let modifiers = if shift { egui::Modifiers::SHIFT } else { egui::Modifiers::NONE };
1032+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers });
1033+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: false, repeat: false, modifiers });
1034+
};
1035+
match button {
1036+
gilrs::Button::DPadLeft => { push_tab(true); continue; }
1037+
gilrs::Button::DPadRight => { push_tab(false); continue; }
1038+
gilrs::Button::South => {
1039+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE });
1040+
state.egui_gamepad_events.push(egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: false, repeat: false, modifiers: egui::Modifiers::NONE });
1041+
continue;
1042+
}
1043+
_ => {}
1044+
}
1045+
}
9781046
}
9791047

9801048
match button {

src/state.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ pub struct AppState {
169169
pub is_file_picker_open: bool,
170170
pub open_file_request: bool,
171171
pub is_url_dialog_open: bool,
172+
pub focus_url_input: bool,
172173
pub url_input_text: String,
174+
pub url_history: Vec<(String, String)>,
173175
pub egui_gamepad_events: Vec<egui::Event>,
174176
pub force_stereo_downmix: bool,
175177
pub append_to_playlist: bool,
@@ -185,6 +187,18 @@ pub struct AppState {
185187
pub cumulative_scrub: f64,
186188
}
187189

190+
pub fn get_history_file_path() -> std::path::PathBuf {
191+
if let Some(proj_dirs) = directories::ProjectDirs::from("com", "RustTracker", "RustTracker") {
192+
let dir = proj_dirs.data_dir();
193+
if !dir.exists() {
194+
let _ = std::fs::create_dir_all(dir);
195+
}
196+
dir.join("url_history.txt")
197+
} else {
198+
std::path::PathBuf::from("url_history.txt")
199+
}
200+
}
201+
188202
impl AppState {
189203
pub fn new(title: String) -> Self {
190204
let mut history = VecDeque::new();
@@ -212,6 +226,17 @@ impl AppState {
212226
vis_enabled[9] = false; // Ferrofluid Particle Sim
213227
vis_enabled[10] = false; // Neon Corridor
214228
}
229+
230+
let mut url_history = Vec::new();
231+
if let Ok(data) = std::fs::read_to_string(get_history_file_path()) {
232+
for line in data.lines() {
233+
if let Some((url, title)) = line.split_once('|') {
234+
if !url_history.iter().any(|(u, _)| u == url) {
235+
url_history.push((url.to_string(), title.to_string()));
236+
}
237+
}
238+
}
239+
}
215240

216241
AppState {
217242
passthrough_enabled: true,
@@ -272,7 +297,9 @@ impl AppState {
272297
is_file_picker_open: false,
273298
open_file_request: false,
274299
is_url_dialog_open: false,
300+
focus_url_input: false,
275301
url_input_text: String::new(),
302+
url_history,
276303
egui_gamepad_events: Vec::new(),
277304
force_stereo_downmix: is_steam_deck,
278305
append_to_playlist: false,
@@ -357,7 +384,9 @@ impl AppState {
357384
is_file_picker_open: self.is_file_picker_open,
358385
open_file_request: false,
359386
is_url_dialog_open: self.is_url_dialog_open,
387+
focus_url_input: self.focus_url_input,
360388
url_input_text: self.url_input_text.clone(),
389+
url_history: self.url_history.clone(),
361390
egui_gamepad_events: Vec::new(),
362391
force_stereo_downmix: self.force_stereo_downmix,
363392
append_to_playlist: self.append_to_playlist,

0 commit comments

Comments
 (0)