using XFESystem.Desktop; using XFESystem.NativeKernel; namespace XFESystem.NativeRuntime.Application; public readonly record struct NativeInputContext(bool BrowserFocused, DesktopApplication? ActiveApplication); public interface IPs2ReportBackend { /// Only successfully initialized devices: bit 0 keyboard, bit 1 standard mouse. uint Capabilities { get; } bool TryRead(out NativeInputReport report); } internal sealed class NativePs2ReportBackend : IPs2ReportBackend { public uint Capabilities => NativePlatform.InputCapabilities(); public bool TryRead(out NativeInputReport report) => NativePlatform.ReadInput(out report); } /// Maps actual ordered kernel reports to the existing desktop/browser APIs. No hardware-driver dependency. public sealed class Ps2InputSource : INativeInputSource { private readonly IPs2ReportBackend backend; private readonly Func context; private readonly int nativeWidth, nativeHeight, scalePercent; private readonly Queue events = new(); private readonly HashSet held = []; private readonly Dictionary browserHeld = []; private int nativeX, nativeY; private uint modifiers; private bool windowsUsed, switching; public uint Capabilities => backend.Capabilities & 7; public bool IsAvailable => Capabilities != 0; public int PointerX => (int)Math.Min((long)nativeX * 100 / scalePercent, (long)nativeWidth * 100 / scalePercent - 1); public int PointerY => (int)Math.Min((long)nativeY * 100 / scalePercent, (long)nativeHeight * 100 / scalePercent - 1); public long ReportsRead { get; private set; } public long LostReports { get; private set; } public Ps2InputSource(IPs2ReportBackend backend, int nativeWidth, int nativeHeight, int scalePercent, Func context) { ArgumentNullException.ThrowIfNull(backend); ArgumentNullException.ThrowIfNull(context); if (nativeWidth <= 0 || nativeHeight <= 0 || scalePercent is < 100 or > 400 || nativeWidth * 100L / scalePercent < 1 || nativeHeight * 100L / scalePercent < 1) throw new ArgumentOutOfRangeException(nameof(nativeWidth)); this.backend = backend; this.nativeWidth = nativeWidth; this.nativeHeight = nativeHeight; this.scalePercent = scalePercent; this.context = context; nativeX = nativeWidth / 2; nativeY = nativeHeight / 2; } public bool TryRead(out NativeInputEvent result) { if (events.TryDequeue(out result)) return true; if (!IsAvailable) { result = default; return false; } for (int consumed = 0; consumed < 256 && backend.TryRead(out var report); consumed++) { ReportsRead++; Translate(report, context()); if (events.TryDequeue(out result)) return true; } result = default; return false; } private void Translate(NativeInputReport report, NativeInputContext current) { if (report.Kind == Ps2Decoder.Pointer) { nativeX = (int)Math.Clamp((long)nativeX + report.DeltaX, 0, nativeWidth - 1); nativeY = (int)Math.Clamp((long)nativeY + report.DeltaY, 0, nativeHeight - 1); Emit(new(NativeInputKind.Pointer, PointerX, PointerY, (report.Buttons & 1) != 0, report.Wheel)); return; } if (report.Kind == Ps2Decoder.Lost) { Recover(report, current); return; } if (report.Kind is not (Ps2Decoder.KeyDown or Ps2Decoder.KeyUp)) throw new InvalidDataException("Unknown native input report kind."); uint key = report.KeyCode; if (key > 0x17F && key != Ps2Decoder.PauseKey) throw new InvalidDataException("Native set-1 key code is outside the supported ABI."); bool released = report.Kind == Ps2Decoder.KeyUp; bool wasHeld = held.Contains(key), repeat = !released && wasHeld; if (key != Ps2Decoder.PauseKey) { if (released) held.Remove(key); else held.Add(key); } bool previousSuper = (modifiers & Ps2Decoder.Super) != 0; modifiers = report.Modifiers; bool shift = (modifiers & Ps2Decoder.Shift) != 0, control = (modifiers & Ps2Decoder.Control) != 0; bool alt = (modifiers & Ps2Decoder.Alt) != 0, super = (modifiers & Ps2Decoder.Super) != 0; if (!released && super && key is not (0x15B or 0x15C)) windowsUsed = true; if (key is 0x15B or 0x15C) { if (!released && !previousSuper) windowsUsed = shift || control || alt; if (released && wasHeld && !super && !windowsUsed) EmitKey(DesktopKey.StartMenu); browserHeld.Remove(key); return; } if (ModifierName(key) is { } modifier) { if (current.BrowserFocused && !super && !switching) EmitBrowser(key, new(released ? "keyup" : "keydown", modifier, PhysicalCode(key), Shift: shift, Control: control, Alt: alt, Repeat: repeat)); if (released && key is 0x38 or 0x138 && !alt && switching) { switching = false; EmitKey(DesktopKey.EndWindowSwitch); } return; } if (key == 0x3F) { if (!released) EmitKey(DesktopKey.Run); return; } bool desktopChord = super || (alt && key is 0x0F or 0x3E) || (control && (key == 0x26 || key == 0x1F && current.ActiveApplication == DesktopApplication.Editor)); if (current.BrowserFocused && !desktopChord) { string keyName = KeyName(key, report.Unicode, modifiers); if (keyName.Length != 0) { string text = !released && !control && !alt && !super && report.Unicode is >= 32 and <= 0x7E ? ((char)report.Unicode).ToString() : ""; EmitBrowser(key, new(released ? "keyup" : "keydown", keyName, PhysicalCode(key), text, shift, control, alt, repeat)); } return; } if (released) { browserHeld.Remove(key); return; } if (super) { DesktopKey? action = key switch { 0x12 => DesktopKey.OpenFiles, 0x20 => DesktopKey.ShowDesktop, 0x14B => DesktopKey.SnapLeft, 0x14D => DesktopKey.SnapRight, 0x148 => DesktopKey.Maximize, 0x150 => DesktopKey.Restore, 0x4B when (modifiers & Ps2Decoder.NumLock) == 0 => DesktopKey.SnapLeft, 0x4D when (modifiers & Ps2Decoder.NumLock) == 0 => DesktopKey.SnapRight, 0x48 when (modifiers & Ps2Decoder.NumLock) == 0 => DesktopKey.Maximize, 0x50 when (modifiers & Ps2Decoder.NumLock) == 0 => DesktopKey.Restore, _ => null }; if (action is { } value) EmitKey(value); return; } if (alt) { if (key == 0x0F) { switching = true; EmitKey(shift ? DesktopKey.SwitchWindowBack : DesktopKey.SwitchWindow); } else if (key == 0x3E) EmitKey(DesktopKey.CloseWindow); return; } if (control) { DesktopKey? action = key switch { 0x2E when current.ActiveApplication == DesktopApplication.Terminal => DesktopKey.Cancel, 0x1F => DesktopKey.Save, 0x26 => DesktopKey.FocusAddress, 0x01 => DesktopKey.StartMenu, 0x1E when current.ActiveApplication == DesktopApplication.Browser => DesktopKey.FocusAddress, _ => null }; if (action is { } value) EmitKey(value); return; } if (report.Unicode is >= 32 and <= 0x7E) { Emit(new(NativeInputKind.Key, Key: DesktopKey.Character, Character: (char)report.Unicode)); return; } DesktopKey? ordinary = key switch { 0x1C or 0x11C => DesktopKey.Enter, 0x0E => DesktopKey.Backspace, 0x53 or 0x153 => DesktopKey.Delete, 0x4B or 0x14B => DesktopKey.Left, 0x4D or 0x14D => DesktopKey.Right, 0x48 or 0x148 => DesktopKey.Up, 0x50 or 0x150 => DesktopKey.Down, 0x47 or 0x147 => DesktopKey.Home, 0x4F or 0x14F => DesktopKey.End, 0x52 or 0x152 => DesktopKey.Insert, 0x0F => DesktopKey.Tab, 0x01 => DesktopKey.Escape, _ => null }; if (ordinary is { } mapped) EmitKey(mapped); } private void Recover(NativeInputReport report, NativeInputContext current) { LostReports++; if (current.BrowserFocused) foreach (var entry in browserHeld.Values) Emit(new(NativeInputKind.BrowserKey, BrowserKey: new("keyup", entry.Key, entry.Code))); browserHeld.Clear(); held.Clear(); modifiers = report.Modifiers; windowsUsed = true; switching = false; Emit(new(NativeInputKind.ReportsLost, PointerX, PointerY, (report.Buttons & 1) != 0)); } private void EmitKey(DesktopKey key) => Emit(new(NativeInputKind.Key, Key: key)); private void EmitBrowser(uint rawKey, BrowserKeyboardInput input) { if (input.Type == "keydown" && rawKey != Ps2Decoder.PauseKey) browserHeld[rawKey] = input; else browserHeld.Remove(rawKey); Emit(new(NativeInputKind.BrowserKey, BrowserKey: input)); } private void Emit(NativeInputEvent input) { if (events.Count >= 512) throw new InvalidOperationException("Application input recovery exceeded its bounded event budget."); events.Enqueue(input); } private static string? ModifierName(uint key) => key switch { 0x2A or 0x36 => "Shift", 0x1D or 0x11D => "Control", 0x38 or 0x138 => "Alt", 0x3A => "CapsLock", 0x45 => "NumLock", _ => null }; private static string KeyName(uint key, uint unicode, uint modifiers) { if (unicode is >= 32 and <= 0x7E) return ((char)unicode).ToString(); char printable = Ps2Decoder.Character(key, modifiers); if (printable >= ' ') return printable.ToString(); return key switch { 0x1C or 0x11C => "Enter", 0x0E => "Backspace", 0x0F => "Tab", 0x01 => "Escape", 0x47 or 0x147 => "Home", 0x4F or 0x14F => "End", 0x4B or 0x14B => "ArrowLeft", 0x4D or 0x14D => "ArrowRight", 0x48 or 0x148 => "ArrowUp", 0x50 or 0x150 => "ArrowDown", 0x49 or 0x149 => "PageUp", 0x51 or 0x151 => "PageDown", 0x52 or 0x152 => "Insert", 0x53 or 0x153 => "Delete", 0x137 => "PrintScreen", Ps2Decoder.PauseKey => "Pause", >= 0x3B and <= 0x44 => "F" + (key - 0x3A), 0x57 => "F11", 0x58 => "F12", _ => "" }; } private static string PhysicalCode(uint key) { char unshifted = Ps2Decoder.Character(key, 0); if (unshifted is >= 'a' and <= 'z') return "Key" + char.ToUpperInvariant(unshifted); if (key is >= 0x02 and <= 0x0B) return "Digit" + unshifted; return key switch { 0x2A => "ShiftLeft", 0x36 => "ShiftRight", 0x1D => "ControlLeft", 0x11D => "ControlRight", 0x38 => "AltLeft", 0x138 => "AltRight", 0x3A => "CapsLock", 0x45 => "NumLock", 0x1C => "Enter", 0x11C => "NumpadEnter", 0x0E => "Backspace", 0x0F => "Tab", 0x01 => "Escape", 0x0C => "Minus", 0x0D => "Equal", 0x1A => "BracketLeft", 0x1B => "BracketRight", 0x27 => "Semicolon", 0x28 => "Quote", 0x29 => "Backquote", 0x2B => "Backslash", 0x33 => "Comma", 0x34 => "Period", 0x35 => "Slash", 0x39 => "Space", 0x37 => "NumpadMultiply", 0x135 => "NumpadDivide", 0x4A => "NumpadSubtract", 0x4E => "NumpadAdd", 0x47 => "Numpad7", 0x48 => "Numpad8", 0x49 => "Numpad9", 0x4B => "Numpad4", 0x4C => "Numpad5", 0x4D => "Numpad6", 0x4F => "Numpad1", 0x50 => "Numpad2", 0x51 => "Numpad3", 0x52 => "Numpad0", 0x53 => "NumpadDecimal", _ => KeyName(key, 0, 0) }; } }