Skip to main content

cardinal_gui/
uxn.rs

1/// Returns a reference to the build_orca_inject_queue function for use in event injection
2pub fn build_orca_inject_queue_ref() -> Option<fn(&str) -> std::collections::VecDeque<InjectEvent>>
3{
4    Some(build_orca_inject_queue)
5}
6/// Build an InjectEvent queue for orca file injection with rectangle and efficient movement
7pub fn build_orca_inject_queue(file_path: &str) -> std::collections::VecDeque<InjectEvent> {
8    use std::collections::VecDeque;
9    use std::fs::File;
10    use std::io::{BufRead, BufReader};
11    use varvara::Key;
12
13    let mut queue = VecDeque::new();
14    const CTRL_H: Key = Key::Ctrl;
15    const RIGHT: Key = Key::Right;
16    const LEFT: Key = Key::Left;
17    const UP: Key = Key::Up;
18    const DOWN: Key = Key::Down;
19    // Read file into lines
20    let mut lines: Vec<Vec<char>> = Vec::new();
21    let mut max_len = 0;
22    if let Ok(file) = File::open(file_path) {
23        let reader = BufReader::new(file);
24        for line in reader.lines().map_while(Result::ok) {
25            let chars: Vec<char> = line.chars().collect();
26            max_len = max_len.max(chars.len());
27            lines.push(chars);
28        }
29    }
30    let rows = lines.len();
31    let cols = max_len;
32    // Build rectangle with '/' border
33    let mut grid = vec![vec!['/'; cols + 2]; rows + 2];
34    for i in 1..=rows {
35        for j in 1..=cols {
36            grid[i][j] = if j - 1 < lines[i - 1].len() {
37                lines[i - 1][j - 1]
38            } else {
39                ' '
40            };
41        }
42    }
43    // Start at (1,1)
44    let mut cur_row = 1;
45    let mut cur_col = 1;
46    queue.push_back(InjectEvent::KeyPress(CTRL_H));
47    queue.push_back(InjectEvent::KeyRelease(CTRL_H));
48    // Visit all non '.' cells efficiently (row-major order)
49    for (r, row) in grid.iter().enumerate().take(rows + 2) {
50        for (c, &ch) in row.iter().enumerate().take(cols + 2) {
51            if ch != '.' {
52                // Move to (r,c)
53                let dr = r as isize - cur_row as isize;
54                let dc = c as isize - cur_col as isize;
55                for _ in 0..dr.abs() {
56                    queue.push_back(if dr > 0 {
57                        InjectEvent::KeyPress(DOWN)
58                    } else {
59                        InjectEvent::KeyPress(UP)
60                    });
61                    queue.push_back(if dr > 0 {
62                        InjectEvent::KeyRelease(DOWN)
63                    } else {
64                        InjectEvent::KeyRelease(UP)
65                    });
66                }
67                for _ in 0..dc.abs() {
68                    queue.push_back(if dc > 0 {
69                        InjectEvent::KeyPress(RIGHT)
70                    } else {
71                        InjectEvent::KeyPress(LEFT)
72                    });
73                    queue.push_back(if dc > 0 {
74                        InjectEvent::KeyRelease(RIGHT)
75                    } else {
76                        InjectEvent::KeyRelease(LEFT)
77                    });
78                }
79                cur_row = r;
80                cur_col = c;
81                // Print char
82                queue.push_back(InjectEvent::Char(ch as u8));
83                // If this is a border '/' (not top/bottom), write hex x,y to the right
84                if ch == '/' && r != 0 && r != rows + 1 {
85                    // x = c, y = r
86                    let hex = format!("{c:02X}{r:02X}");
87                    for b in hex.bytes() {
88                        queue.push_back(InjectEvent::Char(b));
89                    }
90                }
91            }
92        }
93    }
94    queue
95}
96
97#[cfg(not(target_arch = "wasm32"))]
98use getrandom;
99#[derive(Debug, Clone)]
100pub enum InjectEvent {
101    Char(u8),
102    KeyPress(Key),
103    KeyRelease(Key),
104    Sleep(u64),      // milliseconds
105    Chord(Vec<Key>), // Simultaneous keypresses
106}
107// uxn.rs - Uxn integration for e_window
108
109#[allow(clippy::module_inception)]
110pub mod uxn {
111
112    use std::path::Path;
113    use std::sync::{Arc, Mutex};
114    use uxn::{Backend, Uxn};
115    use varvara::Varvara;
116    /// UxnModule: Encapsulates a Uxn VM and its state for e_window
117    pub struct UxnModule {
118        pub uxn: Arc<Mutex<Uxn<'static>>>,
119        pub varvara: Option<Varvara>,
120    }
121
122    impl UxnModule {
123        /// Create a new UxnModule, optionally loading a ROM file
124        pub fn new(rom_path: Option<&Path>) -> Result<Self, String> {
125            // Use a static RAM buffer for the Uxn VM
126            static mut RAM: [u8; 65536] = [0; 65536];
127            #[allow(static_mut_refs)]
128            let ram: &'static mut [u8; 65536] = unsafe { &mut RAM };
129            let mut uxn = Uxn::new(ram, Backend::Interpreter);
130            let mut varvara = Varvara::default();
131
132            // varvara.screen.buffer.resize(1024 * 768 * 4, 0);
133            // varvara.screen.changed = true;
134            if let Some(path) = rom_path {
135                let rom = std::fs::read(path).map_err(|e| format!("Failed to read ROM: {e}"))?;
136                let _ = uxn.reset(&rom);
137                varvara.reset(&rom);
138
139                varvara.load_sym_with_rom_path(path);
140            }
141
142            Ok(UxnModule {
143                uxn: Arc::new(Mutex::new(uxn)),
144                varvara: Some(varvara),
145            })
146        }
147
148        // /// Reset the Uxn VM (clears memory and state)
149        // pub fn reset(&self, rom: &[u8]) {
150        //     let mut uxn = self.uxn.lock().unwrap();
151        //     let _ = uxn.reset(rom);
152        // }
153
154        /// Load a new ROM into the Uxn VM (resets VM)
155        pub fn load_rom(&mut self, rom_path: &Path) -> Result<(), String> {
156            let rom = std::fs::read(rom_path).map_err(|e| format!("Failed to read ROM: {e}"))?;
157            let mut uxn = self.uxn.lock().unwrap();
158            let _ = uxn.reset(&rom);
159            if let Some(varvara) = self.varvara.as_mut() {
160                varvara.load_sym_with_rom_path(rom_path);
161            }
162            Ok(())
163        }
164    }
165}
166
167use log::{error, info};
168use std::path::Path;
169use std::sync::{Arc, Mutex};
170// Re-export the cardinal-uxn and cardinal-varvara crates for Uxn VM and utilities
171use ::uxn::{Backend, Uxn};
172use varvara::Key;
173use varvara::MouseState;
174use varvara::TrackerState;
175use varvara::Varvara;
176/// UxnModule: Encapsulates a Uxn VM and its state for e_window
177pub struct UxnModule {
178    pub uxn: Arc<Mutex<Uxn<'static>>>,
179    pub varvara: Option<Varvara>,
180}
181
182impl UxnModule {
183    /// Create a new UxnModule, optionally loading a ROM file
184    pub fn new(rom_path: Option<&Path>) -> Result<Self, String> {
185        // Use a static RAM buffer for the Uxn VM
186        static mut RAM: [u8; 65536] = [0; 65536];
187        #[allow(static_mut_refs)]
188        let ram: &'static mut [u8; 65536] = unsafe { &mut RAM };
189        let mut uxn = Uxn::new(ram, Backend::Interpreter);
190        let mut varvara = Varvara::default();
191        if let Some(path) = rom_path {
192            let rom = std::fs::read(path).map_err(|e| format!("Failed to read ROM: {e}"))?;
193            let _ = uxn.reset(&rom);
194            varvara.load_sym_with_rom_path(path);
195        }
196        Ok(UxnModule {
197            uxn: Arc::new(Mutex::new(uxn)),
198            varvara: Some(varvara),
199        })
200    }
201
202    // Step/run methods are not available in cardinal-uxn. Use run/reset as needed.
203
204    // No run_cycles method; use run/reset as needed.
205
206    /// Reset the Uxn VM (clears memory and state)
207    pub fn reset(&self, rom: &[u8]) {
208        let mut uxn = self.uxn.lock().unwrap();
209        let _ = uxn.reset(rom);
210    }
211
212    /// Load a new ROM into the Uxn VM (resets VM)
213    pub fn load_rom(&mut self, rom_path: &Path) -> Result<(), String> {
214        let rom = std::fs::read(rom_path).map_err(|e| format!("Failed to read ROM: {e}"))?;
215        if let Some(varvara) = self.varvara.as_mut() {
216            varvara.load_sym_with_rom_path(rom_path);
217        }
218        self.reset(&rom);
219        Ok(())
220    }
221
222    // No get_state method; UxnState is not available in cardinal-uxn.
223}
224
225// Optionally, add egui integration for UxnModule (UI panel, etc.)
226pub mod egui_ui {
227    use super::*;
228    use egui::{CollapsingHeader, Ui};
229
230    pub fn show_uxn_panel(ui: &mut Ui, _uxn_mod: &UxnModule) {
231        CollapsingHeader::new("Uxn VM State").show(ui, |ui| {
232            ui.label("Uxn state display not implemented (no UxnState in _uxn)");
233        });
234    }
235}
236use eframe::egui;
237use std::sync::mpsc;
238// use log::{error, info};
239
240#[derive(Debug)]
241pub enum Event {
242    LoadRom(Vec<u8>),
243    SetMuted(bool),
244    Console(u8),
245}
246
247pub struct UxnApp<'a> {
248    pub vm: Uxn<'a>,
249    pub dev: Varvara,
250    scale: f32,
251    size: (u16, u16),
252    next_frame: f64,
253    scroll: (f32, f32),
254    cursor_pos: Option<(f32, f32)>,
255    texture: egui::TextureHandle,
256    event_rx: mpsc::Receiver<Event>,
257    resized: Option<Box<dyn FnMut(u16, u16)>>,
258    _window_mode: String,
259    _aspect_ratio: f32,
260    // For auto ROM cycling
261    auto_rom_select: bool,
262    auto_timer: f64,
263    auto_index: usize,
264    auto_roms: Vec<varvara::RomData>,
265    /// Parallel to auto_roms: labels or filenames for each ROM
266    auto_rom_labels: Vec<String>,
267    /// Callback for when the ROM changes (filename or label)
268    #[allow(clippy::type_complexity)]
269    on_rom_change: Option<Box<dyn Fn(&str) + Send + Sync>>,
270    /// Callback for first update/frame (for deferred actions)
271    #[allow(clippy::type_complexity)]
272    on_first_update: Option<Box<dyn FnOnce(&mut UxnApp<'a>) + Send + 'a>>,
273    first_update_done: bool,
274    /// The current ROM label or filename (if available)
275    current_rom_label: Option<String>,
276    /// Queue for deferred input events (for orca injection)
277    input_queue: std::collections::VecDeque<InjectEvent>,
278    /// The last ROM file path loaded (if any)
279    last_rom_path: Option<std::path::PathBuf>,
280    // --- Hot-reload support ---
281    pub reload_rx: std::sync::mpsc::Receiver<()>,
282    pub rom_path_arc: std::sync::Arc<std::sync::Mutex<Option<std::path::PathBuf>>>,
283    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
284    pub usb_controller: Option<UsbControllerHandle>,
285    #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
286    pub last_usb_event: Option<(u8, Vec<u8>)>, // (pedal_state, raw data)
287}
288
289#[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
290pub struct UsbControllerHandle {
291    pub rx: std::sync::mpsc::Receiver<varvara::controller_usb::UsbControllerMessage>,
292}
293
294impl<'a> UxnApp<'a> {
295    /// Set a callback to be called on the first update/frame (for deferred actions)
296    pub fn set_on_first_update(&mut self, f: Box<dyn FnOnce(&mut UxnApp<'a>) + Send + 'a>) {
297        self.on_first_update = Some(f);
298        self.first_update_done = false;
299    }
300    /// Set the current ROM label or filename (for title/callback)
301    pub fn set_rom_label<S: Into<String>>(&mut self, label: S) {
302        self.current_rom_label = Some(label.into());
303    }
304    #[allow(clippy::too_many_arguments)]
305    pub fn new_with_mode(
306        mut vm: Uxn<'a>,
307        mut dev: Varvara,
308        mut size: (u16, u16),
309        scale: f32,
310        event_rx: mpsc::Receiver<Event>,
311        ctx: &egui::Context,
312        window_mode: String,
313        auto_roms: Vec<varvara::RomData>,
314        auto_rom_labels: Vec<String>,
315        auto_rom_select: bool,
316        reload_rx: std::sync::mpsc::Receiver<()>,
317        rom_path_arc: std::sync::Arc<std::sync::Mutex<Option<std::path::PathBuf>>>,
318    ) -> Self {
319        if let Some(path) = rom_path_arc.lock().unwrap().as_ref() {
320            if path.exists() {
321                dev.load_sym_with_rom_path(path);
322            }
323        }
324        // Run the VM and redraw once to initialize the framebuffer
325        vm.run(&mut dev, 0x100);
326
327        dev.redraw(&mut vm);
328
329        let w = 2048_usize; //size.0);
330        let h = 2048_usize;
331        size.0 = w as u16;
332        size.1 = h as u16;
333        let image = egui::ColorImage::new([w, h], vec![egui::Color32::BLACK; w * h]);
334        let texture = ctx.load_texture("frame", image, egui::TextureOptions::NEAREST);
335        let aspect_ratio = w as f32 / h as f32;
336        let mut auto_index = 0;
337        let mut auto_timer = 0.0;
338        let mut current_rom_label = None;
339        if auto_rom_select && !auto_roms.is_empty() {
340            println!(
341                "[AUTO ROM CYCLING] Enabled. Cycling through {} ROMs every 10s.",
342                auto_roms.len()
343            );
344            // Load the first ROM immediately
345            let rom = &auto_roms[0];
346            let _ = vm.reset(&rom.rom);
347            dev.reset(&rom.rom);
348            vm.run(&mut dev, 0x100);
349            dev.redraw(&mut vm);
350
351            auto_index = 0;
352            auto_timer = 0.0;
353            // Use the provided label if available, else fallback
354            if !auto_rom_labels.is_empty() {
355                current_rom_label = Some(auto_rom_labels[0].clone());
356            } else {
357                current_rom_label = Some("ROM 1".to_string());
358            }
359        }
360        UxnApp {
361            vm,
362            dev,
363            scale,
364            size,
365            next_frame: 0.0,
366            event_rx,
367            resized: None,
368            scroll: (0.0, 0.0),
369            cursor_pos: None,
370            texture,
371            _window_mode: window_mode,
372            _aspect_ratio: aspect_ratio,
373            auto_rom_select,
374            auto_timer,
375            auto_index,
376            auto_roms,
377            on_rom_change: None,
378            current_rom_label,
379            auto_rom_labels,
380            on_first_update: None,
381            first_update_done: false,
382            input_queue: std::collections::VecDeque::new(),
383            reload_rx,
384            rom_path_arc,
385            last_rom_path: None,
386            #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
387            usb_controller: None,
388            #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
389            last_usb_event: None,
390        }
391    }
392    /// Set a callback to be called when the ROM changes (filename or label)
393    pub fn set_on_rom_change<F>(&mut self, f: F)
394    where
395        F: Fn(&str) + Send + Sync + 'static,
396    {
397        self.on_rom_change = Some(Box::new(f));
398    }
399
400    pub fn load_symbols(&mut self, path: &std::path::Path) -> Result<(), String> {
401        println!("[UxnApp] Loading symbols from: {}", path.display());
402        let _ = self.dev.load_symbols_into_self(path.to_str().unwrap());
403        Ok(())
404    }
405
406    /// Reload the ROM from the given path (for hot-reload)
407    pub fn reload_rom(&mut self, path: &std::path::Path) -> Result<(), String> {
408        println!("[UxnApp] Reloading ROM from: {}", path.display());
409        let rom = std::fs::read(path).map_err(|e| format!("Failed to read ROM: {e}"))?;
410        let _ = self.vm.reset(&rom);
411        self.dev.reset(&rom);
412        self.vm.run(&mut self.dev, 0x100);
413        self.dev.redraw(&mut self.vm);
414
415        self.load_symbols(path)?;
416
417        // Explicitly beep after reload
418        #[cfg(windows)]
419        {
420            unsafe {
421                winapi::um::winuser::MessageBeep(0xFFFFFFFF);
422            }
423        }
424        Ok(())
425    }
426
427    pub fn set_resize_callback(&mut self, f: Box<dyn FnMut(u16, u16)>) {
428        self.resized = Some(f);
429    }
430
431    /// Load a ROM from a file path, tracking the file path for symbol support
432    pub fn load_rom_with_path<P: AsRef<std::path::Path>>(&mut self, path: P) -> anyhow::Result<()> {
433        let path = path.as_ref();
434        let data = std::fs::read(path)?;
435        self.last_rom_path = Some(path.to_path_buf());
436        let _ = self.vm.reset(&data);
437        self.dev.reset(&data);
438        self.vm.run(&mut self.dev, 0x100);
439        // Try to load .sym file if ROM was loaded from a file
440        let sym_path = path.with_extension("sym");
441        if sym_path.exists() {
442            let _ = self.dev.load_symbols_into_self(sym_path.to_str().unwrap());
443        }
444        // Optionally update the ROM label
445        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
446            self.set_rom_label(name);
447        }
448        let out = self.dev.output(&self.vm);
449        out.check()?;
450        Ok(())
451    }
452
453    /// Backward-compatible load_rom for in-memory loads (no file path)
454    fn load_rom(&mut self, data: &[u8]) -> anyhow::Result<()> {
455        self.last_rom_path = None;
456        let _ = self.vm.reset(data);
457        self.dev.reset(data);
458        self.vm.run(&mut self.dev, 0x100);
459        let out = self.dev.output(&self.vm);
460        out.check()?;
461        Ok(())
462    }
463
464    // Helper to get the last ROM path if available (for symbol loading)
465    // fn get_last_rom_path(&self) -> Option<std::path::PathBuf> {
466    //     self.last_rom_path.clone()
467    // }
468
469    /// Queue a sequence of input events to be sent per frame
470    pub fn queue_input<I: IntoIterator<Item = InjectEvent>>(&mut self, input: I) {
471        self.input_queue.extend(input);
472    }
473}
474
475impl eframe::App for UxnApp<'_> {
476    fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
477        [0.0, 0.0, 0.0, 0.0]
478    }
479    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
480        // --- ROM hot-reload check ---
481        if self.reload_rx.try_recv().is_ok() {
482            let path_opt = { self.rom_path_arc.lock().unwrap().clone() };
483            if let Some(ref path) = path_opt {
484                println!(
485                    "[ROM WATCHER] Detected ROM file change, reloading: {}",
486                    path.display()
487                );
488                if let Err(e) = self.reload_rom(path) {
489                    eprintln!("[ROM WATCHER] Failed to reload ROM in app: {e}");
490                } else {
491                    println!("[ROM WATCHER] ROM reloaded in app successfully.");
492                }
493            }
494        }
495        // --- First update callback for deferred actions (e.g., orca file injection) ---
496        if !self.first_update_done {
497            if let Some(cb) = self.on_first_update.take() {
498                cb(self);
499            }
500            self.first_update_done = true;
501        }
502        // --- Ctrl+C and Ctrl+R event handling ---
503        let orca_dir = r"C:\w\music\Orca-c\examples\basics";
504        let files: Vec<std::path::PathBuf> = std::fs::read_dir(orca_dir)
505            .map(|read_dir| {
506                read_dir
507                    .filter_map(|entry| {
508                        entry.ok().and_then(|e| {
509                            let path = e.path();
510                            if path.extension().and_then(|ext| ext.to_str()) == Some("orca") {
511                                Some(path)
512                            } else {
513                                None
514                            }
515                        })
516                    })
517                    .collect::<Vec<_>>()
518            })
519            .unwrap_or_else(|_| Vec::new());
520        static mut LAST_CTRL_R: Option<std::time::Instant> = None;
521        let mut exit_requested = false;
522        for event in ctx.input(|i| i.events.clone()) {
523            if let egui::Event::Key { key, pressed, .. } = event {
524                if key == egui::Key::C && ctx.input(|i| i.modifiers.ctrl) && pressed {
525                    exit_requested = true;
526                }
527                if key == egui::Key::R && ctx.input(|i| i.modifiers.ctrl) && pressed {
528                    let now = std::time::Instant::now();
529                    let last = unsafe { LAST_CTRL_R };
530                    let allow = match last {
531                        Some(t) => now.duration_since(t).as_millis() > 500,
532                        None => true,
533                    };
534                    if allow {
535                        unsafe {
536                            LAST_CTRL_R = Some(now);
537                        }
538                        if !files.is_empty() {
539                            #[cfg(target_arch = "wasm32")]
540                            {
541                                // On wasm, just cycle sequentially through the files
542                                static mut LAST_IDX: usize = 0;
543                                let idx = unsafe {
544                                    let i = LAST_IDX;
545                                    LAST_IDX = (LAST_IDX + 1) % files.len();
546                                    i
547                                };
548                                let random_file = &files[idx];
549                                let queue = build_orca_inject_queue(random_file.to_str().unwrap());
550                                self.queue_input(queue);
551                            }
552                            #[cfg(not(target_arch = "wasm32"))]
553                            {
554                                // On native, pick a random file
555                                if let Ok(idx) =
556                                    get_random_u128().map(|v| (v as usize) % files.len())
557                                {
558                                    let idx: usize = idx;
559                                    let random_file = &files[idx];
560                                    let queue =
561                                        build_orca_inject_queue(random_file.to_str().unwrap());
562                                    self.queue_input(queue);
563                                }
564                            }
565                        }
566                    }
567                }
568            }
569        }
570
571        /// Returns a random u128 using getrandom
572        #[cfg(not(target_arch = "wasm32"))]
573        pub fn get_random_u128() -> Result<u128, getrandom::Error> {
574            let mut buf = [0u8; 16];
575            getrandom::fill(&mut buf)?;
576            Ok(u128::from_ne_bytes(buf))
577        }
578
579        #[cfg(target_arch = "wasm32")]
580        #[allow(dead_code)]
581        pub fn get_random_u128() -> Result<u128, Result<u128, ()>> {
582            // WASM: fallback to a deterministic value or use JS random if needed
583            Ok(42)
584        }
585        if exit_requested {
586            std::process::exit(0);
587        }
588        // --- Per-frame input injection ---
589        if let Some(event) = self.input_queue.pop_front() {
590            match event {
591                InjectEvent::Char(c) => self.dev.char(&mut self.vm, c),
592                InjectEvent::KeyPress(k) => self.dev.pressed(&mut self.vm, k, false),
593                InjectEvent::KeyRelease(k) => self.dev.released(&mut self.vm, k),
594                InjectEvent::Sleep(ms) => {
595                    std::thread::sleep(std::time::Duration::from_millis(ms));
596                }
597                InjectEvent::Chord(keys) => {
598                    for k in &keys {
599                        self.dev.pressed(&mut self.vm, *k, false);
600                    }
601                }
602            }
603        }
604        // --- AUTO ROM CYCLING: Switch between ROMs every 10 seconds if enabled ---
605        if self.auto_rom_select && !self.auto_roms.is_empty() {
606            let dt = ctx.input(|i| i.stable_dt) as f64;
607            log::debug!(
608                "[AUTO ROM CYCLING] auto_timer: {:.3}, dt: {:.3}, auto_index: {}, roms: {}",
609                self.auto_timer,
610                dt,
611                self.auto_index,
612                self.auto_roms.len()
613            );
614            self.auto_timer += dt;
615            if self.auto_timer == 0.0 && self.auto_index == 0 {
616                log::debug!("[AUTO ROM CYCLING] First ROM already loaded.");
617                // Already loaded first ROM in new_with_mode
618            } else if self.auto_timer > 10.0 {
619                log::debug!(
620                    "[AUTO ROM CYCLING] Switching ROM: {} -> {}",
621                    self.auto_index,
622                    (self.auto_index + 1) % self.auto_roms.len()
623                );
624                self.auto_timer = 0.0;
625                self.auto_index = (self.auto_index + 1) % self.auto_roms.len();
626                let rom = self.auto_roms[self.auto_index].clone();
627                // Use the correct label for the new ROM
628                let label = if self.auto_index < self.auto_rom_labels.len() {
629                    self.auto_rom_labels[self.auto_index].clone()
630                } else {
631                    format!("ROM {}", self.auto_index + 1)
632                };
633                self.set_rom_label(label);
634                let _ = self.load_rom(&rom.rom);
635                if rom.sym.is_some() {
636                    // Load symbols if available
637                    if let Some(sym) = &rom.sym {
638                        let _ = self.dev.load_symbols_from_vec(sym);
639                    }
640                }
641            }
642        }
643        while let Ok(e) = self.event_rx.try_recv() {
644            match e {
645                Event::LoadRom(data) => {
646                    if let Err(e) = self.load_rom(&data) {
647                        error!("could not load rom: {e:?}");
648                    }
649                }
650                Event::SetMuted(m) => {
651                    self.dev.audio_set_muted(m);
652                }
653                Event::Console(b) => {
654                    self.dev.console(&mut self.vm, b);
655                }
656            }
657        }
658        // if let Some(ref mut varvara_controller) =
659        //     self.dev
660        //         .controller
661        //         .as_any()
662        //         .downcast_mut::<varvara::controller::Controller>()
663        // {
664        //     // Example pedal event mapping (replace with your actual pedal state logic)
665        //     // Assume pedal_state is a u8 bitmask from your input source
666        //     let pedal_state: u8 = 0; // TODO: get real pedal state
667        //     static mut PREV_PEDAL: u8 = 0;
668        //     let _prev;
669        //     unsafe {
670        //         _prev = PREV_PEDAL;
671        //         PREV_PEDAL = pedal_state;
672        //     }
673        //     // Use canonical helper from varvara::controller
674        //     varvara::controller::inject_pedal_keys(
675        //         varvara_controller,
676        //         &mut self.vm,
677        //         _prev,
678        //         pedal_state,
679        //     );
680        // }
681        #[cfg(target_arch = "wasm32")]
682        let events = Vec::new();
683        #[cfg(not(target_arch = "wasm32"))]
684        let mut events = Vec::new();
685        #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
686        {
687            // Only borrow self.dev.controller mutably once in this block
688            let mut last_pedal = None;
689            if let Some(controller_usb) = self
690                .dev
691                .controller
692                .as_any()
693                .downcast_mut::<varvara::controller_usb::ControllerUsb>()
694            {
695                events = varvara::controller_usb::ControllerPollEvents::poll_usb_events(
696                    controller_usb,
697                    &mut self.vm,
698                );
699                last_pedal = controller_usb.last_pedal;
700            }
701            // Now, after the mutable borrow is done, update last_usb_event, etc.
702            static mut PREV_PEDAL: u8 = 0;
703            if let Some(pedal) = last_pedal {
704                // Update last_usb_event for debug panel
705                self.last_usb_event = Some((pedal, vec![]));
706                let _prev;
707                unsafe {
708                    _prev = PREV_PEDAL;
709                    PREV_PEDAL = pedal;
710                }
711                // Use shared controller pedal key injection logic
712                // Controller::inject_pedal_keys(&mut self.vm, prev, pedal);
713            }
714        }
715        let pedal_data: Vec<u8> = events
716            .iter()
717            .flat_map(|e: &varvara::Event| e.data.iter().map(|b| b.value))
718            .collect();
719        for value in pedal_data {
720            self.dev
721                .pressed(&mut self.vm, varvara::Key::Char(value), true);
722            self.dev.char(&mut self.vm, value);
723        }
724
725        ctx.request_repaint();
726        ctx.input(|i| {
727            while i.time >= self.next_frame {
728                self.next_frame += 0.0166667;
729                self.dev.redraw(&mut self.vm);
730            }
731            if i.raw.dropped_files.len() == 1 {
732                let target = &i.raw.dropped_files[0];
733                let r = if let Some(path) = &target.path {
734                    info!("loading ROM from {path:?}");
735                    self.load_rom_with_path(path)
736                } else if let Some(data) = &target.bytes {
737                    self.load_rom(data)
738                } else {
739                    Ok(())
740                };
741                if let Err(e) = r {
742                    error!("could not load ROM: {e:?}");
743                }
744            }
745            let shift_held = i.modifiers.shift;
746            for e in i.events.iter() {
747                match e {
748                    egui::Event::Text(s) => {
749                        for c in s.bytes() {
750                            self.dev.char(&mut self.vm, c);
751                        }
752                    }
753                    egui::Event::Key {
754                        key,
755                        pressed,
756                        repeat,
757                        ..
758                    } => {
759                        if let Some(k) = decode_key(*key, shift_held) {
760                            if *pressed {
761                                self.dev.pressed(&mut self.vm, k, *repeat);
762                            } else {
763                                self.dev.released(&mut self.vm, k);
764                            }
765                        }
766                    }
767                    _ => (),
768                }
769            }
770            for (b, k) in [
771                (i.modifiers.ctrl, Key::Ctrl),
772                (i.modifiers.alt, Key::Alt),
773                (i.modifiers.shift, Key::Shift),
774            ] {
775                if b {
776                    self.dev.pressed(&mut self.vm, k, false)
777                } else {
778                    self.dev.released(&mut self.vm, k)
779                }
780            }
781            let ptr = &i.pointer;
782            if let Some(p) = ptr.latest_pos() {
783                self.cursor_pos = Some((p.x / self.scale, p.y / self.scale));
784            }
785            let buttons = [
786                egui::PointerButton::Primary,
787                egui::PointerButton::Middle,
788                egui::PointerButton::Secondary,
789            ]
790            .into_iter()
791            .enumerate()
792            .map(|(i, b)| (ptr.button_down(b) as u8) << i)
793            .fold(0, |a, b| a | b);
794            let m = MouseState {
795                pos: self.cursor_pos.unwrap_or((0.0, 0.0)),
796                scroll: std::mem::take(&mut self.scroll),
797                buttons,
798            };
799            self.dev.mouse(&mut self.vm, m);
800            let m = TrackerState {
801                pos: self
802                    .cursor_pos
803                    .map(|(x, y)| (x + 16.0, y + 16.0))
804                    .unwrap_or((16.0, 16.0)),
805                scroll: std::mem::take(&mut self.scroll),
806                buttons,
807            };
808            self.dev.tracker(&mut self.vm, m);
809            i.time
810        });
811        self.dev.audio(&mut self.vm);
812        let out = self.dev.output(&self.vm);
813        if out.hide_mouse {
814            ctx.set_cursor_icon(egui::CursorIcon::None);
815        }
816        if self.size != out.size {
817            // Get current window size in logical points
818            let _current_window_size = ctx.input(|i| {
819                i.viewport()
820                    .inner_rect
821                    .map_or(egui::Vec2::ZERO, |rect| rect.size())
822            });
823            let _new_size = egui::Vec2::new(out.size.0 as f32, out.size.1 as f32) * self.scale;
824            // let should_resize = new_size.x > current_window_size.x || new_size.y > current_window_size.y
825            //     || new_size.x < current_window_size.x || new_size.y < current_window_size.y;
826            // // Only resize if the new frame is larger, or if it's smaller than the window
827            // if should_resize {
828            // Only resize if the new frame is larger than the current window
829            // if new_size.x > current_window_size.x || new_size.y > current_window_size.y {
830            //     info!("resizing window to {:?}", out.size);
831            //     self.size = out.size;
832            //     let mut size = new_size;
833            //     // Enforce proportional resizing if needed
834            //     if self.window_mode == "proportional" {
835            //         let aspect = self.aspect_ratio;
836            //         let w = size.x;
837            //         let h = size.y;
838            //         let (new_w, new_h) = if w / h > aspect {
839            //             (h * aspect, h)
840            //         } else {
841            //             (w, w / aspect)
842            //         };
843            //         size = egui::Vec2::new(new_w, new_h);
844            //     }
845            //     ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
846            //     if let Some(f) = self.resized.as_mut() {
847            //         f(out.size.0, out.size.1);
848            //     }
849            // }
850        }
851        let w = out.size.0 as usize;
852        let h = out.size.1 as usize;
853        let mut image = egui::ColorImage::new([w, h], vec![egui::Color32::BLACK; w * h]);
854        for (i, o) in out.frame.chunks(4).zip(image.pixels.iter_mut()) {
855            *o = egui::Color32::from_rgba_unmultiplied(i[2], i[1], i[0], i[3]);
856        }
857        self.texture.set(image, egui::TextureOptions::NEAREST);
858        egui::CentralPanel::default()
859            .frame(egui::Frame::default().fill(egui::Color32::TRANSPARENT))
860            .show(ctx, |ui| {
861                let available = ui.available_size();
862                let frame_size = egui::Vec2::new(
863                    out.size.0 as f32 * self.scale,
864                    out.size.1 as f32 * self.scale,
865                );
866                let offset = egui::Vec2::new(
867                    (available.x - frame_size.x) * 0.5,
868                    (available.y - frame_size.y) * 0.5,
869                );
870                let top_left = ui.min_rect().min + offset;
871                let mut mesh = egui::Mesh::with_texture(self.texture.id());
872                mesh.add_rect_with_uv(
873                    egui::Rect {
874                        min: top_left,
875                        max: top_left + frame_size,
876                    },
877                    egui::Rect {
878                        min: egui::Pos2::new(0.0, 0.0),
879                        max: egui::Pos2::new(1.0, 1.0),
880                    },
881                    egui::Color32::WHITE,
882                );
883                ui.painter().add(egui::Shape::mesh(mesh));
884                // --- USB Debug Panel ---
885                #[cfg(all(feature = "uses_usb", not(target_arch = "wasm32")))]
886                egui::CollapsingHeader::new("USB Pedal Debug")
887                    .default_open(true)
888                    .show(ui, |ui| {
889                        if let Some((pedal_state, ref data)) = self.last_usb_event {
890                            ui.label(format!(
891                                "[USB] Last pedal event: state=0x{pedal_state:02X}, data={data:?}"
892                            ));
893                        } else {
894                            ui.label("[USB] No pedal events received yet.");
895                        }
896                    });
897                // Show auto ROM cycling info
898                if self.auto_rom_select && !self.auto_roms.is_empty() {
899                    ui.label(format!(
900                        "[AUTO ROM CYCLING] Switching ROM every 10s. ROMs loaded: {}",
901                        self.auto_roms.len()
902                    ));
903                }
904            });
905        out.check().ok();
906    }
907}
908
909pub fn decode_key(k: egui::Key, shift: bool) -> Option<Key> {
910    match (k, shift) {
911        (egui::Key::ArrowUp, _) => Some(Key::Up),
912        (egui::Key::ArrowDown, _) => Some(Key::Down),
913        (egui::Key::ArrowLeft, _) => Some(Key::Left),
914        (egui::Key::ArrowRight, _) => Some(Key::Right),
915        (egui::Key::Home, _) => Some(Key::Home),
916        (egui::Key::Num0, false) => Some(Key::Char(b'0')),
917        (egui::Key::Num0, true) => Some(Key::Char(b')')),
918        (egui::Key::Num1, false) => Some(Key::Char(b'1')),
919        (egui::Key::Num1, true) => Some(Key::Char(b'!')),
920        (egui::Key::Num2, false) => Some(Key::Char(b'2')),
921        (egui::Key::Num2, true) => Some(Key::Char(b'@')),
922        (egui::Key::Num3, false) => Some(Key::Char(b'3')),
923        (egui::Key::Num3, true) => Some(Key::Char(b'#')),
924        (egui::Key::Num4, false) => Some(Key::Char(b'4')),
925        (egui::Key::Num4, true) => Some(Key::Char(b'$')),
926        (egui::Key::Num5, false) => Some(Key::Char(b'5')),
927        (egui::Key::Num5, true) => Some(Key::Char(b'%')),
928        (egui::Key::Num6, false) => Some(Key::Char(b'6')),
929        (egui::Key::Num6, true) => Some(Key::Char(b'^')),
930        (egui::Key::Num7, false) => Some(Key::Char(b'7')),
931        (egui::Key::Num7, true) => Some(Key::Char(b'&')),
932        (egui::Key::Num8, false) => Some(Key::Char(b'8')),
933        (egui::Key::Num8, true) => Some(Key::Char(b'*')),
934        (egui::Key::Num9, false) => Some(Key::Char(b'9')),
935        (egui::Key::Num9, true) => Some(Key::Char(b'(')),
936        (egui::Key::A, false) => Some(Key::Char(b'a')),
937        (egui::Key::A, true) => Some(Key::Char(b'A')),
938        (egui::Key::B, false) => Some(Key::Char(b'b')),
939        (egui::Key::B, true) => Some(Key::Char(b'B')),
940        (egui::Key::C, false) => Some(Key::Char(b'c')),
941        (egui::Key::C, true) => Some(Key::Char(b'C')),
942        (egui::Key::D, false) => Some(Key::Char(b'd')),
943        (egui::Key::D, true) => Some(Key::Char(b'D')),
944        (egui::Key::E, false) => Some(Key::Char(b'e')),
945        (egui::Key::E, true) => Some(Key::Char(b'E')),
946        (egui::Key::F, false) => Some(Key::Char(b'f')),
947        (egui::Key::F, true) => Some(Key::Char(b'F')),
948        (egui::Key::G, false) => Some(Key::Char(b'g')),
949        (egui::Key::G, true) => Some(Key::Char(b'G')),
950        (egui::Key::H, false) => Some(Key::Char(b'h')),
951        (egui::Key::H, true) => Some(Key::Char(b'H')),
952        (egui::Key::I, false) => Some(Key::Char(b'i')),
953        (egui::Key::I, true) => Some(Key::Char(b'I')),
954        (egui::Key::J, false) => Some(Key::Char(b'j')),
955        (egui::Key::J, true) => Some(Key::Char(b'J')),
956        (egui::Key::K, false) => Some(Key::Char(b'k')),
957        (egui::Key::K, true) => Some(Key::Char(b'K')),
958        (egui::Key::L, false) => Some(Key::Char(b'l')),
959        (egui::Key::L, true) => Some(Key::Char(b'L')),
960        (egui::Key::M, false) => Some(Key::Char(b'm')),
961        (egui::Key::M, true) => Some(Key::Char(b'M')),
962        (egui::Key::N, false) => Some(Key::Char(b'n')),
963        (egui::Key::N, true) => Some(Key::Char(b'N')),
964        (egui::Key::O, false) => Some(Key::Char(b'o')),
965        (egui::Key::O, true) => Some(Key::Char(b'O')),
966        (egui::Key::P, false) => Some(Key::Char(b'p')),
967        (egui::Key::P, true) => Some(Key::Char(b'P')),
968        (egui::Key::Q, false) => Some(Key::Char(b'q')),
969        (egui::Key::Q, true) => Some(Key::Char(b'Q')),
970        (egui::Key::R, false) => Some(Key::Char(b'r')),
971        (egui::Key::R, true) => Some(Key::Char(b'R')),
972        (egui::Key::S, false) => Some(Key::Char(b's')),
973        (egui::Key::S, true) => Some(Key::Char(b'S')),
974        (egui::Key::T, false) => Some(Key::Char(b't')),
975        (egui::Key::T, true) => Some(Key::Char(b'T')),
976        (egui::Key::U, false) => Some(Key::Char(b'u')),
977        (egui::Key::U, true) => Some(Key::Char(b'U')),
978        (egui::Key::V, false) => Some(Key::Char(b'v')),
979        (egui::Key::V, true) => Some(Key::Char(b'V')),
980        (egui::Key::W, false) => Some(Key::Char(b'w')),
981        (egui::Key::W, true) => Some(Key::Char(b'W')),
982        (egui::Key::X, false) => Some(Key::Char(b'x')),
983        (egui::Key::X, true) => Some(Key::Char(b'X')),
984        (egui::Key::Y, false) => Some(Key::Char(b'y')),
985        (egui::Key::Y, true) => Some(Key::Char(b'Y')),
986        (egui::Key::Z, false) => Some(Key::Char(b'z')),
987        (egui::Key::Z, true) => Some(Key::Char(b'Z')),
988        (egui::Key::Backtick, false) => Some(Key::Char(b'`')),
989        (egui::Key::Backtick, true) => Some(Key::Char(b'~')),
990        (egui::Key::Backslash, _) => Some(Key::Char(b'\\')),
991        (egui::Key::Pipe, _) => Some(Key::Char(b'|')),
992        (egui::Key::Comma, false) => Some(Key::Char(b',')),
993        (egui::Key::Comma, true) => Some(Key::Char(b'<')),
994        (egui::Key::Equals, _) => Some(Key::Char(b'=')),
995        (egui::Key::Plus, _) => Some(Key::Char(b'+')),
996        (egui::Key::OpenBracket, false) => Some(Key::Char(b'[')),
997        (egui::Key::OpenBracket, true) => Some(Key::Char(b'{')),
998        (egui::Key::Minus, false) => Some(Key::Char(b'-')),
999        (egui::Key::Minus, true) => Some(Key::Char(b'_')),
1000        (egui::Key::Period, false) => Some(Key::Char(b'.')),
1001        (egui::Key::Period, true) => Some(Key::Char(b'>')),
1002        (egui::Key::CloseBracket, false) => Some(Key::Char(b']')),
1003        (egui::Key::CloseBracket, true) => Some(Key::Char(b'}')),
1004        (egui::Key::Semicolon, _) => Some(Key::Char(b';')),
1005        (egui::Key::Colon, _) => Some(Key::Char(b':')),
1006        (egui::Key::Slash, _) => Some(Key::Char(b'/')),
1007        (egui::Key::Questionmark, _) => Some(Key::Char(b'?')),
1008        (egui::Key::Space, _) => Some(Key::Char(b' ')),
1009        (egui::Key::Tab, _) => Some(Key::Char(b'\t')),
1010        (egui::Key::Enter, _) => Some(Key::Char(b'\r')),
1011        (egui::Key::Backspace, _) => Some(Key::Char(0x08)),
1012        _ => None,
1013    }
1014}
1015
1016/// Stub for audio_setup on wasm32
1017// #[cfg(target_arch = "wasm32")]
1018// pub fn audio_setup(
1019//     _data: [Arc<Mutex<varvara::StreamData>>; 4],
1020// ) -> Option<()> {
1021//     None
1022// }
1023/// Shared audio setup for Varvara audio streams
1024// #[cfg(not(target_arch = "wasm32"))]
1025pub fn audio_setup(
1026    data: [Arc<Mutex<varvara::StreamData>>; 4],
1027) -> Option<(cpal::Device, [cpal::Stream; 4])> {
1028    use cpal::traits::{DeviceTrait, HostTrait};
1029
1030    let host = cpal::default_host();
1031    let device = host
1032        .default_output_device()
1033        .expect("no output device available");
1034    let supported_configs_range = device
1035        .supported_output_configs()
1036        .expect("error while querying configs")
1037        .collect::<Vec<_>>();
1038
1039    let mut supported_config = None;
1040    let mut used_rate = 0;
1041    for &rate in &[48000, 44100] {
1042        supported_config = supported_configs_range
1043            .iter()
1044            .filter(|c| usize::from(c.channels()) == varvara::AUDIO_CHANNELS)
1045            .filter(|c| c.sample_format() == cpal::SampleFormat::F32)
1046            .find_map(|c| c.try_with_sample_rate(cpal::SampleRate(rate)));
1047        if supported_config.is_some() {
1048            used_rate = rate;
1049            break;
1050        }
1051    }
1052    let supported_config = match supported_config {
1053        Some(cfg) => cfg,
1054        None => {
1055            log::error!(
1056                "could not find supported audio config ({} channels, 48000 or 44100 Hz, f32)",
1057                varvara::AUDIO_CHANNELS
1058            );
1059            log::error!("available configs:");
1060            for c in &supported_configs_range {
1061                if c.min_sample_rate() == c.max_sample_rate() {
1062                    log::error!(
1063                        "  channels: {}, sample_rate: {} Hz, {}",
1064                        c.channels(),
1065                        c.min_sample_rate().0,
1066                        c.sample_format(),
1067                    );
1068                } else {
1069                    log::error!(
1070                        "  channels: {}, sample_rate: {} - {} Hz, {}",
1071                        c.channels(),
1072                        c.min_sample_rate().0,
1073                        c.max_sample_rate().0,
1074                        c.sample_format(),
1075                    );
1076                }
1077            }
1078            return None;
1079        }
1080    };
1081    // Set the sample rate in the audio engine
1082    varvara::set_sample_rate(used_rate);
1083    let config = supported_config.config();
1084
1085    let streams = data.map(|d| {
1086        let stream = device
1087            .build_output_stream(
1088                &config,
1089                move |data: &mut [f32], _opt: &cpal::OutputCallbackInfo| {
1090                    d.lock().unwrap().next(data);
1091                },
1092                move |err| {
1093                    panic!("{err}");
1094                },
1095                None,
1096            )
1097            .expect("could not build stream");
1098        cpal::traits::StreamTrait::play(&stream).unwrap();
1099        stream
1100    });
1101    Some((device, streams))
1102}
1103
1104/// Inject key events from egui input into the VM, handling press/release and modifiers
1105pub fn inject_key_events(
1106    vm: &mut ::uxn::Uxn,
1107    dev: &mut varvara::Varvara,
1108    events: &[egui::Event],
1109    modifiers: &egui::Modifiers,
1110    _repeat: bool,
1111) {
1112    use varvara::Key;
1113    let shift_held = modifiers.shift;
1114    for e in events {
1115        match e {
1116            egui::Event::Text(s) => {
1117                const RAW_CHARS: [u8; 16] = [
1118                    b'"', b'\'', b'{', b'}', b'_', b')', b'(', b'*', b'&', b'^', b'%', b'$', b'#',
1119                    b'@', b'!', b'~',
1120                ];
1121                for c in s.bytes() {
1122                    if RAW_CHARS.contains(&c) {
1123                        dev.char(vm, c);
1124                    }
1125                }
1126            }
1127            egui::Event::Key {
1128                key,
1129                pressed,
1130                repeat,
1131                ..
1132            } => {
1133                if let Some(k) = decode_key(*key, shift_held) {
1134                    if *pressed {
1135                        dev.pressed(vm, k, *repeat);
1136                    } else {
1137                        dev.released(vm, k);
1138                    }
1139                }
1140            }
1141            egui::Event::MouseWheel { delta, .. } => {
1142                dev.mouse(
1143                    vm,
1144                    varvara::MouseState {
1145                        pos: (0.0, 0.0),
1146                        scroll: (delta.x, -delta.y),
1147                        buttons: 0,
1148                    },
1149                );
1150            }
1151            _ => (),
1152        }
1153    }
1154    for (b, k) in [
1155        (modifiers.ctrl, Key::Ctrl),
1156        (modifiers.alt, Key::Alt),
1157        (modifiers.shift, Key::Shift),
1158    ] {
1159        if b {
1160            dev.pressed(vm, k, false)
1161        } else {
1162            dev.released(vm, k)
1163        }
1164    }
1165}