Browse Source

Merge pull request #10328 from AvaloniaUI/ime_improvements

Android/Browser - IME improvements
pull/10341/head
Max Katz 3 years ago
committed by GitHub
parent
commit
c18249f71c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/MobileSandbox/MainView.xaml
  2. 38
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  3. 127
      src/Android/Avalonia.Android/InputEditable.cs
  4. 141
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 23
      src/Avalonia.Base/Input/TextInput/ITextEditable.cs
  6. 11
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  7. 33
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  8. 117
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  9. 27
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  10. 2
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  11. 20
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

4
samples/MobileSandbox/MainView.xaml

@ -5,8 +5,8 @@
x:DataType="mobileSandbox:MainView">
<StackPanel Margin="100 50" Spacing="50">
<TextBlock Text="Login" Foreground="White" />
<TextBox Watermark="Text" />
<TextBox Watermark="Username" TextInputOptions.ContentType="Email" AcceptsReturn="True" TextInputOptions.ReturnKeyType="Search" />
<TextBox TextInputOptions.Multiline="True" AcceptsReturn="True" Watermark="Text" Height="200" TextWrapping="Wrap"/>
<TextBox Watermark="Username" TextInputOptions.ContentType="Email" TextInputOptions.ReturnKeyType="Done" />
<TextBox Watermark="Password" PasswordChar="*" TextInputOptions.ContentType="Password" />
<TextBox Watermark="Pin" PasswordChar="*" TextInputOptions.ContentType="Digits" />
<Button Content="Login" Command="{Binding ButtonCommand}" />

38
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -5,8 +5,10 @@ using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Reactive;
namespace Avalonia.Android
{
@ -68,23 +70,10 @@ namespace Avalonia.Android
public void SetClient(ITextInputMethodClient client)
{
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
if(_inputConnection != null)
{
_inputConnection.ComposingText = null;
_inputConnection.ComposingRegion = default;
}
_client = client;
if (IsActive)
{
_client.SurroundingTextChanged += SurroundingTextChanged;
_host.RequestFocus();
_imm.RestartInput(View);
@ -101,24 +90,6 @@ namespace Avalonia.Android
}
}
private void SurroundingTextChanged(object sender, EventArgs e)
{
if (IsActive && _inputConnection != null)
{
var surroundingText = Client.SurroundingText;
_inputConnection.SurroundingText = surroundingText;
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset)
{
_inputConnection.CommitText(_inputConnection.ComposingText, 0);
_inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
}
public void SetCursorRect(Rect rect)
{
@ -157,11 +128,14 @@ namespace Avalonia.Android
TextInputReturnKeyType.Search => (ImeFlags)CustomImeFlags.ActionSearch,
TextInputReturnKeyType.Next => (ImeFlags)CustomImeFlags.ActionNext,
TextInputReturnKeyType.Previous => (ImeFlags)CustomImeFlags.ActionPrevious,
_ => (ImeFlags)CustomImeFlags.ActionDone
TextInputReturnKeyType.Done => (ImeFlags)CustomImeFlags.ActionDone,
_ => options.Multiline ? ImeFlags.NoEnterAction : (ImeFlags)CustomImeFlags.ActionDone
};
outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
_client.TextEditable = _inputConnection.InputEditable;
return _inputConnection;
});
}

127
src/Android/Avalonia.Android/InputEditable.cs

