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