From b2b706ed20db8fd4a9b5c26ab397076702514cf8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 19:14:40 +0100 Subject: [PATCH] working input. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web/AvaloniaView.cs | 128 ++++++++++++++++-- src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 9 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 21 ++- .../webapp/modules/avalonia/input.ts | 27 ++-- 5 files changed, 161 insertions(+), 25 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index ca4d29aa21..df254df55b 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -14,6 +14,7 @@ true true + full true true true diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 4638709a54..ad45c6a5a2 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -33,6 +33,7 @@ namespace Avalonia.Web private bool _useGL; private bool _inputElementFocused; + private ITextInputMethodClient? _client; private static int _canvasCount; public AvaloniaView(string divId) @@ -73,14 +74,17 @@ namespace Avalonia.Web _topLevel.Renderer.Start(); - InputHelper.SubscribeKeyboardEvents( + InputHelper.SubscribeKeyEvents( _containerElement, - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier), - (args) => { return false; }, - (args) => { return false; }, - (args) => { return false; }, - (args) => { return false; }); + OnKeyDown, + OnKeyUp); + + InputHelper.SubscribeTextEvents( + _inputElement, + OnTextInput, + OnCompositionStart, + OnCompositionUpdate, + OnCompositionEnd); InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); @@ -235,6 +239,59 @@ namespace Avalonia.Web return modifiers; } + private bool OnKeyDown (string code, string key, int modifier) + { + return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); + } + + private bool OnKeyUp(string code, string key, int modifier) + { + return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier); + } + + private bool OnTextInput (string type, string? data) + { + if(data == null || IsComposing) + { + return false; + } + + return _topLevelImpl.RawTextEvent(data); + } + + private bool OnCompositionStart (JSObject args) + { + if (_client == null) + return false; + + _client.SetPreeditText(null); + IsComposing = true; + + return false; + } + + private bool OnCompositionUpdate(JSObject args) + { + if (_client == null) + return false; + + _client.SetPreeditText(args.GetPropertyAsString("data")); + + return false; + } + + private bool OnCompositionEnd(JSObject args) + { + if (_client == null) + return false; + + IsComposing = false; + _client.SetPreeditText(null); + _topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!); + + return false; + } + private void OnRenderFrame() { if (_useGL && (_jsGlInfo == null)) @@ -257,6 +314,8 @@ namespace Avalonia.Web set => _topLevel.Content = value; } + public bool IsComposing { get; private set; } + internal INativeControlHostImpl GetNativeControlHostImpl() { throw new NotImplementedException(); @@ -314,13 +373,63 @@ namespace Avalonia.Web } } + private void HideIme() + { + InputHelper.HideElement(_inputElement); + InputHelper.FocusElement(_containerElement); + } + public void SetClient(ITextInputMethodClient? client) { - + Console.WriteLine("Set Client"); + if (_client != null) + { + _client.SurroundingTextChanged -= SurroundingTextChanged; + } + + if (client != null) + { + client.SurroundingTextChanged += SurroundingTextChanged; + } + + InputHelper.ClearInputElement(_inputElement); + + _client = client; + + if (_client != null) + { + InputHelper.ShowElement(_inputElement); + _inputElementFocused = true; + InputHelper.FocusElement(_inputElement); + + var surroundingText = _client.SurroundingText; + + InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + + Console.WriteLine("Shown, focused and surrounded."); + } + else + { + _inputElementFocused = false; + HideIme(); + } + } + + private void SurroundingTextChanged(object? sender, EventArgs e) + { + if (_client != null) + { + var surroundingText = _client.SurroundingText; + + InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + } } public void SetCursorRect(Rect rect) { + InputHelper.FocusElement(_inputElement); + InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0); + InputHelper.FocusElement(_inputElement); } public void SetOptions(TextInputOptions options) @@ -329,7 +438,8 @@ namespace Avalonia.Web public void Reset() { - + InputHelper.ClearInputElement(_inputElement); + InputHelper.SetSurroundingText(_inputElement, "", 0, 0); } } } diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index f9812c4ae8..c514101d53 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -141,12 +141,17 @@ namespace Avalonia.Web return false; } - public void RawTextEvent(string text) + public bool RawTextEvent(string text) { if (_inputRoot is { }) { - Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); + var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text); + Input?.Invoke(args); + + return args.Handled; } + + return false; } public void Dispose() diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 3a86416e6b..e728acdc52 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -1,20 +1,26 @@ using System; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Avalonia.Web.Interop; internal static partial class InputHelper { - [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] - public static partial void SubscribeKeyboardEvents( + [JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")] + public static partial void SubscribeKeyEvents( JSObject htmlElement, [JSMarshalAs>] Func keyDown, [JSMarshalAs>] - Func keyUp, - [JSMarshalAs>] - Func onInput, + Func keyUp); + + [JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")] + public static partial void SubscribeTextEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func onInput, [JSMarshalAs>] Func onCompositionStart, [JSMarshalAs>] @@ -60,6 +66,11 @@ internal static partial class InputHelper [JSImport("InputHelper.show", "avalonia.ts")] public static partial void ShowElement(JSObject htmlElement); + [JSImport("InputHelper.setSurroundingText", "avalonia.ts")] + public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end); + + [JSImport("InputHelper.setBounds", "avalonia.ts")] + public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); [JSImport("navigator.clipboard.readText")] public static partial Task ReadClipboardTextAsync(); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index 2aaa25592b..b5c8a60a88 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -21,11 +21,11 @@ enum RawInputModifiers } export class InputHelper { - public static subscribeKeyboardEvents( + public static subscribeKeyEvents( element: HTMLInputElement, keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, - inputCallback: (args: InputEvent) => boolean, + inputCallback: (type: string, data: string|null) => boolean, compositionStartCallback: (args: CompositionEvent) => boolean, compositionUpdateCallback: (args: CompositionEvent) => boolean, compositionEndCallback: (args: CompositionEvent) => boolean) @@ -42,13 +42,27 @@ export class InputHelper { args.preventDefault(); } }; + element.addEventListener("keyup", keyUpHandler); + return () => { + element.removeEventListener("keydown", keyDownHandler); + element.removeEventListener("keyup", keyUpHandler); + }; + } + + public static subscribeTextEvents( + element: HTMLInputElement, + inputCallback: (type: string, data: string | null) => boolean, + compositionStartCallback: (args: CompositionEvent) => boolean, + compositionUpdateCallback: (args: CompositionEvent) => boolean, + compositionEndCallback: (args: CompositionEvent) => boolean) { + const inputHandler = (args: Event) => { var inputEvent = args as InputEvent; // todo check cast - if (inputCallback(inputEvent)) { + if (inputCallback(inputEvent.type, inputEvent.data)) { args.preventDefault(); } }; @@ -76,16 +90,11 @@ export class InputHelper { element.addEventListener("compositionend", compositionEndHandler); return () => { - element.removeEventListener("keydown", keyDownHandler); - element.removeEventListener("keyup", keyUpHandler); + element.removeEventListener("input", inputHandler); element.removeEventListener("compositionstart", compositionStartHandler); element.removeEventListener("compositionupdate", compositionUpdateHandler); element.removeEventListener("compositionend", compositionEndHandler); }; - - - - } public static subscribePointerEvents(