@ -0,0 +1,127 @@
using System;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Android
{
internal class InputEditable : SpannableStringBuilder, ITextEditable
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
private readonly AvaloniaInputConnection _avaloniaInputConnection;
private int _currentBatchLevel;
private string _previousText;
private int _previousSelectionStart;
private int _previousSelectionEnd;
public event EventHandler TextChanged;
public event EventHandler SelectionChanged;
public event EventHandler CompositionChanged;
public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod, AvaloniaInputConnection avaloniaInputConnection)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
_avaloniaInputConnection = avaloniaInputConnection;
}
public InputEditable(ICharSequence text) : base(text)
{
}
public InputEditable(string text) : base(text)
{
}
public InputEditable(ICharSequence text, int start, int end) : base(text, start, end)
{
}
public InputEditable(string text, int start, int end) : base(text, start, end)
{
}
protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
public int SelectionStart
{
get => Selection.GetSelectionStart(this); set
{
var end = SelectionEnd < 0 ? 0 : SelectionEnd;
_avaloniaInputConnection.SetSelection(value, end);
_inputMethod.IMM.UpdateSelection(_topLevel.View, value, end, value, end);
}
}
public int SelectionEnd
{
get => Selection.GetSelectionEnd(this); set
{
var start = SelectionStart < 0 ? 0 : SelectionStart;
_avaloniaInputConnection.SetSelection(start, value);
_inputMethod.IMM.UpdateSelection(_topLevel.View, start, value, start, value);
}
}
public string? Text
{
get => ToString(); set
{
if (Text != value)
{
Clear();
Insert(0, value ?? "");
}
}
}
public int CompositionStart => BaseInputConnection.GetComposingSpanStart(this);
public int CompositionEnd => BaseInputConnection.GetComposingSpanEnd(this);
public void BeginBatchEdit()
{
_currentBatchLevel++;
if (_currentBatchLevel == 1)
{
_previousText = ToString();
_previousSelectionStart = SelectionStart;
_previousSelectionEnd = SelectionEnd;
}
}
public void EndBatchEdit()
{
if (_currentBatchLevel == 1)
{
if(_previousText != Text)
{
TextChanged?.Invoke(this, EventArgs.Empty);
}
if (_previousSelectionStart != SelectionStart || _previousSelectionEnd != SelectionEnd)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
_currentBatchLevel--;
}
public void RaiseCompositionChanged()
{
CompositionChanged?.Invoke(this, EventArgs.Empty);
}
}
}

