Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ ultra64 = ["dep:shared_memory", "dep:raw_sync"]
clap = { version = "4", features = ["derive"] }
log = "0.4"
env_logger = "0.10"
winit = "0.29"
glutin = "0.31"
glutin-winit = "0.4"
winit = "0.30"
glutin = "0.32"
glutin-winit = "0.5"
glow = "0.13"
raw-window-handle = "0.5"
raw-window-handle = "0.6"
rtrb = "0.3"
socket2 = { version = "0.5", features = ["all"] }
crossbeam-utils = "0.8"
Expand Down
6 changes: 3 additions & 3 deletions iris-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ premiere = ["iris/lightning", "iris/idle-pause"]
# ultra64 GIO device + POSIX shm bridge are only created when the user enables
# the board on the General tab. The toggle itself is hidden in App Store builds.
iris = { path = "..", features = ["chd", "camera", "jit", "rex-jit", "ultra64"] }
eframe = { version = "0.29", default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] }
egui = "0.29"
eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] }
egui = "0.35"
crossbeam-channel = "0.5"
parking_lot = "0.12"
# Match iris's winit version — Ps2Controller::push_kb takes
# winit::keyboard::KeyCode.
winit = "0.29"
winit = "0.30"
png = "0.17"
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] }
serde = { version = "1", features = ["derive"] }
Expand Down
12 changes: 6 additions & 6 deletions iris-gui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,25 +972,25 @@ fn show_network(
.clicked()
{
add = Some(PortForwardConfig { proto: ForwardProto::Tcp, host_port: 2323, guest_port: 23, bind: ForwardBind::Localhost });
ui.close_menu();
ui.close();
}
if ui.add_enabled(!has_port(21), egui::Button::new("FTP (host 2121 to guest 21)"))
.on_hover_text("Reach the guest's FTP server. Forwards the control port; file transfer also needs the data channel (see docs).")
.clicked()
{
add = Some(PortForwardConfig { proto: ForwardProto::Tcp, host_port: 2121, guest_port: 21, bind: ForwardBind::Localhost });
ui.close_menu();
ui.close();
}
if ui.add_enabled(!has_port(177), egui::Button::new("XDMCP (host 11177 to guest 177, UDP)"))
.on_hover_text("Remote X login: an X server XDMCP-queries the guest's xdm. Binds all interfaces (LAN X servers OK). Stock X servers use UDP 177 — redirect 177→11177 on the X-server host, or use a chooser that accepts host:port.")
.clicked()
{
add = Some(PortForwardConfig { proto: ForwardProto::Udp, host_port: 11177, guest_port: 177, bind: ForwardBind::Any });
ui.close_menu();
ui.close();
}
if ui.button("Custom (empty row)").clicked() {
add = Some(PortForwardConfig { proto: ForwardProto::Tcp, host_port: 0, guest_port: 0, bind: ForwardBind::Localhost });
ui.close_menu();
ui.close();
}
});
if let Some(pf) = add { cfg.port_forward.push(pf); out.changed = true; out.forwards_changed = true; }
Expand Down Expand Up @@ -1594,7 +1594,7 @@ struct PathEdit {
/// A TextEdit + 📁 Browse button that updates `value` in place. See [`PathEdit`].
fn path_row(
ui: &mut Ui,
id: impl std::hash::Hash,
id: impl std::hash::Hash + std::fmt::Debug, // egui 0.35 push_id needs AsIdSalt (Hash + Debug)
value: &mut String,
mode: Pick,
filters: &[(&str, &[&str])],
Expand Down Expand Up @@ -1649,7 +1649,7 @@ fn path_row(
/// the user can clear by emptying the text.
fn path_row_opt(
ui: &mut Ui,
id: impl std::hash::Hash,
id: impl std::hash::Hash + std::fmt::Debug,
value: &mut Option<String>,
mode: Pick,
filters: &[(&str, &[&str])],
Expand Down
144 changes: 76 additions & 68 deletions iris-gui/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
//! and lock it in place (`CursorGrab::Locked`); raw motion then arrives as
//! `egui::Event::MouseMoved` deltas (eframe forwards `DeviceEvent::MouseMotion`
//! regardless of grab), which we feed straight to the guest. Only the guest's
//! own pointer is visible, so there is nothing to misalign. **Ctrl+Alt+Esc
//! releases** (Alt is the Option key on macOS); a chord rather than bare Esc
//! so plain Esc still reaches the guest.
//! own pointer is visible, so there is nothing to misalign. `RELEASE_CHORD` releases.
//!
//! While captured we also forward keyboard input to the guest; while *not*
//! captured we forward nothing, so menu clicks and typing into the config
Expand All @@ -24,12 +22,15 @@
//! hit-testing already routed clicks on open menus / popups to those widgets,
//! so navigating menus over the display never gets "eaten" into a capture.

use egui::{CursorGrab, Event, Key, Modifiers, MouseWheelUnit, PointerButton, ViewportCommand};
use egui::{CursorGrab, Event, Key, MouseWheelUnit, PointerButton, ViewportCommand};
use iris::ps2::Ps2Controller;
use winit::keyboard::KeyCode;

pub struct InputState {
last_mods: Modifiers,
/// Modifier keys currently held down in the guest, so they can be lifted on release.
held_mods: Vec<KeyCode>,
/// Set when another key is pressed while the release chord is held, so the chord sends its modifiers to the guest instead of releasing capture.
chord_consumed: bool,
last_buttons: u8, // bit0=L, bit1=R, bit2=M, bit3=B4, bit4=B5
/// True while the host cursor is grabbed and input is routed to the guest.
pub captured: bool,
Expand All @@ -42,10 +43,27 @@ pub struct InputState {

impl Default for InputState {
fn default() -> Self {
Self { last_mods: Modifiers::NONE, last_buttons: 0, captured: false, unfocused_since: None }
Self { held_mods: Vec::new(), chord_consumed: false, last_buttons: 0, captured: false, unfocused_since: None }
}
}

/// Hold these two together (and press nothing else) to release capture.
#[cfg(target_os = "macos")]
const RELEASE_CHORD: [KeyCode; 2] = [KeyCode::AltLeft, KeyCode::SuperLeft];
#[cfg(not(target_os = "macos"))]
const RELEASE_CHORD: [KeyCode; 2] = [KeyCode::ControlLeft, KeyCode::AltLeft];

/// Human-readable name of `RELEASE_CHORD`, for the capture hint in the control column.
#[cfg(target_os = "macos")]
pub const RELEASE_HINT: &str = "Left Option+Cmd";
#[cfg(not(target_os = "macos"))]
pub const RELEASE_HINT: &str = "Left Ctrl+Alt";

fn is_modifier(kc: KeyCode) -> bool {
matches!(kc, KeyCode::ShiftLeft | KeyCode::ShiftRight | KeyCode::ControlLeft | KeyCode::ControlRight
| KeyCode::AltLeft | KeyCode::AltRight | KeyCode::SuperLeft | KeyCode::SuperRight)
}

/// How long the window must stay unfocused before a capture is released. Long
/// enough to ride out macOS's transient focus flicker, short enough that a real
/// alt-tab frees the cursor promptly.
Expand All @@ -62,7 +80,6 @@ pub fn pump(ctx: &egui::Context, fb_clicked: bool, ps2: &Ps2Controller, state: &
let mut dy = 0.0f32;
let mut dz = 0.0f32;
let mut buttons = state.last_buttons;
let mut mods = state.last_mods;
let mut keys: Vec<(KeyCode, bool)> = Vec::new();
let mut f11_to_guest = false;

Expand All @@ -76,31 +93,23 @@ pub fn pump(ctx: &egui::Context, fb_clicked: bool, ps2: &Ps2Controller, state: &
return;
}

// Captured. Ctrl+Alt+Esc (Alt == Option on macOS) is the release chord;
// a real focus loss (alt-tab) also releases, but only after a grace
// period (decided below) so a one-frame `focused=false` flicker doesn't
// drop capture while you're typing. A chord rather than bare Esc lets
// plain Esc reach the guest. Keep reading events even on a flicker frame
// so typing keeps flowing to the guest.
// Captured. RELEASE_CHORD releases (see below); Ctrl+Alt+Esc still works as an explicit fallback, and a real focus loss (alt-tab) releases after a grace period so a one-frame flicker doesn't drop capture mid-typing.
esc_chord = i.key_pressed(Key::Escape) && i.modifiers.ctrl && i.modifiers.alt;
focused = i.focused;

mods = i.modifiers;

for ev in &i.events {
match ev {
// Raw relative motion (eframe → DeviceEvent::MouseMotion).
Event::MouseMoved(d) => { dx += d.x; dy += d.y; }
Event::Key { key, pressed, repeat, .. } => {
if *key == Key::F11 {
// Plain F11 is the GUI's fullscreen toggle and is never
// forwarded. Ctrl+Alt+F11 is the escape hatch that delivers
// a real F11 to IRIX — recorded here on the press edge and
// sent (as a bare F11) after the modifier diff below.
// Prefer the physical position: `key` is already layout-translated by the host, and the guest applies its own `keybd=` layout on top, which mangles every non-US layout.
Event::Key { key, physical_key, pressed, repeat, .. } => {
let k = physical_key.unwrap_or(*key);
if k == Key::F11 {
// Plain F11 is the GUI's fullscreen toggle; Ctrl+Alt+F11 is the escape hatch that sends a bare F11 to IRIX.
if *pressed && !*repeat && i.modifiers.ctrl && i.modifiers.alt {
f11_to_guest = true;
}
} else if let Some(kc) = map_key(*key) {
} else if let Some(kc) = map_key(k) {
keys.push((kc, *pressed));
}
}
Expand Down Expand Up @@ -164,39 +173,38 @@ pub fn pump(ctx: &egui::Context, fb_clicked: bool, ps2: &Ps2Controller, state: &
return;
}

// ---- modifiers: diff previous → current, synth press/release. ----
let m = mods;
if m.shift && !state.last_mods.shift { ps2.push_kb(KeyCode::ShiftLeft, true); }
if !m.shift && state.last_mods.shift { ps2.push_kb(KeyCode::ShiftLeft, false); }
if m.ctrl && !state.last_mods.ctrl { ps2.push_kb(KeyCode::ControlLeft, true); }
if !m.ctrl && state.last_mods.ctrl { ps2.push_kb(KeyCode::ControlLeft, false); }
if m.alt && !state.last_mods.alt { ps2.push_kb(KeyCode::AltLeft, true); }
if !m.alt && state.last_mods.alt { ps2.push_kb(KeyCode::AltLeft, false); }
if m.mac_cmd && !state.last_mods.mac_cmd { ps2.push_kb(KeyCode::SuperLeft, true); }
if !m.mac_cmd && state.last_mods.mac_cmd { ps2.push_kb(KeyCode::SuperLeft, false); }
state.last_mods = m;

// ---- key events ----
for (kc, pressed) in keys { ps2.push_kb(kc, pressed); }
// Modifiers arrive as real L/R key events (egui 0.35), so forward them like any other key and track what's held.
if f11_to_guest { state.chord_consumed = true; }
let mut chord_release = false;
for (kc, pressed) in keys {
let was_chord = RELEASE_CHORD.iter().all(|k| state.held_mods.contains(k));
if is_modifier(kc) {
if pressed {
if !state.held_mods.contains(&kc) { state.held_mods.push(kc); }
} else {
state.held_mods.retain(|k| *k != kc);
}
} else if pressed && was_chord {
state.chord_consumed = true;
}
let now_chord = RELEASE_CHORD.iter().all(|k| state.held_mods.contains(k));
if now_chord && !was_chord { state.chord_consumed = false; }
if was_chord && !now_chord && !state.chord_consumed { chord_release = true; }
ps2.push_kb(kc, pressed);
}

// Ctrl+Alt+F11 → a *bare* F11 to the guest. Plain F11 is swallowed by the
// GUI's fullscreen toggle, so this chord is the only path for F11 into IRIX.
// The modifier diff above has left the chord's Ctrl+Alt (and any Shift/Cmd)
// pressed in the guest, so lift whatever is held, tap F11, then re-press —
// IRIX sees an unmodified F11. `state.last_mods` is left untouched, so the
// next frame's diff stays consistent (no spurious modifier press/release).
// Ctrl+Alt+F11 → a *bare* F11 to the guest: plain F11 is the GUI's fullscreen toggle, so this chord is the only path for F11 into IRIX.
if f11_to_guest {
let held = state.last_mods;
if held.shift { ps2.push_kb(KeyCode::ShiftLeft, false); }
if held.ctrl { ps2.push_kb(KeyCode::ControlLeft, false); }
if held.alt { ps2.push_kb(KeyCode::AltLeft, false); }
if held.mac_cmd { ps2.push_kb(KeyCode::SuperLeft, false); }
let held = state.held_mods.clone();
for kc in &held { ps2.push_kb(*kc, false); }
ps2.push_kb(KeyCode::F11, true);
ps2.push_kb(KeyCode::F11, false);
if held.shift { ps2.push_kb(KeyCode::ShiftLeft, true); }
if held.ctrl { ps2.push_kb(KeyCode::ControlLeft, true); }
if held.alt { ps2.push_kb(KeyCode::AltLeft, true); }
if held.mac_cmd { ps2.push_kb(KeyCode::SuperLeft, true); }
for kc in &held { ps2.push_kb(*kc, true); }
}

if chord_release {
release_capture(ctx, ps2, state);
return;
}

// ---- mouse: raw per-frame delta + button diff + scroll. ----
Expand Down Expand Up @@ -241,9 +249,9 @@ fn grab_mode() -> CursorGrab {
pub fn engage_capture(ctx: &egui::Context, state: &mut InputState) {
if state.captured { return; }
state.captured = true;
// Anchor modifier/button state so we don't synth a spurious press for a
// key/button already held at capture time.
state.last_mods = ctx.input(|i| i.modifiers);
// Anchor modifier/button state so we don't leave a stale key held from before capture.
state.held_mods.clear();
state.chord_consumed = false;
state.last_buttons = 0;
state.unfocused_since = None;
ctx.send_viewport_cmd(ViewportCommand::CursorVisible(false));
Expand All @@ -256,12 +264,9 @@ pub fn engage_capture(ctx: &egui::Context, state: &mut InputState) {
/// stops while the framebuffer still had the grab.
pub fn release_capture(ctx: &egui::Context, ps2: &Ps2Controller, state: &mut InputState) {
if !state.captured { return; }
if state.last_mods.shift { ps2.push_kb(KeyCode::ShiftLeft, false); }
if state.last_mods.ctrl { ps2.push_kb(KeyCode::ControlLeft, false); }
if state.last_mods.alt { ps2.push_kb(KeyCode::AltLeft, false); }
if state.last_mods.mac_cmd { ps2.push_kb(KeyCode::SuperLeft, false); }
for kc in state.held_mods.drain(..) { ps2.push_kb(kc, false); }
state.captured = false;
state.last_mods = Modifiers::NONE;
state.chord_consumed = false;
state.last_buttons = 0;
state.unfocused_since = None;
ctx.send_viewport_cmd(ViewportCommand::CursorVisible(true));
Expand All @@ -274,17 +279,16 @@ pub fn release_capture(ctx: &egui::Context, ps2: &Ps2Controller, state: &mut Inp
pub fn force_release(ctx: &egui::Context, state: &mut InputState) {
if !state.captured { return; }
state.captured = false;
state.last_mods = Modifiers::NONE;
state.held_mods.clear();
state.chord_consumed = false;
state.last_buttons = 0;
state.unfocused_since = None;
ctx.send_viewport_cmd(ViewportCommand::CursorVisible(true));
ctx.send_viewport_cmd(ViewportCommand::CursorGrab(CursorGrab::None));
}


/// egui::Key → winit::keyboard::KeyCode. Returns None for keys iris's
/// scancode mapper doesn't recognise (we just drop them rather than
/// inventing a fallback).
/// egui::Key (a physical position, per the caller) → winit KeyCode; None for keys iris's scancode mapper doesn't recognise.
fn map_key(k: Key) -> Option<KeyCode> {
Some(match k {
// Letters
Expand Down Expand Up @@ -333,13 +337,17 @@ fn map_key(k: Key) -> Option<KeyCode> {
Key::OpenBracket => KeyCode::BracketLeft,
Key::CloseBracket => KeyCode::BracketRight,
Key::Backtick => KeyCode::Backquote,
// egui reports the *shifted* symbol as its own Key; these two share a
// physical key with Backslash/Slash (Shift is sent separately, so the
// guest forms '|' and '?'). Without them those keys send nothing.
// Only reachable on the logical fallback (no physical_key): shifted symbols sharing a key with Backslash/Slash.
Key::Pipe => KeyCode::Backslash,
Key::Questionmark => KeyCode::Slash,
// F-keys. F11 is reserved by the GUI (fullscreen toggle), so it isn't
// forwarded; iris's PS/2 scancode set stops at F12, so F13+ are dropped.
// ISO 102nd key (< > |), left of Z on European keyboards — new in egui 0.35.
Key::IntlBackslash => KeyCode::IntlBackslash,
// Modifiers as real L/R keys (egui 0.35); AltRight is AltGr, which the guest needs for @ \ | { } [ ] ~ on DE/de_CH.
Key::ShiftLeft => KeyCode::ShiftLeft, Key::ShiftRight => KeyCode::ShiftRight,
Key::ControlLeft => KeyCode::ControlLeft, Key::ControlRight => KeyCode::ControlRight,
Key::AltLeft => KeyCode::AltLeft, Key::AltRight => KeyCode::AltRight,
Key::SuperLeft => KeyCode::SuperLeft, Key::SuperRight => KeyCode::SuperRight,
// F11 is reserved by the GUI (fullscreen); iris's scancode sets stop at F12, so F13+ are dropped.
Key::F1 => KeyCode::F1, Key::F2 => KeyCode::F2, Key::F3 => KeyCode::F3,
Key::F4 => KeyCode::F4, Key::F5 => KeyCode::F5, Key::F6 => KeyCode::F6,
Key::F7 => KeyCode::F7, Key::F8 => KeyCode::F8, Key::F9 => KeyCode::F9,
Expand Down
Loading
Loading