From 4b5885fab6f9662996fef949af75b2e8ffdd2764 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:01:30 -0400 Subject: [PATCH] Add initial InputHelper --- src/Web/Avalonia.Web.Sample/index.html | 2 +- src/Web/Avalonia.Web/AvaloniaView.cs | 8 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 46 +++++++ src/Web/Avalonia.Web/Keycodes.cs | 129 ++++++++++++++++++ src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 11 +- .../webapp/modules/avalonia/input.ts | 107 +++++++++++++++ .../webapp/modules/avalonia/runtime.ts | 4 +- 7 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 src/Web/Avalonia.Web/Interop/InputHelper.cs create mode 100644 src/Web/Avalonia.Web/Keycodes.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 25b92798ad..518abd4a15 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -13,7 +13,7 @@ -
+
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index f7af35d2b0..54652bc7e2 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -13,6 +13,8 @@ using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Web.Blazor; +using Avalonia.Web.Interop; + using SkiaSharp; using static Avalonia.Web.AvaloniaRuntime; @@ -72,8 +74,10 @@ namespace Avalonia.Web _topLevel.Renderer.Start(); - - + InputHelper.SubscribeKeyboardEvents( + div, + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs new file mode 100644 index 0000000000..6856a021f2 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class InputHelper +{ + //[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] + //public static partial void SubscribePointerEvents( + // JSObject htmlElement, + // [JSMarshalAs>] + // Func wheel); + + [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] + public static partial void SubscribeKeyboardEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func keyDown, + [JSMarshalAs>] + Func keyUp); + + [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] + public static partial void SubscribeInputEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func input); + + + [JSImport("InputHelper.clearInput", "avalonia.ts")] + public static partial void ClearInputElement(JSObject htmlElement); + + [JSImport("InputHelper.isInputElement", "avalonia.ts")] + public static partial void IsInputElement(JSObject htmlElement); + + [JSImport("InputHelper.focusElement", "avalonia.ts")] + public static partial void FocusElement(JSObject htmlElement); + + [JSImport("InputHelper.setCursor", "avalonia.ts")] + public static partial void SetCursor(JSObject htmlElement, string kind); + + [JSImport("InputHelper.hide", "avalonia.ts")] + public static partial void HideElement(JSObject htmlElement); + + [JSImport("InputHelper.show", "avalonia.ts")] + public static partial void ShowElement(JSObject htmlElement); +} diff --git a/src/Web/Avalonia.Web/Keycodes.cs b/src/Web/Avalonia.Web/Keycodes.cs new file mode 100644 index 0000000000..d1185f6e45 --- /dev/null +++ b/src/Web/Avalonia.Web/Keycodes.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; + +using Avalonia.Input; + +namespace Avalonia.Web +{ + internal static class Keycodes + { + public static Dictionary KeyCodes = new() + { + { "Escape", Key.Escape }, + { "Digit1", Key.D1 }, + { "Digit2", Key.D2 }, + { "Digit3", Key.D3 }, + { "Digit4", Key.D4 }, + { "Digit5", Key.D5 }, + { "Digit6", Key.D6 }, + { "Digit7", Key.D7 }, + { "Digit8", Key.D8 }, + { "Digit9", Key.D9 }, + { "Digit0", Key.D0 }, + { "Minus", Key.OemMinus }, + //{ "Equal" , Key. }, + { "Backspace", Key.Back }, + { "Tab", Key.Tab }, + { "KeyQ", Key.Q }, + { "KeyW", Key.W }, + { "KeyE", Key.E }, + { "KeyR", Key.R }, + { "KeyT", Key.T }, + { "KeyY", Key.Y }, + { "KeyU", Key.U }, + { "KeyI", Key.I }, + { "KeyO", Key.O }, + { "KeyP", Key.P }, + { "BracketLeft", Key.OemOpenBrackets }, + { "BracketRight", Key.OemCloseBrackets }, + { "Enter", Key.Enter }, + { "ControlLeft", Key.LeftCtrl }, + { "KeyA", Key.A }, + { "KeyS", Key.S }, + { "KeyD", Key.D }, + { "KeyF", Key.F }, + { "KeyG", Key.G }, + { "KeyH", Key.H }, + { "KeyJ", Key.J }, + { "KeyK", Key.K }, + { "KeyL", Key.L }, + { "Semicolon", Key.OemSemicolon }, + { "Quote", Key.OemQuotes }, + //{ "Backquote" , Key. }, + { "ShiftLeft", Key.LeftShift }, + { "Backslash", Key.OemBackslash }, + { "KeyZ", Key.Z }, + { "KeyX", Key.X }, + { "KeyC", Key.C }, + { "KeyV", Key.V }, + { "KeyB", Key.B }, + { "KeyN", Key.N }, + { "KeyM", Key.M }, + { "Comma", Key.OemComma }, + { "Period", Key.OemPeriod }, + //{ "Slash" , Key. }, + { "ShiftRight", Key.RightShift }, + { "NumpadMultiply", Key.Multiply }, + { "AltLeft", Key.LeftAlt }, + { "Space", Key.Space }, + { "CapsLock", Key.CapsLock }, + { "F1", Key.F1 }, + { "F2", Key.F2 }, + { "F3", Key.F3 }, + { "F4", Key.F4 }, + { "F5", Key.F5 }, + { "F6", Key.F6 }, + { "F7", Key.F7 }, + { "F8", Key.F8 }, + { "F9", Key.F9 }, + { "F10", Key.F10 }, + { "NumLock", Key.NumLock }, + { "ScrollLock", Key.Scroll }, + { "Numpad7", Key.NumPad7 }, + { "Numpad8", Key.NumPad8 }, + { "Numpad9", Key.NumPad9 }, + { "NumpadSubtract", Key.Subtract }, + { "Numpad4", Key.NumPad4 }, + { "Numpad5", Key.NumPad5 }, + { "Numpad6", Key.NumPad6 }, + { "NumpadAdd", Key.Add }, + { "Numpad1", Key.NumPad1 }, + { "Numpad2", Key.NumPad2 }, + { "Numpad3", Key.NumPad3 }, + { "Numpad0", Key.NumPad0 }, + { "NumpadDecimal", Key.Decimal }, + { "Unidentified", Key.NoName }, + //{ "IntlBackslash" , Key.bac }, + { "F11", Key.F11 }, + { "F12", Key.F12 }, + //{ "IntlRo" , Key.Ro }, + //{ "Unidentified" , Key. }, + { "Convert", Key.ImeConvert }, + { "KanaMode", Key.KanaMode }, + { "NonConvert", Key.ImeNonConvert }, + //{ "Unidentified" , Key. }, + { "NumpadEnter", Key.Enter }, + { "ControlRight", Key.RightCtrl }, + { "NumpadDivide", Key.Divide }, + { "PrintScreen", Key.PrintScreen }, + { "AltRight", Key.RightAlt }, + //{ "Unidentified" , Key. }, + { "Home", Key.Home }, + { "ArrowUp", Key.Up }, + { "PageUp", Key.PageUp }, + { "ArrowLeft", Key.Left }, + { "ArrowRight", Key.Right }, + { "End", Key.End }, + { "ArrowDown", Key.Down }, + { "PageDown", Key.PageDown }, + { "Insert", Key.Insert }, + { "Delete", Key.Delete }, + //{ "Unidentified" , Key. }, + { "AudioVolumeMute", Key.VolumeMute }, + { "AudioVolumeDown", Key.VolumeDown }, + { "AudioVolumeUp", Key.VolumeUp }, + //{ "NumpadEqual" , Key. }, + { "Pause", Key.Pause }, + { "NumpadComma", Key.OemComma } + }; + } +} diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs index 82f4064e1a..2bcc36e687 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -110,14 +110,15 @@ namespace Avalonia.Web.Blazor public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) { - /*if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) + Console.WriteLine($"{type} {code} {key} {modifiers}"); + if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) { if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); - + return args.Handled; } } @@ -126,12 +127,12 @@ namespace Avalonia.Web.Blazor if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); return args.Handled; } - }*/ + } return false; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts new file mode 100644 index 0000000000..a6734c8ce0 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -0,0 +1,107 @@ +enum RawInputModifiers +{ + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, + KeyboardMask = Alt | Control | Shift | Meta, + + PenInverted = 512, + PenEraser = 1024, + PenBarrelButton = 2048 +} + +export class InputHelper { + public static subscribeKeyboardEvents( + element: HTMLInputElement, + keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + ) { + const keyDownHandler = (args: Event) => { + const keyArgs = args; + if (keyDownCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + args.preventDefault(); + } + }; + element.addEventListener("keydown", keyDownHandler); + + const keyUpHandler = (args: Event) => { + const keyArgs = args; + if (keyUpCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + args.preventDefault(); + } + }; + element.addEventListener("keyup", keyUpHandler); + + return () => { + element.removeEventListener("keydown", keyDownHandler); + element.removeEventListener("keyup", keyUpHandler); + }; + } + + public static subscribeInputEvents( + element: HTMLInputElement, + inputCallback: (value: string) => boolean + ) { + const inputHandler = (args: Event) => { + if (inputCallback((args as any).value)) { + args.preventDefault(); + } + }; + element.addEventListener("input", inputHandler); + + return () => { + element.removeEventListener("input", inputHandler); + }; + } + + public static clearInput(inputElement: HTMLInputElement) { + inputElement.value = ""; + } + + public static isInputElement(element: HTMLInputElement | HTMLElement): element is HTMLInputElement { + return (element as HTMLInputElement).setSelectionRange !== undefined; + } + + public static focusElement(inputElement: HTMLElement) { + inputElement.focus(); + + if (this.isInputElement(inputElement)) { + (inputElement as HTMLInputElement).setSelectionRange(0, 0); + } + } + + public static setCursor(inputElement: HTMLInputElement, kind: string) { + inputElement.style.cursor = kind; + } + + public static hide(inputElement: HTMLInputElement) { + inputElement.style.display = 'none'; + } + + public static show(inputElement: HTMLInputElement) { + inputElement.style.display = 'block'; + } + + private static getModifiers(args: KeyboardEvent): RawInputModifiers { + var modifiers = RawInputModifiers.None; + + if (args.ctrlKey) + modifiers |= RawInputModifiers.Control; + if (args.altKey) + modifiers |= RawInputModifiers.Alt; + if (args.shiftKey) + modifiers |= RawInputModifiers.Shift; + if (args.metaKey) + modifiers |= RawInputModifiers.Meta; + + return modifiers; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index bb97d0c9b7..2f154aeb3f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -1,6 +1,7 @@ import { RuntimeAPI } from "../../types/dotnet"; import { Canvas } from "./canvas"; +import { InputHelper } from "./input"; export class AvaloniaRuntime { constructor( @@ -8,7 +9,8 @@ export class AvaloniaRuntime { api: RuntimeAPI ) { api.setModuleImports("avalonia.ts", { - Canvas + Canvas, + InputHelper }); } }