141
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -28,6 +28,7 @@ using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables;
using Java.Util;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -410,159 +411,73 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
private readonly InputEditable _editable;
public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
_editable = new InputEditable(_topLevel, _inputMethod, this);
}
public TextInputMethodSurroundingText SurroundingText { get; set; }
public override IEditable Editable => _editable;
public string ComposingText { get; internal set; }
public ComposingRegion? ComposingRegion { get; internal set; }
public bool IsComposing => !string.IsNullOrEmpty(ComposingText);
public bool IsCommiting { get; private set; }
internal InputEditable InputEditable => _editable;
public override bool SetComposingRegion(int start, int end)
{
//System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}");
var ret = base.SetComposingRegion(start, end);
ComposingRegion = new ComposingRegion(start, end);
InputEditable.RaiseCompositionChanged();
return base.SetComposingRegion(start, end);
return ret;
}
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{
var composingText = text.ToString();
ComposingText = composingText;
_inputMethod.Client?.SetPreeditText(ComposingText);
return base.SetComposingText(text, newCursorPosition);
}
public override bool FinishComposingText()
{
if (!string.IsNullOrEmpty(ComposingText))
if (string.IsNullOrEmpty(composingText))
{
CommitText(ComposingText, ComposingText.Length);
return CommitText(text, newCursorPosition);
}
else
{
ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
}
return base.FinishComposingText();
}
public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
{
if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0)
{
var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
var ret = base.SetComposingText(text, newCursorPosition);
var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
InputEditable.RaiseCompositionChanged();
var text = SurroundingText.Text.Substring(start, end - start);
//System.Diagnostics.Debug.WriteLine($"Text Before: {text}");
return new Java.Lang.String(text);
return ret;
}
return null;
}
public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
public override bool BeginBatchEdit()
{
if (!string.IsNullOrEmpty(SurroundingText.Text))
{
var start = SurroundingText.CursorOffset;
var end = System.Math.Min(start + length, SurroundingText.Text.Length);
var text = SurroundingText.Text.Substring(start, end - start);
_editable.BeginBatchEdit();
//System.Diagnostics.Debug.WriteLine($"Text After: {text}");
return new Java.Lang.String(text);
}
return null;
return base.BeginBatchEdit();
}
public override bool CommitText(ICharSequence text, int newCursorPosition)
public override bool EndBatchEdit()
{
IsCommiting = true;
var committedText = text.ToString();
_inputMethod.Client.SetPreeditText(null);
var ret = base.EndBatchEdit();
_editable.EndBatchEdit();
int? start, end;
if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset)
{
start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
}
else if (ComposingRegion != null)
{
start = ComposingRegion?.Start;
end = ComposingRegion?.End;
ComposingRegion = null;
}
else
{
start = end = _inputMethod.Client.SurroundingText.CursorOffset;
}
_inputMethod.Client.SelectInSurroundingText((int)start, (int)end);
var time = DateTime.Now.TimeOfDay;
var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText);
_topLevel.Input(rawTextEvent);
ComposingText = null;
ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition);
return base.CommitText(text, newCursorPosition);
return ret;
}
public override bool DeleteSurroundingText(int beforeLength, int afterLength)
public override bool FinishComposingText()
{
var surroundingText = _inputMethod.Client.SurroundingText;
var selectionStart = surroundingText.CursorOffset;
_inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength);
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
surroundingText = _inputMethod.Client.SurroundingText;
selectionStart = surroundingText.CursorOffset;
ComposingRegion = new ComposingRegion(selectionStart, selectionStart);
return base.DeleteSurroundingText(beforeLength, afterLength);
var ret = base.FinishComposingText();
InputEditable.RaiseCompositionChanged();
return ret;
}
public override bool SetSelection(int start, int end)
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
_inputMethod.Client.SelectInSurroundingText(start, end);
ComposingRegion = new ComposingRegion(start, end);
return base.SetSelection(start, end);
var ret = base.CommitText(text, newCursorPosition);
InputEditable.RaiseCompositionChanged();
return ret;
}
public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)

23
src/Avalonia.Base/Input/TextInput/ITextEditable.cs

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Input.TextInput
{
[NotClientImplementable]
public interface ITextEditable
{
event EventHandler TextChanged;
event EventHandler SelectionChanged;
event EventHandler CompositionChanged;
int SelectionStart { get; set; }
int SelectionEnd { get; set; }
int CompositionStart { get; }
int CompositionEnd { get; }
string? Text { get; set; }
}
}

11
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
@ -30,6 +31,11 @@ namespace Avalonia.Input.TextInput
/// </summary>
void SetPreeditText(string? text);
/// <summary>
/// Sets the current composing region. This doesn't remove the composing text from the commited text.
/// </summary>
void SetComposingRegion(TextRange? region);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
@ -43,6 +49,11 @@ namespace Avalonia.Input.TextInput
/// </summary>
event EventHandler? SurroundingTextChanged;
/// <summary>
/// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client.
/// </summary>
ITextEditable? TextEditable { get; set; }
void SelectInSurroundingText(int start, int end);
}

33
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -63,6 +63,15 @@ namespace Avalonia.Controls.Presenters
o => o.PreeditText,
(o, v) => o.PreeditText = v);
/// <summary>
/// Defines the <see cref="CompositionRegion"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, TextRange?> CompositionRegionProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, TextRange?>(
nameof(CompositionRegion),
o => o.CompositionRegion,
(o, v) => o.CompositionRegion = v);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -106,6 +115,7 @@ namespace Avalonia.Controls.Presenters
private Rect _caretBounds;
private Point _navigationPosition;
private string? _preeditText;
private TextRange? _compositionRegion;
static TextPresenter()
{
@ -146,6 +156,12 @@ namespace Avalonia.Controls.Presenters
set => SetAndRaise(PreeditTextProperty, ref _preeditText, value);
}
public TextRange? CompositionRegion
{
get => _compositionRegion;
set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -548,7 +564,20 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground;
if (!string.IsNullOrEmpty(_preeditText))
if(_compositionRegion != null)
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
textStyleOverrides = new[]
{
preeditHighlight
};
}
else if (!string.IsNullOrEmpty(_preeditText))
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length,
new GenericTextRunProperties(typeface, FontSize,
@ -911,6 +940,7 @@ namespace Avalonia.Controls.Presenters
break;
}
case nameof(CompositionRegion):
case nameof(Foreground):
case nameof(FontSize):
case nameof(FontStyle):
@ -931,7 +961,6 @@ namespace Avalonia.Controls.Presenters
case nameof(PasswordChar):
case nameof(RevealPassword):
case nameof(FlowDirection):
{
InvalidateTextLayout();

117
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -5,6 +5,7 @@ using Avalonia.Media.TextFormatting;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -12,6 +13,7 @@ namespace Avalonia.Controls
{
private TextBox? _parent;
private TextPresenter? _presenter;
private ITextEditable? _textEditable;
public Visual TextViewVisual => _presenter!;
@ -45,7 +47,7 @@ namespace Avalonia.Controls
{
get
{
if(_presenter is null || _parent is null)
if (_presenter is null || _parent is null)
{
return default;
}
@ -71,13 +73,70 @@ namespace Avalonia.Controls
}
}
public ITextEditable? TextEditable
{
get => _textEditable; set
{
if(_textEditable != null)
{
_textEditable.TextChanged -= TextEditable_TextChanged;
_textEditable.SelectionChanged -= TextEditable_SelectionChanged;
_textEditable.CompositionChanged -= TextEditable_CompositionChanged;
}
_textEditable = value;
if(_textEditable != null)
{
_textEditable.TextChanged += TextEditable_TextChanged;
_textEditable.SelectionChanged += TextEditable_SelectionChanged;
_textEditable.CompositionChanged += TextEditable_CompositionChanged;
if (_presenter != null)
{
_textEditable.Text = _presenter.Text;
_textEditable.SelectionStart = _presenter.SelectionStart;
_textEditable.SelectionEnd = _presenter.SelectionEnd;
}
}
}
}
private void TextEditable_CompositionChanged(object? sender, EventArgs e)
{
if (_presenter != null && _textEditable != null)
{
_presenter.CompositionRegion = new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd);
}
}
private void TextEditable_SelectionChanged(object? sender, EventArgs e)
{
if(_parent != null && _textEditable != null)
{
_parent.SelectionStart = _textEditable.SelectionStart;
_parent.SelectionEnd = _textEditable.SelectionEnd;
}
}
private void TextEditable_TextChanged(object? sender, EventArgs e)
{
if (_parent != null)
{
if (_parent.Text != _textEditable?.Text)
{
_parent.Text = _textEditable?.Text;
}
}
}
private static string GetTextLineText(TextLine textLine)
{
var builder = StringBuilderCache.Acquire(textLine.Length);
foreach (var run in textLine.TextRuns)
{
if(run.Length > 0)
if (run.Length > 0)
{
#if NET6_0_OR_GREATER
builder.Append(run.Text.Span);
@ -110,9 +169,18 @@ namespace Avalonia.Controls
_presenter.PreeditText = text;
}
public void SetComposingRegion(TextRange? region)
{
if (_presenter == null)
{
return;
}
_presenter.CompositionRegion = region;
}
public void SelectInSurroundingText(int start, int end)
{
if(_parent is null ||_presenter is null)
if (_parent is null || _presenter is null)
{
return;
}
@ -125,21 +193,21 @@ namespace Avalonia.Controls
var selectionStart = lineStart + start;
var selectionEnd = lineStart + end;
_parent.SelectionStart = selectionStart;
_parent.SelectionEnd = selectionEnd;
}
}
public void SetPresenter(TextPresenter? presenter, TextBox? parent)
{
if(_parent != null)
if (_parent != null)
{
_parent.PropertyChanged -= OnParentPropertyChanged;
}
_parent = parent;
if(_parent != null)
if (_parent != null)
{
_parent.PropertyChanged += OnParentPropertyChanged;
}
@ -148,16 +216,18 @@ namespace Avalonia.Controls
{
_presenter.PreeditText = null;
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
_presenter.CompositionRegion = null;
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
}
_presenter = presenter;
if (_presenter != null)
{
_presenter.CaretBoundsChanged += OnCaretBoundsChanged;
}
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
OnCaretBoundsChanged(this, EventArgs.Empty);
@ -165,12 +235,33 @@ namespace Avalonia.Controls
private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if(e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
{
if (SupportsSurroundingText)
{
SurroundingTextChanged?.Invoke(this, e);
}
if (_textEditable != null)
{
var value = (int)(e.NewValue ?? 0);
if (e.Property == TextBox.SelectionStartProperty)
{
_textEditable.SelectionStart = value;
}
if (e.Property == TextBox.SelectionEndProperty)
{
_textEditable.SelectionEnd = value;
}
}
}
if(e.Property == TextBox.TextProperty)
{
if(_textEditable != null)
{
_textEditable.Text = (string?)e.NewValue;
}
}
}

27
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
@ -15,6 +16,7 @@ using Avalonia.Platform;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using SkiaSharp;
using static System.Runtime.CompilerServices.RuntimeHelpers;
namespace Avalonia.Browser
{
@ -94,6 +96,7 @@ namespace Avalonia.Browser
InputHelper.SubscribeTextEvents(
_inputElement,
OnBeforeInput,
OnTextInput,
OnCompositionStart,
OnCompositionUpdate,
@ -316,6 +319,30 @@ namespace Avalonia.Browser
return _topLevelImpl.RawTextEvent(data);
}
private bool OnBeforeInput(JSObject arg, int start, int end)
{
var type = arg.GetPropertyAsString("inputType");
if (type != "deleteByComposition")
{
if (type == "deleteContentBackward")
{
start = _inputElement.GetPropertyAsInt32("selectionStart");
end = _inputElement.GetPropertyAsInt32("selectionEnd");
}
else
{
start = -1;
end = -1;
}
}
if(start != -1 && end != -1 && _client != null)
{
_client.SelectInSurroundingText(start, end);
}
return false;
}
private bool OnCompositionStart (JSObject args)
{
if (_client == null)

2
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -18,6 +18,8 @@ internal static partial class InputHelper
[JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeTextEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Number, JSType.Number, JSType.Boolean>>]
Func<JSObject, int, int, bool> onBeforeInput,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Boolean>>]
Func<string, string?, bool> onInput,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]

20
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -47,6 +47,7 @@ export class InputHelper {
public static subscribeTextEvents(
element: HTMLInputElement,
beforeInputCallback: (args: InputEvent, start: number, end: number) => boolean,
inputCallback: (type: string, data: string | null) => boolean,
compositionStartCallback: (args: CompositionEvent) => boolean,
compositionUpdateCallback: (args: CompositionEvent) => boolean,
@ -68,6 +69,25 @@ export class InputHelper {
};
element.addEventListener("compositionstart", compositionStartHandler);
const beforeInputHandler = (args: InputEvent) => {
const ranges = args.getTargetRanges();
let start = -1;
let end = -1;
if (ranges.length > 0) {
start = ranges[0].startOffset;
end = ranges[0].endOffset;
}
if (args.inputType === "insertCompositionText") {
start = 2;
end = start + 2;
}
if (beforeInputCallback(args, start, end)) {
args.preventDefault();
}
};
element.addEventListener("beforeinput", beforeInputHandler);
const compositionUpdateHandler = (args: CompositionEvent) => {
if (compositionUpdateCallback(args)) {
args.preventDefault();

Loading…
Cancel
Save