Browse Source

Merge branch 'SetAdronerFromXaml' of https://github.com/wieslawsoltes/Avalonia into SetAdronerFromXaml

pull/9012/head
Wiesław Šoltés 3 years ago
parent
commit
e5d6c6fe1f
  1. 28
      Avalonia.sln
  2. 2
      dirs.proj
  3. 3
      samples/ControlCatalog.Web/App.razor.cs
  4. 1
      samples/ControlCatalog.Web/wwwroot/index.html
  5. 10
      samples/ControlCatalog.Web/wwwroot/js/app.js
  6. 3
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  7. 12
      samples/MobileSandbox.Android/MainActivity.cs
  8. 44
      samples/MobileSandbox.Android/MobileSandbox.Android.csproj
  9. 5
      samples/MobileSandbox.Android/Properties/AndroidManifest.xml
  10. 44
      samples/MobileSandbox.Android/Resources/AboutResources.txt
  11. 13
      samples/MobileSandbox.Android/Resources/drawable/splash_screen.xml
  12. 4
      samples/MobileSandbox.Android/Resources/values/colors.xml
  13. 17
      samples/MobileSandbox.Android/Resources/values/styles.xml
  14. 17
      samples/MobileSandbox.Android/SplashActivity.cs
  15. 1
      samples/MobileSandbox.Android/environment.device.txt
  16. 1
      samples/MobileSandbox.Android/environment.emulator.txt
  17. 47
      samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
  18. 21
      samples/MobileSandbox.Desktop/Program.cs
  19. 28
      samples/MobileSandbox.Desktop/app.manifest
  20. 22
      samples/MobileSandbox.iOS/AppDelegate.cs
  21. 5
      samples/MobileSandbox.iOS/Entitlements.plist
  22. 47
      samples/MobileSandbox.iOS/Info.plist
  23. 15
      samples/MobileSandbox.iOS/Main.cs
  24. 16
      samples/MobileSandbox.iOS/MobileSandbox.iOS.csproj
  25. 43
      samples/MobileSandbox.iOS/Resources/LaunchScreen.xib
  26. 9
      samples/MobileSandbox/App.xaml
  27. 28
      samples/MobileSandbox/App.xaml.cs
  28. BIN
      samples/MobileSandbox/Assets/Fonts/SourceSansPro-Bold.ttf
  29. BIN
      samples/MobileSandbox/Assets/Fonts/SourceSansPro-BoldItalic.ttf
  30. BIN
      samples/MobileSandbox/Assets/Fonts/SourceSansPro-Italic.ttf
  31. BIN
      samples/MobileSandbox/Assets/Fonts/SourceSansPro-Regular.ttf
  32. BIN
      samples/MobileSandbox/Assets/Fonts/WenQuanYiMicroHei-01.ttf
  33. BIN
      samples/MobileSandbox/Assets/avalonia-32.png
  34. BIN
      samples/MobileSandbox/Assets/test_icon.ico
  35. 11
      samples/MobileSandbox/MainView.xaml
  36. 22
      samples/MobileSandbox/MainView.xaml.cs
  37. 12
      samples/MobileSandbox/MainWindow.xaml
  38. 21
      samples/MobileSandbox/MainWindow.xaml.cs
  39. 49
      samples/MobileSandbox/MobileSandbox.csproj
  40. 19
      samples/MobileSandbox/Views/CustomNotificationView.xaml
  41. 18
      samples/MobileSandbox/Views/CustomNotificationView.xaml.cs
  42. 1
      samples/Sandbox/MainWindow.axaml
  43. 1
      samples/Sandbox/MainWindow.axaml.cs
  44. 91
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  45. 7
      src/Android/Avalonia.Android/IInitEditorInfo.cs
  46. 166
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  47. 12
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  48. 36
      src/Avalonia.Base/Input/TextInput/TextInputOptions.cs
  49. 13
      src/Avalonia.Base/Input/TextInput/TextInputReturnKeyType.cs
  50. 2
      src/Avalonia.Base/Media/FontFamily.cs
  51. 223
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  52. 9
      src/Avalonia.Controls/TextBox.cs
  53. 155
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  54. 7
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  55. 7
      src/Avalonia.X11/X11Window.Xim.cs
  56. 2
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  57. 23
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  58. 108
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  59. 61
      src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs
  60. 6
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  61. 20
      src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs
  62. 22
      src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs
  63. 98
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  64. 6
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  65. 2
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
  66. 149
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts
  67. 9
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts
  68. 81
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  69. 8
      src/Windows/Avalonia.Win32/Input/Imm32CaretManager.cs
  70. 203
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  71. 71
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  72. 45
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  73. 2
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  74. 11
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  75. 146
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  76. 25
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  77. 40
      src/iOS/Avalonia.iOS/CombinedSpan3.cs
  78. 92
      src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs
  79. 491
      src/iOS/Avalonia.iOS/TextInputResponder.cs

28
Avalonia.sln

@ -214,6 +214,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Te
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.iOS", "samples\MobileSandbox.iOS\MobileSandbox.iOS.csproj", "{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -502,6 +510,22 @@ Global
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.Build.0 = Release|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Release|Any CPU.Build.0 = Release|Any CPU
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}.Release|Any CPU.Build.0 = Release|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -560,6 +584,10 @@ Global
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C90FE60B-B01E-4F35-91D6-379D6966030F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{62D392C9-81CF-487F-92E8-598B2AF3FDCE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
dirs.proj

@ -11,8 +11,10 @@
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<ProjectReference Remove="tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj" />
<ProjectReference Remove="samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj" />
<ProjectReference Remove="samples/MobileSandbox.iOS/MobileSandbox.iOS.csproj" />
<ProjectReference Remove="samples/ControlCatalog.iOS.Legacy/ControlCatalog.iOS.Legacy.csproj" />
<ProjectReference Remove="samples/ControlCatalog.Android/ControlCatalog.Android.csproj" />
<ProjectReference Remove="samples/MobileSandbox.Android/MobileSandbox.Android.csproj" />
<ProjectReference Remove="src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'">

3
samples/ControlCatalog.Web/App.razor.cs

@ -1,3 +1,4 @@
using Avalonia;
using Avalonia.Web.Blazor;
namespace ControlCatalog.Web;
@ -11,7 +12,7 @@ public partial class App
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
})
//.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();

1
samples/ControlCatalog.Web/wwwroot/index.html

@ -17,7 +17,6 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="js/app.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

10
samples/ControlCatalog.Web/wwwroot/js/app.js

@ -1,10 +0,0 @@
window.createAppButton = function () {
var button = document.createElement('button');
button.innerText = 'Hello world';
var clickCount = 0;
button.onclick = () => {
clickCount++;
button.innerText = 'Click count ' + clickCount;
};
return button;
}

3
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -9,6 +9,9 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<!-- <RuntimeIdentifier>ios-arm64</RuntimeIdentifier>-->
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodesignKey>iPhone Developer</CodesignKey>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />

12
samples/MobileSandbox.Android/MainActivity.cs

@ -0,0 +1,12 @@
using Android.App;
using Android.Content.PM;
using Avalonia;
using Avalonia.Android;
namespace MobileSandbox.Android
{
[Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class MainActivity : AvaloniaActivity<App>
{
}
}

44
samples/MobileSandbox.Android/MobileSandbox.Android.csproj

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ApplicationId>com.Avalonia.MobileSandbox</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='' and '$(Configuration)'=='Release' and '$(TF_BUILD)'==''">
<RunAOTCompilation>True</RunAOTCompilation>
</PropertyGroup>
<!-- PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<EnableLLVM>True</EnableLLVM>
<AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
<AndroidAotMode>Hybrid</AndroidAotMode>
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
</PropertyGroup -->
<PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />
</ItemGroup>
</Project>

5
samples/MobileSandbox.Android/Properties/AndroidManifest.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="MobileSandbox.Android" android:icon="@drawable/Icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

44
samples/MobileSandbox.Android/Resources/AboutResources.txt

@ -0,0 +1,44 @@
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.axml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable/
icon.png
layout/
main.axml
values/
strings.xml
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called "R"
(this is an Android convention) that contains the tokens for each one of the resources
included. For example, for the above Resources layout, this is what the R class would expose:
public class R {
public class drawable {
public const int icon = 0x123;
}
public class layout {
public const int main = 0x456;
}
public class strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main
to reference the layout/main.axml file, or R.strings.first_string to reference the first
string in the dictionary file values/strings.xml.

13
samples/MobileSandbox.Android/Resources/drawable/splash_screen.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/splash_background"/>
</item>
<item android:drawable="@drawable/icon"
android:width="120dp"
android:height="120dp"
android:gravity="center" />
</layer-list>

4
samples/MobileSandbox.Android/Resources/values/colors.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#FFFFFF</color>
</resources>

17
samples/MobileSandbox.Android/Resources/values/styles.xml

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>

17
samples/MobileSandbox.Android/SplashActivity.cs

@ -0,0 +1,17 @@
using Android.App;
using Android.Content;
using Android.OS;
namespace MobileSandbox.Android
{
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : Activity
{
protected override void OnResume()
{
base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity)));
}
}
}

1
samples/MobileSandbox.Android/environment.device.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend

1
samples/MobileSandbox.Android/environment.emulator.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend

47
samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj

@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">
<IlcTrimMetadata>true</IlcTrimMetadata>
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json</RestoreAdditionalProjectSources>
<NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.X11\NativeDialogs\Gtk.cs" Link="NativeControls\Gtk\Gtk.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<!-- For native controls test -->
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
</ItemGroup>
<ItemGroup Condition="'$(RunNativeAotCompilation)' == 'true'">
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
<!-- Cross-compilation for Windows x64-arm64 and Linux x64-arm64 -->
<PackageReference Condition="'$(RuntimeIdentifier)'=='win-arm64'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
<PackageReference Condition="'$(RuntimeIdentifier)'=='linux-arm64'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
</ItemGroup>
<PropertyGroup>
<!-- For Microsoft.CodeAnalysis -->
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
</Project>

21
samples/MobileSandbox.Desktop/Program.cs

@ -0,0 +1,21 @@
using System;
using Avalonia;
namespace MobileSandbox.Desktop
{
static class Program
{
[STAThread]
static int Main(string[] args) =>
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
/// <summary>
/// This method is needed for IDE previewer infrastructure
/// </summary>
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}

28
samples/MobileSandbox.Desktop/app.manifest

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

22
samples/MobileSandbox.iOS/AppDelegate.cs

@ -0,0 +1,22 @@
using Foundation;
using UIKit;
using Avalonia;
using Avalonia.Controls;
using Avalonia.iOS;
using Avalonia.Logging;
using Avalonia.Media;
namespace MobileSandbox
{
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
public partial class AppDelegate : AvaloniaAppDelegate<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return builder.LogToTrace(LogEventLevel.Debug, "IOSIME");
}
}
}

5
samples/MobileSandbox.iOS/Entitlements.plist

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

47
samples/MobileSandbox.iOS/Info.plist

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>ControlCatalog.iOS</string>
<key>CFBundleIdentifier</key>
<string>Avalonia.ControlCatalog</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>13.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

15
samples/MobileSandbox.iOS/Main.cs

@ -0,0 +1,15 @@
using UIKit;
namespace MobileSandbox.iOS
{
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

16
samples/MobileSandbox.iOS/MobileSandbox.iOS.csproj

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ProvisioningType>manual</ProvisioningType>
<TargetFramework>net6.0-ios</TargetFramework>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<!-- temporal workaround for our GL interface backend -->
<UseInterpreter>True</UseInterpreter>
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<!-- <RuntimeIdentifier>ios-arm64</RuntimeIdentifier>-->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />
</ItemGroup>
</Project>

43
samples/MobileSandbox.iOS/Resources/LaunchScreen.xib

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480" />
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21" />
<fontDescription key="fontDescription" type="system" pointSize="17" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ControlCatalog.iOS" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43" />
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" />
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" />
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" />
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
</constraints>
<nil key="simulatedStatusBarMetrics" />
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
<point key="canvasLocation" x="548" y="455" />
</view>
</objects>
</document>

9
samples/MobileSandbox/App.xaml

@ -0,0 +1,9 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True"
Name="Mobile Sandbox"
x:Class="MobileSandbox.App">
<Application.Styles>
<FluentTheme Mode="Dark" />
</Application.Styles>
</Application>

28
samples/MobileSandbox/App.xaml.cs

@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace MobileSandbox
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();
}
base.OnFrameworkInitializationCompleted();
}
}
}

BIN
samples/MobileSandbox/Assets/Fonts/SourceSansPro-Bold.ttf

Binary file not shown.

BIN
samples/MobileSandbox/Assets/Fonts/SourceSansPro-BoldItalic.ttf

Binary file not shown.

BIN
samples/MobileSandbox/Assets/Fonts/SourceSansPro-Italic.ttf

Binary file not shown.

BIN
samples/MobileSandbox/Assets/Fonts/SourceSansPro-Regular.ttf

Binary file not shown.

BIN
samples/MobileSandbox/Assets/Fonts/WenQuanYiMicroHei-01.ttf

Binary file not shown.

BIN
samples/MobileSandbox/Assets/avalonia-32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
samples/MobileSandbox/Assets/test_icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

11
samples/MobileSandbox/MainView.xaml

@ -0,0 +1,11 @@
<UserControl x:Class="MobileSandbox.MainView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="100 50" Spacing="50">
<TextBlock Text="Login" Foreground="White" />
<TextBox Watermark="Username" TextInputOptions.ContentType="Email" AcceptsReturn="True" TextInputOptions.ReturnKeyType="Search" />
<TextBox Watermark="Password" PasswordChar="*" TextInputOptions.ContentType="Password" />
<TextBox Watermark="Pin" PasswordChar="*" TextInputOptions.ContentType="Digits" />
<Button Content="Login" Command="{Binding ButtonCommand}" />
</StackPanel>
</UserControl>

22
samples/MobileSandbox/MainView.xaml.cs

@ -0,0 +1,22 @@
using System;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace MobileSandbox
{
public class MainView : UserControl
{
public MainView()
{
AvaloniaXamlLoader.Load(this);
DataContext = this;
}
public void ButtonCommand()
{
Console.WriteLine("Button pressed");
}
}
}

12
samples/MobileSandbox/MainWindow.xaml

@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:local="clr-namespace:MobileSandbox"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="MainWindow"
CanResize="False"
Width="720" Height="1280"
Title="Mobile Sandbox"
Icon="/Assets/test_icon.ico"
WindowStartupLocation="CenterScreen"
x:Class="MobileSandbox.MainWindow">
<local:MainView />
</Window>

21
samples/MobileSandbox/MainWindow.xaml.cs

@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace MobileSandbox
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
//Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

49
samples/MobileSandbox/MobileSandbox.csproj

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\*" />
<AvaloniaResource Include="Assets\Fonts\*" />
</ItemGroup>
<ItemGroup>
<None Remove="Pages\NativeEmbedPage.xaml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Bold.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Italic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Regular.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Update="Pages\NativeEmbedPage.xaml">
<Generator>MSBuild:Compile</Generator>
</AvaloniaResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Pages\NativeEmbedPage.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

19
samples/MobileSandbox/Views/CustomNotificationView.xaml

@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MobileSandbox.Views.CustomNotificationView">
<Border Padding="12" MinHeight="20" Background="DodgerBlue">
<Grid ColumnDefinitions="Auto,*">
<Panel Margin="0,0,12,0" Width="25" Height="25" VerticalAlignment="Top">
<TextBlock Text="&#xE115;" FontFamily="Segoe UI Symbol" FontSize="20" TextAlignment="Center" VerticalAlignment="Center"/>
</Panel>
<DockPanel Grid.Column="1">
<TextBlock DockPanel.Dock="Top" Text="{Binding Title}" FontWeight="Medium" />
<StackPanel Spacing="20" DockPanel.Dock="Bottom" Margin="0,8,0,0" Orientation="Horizontal">
<Button Content="No" DockPanel.Dock="Right" NotificationCard.CloseOnClick="True" Command="{Binding NoCommand}" Margin="0,0,8,0" />
<Button Content="Yes" DockPanel.Dock="Right" NotificationCard.CloseOnClick="True" Command="{Binding YesCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}" TextWrapping="Wrap" Opacity=".8" Margin="0,8,0,0"/>
</DockPanel>
</Grid>
</Border>
</UserControl>

18
samples/MobileSandbox/Views/CustomNotificationView.xaml.cs

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace MobileSandbox.Views
{
public class CustomNotificationView : UserControl
{
public CustomNotificationView()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

1
samples/Sandbox/MainWindow.axaml

@ -1,4 +1,5 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow">
<TextBox />
</Window>

1
samples/Sandbox/MainWindow.axaml.cs

@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Markup.Xaml;
using Avalonia.Win32.WinRT.Composition;

91
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -3,16 +3,41 @@ using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Input;
using Avalonia.Input.TextInput;
namespace Avalonia.Android
{
class AndroidInputMethod<TView> : ITextInputMethodImpl
where TView: View, IInitEditorInfo
internal interface IAndroidInputMethod
{
public View View { get; }
public ITextInputMethodClient Client { get; }
public bool IsActive { get; }
public InputMethodManager IMM { get; }
}
enum CustomImeFlags
{
ActionNone = 0x00000001,
ActionGo = 0x00000002,
ActionSearch = 0x00000003,
ActionSend = 0x00000004,
ActionNext = 0x00000005,
ActionDone = 0x00000006,
ActionPrevious = 0x00000007,
}
class AndroidInputMethod<TView> : ITextInputMethodImpl, IAndroidInputMethod
where TView : View, IInitEditorInfo
{
private readonly TView _host;
private readonly InputMethodManager _imm;
private ITextInputMethodClient _client;
private AvaloniaInputConnection _inputConnection;
public AndroidInputMethod(TView host)
{
@ -27,6 +52,14 @@ namespace Avalonia.Android
_host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListener(_host));
}
public View View => _host;
public bool IsActive => Client != null;
public ITextInputMethodClient Client => _client;
public InputMethodManager IMM => _imm;
public void Reset()
{
_imm.RestartInput(_host);
@ -34,26 +67,57 @@ namespace Avalonia.Android
public void SetClient(ITextInputMethodClient client)
{
var active = client is { };
if (active)
if(client is null)
{
_inputConnection?.SetComposingText("", 0);
}
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
Reset();
_client = client;
if (IsActive)
{
_client.SurroundingTextChanged += SurroundingTextChanged;
_host.RequestFocus();
Reset();
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
}
else
{
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
}
}
private void SurroundingTextChanged(object sender, EventArgs e)
{
if (IsActive)
{
var surroundingText = Client.SurroundingText;
_inputConnection.SurroundingText = surroundingText;
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
public void SetCursorRect(Rect rect)
{
}
public void SetOptions(TextInputOptions options)
{
_host.InitEditorInfo((outAttrs) =>
_host.InitEditorInfo((topLevel, outAttrs) =>
{
_inputConnection = new AvaloniaInputConnection(topLevel, this);
outAttrs.InputType = options.ContentType switch
{
TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress,
@ -73,7 +137,20 @@ namespace Avalonia.Android
if (options.Multiline)
outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine;
outAttrs.ImeOptions = options.ReturnKeyType switch
{
TextInputReturnKeyType.Return => ImeFlags.NoEnterAction,
TextInputReturnKeyType.Go => (ImeFlags)CustomImeFlags.ActionGo,
TextInputReturnKeyType.Send => (ImeFlags)CustomImeFlags.ActionSend,
TextInputReturnKeyType.Search => (ImeFlags)CustomImeFlags.ActionSearch,
TextInputReturnKeyType.Next => (ImeFlags)CustomImeFlags.ActionNext,
TextInputReturnKeyType.Previous => (ImeFlags)CustomImeFlags.ActionPrevious,
_ => (ImeFlags)CustomImeFlags.ActionDone
};
outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
return _inputConnection;
});
}

7
src/Android/Avalonia.Android/IInitEditorInfo.cs

@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
namespace Avalonia.Android
{
interface IInitEditorInfo
internal interface IInitEditorInfo
{
void InitEditorInfo(Action<EditorInfo> init);
void InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init);
}
}

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

@ -3,13 +3,20 @@ using System.Collections.Generic;
using Android.Content;
using Android.Graphics;
using Android.Media.TV;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Android.Widget;
using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
@ -21,6 +28,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -32,7 +41,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly ITextInputMethodImpl _textInputMethod;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -148,13 +157,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private readonly TopLevelImpl _tl;
private Size _oldSize;
public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
{
_tl = tl;
if (placeOnTop)
SetZOrderOnTop(true);
}
public TopLevelImpl TopLevelImpl => _tl;
protected override void Draw()
{
_tl.Draw();
@ -187,7 +199,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return res != null ? res.Value : baseResult;
}
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
@ -206,9 +217,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return true;
}
private Action<EditorInfo> _initEditorInfo;
private Func<TopLevelImpl, EditorInfo, IInputConnection> _initEditorInfo;
public void InitEditorInfo(Action<EditorInfo> init)
public void InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init)
{
_initEditorInfo = init;
}
@ -216,9 +227,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs)
{
if (_initEditorInfo != null)
_initEditorInfo(outAttrs);
return base.OnCreateInputConnection(outAttrs);
{
return _initEditorInfo(_tl, outAttrs);
}
return null;
}
}
@ -248,5 +261,142 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
throw new NotImplementedException();
}
}
internal class AvaloniaInputConnection : BaseInputConnection
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
}
public TextInputMethodSurroundingText SurroundingText { get; set; }
public string ComposingText { get; private set; }
public ComposingRegion ComposingRegion { get; private set; }
public bool IsComposing { get; private set; }
public override bool SetComposingRegion(int start, int end)
{
//System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}");
ComposingRegion = new ComposingRegion(start, end);
return base.SetComposingRegion(start, end);
}
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{
var composingText = text.ToString();
ComposingText = composingText;
IsComposing = true;
_inputMethod.Client.SetPreeditText(ComposingText);
return base.SetComposingText(text, newCursorPosition);
}
public override bool FinishComposingText()
{
IsComposing = false;
ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
return base.FinishComposingText();
}
public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
{
if (!string.IsNullOrEmpty(SurroundingText.Text))
{
var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
var end = System.Math.Min(start + length, SurroundingText.CursorOffset);
var text = SurroundingText.Text.Substring(start, end - start);
//System.Diagnostics.Debug.WriteLine($"Text Before: {text}");
return new Java.Lang.String(text);
}
return null;
}
public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
{
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);
//System.Diagnostics.Debug.WriteLine($"Text After: {text}");
return new Java.Lang.String(text);
}
return null;
}
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
var committedText = text.ToString();
_inputMethod.Client.SetPreeditText(null);
_inputMethod.Client.SelectInSurroundingText(ComposingRegion.Start, ComposingRegion.End);
var time = DateTime.Now.TimeOfDay;
var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText);
_topLevel.Input(rawTextEvent);
return base.CommitText(text, newCursorPosition);
}
public override bool DeleteSurroundingText(int beforeLength, int afterLength)
{
_inputMethod.Client.SelectInSurroundingText(beforeLength, afterLength);
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
return base.DeleteSurroundingText(beforeLength, afterLength);
}
public override bool SetSelection(int start, int end)
{
_inputMethod.Client.SelectInSurroundingText(start, end);
return base.SetSelection(start, end);
}
}
public readonly struct ComposingRegion
{
private readonly int _start = -1;
private readonly int _end = -1;
public ComposingRegion(int start, int end)
{
_start = start;
_end = end;
}
public int Start => _start;
public int End => _end;
}
}

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

@ -28,7 +28,7 @@ namespace Avalonia.Input.TextInput
/// <summary>
/// Sets the non-committed input string
/// </summary>
void SetPreeditText(string text);
void SetPreeditText(string? text);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
@ -41,14 +41,8 @@ namespace Avalonia.Input.TextInput
/// Should be fired when surrounding text changed
/// </summary>
event EventHandler? SurroundingTextChanged;
/// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
/// </summary>
string? TextBeforeCursor { get; }
/// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry
/// </summary>
string? TextAfterCursor { get; }
void SelectInSurroundingText(int start, int end);
}
public struct TextInputMethodSurroundingText

36
src/Avalonia.Base/Input/TextInput/TextInputOptions.cs

@ -7,6 +7,7 @@ public class TextInputOptions
var result = new TextInputOptions
{
ContentType = GetContentType(avaloniaObject),
ReturnKeyType = GetReturnKeyType(avaloniaObject),
Multiline = GetMultiline(avaloniaObject),
AutoCapitalization = GetAutoCapitalization(avaloniaObject),
IsSensitive = GetIsSensitive(avaloniaObject),
@ -53,6 +54,41 @@ public class TextInputOptions
/// </summary>
public TextInputContentType ContentType { get; set; }
/// <summary>
/// Defines the <see cref="ReturnKeyType"/> property.
/// </summary>
public static readonly AttachedProperty<TextInputReturnKeyType> ReturnKeyTypeProperty =
AvaloniaProperty.RegisterAttached<TextInputOptions, StyledElement, TextInputReturnKeyType>(
"ReturnKeyType",
defaultValue: TextInputReturnKeyType.Default,
inherits: true);
/// <summary>
/// Sets the value of the attached <see cref="ReturnKeyTypeProperty"/> on a control.
/// </summary>
/// <param name="avaloniaObject">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetReturnKeyType(StyledElement avaloniaObject, TextInputReturnKeyType value)
{
avaloniaObject.SetValue(ReturnKeyTypeProperty, value);
}
/// <summary>
/// Gets the value of the attached <see cref="ReturnKeyTypeProperty"/>.
/// </summary>
/// <param name="avaloniaObject">The target.</param>
/// <returns>TextInputReturnKeyType</returns>
public static TextInputReturnKeyType GetReturnKeyType(StyledElement avaloniaObject)
{
return avaloniaObject.GetValue(ReturnKeyTypeProperty);
}
/// <summary>
/// Determines what the Return key says and how it behaves.
/// </summary>
public TextInputReturnKeyType ReturnKeyType { get; set; }
/// <summary>
/// Defines the <see cref="Multiline"/> property.
/// </summary>

13
src/Avalonia.Base/Input/TextInput/TextInputReturnKeyType.cs

@ -0,0 +1,13 @@
namespace Avalonia.Input.TextInput;
public enum TextInputReturnKeyType
{
Default,
Return,
Done,
Go,
Send,
Search,
Next,
Previous
}

2
src/Avalonia.Base/Media/FontFamily.cs

@ -128,7 +128,7 @@ namespace Avalonia.Media
default:
{
throw new ArgumentException("Specified family is not supported.");
return new FontFamilyIdentifier(name, null);
}
}
}

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

@ -9,7 +9,6 @@ using Avalonia.VisualTree;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls.Presenters
{
@ -34,7 +33,7 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
o => o.SelectionStart,
@ -44,7 +43,7 @@ namespace Avalonia.Controls.Presenters
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
@ -54,6 +53,15 @@ namespace Avalonia.Controls.Presenters
o => o.Text,
(o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="PreeditText"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, string?> PreeditTextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
nameof(PreeditText),
o => o.PreeditText,
(o, v) => o.PreeditText = v);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -71,7 +79,7 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
@ -90,6 +98,8 @@ namespace Avalonia.Controls.Presenters
private CharacterHit _lastCharacterHit;
private Rect _caretBounds;
private Point _navigationPosition;
private string? _preeditText;
private CharacterHit _compositionStartHit = new CharacterHit(-1);
static TextPresenter()
{
@ -104,7 +114,7 @@ namespace Avalonia.Controls.Presenters
}
public event EventHandler? CaretBoundsChanged;
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
@ -124,6 +134,12 @@ namespace Avalonia.Controls.Presenters
set => SetAndRaise(TextProperty, ref _text, value);
}
public string? PreeditText
{
get => _preeditText;
set => SetAndRaise(PreeditTextProperty, ref _preeditText, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -186,7 +202,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(TextWrappingProperty);
set => SetValue(TextWrappingProperty, value);
}
/// <summary>
/// Gets or sets the line height. By default, this is set to <see cref="double.NaN"/>, which determines the appropriate height automatically.
/// </summary>
@ -216,11 +232,11 @@ namespace Avalonia.Controls.Presenters
{
return _textLayout;
}
_textLayout = CreateTextLayout();
UpdateCaret(_lastCharacterHit);
UpdateCaret(_lastCharacterHit, false);
return _textLayout;
}
}
@ -269,7 +285,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(CaretBrushProperty);
set => SetValue(CaretBrushProperty, value);
}
public int SelectionStart
{
get
@ -297,7 +313,7 @@ namespace Avalonia.Controls.Presenters
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
}
}
protected override bool BypassFlowDirectionPolicies => true;
/// <summary>
@ -314,9 +330,9 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground;
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: LineHeight);
return textLayout;
@ -382,7 +398,7 @@ namespace Avalonia.Controls.Presenters
{
return;
}
var caretBrush = CaretBrush?.ToImmutable();
if (caretBrush is null)
@ -407,13 +423,13 @@ namespace Avalonia.Controls.Presenters
context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
}
private (Point, Point) GetCaretPoints()
{
var x = Math.Floor(_caretBounds.X) + 0.5;
var y = Math.Floor(_caretBounds.Y) + 0.5;
var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
var caretIndex = _lastCharacterHit.FirstCharacterIndex + _lastCharacterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0);
var textLine = TextLayout.TextLines[lineIndex];
@ -422,7 +438,7 @@ namespace Avalonia.Controls.Presenters
{
x -= 1;
}
return (new Point(x, y), new Point(x, b));
}
@ -479,6 +495,18 @@ namespace Avalonia.Controls.Presenters
}
}
private string? GetText()
{
if (!string.IsNullOrEmpty(_preeditText))
{
var text = _text?.Substring(0, _caretIndex) + _preeditText + _text?.Substring(_caretIndex);
return text;
}
return _text;
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -487,7 +515,7 @@ namespace Avalonia.Controls.Presenters
{
TextLayout result;
var text = Text;
var text = GetText();
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
@ -498,14 +526,31 @@ namespace Avalonia.Controls.Presenters
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
if (length > 0 && SelectionForegroundBrush != null)
var foreground = Foreground;
if (!string.IsNullOrEmpty(_preeditText))
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
textStyleOverrides = new[]
{
new ValueSpan<TextRunProperties>(start, length,
preeditHighlight
};
}
else
{
if (length > 0 && SelectionForegroundBrush != null)
{
textStyleOverrides = new[]
{
new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: SelectionForegroundBrush))
};
};
}
}
if (PasswordChar != default(char) && !RevealPassword)
@ -524,11 +569,10 @@ namespace Avalonia.Controls.Presenters
protected virtual void InvalidateTextLayout()
{
_textLayout = null;
InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
_constraint = availableSize;
@ -573,7 +617,7 @@ namespace Avalonia.Controls.Presenters
private void CaretTimerTick(object? sender, EventArgs e)
{
_caretBlink = !_caretBlink;
InvalidateVisual();
}
@ -583,7 +627,7 @@ namespace Avalonia.Controls.Presenters
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition)
@ -603,8 +647,8 @@ namespace Avalonia.Controls.Presenters
_navigationPosition = _caretBounds.Position;
CaretChanged();
}
}
public void MoveCaretToPoint(Point point)
{
var hit = TextLayout.HitTestPoint(point);
@ -635,7 +679,7 @@ namespace Avalonia.Controls.Presenters
}
var textLine = TextLayout.TextLines[lineIndex];
currentY += textLine.Height;
}
else
@ -651,9 +695,9 @@ namespace Avalonia.Controls.Presenters
}
var navigationPosition = _navigationPosition;
MoveCaretToPoint(new Point(currentX, currentY));
_navigationPosition = navigationPosition.WithY(_caretBounds.Y);
CaretChanged();
@ -664,11 +708,11 @@ namespace Avalonia.Controls.Presenters
if (Text is null)
{
return default;
}
}
var characterHit = _lastCharacterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
if (lineIndex < 0)
@ -690,11 +734,11 @@ namespace Avalonia.Controls.Presenters
{
characterHit = new CharacterHit(caretIndex);
}
if (caretIndex >= Text.Length)
{
characterHit = new CharacterHit(Text.Length);
break;
}
@ -706,10 +750,10 @@ namespace Avalonia.Controls.Presenters
if (caretIndex <= CaretIndex)
{
lineIndex++;
continue;
}
break;
}
}
@ -736,7 +780,7 @@ namespace Avalonia.Controls.Presenters
return characterHit;
}
public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
{
if (FlowDirection == FlowDirection.RightToLeft)
@ -755,12 +799,12 @@ namespace Avalonia.Controls.Presenters
CaretChanged();
}
private void UpdateCaret(CharacterHit characterHit)
private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true)
{
_lastCharacterHit = characterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0);
var textLine = TextLayout.TextLines[lineIndex];
var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
@ -775,7 +819,7 @@ namespace Avalonia.Controls.Presenters
}
var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height);
if (caretBounds != _caretBounds)
{
_caretBounds = caretBounds;
@ -783,7 +827,10 @@ namespace Avalonia.Controls.Presenters
CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
}
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
if (updateCaretIndex)
{
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
}
}
internal Rect GetCursorRectangle()
@ -796,39 +843,91 @@ namespace Avalonia.Controls.Presenters
base.OnDetachedFromVisualTree(e);
_caretTimer.Stop();
_caretTimer.Tick -= CaretTimerTick;
}
protected void OnPreeditTextChanged(string? oldValue, string? newValue)
{
InvalidateTextLayout();
if (string.IsNullOrEmpty(newValue))
{
if (!string.IsNullOrEmpty(oldValue))
{
var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + newValue?.Length ?? 0;
var characterHit = GetCharacterHitFromTextPosition(textPosition);
UpdateCaret(characterHit, true);
}
_compositionStartHit = new CharacterHit(-1);
}
else
{
if (_compositionStartHit.FirstCharacterIndex == -1)
{
_compositionStartHit = _lastCharacterHit;
}
}
if (_compositionStartHit.FirstCharacterIndex != -1)
{
var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + newValue?.Length ?? 0;
var characterHit = GetCharacterHitFromTextPosition(textPosition);
UpdateCaret(characterHit, false);
}
}
private CharacterHit GetCharacterHitFromTextPosition(int textPosition)
{
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true);
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1));
return characterHit;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof (Foreground):
case nameof (FontSize):
case nameof (FontStyle):
case nameof (FontWeight):
case nameof (FontFamily):
case nameof (FontStretch):
case nameof(PreeditText):
{
OnPreeditTextChanged(change.OldValue as string, change.NewValue as string);
break;
}
case nameof(Foreground):
case nameof(FontSize):
case nameof(FontStyle):
case nameof(FontWeight):
case nameof(FontFamily):
case nameof(FontStretch):
case nameof (Text):
case nameof (TextAlignment):
case nameof (TextWrapping):
case nameof(Text):
case nameof(TextAlignment):
case nameof(TextWrapping):
case nameof (SelectionStart):
case nameof (SelectionEnd):
case nameof (SelectionForegroundBrush):
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):
case nameof (PasswordChar):
case nameof (RevealPassword):
case nameof(PasswordChar):
case nameof(RevealPassword):
case nameof(FlowDirection):
{
InvalidateTextLayout();
break;
}
{
InvalidateTextLayout();
break;
}
}
}
}

9
src/Avalonia.Controls/TextBox.cs

@ -831,6 +831,11 @@ namespace Avalonia.Controls
return;
}
if (!string.IsNullOrEmpty(_presenter.PreeditText))
{
return;
}
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
var movement = false;
@ -1142,7 +1147,7 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (_presenter == null)
if (_presenter == null || !string.IsNullOrEmpty(_presenter.PreeditText))
{
return;
}
@ -1482,7 +1487,7 @@ namespace Avalonia.Controls
SelectionEnd = Text?.Length ?? 0;
}
private bool DeleteSelection(bool raiseTextChanged = true)
internal bool DeleteSelection(bool raiseTextChanged = true)
{
if (IsReadOnly)
return true;

155
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,17 +1,24 @@
using System;
using System.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Media.TextFormatting;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal class TextBoxTextInputMethodClient : ITextInputMethodClient
{
private InputElement? _parent;
private TextBox? _parent;
private TextPresenter? _presenter;
public IVisual TextViewVisual => _presenter!;
public bool SupportsPreedit => true;
public bool SupportsSurroundingText => true;
public Rect CursorRectangle
{
get
@ -20,41 +27,128 @@ namespace Avalonia.Controls
{
return default;
}
var transform = _presenter.TransformToVisual(_parent);
if (transform == null)
{
return default;
}
var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
return rect;
}
}
public event EventHandler? CursorRectangleChanged;
public IVisual TextViewVisual => _presenter!;
public TextInputMethodSurroundingText SurroundingText
{
get
{
if(_presenter is null || _parent is null)
{
return default;
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
var lineText = GetTextLineText(textLine);
var anchorOffset = Math.Max(0, _parent.SelectionStart - lineStart);
var cursorOffset = Math.Max(0, _presenter.SelectionEnd - lineStart);
return new TextInputMethodSurroundingText
{
Text = lineText ?? "",
AnchorOffset = anchorOffset,
CursorOffset = cursorOffset
};
}
}
private static string GetTextLineText(TextLine textLine)
{
var builder = StringBuilderCache.Acquire(textLine.Length);
foreach (var run in textLine.TextRuns)
{
if(run.Text.Length > 0)
{
#if NET6_0
builder.Append(run.Text.Span);
#else
builder.Append(run.Text.Span.ToArray());
#endif
}
}
var lineText = builder.ToString();
StringBuilderCache.Release(builder);
return lineText;
}
public event EventHandler? TextViewVisualChanged;
public bool SupportsPreedit => false;
public void SetPreeditText(string text) => throw new NotSupportedException();
public bool SupportsSurroundingText => false;
public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException();
public event EventHandler? SurroundingTextChanged { add { } remove { } }
public string? TextBeforeCursor => null;
public string? TextAfterCursor => null;
public event EventHandler? CursorRectangleChanged;
public event EventHandler? SurroundingTextChanged;
public void SetPreeditText(string? text)
{
if (_presenter == null)
{
return;
}
_presenter.PreeditText = text;
}
public void SelectInSurroundingText(int start, int end)
{
if(_parent is null ||_presenter is null)
{
return;
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
private void OnCaretBoundsChanged(object? sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
public void SetPresenter(TextPresenter? presenter, InputElement? parent)
var selectionStart = lineStart + start;
var selectionEnd = lineStart + end;
_parent.SelectionStart = selectionStart;
_parent.SelectionEnd = selectionEnd;
}
public void SetPresenter(TextPresenter? presenter, TextBox? parent)
{
if(_parent != null)
{
_parent.PropertyChanged -= OnParentPropertyChanged;
}
_parent = parent;
if(_parent != null)
{
_parent.PropertyChanged += OnParentPropertyChanged;
}
if (_presenter != null)
{
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
_presenter.PreeditText = null;
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
}
_presenter = presenter;
@ -63,9 +157,30 @@ namespace Avalonia.Controls
{
_presenter.CaretBoundsChanged += OnCaretBoundsChanged;
}
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
OnCaretBoundsChanged(this, EventArgs.Empty);
}
private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if(e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
{
if (SupportsSurroundingText)
{
SurroundingTextChanged?.Invoke(this, e);
}
}
}
private void OnCaretBoundsChanged(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
CursorRectangleChanged?.Invoke(this, e);
}, DispatcherPriority.Input);
}
}
}

7
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -40,6 +40,7 @@ namespace Avalonia.FreeDesktop.DBusIme
private PixelRect? _lastReportedRect;
private double _scaling = 1;
private PixelPoint _windowPosition;
private ITextInputMethodClient? _client;
protected bool IsConnected => _currentName != null;
@ -51,6 +52,10 @@ namespace Avalonia.FreeDesktop.DBusIme
Watch();
}
public ITextInputMethodClient Client => _client;
public bool IsActive => _client != null;
async void Watch()
{
foreach (var name in _knownNames)
@ -202,7 +207,7 @@ namespace Avalonia.FreeDesktop.DBusIme
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
_controlActive = client is { };
_client = client;
UpdateActive();
}

7
src/Avalonia.X11/X11Window.Xim.cs

@ -15,11 +15,16 @@ namespace Avalonia.X11
private readonly X11Window _parent;
private bool _controlActive, _windowActive, _imeActive;
private Rect? _queuedCursorRect;
private ITextInputMethodClient? _client;
public XimInputMethod(X11Window parent)
{
_parent = parent;
}
public ITextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
public void SetCursorRect(Rect rect)
{
@ -59,7 +64,7 @@ namespace Avalonia.X11
public void SetClient(ITextInputMethodClient client)
{
_controlActive = client is { };
_client = client;
UpdateActive();
}

2
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@ -32,7 +32,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />

23
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor

@ -16,8 +16,8 @@
<div id="nativeControlsContainer" @ref="_nativeControlsContainer" />
<input id="inputElement" @ref="_inputElement" type="text" @oninput="OnInput"
onpaste="return false;"
<input id="inputElement" @ref="_inputElement" type="text"
spellcheck="false" onpaste="return false;"
oncopy="return false;"
oncut="return false;"
autocapitalize="none"/>
@ -48,13 +48,20 @@
}
#inputElement {
opacity: 0.0;
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
padding: 0;
margin: 0;
position: absolute;
height: 20px;
z-index: 1000;
overflow: hidden;
caret-color: transparent;
border-top-style: hidden;
border-bottom-style: hidden;
border-right-style: hidden;
border-left-style: hidden;
outline: none;
background: transparent;
color: transparent;
}
</style>

108
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -1,3 +1,4 @@
using System.Diagnostics;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
@ -11,6 +12,7 @@ using Avalonia.Web.Blazor.Interop;
using Avalonia.Web.Blazor.Interop.Storage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
@ -31,8 +33,8 @@ namespace Avalonia.Web.Blazor
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
private AvaloniaModule? _avaloniaModule = null;
private InputHelperInterop? _inputHelper = null;
private InputHelperInterop? _canvasHelper = null;
private InputHelperInterop? _containerHelper = null;
private FocusHelperInterop? _canvasHelper = null;
private FocusHelperInterop? _containerHelper = null;
private NativeControlHostInterop? _nativeControlHost = null;
private StorageProviderInterop? _storageProvider = null;
private ElementReference _htmlCanvas;
@ -49,6 +51,9 @@ namespace Avalonia.Web.Blazor
private bool _useGL;
private bool _inputElementFocused;
private ITextInputMethodClient? _client;
[Inject] private IJSRuntime Js { get; set; } = null!;
public AvaloniaView()
@ -64,6 +69,12 @@ namespace Avalonia.Web.Blazor
}
public bool KeyPreventDefault { get; set; }
public ITextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
public bool IsComposing { get; private set; }
internal INativeControlHostImpl GetNativeControlHostImpl()
{
@ -223,20 +234,6 @@ namespace Avalonia.Web.Blazor
}
}
private void OnInput(ChangeEventArgs e)
{
if (e.Value != null)
{
var inputData = e.Value.ToString();
if (inputData != null)
{
_topLevelImpl.RawTextEvent(inputData);
}
}
_inputHelper?.Clear();
}
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
@ -247,10 +244,13 @@ namespace Avalonia.Web.Blazor
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
_avaloniaModule = await AvaloniaModule.ImportAsync(Js);
_canvasHelper = new FocusHelperInterop(_avaloniaModule, _htmlCanvas);
_containerHelper = new FocusHelperInterop(_avaloniaModule, _containerElement);
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
_canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas);
_containerHelper = new InputHelperInterop(_avaloniaModule, _containerElement);
_inputHelper.CompositionEvent += InputHelperOnCompositionEvent;
_inputHelper.InputEvent += InputHelperOnInputEvent;
HideIme();
_canvasHelper.SetCursor("default");
@ -323,6 +323,42 @@ namespace Avalonia.Web.Blazor
}
}
private void InputHelperOnInputEvent(object? sender, WebInputEventArgs e)
{
if (IsComposing)
{
return;
}
_topLevelImpl.RawTextEvent(e.Data);
e.Handled = true;
}
private void InputHelperOnCompositionEvent(object? sender, WebCompositionEventArgs e)
{
if(_client == null)
{
return;
}
switch (e.Type)
{
case WebCompositionEventArgs.WebCompositionEventType.Start:
_client.SetPreeditText(null);
IsComposing = true;
break;
case WebCompositionEventArgs.WebCompositionEventType.Update:
_client.SetPreeditText(e.Data);
break;
case WebCompositionEventArgs.WebCompositionEventType.End:
IsComposing = false;
_client.SetPreeditText(null);
_topLevelImpl.RawTextEvent(e.Data);
break;
}
}
private void OnRenderFrame()
{
if (_useGL && (_jsGlInfo == null))
@ -402,15 +438,29 @@ namespace Avalonia.Web.Blazor
return;
}
if(_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
if(client != null)
{
client.SurroundingTextChanged += SurroundingTextChanged;
}
_inputHelper.Clear();
var active = client is { };
_client = client;
if (active)
if (IsActive && _client != null)
{
_inputHelper.Show();
_inputElementFocused = true;
_inputHelper.Focus();
var surroundingText = _client.SurroundingText;
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
else
{
@ -419,8 +469,23 @@ namespace Avalonia.Web.Blazor
}
}
private void SurroundingTextChanged(object? sender, EventArgs e)
{
if(_client != null)
{
var surroundingText = _client.SurroundingText;
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
public void SetCursorRect(Rect rect)
{
_inputHelper?.Focus();
var bounds = new PixelRect((int)rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height);
_inputHelper?.SetBounds(bounds, _client?.SurroundingText.CursorOffset ?? 0);
_inputHelper?.Focus();
}
public void SetOptions(TextInputOptions options)
@ -430,6 +495,7 @@ namespace Avalonia.Web.Blazor
public void Reset()
{
_inputHelper?.Clear();
_inputHelper?.SetSurroundingText("", 0, 0);
}
}
}

61
src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs

@ -17,4 +17,65 @@ namespace Avalonia.Web.Blazor.Interop
[JSInvokable]
public void Invoke() => action?.Invoke();
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T>
{
private readonly Action<T> action;
public ActionHelper(Action<T> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T param1) => action?.Invoke(param1);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T1, T2>
{
private readonly Action<T1, T2> action;
public ActionHelper(Action<T1, T2> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T1 p1, T2 p2) => action?.Invoke(p1, p2);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T1, T2, T3>
{
private readonly Action<T1, T2, T3> action;
public ActionHelper(Action<T1, T2, T3> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T1 p1, T2 p2, T3 p3) => action?.Invoke(p1, p2, p3);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T1, T2, T3, T4>
{
private readonly Action<T1, T2, T3, T4> action;
public ActionHelper(Action<T1, T2, T3, T4> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T1 p1, T2 p2, T3 p3, T4 p4) => action?.Invoke(p1, p2, p3, p4);
}
}

6
src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs

@ -9,15 +9,15 @@ namespace Avalonia.Web.Blazor.Interop
private const string GetDpiSymbol = "DpiWatcher.getDpi";
private event Action<double>? callbacksEvent;
private readonly FloatFloatActionHelper _callbackHelper;
private readonly ActionHelper<float, float> _callbackHelper;
private readonly AvaloniaModule _module;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
private DotNetObjectReference<ActionHelper<float, float>>? callbackReference;
public DpiWatcherInterop(AvaloniaModule module, Action<double>? callback = null)
{
_module = module;
_callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
_callbackHelper = new ActionHelper<float, float>((o, n) => callbacksEvent?.Invoke(n));
if (callback != null)
Subscribe(callback);

20
src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs

@ -1,20 +0,0 @@
using System;
using System.ComponentModel;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class FloatFloatActionHelper
{
private readonly Action<float, float> action;
public FloatFloatActionHelper(Action<float, float> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(float width, float height) => action?.Invoke(width, height);
}
}

22
src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs

@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Components;
namespace Avalonia.Web.Blazor.Interop;
internal class FocusHelperInterop
{
private const string FocusSymbol = "FocusHelper.focus";
private const string SetCursorSymbol = "FocusHelper.setCursor";
private readonly AvaloniaModule _module;
private readonly ElementReference _inputElement;
public FocusHelperInterop(AvaloniaModule module, ElementReference inputElement)
{
_module = module;
_inputElement = inputElement;
}
public void Focus() => _module.Invoke(FocusSymbol, _inputElement);
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind);
}

98
src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs

@ -1,7 +1,50 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class WebCompositionEventArgs : EventArgs
{
public enum WebCompositionEventType
{
Start,
Update,
End
}
public WebCompositionEventArgs(string type, string data)
{
Type = type switch
{
"compositionstart" => WebCompositionEventType.Start,
"compositionupdate" => WebCompositionEventType.Update,
"compositionend" => WebCompositionEventType.End,
_ => Type
};
Data = data;
}
public WebCompositionEventType Type { get; }
public string Data { get; }
}
internal class WebInputEventArgs
{
public WebInputEventArgs(string type, string data)
{
Type = type;
Data = data;
}
public string Type { get; }
public string Data { get; }
public bool Handled { get; set; }
}
internal class InputHelperInterop
{
private const string ClearSymbol = "InputHelper.clear";
@ -9,14 +52,45 @@ namespace Avalonia.Web.Blazor.Interop
private const string SetCursorSymbol = "InputHelper.setCursor";
private const string HideSymbol = "InputHelper.hide";
private const string ShowSymbol = "InputHelper.show";
private const string StartSymbol = "InputHelper.start";
private const string SetSurroundingTextSymbol = "InputHelper.setSurroundingText";
private const string SetBoundsSymbol = "InputHelper.setBounds";
private readonly AvaloniaModule _module;
private readonly ElementReference _inputElement;
private readonly ActionHelper<string, string> _compositionAction;
private readonly ActionHelper<string, string> _inputAction;
private DotNetObjectReference<ActionHelper<string, string>>? compositionActionReference;
private DotNetObjectReference<ActionHelper<string, string>>? inputActionReference;
public InputHelperInterop(AvaloniaModule module, ElementReference inputElement)
{
_module = module;
_inputElement = inputElement;
_compositionAction = new ActionHelper<string, string>(OnCompositionEvent);
_inputAction = new ActionHelper<string, string>(OnInputEvent);
Start();
}
public event EventHandler<WebCompositionEventArgs>? CompositionEvent;
public event EventHandler<WebInputEventArgs>? InputEvent;
private void OnCompositionEvent(string type, string data)
{
Console.WriteLine($"CompositionEvent Handler Helper {CompositionEvent == null} ");
CompositionEvent?.Invoke(this, new WebCompositionEventArgs(type, data));
}
private void OnInputEvent(string type, string data)
{
Console.WriteLine($"InputEvent Handler Helper {InputEvent == null} ");
var args = new WebInputEventArgs(type, data);
InputEvent?.Invoke(this, args);
}
public void Clear() => _module.Invoke(ClearSymbol, _inputElement);
@ -28,5 +102,29 @@ namespace Avalonia.Web.Blazor.Interop
public void Hide() => _module.Invoke(HideSymbol, _inputElement);
public void Show() => _module.Invoke(ShowSymbol, _inputElement);
private void Start()
{
if(compositionActionReference != null)
{
return;
}
compositionActionReference = DotNetObjectReference.Create(_compositionAction);
inputActionReference = DotNetObjectReference.Create(_inputAction);
_module.Invoke(StartSymbol, _inputElement, compositionActionReference, inputActionReference);
}
public void SetSurroundingText(string text, int start, int end)
{
_module.Invoke(SetSurroundingTextSymbol, _inputElement, text, start, end);
}
public void SetBounds(PixelRect bounds, int caret)
{
_module.Invoke(SetBoundsSymbol, _inputElement, bounds.X, bounds.Y, bounds.Width, bounds.Height, caret);
}
}
}

6
src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs

@ -12,16 +12,16 @@ namespace Avalonia.Web.Blazor.Interop
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlElement;
private readonly string _htmlElementId;
private readonly FloatFloatActionHelper _callbackHelper;
private readonly ActionHelper<float, float> _callbackHelper;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
private DotNetObjectReference<ActionHelper<float, float>>? callbackReference;
public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action<SKSize> callback)
{
_module = module;
_htmlElement = element;
_htmlElementId = element.Id;
_callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
_callbackHelper = new ActionHelper<float, float>((x, y) => callback(new SKSize(x, y)));
}
public void Dispose() => Stop();

2
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts

@ -1,5 +1,7 @@
export { DpiWatcher } from "./Avalonia/DpiWatcher"
export { InputHelper } from "./Avalonia/InputHelper"
export { FocusHelper } from "./Avalonia/FocusHelper"
export { NativeControlHost } from "./Avalonia/NativeControlHost"
export { SizeWatcher } from "./Avalonia/SizeWatcher"
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas"
export { CaretHelper } from "./Avalonia/CaretHelper"

149
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts

@ -0,0 +1,149 @@
// Based on https://github.com/component/textarea-caret-position/blob/master/index.js
export class CaretHelper {
public static getCaretCoordinates(
element: HTMLInputElement | HTMLTextAreaElement,
position: number,
options?: { debug: boolean }
) {
if (!isBrowser) {
throw new Error(
"textarea-caret-position#getCaretCoordinates should only be called in a browser"
);
}
const debug = (options && options.debug) || false;
if (debug) {
const el = document.querySelector(
"#input-textarea-caret-position-mirror-div"
);
if (el) el.parentNode?.removeChild(el);
}
// The mirror div will replicate the textarea's style
const div = document.createElement("div");
div.id = "input-textarea-caret-position-mirror-div";
document.body.appendChild(div);
const style = div.style;
const computed = window.getComputedStyle
? window.getComputedStyle(element)
: ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9
const isInput = element.nodeName === "INPUT";
// Default textarea styles
style.whiteSpace = "pre-wrap";
if (!isInput) style.wordWrap = "break-word"; // only for textarea-s
// Position off-screen
style.position = "absolute"; // required to return coordinates properly
if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach((prop: string) => {
if (isInput && prop === "lineHeight") {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
const height = parseInt(computed.height);
const outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
const targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
} else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = "0";
}
} else {
style.lineHeight = computed.height;
}
} else {
(style as any)[prop] = (computed as any)[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height))
style.overflowY = "scroll";
} else {
style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0");
const span = document.createElement("span");
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
const coordinates = {
top: span.offsetTop + parseInt(computed["borderTopWidth"]),
left: span.offsetLeft + parseInt(computed["borderLeftWidth"]),
height: parseInt(computed["lineHeight"]),
};
if (debug) {
span.style.backgroundColor = "#aaa";
} else {
document.body.removeChild(div);
}
return coordinates;
}
}
var properties = [
"direction", // RTL support
"boxSizing",
"width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
"height",
"overflowX",
"overflowY", // copy the scrollbar for IE
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderStyle",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
"fontStyle",
"fontVariant",
"fontWeight",
"fontStretch",
"fontSize",
"fontSizeAdjust",
"lineHeight",
"fontFamily",
"textAlign",
"textTransform",
"textIndent",
"textDecoration", // might not make a difference, but better be safe
"letterSpacing",
"wordSpacing",
"tabSize",
"MozTabSize",
];
const isBrowser = typeof window !== "undefined";
const isFirefox = isBrowser && (window as any).mozInnerScreenX != null;

9
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts

@ -0,0 +1,9 @@
export class FocusHelper {
public static focus(inputElement: HTMLElement) {
inputElement.focus();
}
public static setCursor(inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
}

81
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts

@ -1,25 +1,42 @@
export class InputHelper {
public static clear(inputElement: HTMLInputElement) {
inputElement.value = "";
}
import {CaretHelper} from "./CaretHelper";
export class InputHelper {
static inputCallback?: DotNet.DotNetObject;
static compositionCallback?: DotNet.DotNetObject
public static isInputElement( element : HTMLInputElement | HTMLElement ) : element is HTMLInputElement {
return ( element as HTMLInputElement).setSelectionRange !== undefined;
}
public static start(inputElement: HTMLInputElement, compositionCallback: DotNet.DotNetObject, inputCallback: DotNet.DotNetObject)
{
InputHelper.compositionCallback = compositionCallback;
inputElement.addEventListener('compositionstart', InputHelper.onCompositionEvent);
inputElement.addEventListener('compositionupdate', InputHelper.onCompositionEvent);
inputElement.addEventListener('compositionend', InputHelper.onCompositionEvent);
public static focus(inputElement: HTMLElement) {
InputHelper.inputCallback = inputCallback;
inputElement.addEventListener('input', InputHelper.onInputEvent);
}
public static clear(inputElement: HTMLInputElement) {
inputElement.value = "";
}
public static focus(inputElement: HTMLInputElement) {
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 setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) {
inputElement.style.left = (x).toFixed(0) + "px";
inputElement.style.top = (y).toFixed(0) + "px";
let {height, left, top} = CaretHelper.getCaretCoordinates(inputElement, caret);
inputElement.style.left = (x - left).toFixed(0) + "px";
inputElement.style.top = (y - top).toFixed(0) + "px";
}
public static hide(inputElement: HTMLInputElement) {
inputElement.style.display = 'none';
@ -28,4 +45,42 @@
public static show(inputElement: HTMLInputElement) {
inputElement.style.display = 'block';
}
public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) {
if (!inputElement) {
return;
}
inputElement.value = text;
inputElement.setSelectionRange(start, end);
inputElement.style.width = "20px";
inputElement.style.width = inputElement.scrollWidth + "px";
}
private static onCompositionEvent(ev: CompositionEvent)
{
if(!InputHelper.compositionCallback)
return;
switch (ev.type)
{
case "compositionstart":
case "compositionupdate":
case "compositionend":
InputHelper.compositionCallback.invokeMethod('Invoke', ev.type, ev.data);
break;
}
}
private static onInputEvent(ev: Event) {
if (!InputHelper.inputCallback)
return;
var inputEvent = ev as InputEvent;
InputHelper.inputCallback.invokeMethod('Invoke', ev.type, inputEvent.data);
}
}

8
src/Windows/Avalonia.Win32/Input/Imm32CaretManager.cs

@ -7,14 +7,11 @@ namespace Avalonia.Win32.Input
{
private bool _isCaretCreated;
public void TryCreate(int _langId, IntPtr hwnd)
public void TryCreate(IntPtr hwnd)
{
if (!_isCaretCreated)
{
if (_langId == LANG_ZH || _langId == LANG_JA)
{
_isCaretCreated = CreateCaret(hwnd, IntPtr.Zero, 2, 10);
}
_isCaretCreated = CreateCaret(hwnd, IntPtr.Zero, 2, 2);
}
}
@ -31,6 +28,7 @@ namespace Avalonia.Win32.Input
if (_isCaretCreated)
{
DestroyCaret();
_isCaretCreated = false;
}
}

203
src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs

@ -1,4 +1,5 @@
using System;
using System.Text;
using Avalonia.Input.TextInput;
using Avalonia.Threading;
@ -12,99 +13,129 @@ namespace Avalonia.Win32.Input
class Imm32InputMethod : ITextInputMethodImpl
{
public IntPtr HWND { get; private set; }
private IntPtr _defaultImc;
private IntPtr _currentHimc;
private WindowImpl _parent;
private bool _active;
private bool _showCompositionWindow;
private ITextInputMethodClient _client;
private Imm32CaretManager _caretManager = new();
private ushort _langId;
private const int _caretMargin = 1;
public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL)
public ITextInputMethodClient Client => _client;
public bool IsActive => _client != null;
public bool IsComposing { get; set; }
public bool ShowCompositionWindow => false;
public void CreateCaret()
{
_caretManager.TryCreate(HWND);
}
public void EnableImm()
{
if (HWND != hwnd)
var himc = ImmGetContext(HWND);
if(himc == IntPtr.Zero)
{
himc = ImmCreateContext();
}
if(himc != _currentHimc)
{
_defaultImc = IntPtr.Zero;
if(_currentHimc != IntPtr.Zero)
{
DisableImm();
}
ImmAssociateContext(HWND, himc);
ImmReleaseContext(HWND, himc);
_currentHimc = himc;
_caretManager.TryCreate(HWND);
}
}
public void DisableImm()
{
_caretManager.TryDestroy();
Reset();
ImmAssociateContext(HWND, IntPtr.Zero);
_caretManager.TryDestroy();
_currentHimc = IntPtr.Zero;
}
public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL)
{
HWND = hwnd;
_parent = parent;
_active = false;
_langId = PRIMARYLANGID(LGID(HKL));
_showCompositionWindow = true;
IsComposing = false;
_parent = parent;
var langId= PRIMARYLANGID(LGID(HKL));
if(langId != _langId)
{
DisableImm();
}
_langId = langId;
EnableImm();
}
public void ClearLanguageAndWindow()
{
if (HWND != IntPtr.Zero && _defaultImc != IntPtr.Zero)
{
ImmReleaseContext(HWND, _defaultImc);
}
DisableImm();
_defaultImc = IntPtr.Zero;
HWND = IntPtr.Zero;
_parent = null;
_active = false;
_client = null;
_langId = 0;
_showCompositionWindow = false;
IsComposing = false;
}
//Dependant on CurrentThread. When Avalonia will support Multiple Dispatchers -
//every Dispatcher should have their own InputMethod.
public static Imm32InputMethod Current { get; } = new Imm32InputMethod();
public static Imm32InputMethod Current { get; } = new Imm32InputMethod();
private IntPtr DefaultImc
public void Reset()
{
get
Dispatcher.UIThread.Post(() =>
{
if (_defaultImc == IntPtr.Zero &&
HWND != IntPtr.Zero)
{
_defaultImc = ImmGetContext(HWND);
ImmReleaseContext(HWND, _defaultImc);
}
var himc = ImmGetContext(HWND);
if (_defaultImc == IntPtr.Zero)
if (IsComposing)
{
_defaultImc = ImmCreateContext();
ImmNotifyIME(himc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
IsComposing = false;
}
return _defaultImc;
}
}
public void Reset()
{
if (IsComposing)
{
Dispatcher.UIThread.Post(() =>
{
ImmNotifyIME(DefaultImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
ImmReleaseContext(HWND, DefaultImc);
IsComposing = false;
});
}
ImmReleaseContext(HWND, himc);
});
}
public void SetClient(ITextInputMethodClient client)
{
_active = client is { };
_client = client;
Dispatcher.UIThread.Post(() =>
{
if (_active)
if (IsActive)
{
if (DefaultImc != IntPtr.Zero)
{
_caretManager.TryCreate(_langId, HWND);
// Load the default IME context.
// NOTE(hbono)
// IMM ignores this call if the IME context is loaded. Therefore, we do
// not have to check whether or not the IME context is loaded.
ImmAssociateContext(HWND, _defaultImc);
}
EnableImm();
}
else
{
@ -113,14 +144,8 @@ namespace Avalonia.Win32.Input
// mouse button and selected a password input while composing a text.
// For this case, we have to complete the ongoing composition and
// clean up the resources attached to this object BEFORE DISABLING THE IME.
if (IsComposing)
{
ImmNotifyIME(DefaultImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
ImmReleaseContext(HWND, DefaultImc);
IsComposing = false;
}
ImmAssociateContext(HWND, IntPtr.Zero);
_caretManager.TryDestroy();
DisableImm();
}
});
}
@ -128,19 +153,23 @@ namespace Avalonia.Win32.Input
public void SetCursorRect(Rect rect)
{
var focused = GetActiveWindow() == HWND;
if (!focused)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
IntPtr himc = DefaultImc;
var himc = ImmGetContext(HWND);
if (himc == IntPtr.Zero)
{
return;
}
MoveImeWindow(rect, himc);
ImmReleaseContext(HWND, himc);
});
}
@ -154,8 +183,7 @@ namespace Avalonia.Win32.Input
var s = _parent?.DesktopScaling ?? 1;
var (x1, y1, x2, y2) = ((int) (p1.X * s), (int) (p1.Y * s), (int) (p2.X * s), (int) (p2.Y * s));
if (!_showCompositionWindow &&
_langId == LANG_ZH)
if (!ShowCompositionWindow && _langId == LANG_ZH)
{
// Chinese IMEs ignore function calls to ::ImmSetCandidateWindow()
// when a user disables TSF (Text Service Framework) and CUAS (Cicero
@ -172,12 +200,13 @@ namespace Avalonia.Win32.Input
dwStyle = CFS_CANDIDATEPOS,
ptCurrentPos = new POINT {X = x2, Y = y2}
};
ImmSetCandidateWindow(himc, ref candidateForm);
}
_caretManager.TryMove(x2, y2);
if (_showCompositionWindow)
if (ShowCompositionWindow)
{
ConfigureCompositionWindow(x1, y1, himc, y2 - y1);
// Don't need to set the position of candidate window.
@ -211,6 +240,7 @@ namespace Avalonia.Win32.Input
ptCurrentPos = new POINT {X = x1, Y = y1},
rcArea = new RECT {left = x1, top = y1, right = x2, bottom = y2 + _caretMargin}
};
ImmSetCandidateWindow(himc, ref excludeRectangle);
}
@ -221,6 +251,7 @@ namespace Avalonia.Win32.Input
dwStyle = CFS_POINT,
ptCurrentPos = new POINT {X = x1, Y = y1},
};
ImmSetCompositionWindow(himc, ref compForm);
var logFont = new LOGFONT()
@ -228,6 +259,7 @@ namespace Avalonia.Win32.Input
lfHeight = height,
lfQuality = 5 //CLEARTYPE_QUALITY
};
ImmSetCompositionFont(himc, ref logFont);
}
@ -235,8 +267,43 @@ namespace Avalonia.Win32.Input
{
// we're skipping this. not usable on windows
}
public void CompositionChanged()
{
if (!IsComposing)
{
return;
}
if(!IsActive || !_client.SupportsPreedit)
{
return;
}
var composition = GetCompositionString();
_client.SetPreeditText(composition);
}
public bool IsComposing { get; set; }
private string GetCompositionString()
{
var himc = ImmGetContext(HWND);
var length = ImmGetCompositionString(himc, GCS.GCS_COMPSTR, IntPtr.Zero, 0);
var buffer = new byte[length];
unsafe
{
fixed (byte* bufferPtr = buffer)
{
var error = ImmGetCompositionString(himc, GCS.GCS_COMPSTR, (IntPtr)bufferPtr, (uint)length);
return Encoding.Unicode.GetString(buffer, 0, buffer.Length);
}
}
}
~Imm32InputMethod()
{

71
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1766,6 +1766,46 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
[Flags]
public enum GCS : uint
{
/// <summary>Retrieve or update the attribute of the composition string.</summary>
GCS_COMPATTR = 0x0010,
/// <summary>Retrieve or update clause information of the composition string.</summary>
GCS_COMPCLAUSE = 0x0020,
/// <summary>Retrieve or update the attributes of the reading string of the current composition.</summary>
GCS_COMPREADATTR = 0x0002,
/// <summary>Retrieve or update the clause information of the reading string of the composition string.</summary>
GCS_COMPREADCLAUSE = 0x0004,
/// <summary>Retrieve or update the reading string of the current composition.</summary>
GCS_COMPREADSTR = 0x0001,
/// <summary>Retrieve or update the current composition string.</summary>
GCS_COMPSTR = 0x0008,
/// <summary>Retrieve or update the cursor position in composition string.</summary>
GCS_CURSORPOS = 0x0080,
/// <summary>Retrieve or update the starting position of any changes in composition string.</summary>
GCS_DELTASTART = 0x0100,
/// <summary>Retrieve or update clause information of the result string.</summary>
GCS_RESULTCLAUSE = 0x1000,
/// <summary>Retrieve or update clause information of the reading string.</summary>
GCS_RESULTREADCLAUSE = 0x0400,
/// <summary>Retrieve or update the reading string.</summary>
GCS_RESULTREADSTR = 0x0200,
/// <summary>Retrieve or update the string of the composition result.</summary>
GCS_RESULTSTR = 0x0800,
}
[DllImport("imm32.dll", SetLastError = true)]
public static extern IntPtr ImmGetContext(IntPtr hWnd);
[DllImport("imm32.dll", SetLastError = true)]
@ -1788,6 +1828,29 @@ namespace Avalonia.Win32.Interop
public static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM lpComp);
[DllImport("imm32.dll")]
public static extern bool ImmSetCompositionFont(IntPtr hIMC, ref LOGFONT lf);
[DllImport("imm32.dll", SetLastError = false, CharSet = CharSet.Unicode)]
public static extern int ImmGetCompositionString(IntPtr hIMC, GCS dwIndex, [Out, Optional] IntPtr lpBuf, uint dwBufLen);
public static string ImmGetCompositionString(IntPtr hIMC, GCS dwIndex)
{
int bufferLength = ImmGetCompositionString(hIMC, dwIndex, IntPtr.Zero, 0);
if (bufferLength > 0)
{
var buffer = new byte[bufferLength];
fixed(byte* bufferPtr = buffer)
{
var error = ImmGetCompositionString(hIMC, dwIndex, (IntPtr)bufferPtr, (uint)bufferLength);
return Marshal.PtrToStringUni((IntPtr)bufferPtr);
}
}
return null;
}
[DllImport("imm32.dll")]
public static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue);
[DllImport("user32.dll")]
@ -1827,7 +1890,13 @@ namespace Avalonia.Win32.Interop
public const int CFS_EXCLUDE = 0x0080;
public const int CFS_POINT = 0x0002;
public const int CFS_RECT = 0x0001;
public const uint ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000;
// lParam for WM_IME_SETCONTEXT
public const long ISC_SHOWUICANDIDATEWINDOW = 0x00000001;
public const long ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000;
public const long ISC_SHOWUIGUIDELINE = 0x40000000;
public const long ISC_SHOWUIALLCANDIDATEWINDOW = 0x0000000F;
public const long ISC_SHOWUIALL = 0xC000000F;
public const int NI_COMPOSITIONSTR = 21;
public const int CPS_COMPLETE = 1;

45
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -527,22 +527,22 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_PAINT:
{
using(NonPumpingSyncContext.Use())
using (_rendererLock.Lock())
{
if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero)
using (NonPumpingSyncContext.Use())
using (_rendererLock.Lock())
{
var f = RenderScaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f,
(r.bottom - r.top) / f));
EndPaint(_hwnd, ref ps);
if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero)
{
var f = RenderScaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f,
(r.bottom - r.top) / f));
EndPaint(_hwnd, ref ps);
}
}
}
return IntPtr.Zero;
}
return IntPtr.Zero;
}
case WindowsMessage.WM_ENTERSIZEMOVE:
@ -551,7 +551,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_SIZE:
{
using(NonPumpingSyncContext.Use())
using (NonPumpingSyncContext.Use())
using (_rendererLock.Lock())
{
// Do nothing here, just block until the pending frame render is completed on the render thread
@ -651,13 +651,19 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_IME_SETCONTEXT:
{
// TODO if we implement preedit, disable the composition window:
// lParam = new IntPtr((int)(((uint)lParam.ToInt64()) & ~ISC_SHOWUICOMPOSITIONWINDOW));
DefWindowProc(Hwnd, msg, wParam, (IntPtr)(lParam.ToInt64() & ~ISC_SHOWUICOMPOSITIONWINDOW));
UpdateInputMethod(GetKeyboardLayout(0));
return IntPtr.Zero;
}
case WindowsMessage.WM_IME_COMPOSITION:
{
Imm32InputMethod.Current.CompositionChanged();
break;
}
case WindowsMessage.WM_IME_CHAR:
case WindowsMessage.WM_IME_COMPOSITION:
case WindowsMessage.WM_IME_COMPOSITIONFULL:
case WindowsMessage.WM_IME_CONTROL:
case WindowsMessage.WM_IME_KEYDOWN:
@ -667,7 +673,7 @@ namespace Avalonia.Win32
break;
case WindowsMessage.WM_IME_STARTCOMPOSITION:
Imm32InputMethod.Current.IsComposing = true;
break;
return IntPtr.Zero;
case WindowsMessage.WM_IME_ENDCOMPOSITION:
Imm32InputMethod.Current.IsComposing = false;
break;
@ -687,7 +693,7 @@ namespace Avalonia.Win32
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
#endif
if(shouldTakeFocus)
if (shouldTakeFocus)
{
SetFocus(_hwnd);
}
@ -916,14 +922,15 @@ namespace Avalonia.Win32
{
// note: for non-ime language, also create it so that emoji panel tracks cursor
var langid = LGID(hkl);
if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd)
{
return;
}
_langid = langid;
Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl);
}
private static int ToInt32(IntPtr ptr)

2
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-ios</TargetFramework>
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup>
<ItemGroup>

11
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@ -20,7 +20,7 @@ namespace Avalonia.iOS
}
}
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseiOS();
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
[Export("window")]
public UIWindow Window { get; set; }
@ -28,7 +28,7 @@ namespace Avalonia.iOS
[Export("application:didFinishLaunchingWithOptions:")]
public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
var builder = AppBuilder.Configure<TApp>();
var builder = AppBuilder.Configure<TApp>().UseiOS();
CustomizeAppBuilder(builder);
var lifetime = new SingleViewLifetime();
@ -36,6 +36,8 @@ namespace Avalonia.iOS
builder.AfterSetup(_ =>
{
Window = new UIWindow();
var view = new AvaloniaView();
lifetime.View = view;
Window.RootViewController = new UIViewController
@ -45,8 +47,9 @@ namespace Avalonia.iOS
});
builder.SetupWithLifetime(lifetime);
Window.Hidden = false;
Window.MakeKeyAndVisible();
return true;
}
}

146
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@ -1,146 +1,54 @@
using Foundation;
using ObjCRuntime;
#nullable enable
using Avalonia.Input.TextInput;
using Avalonia.Input;
using Avalonia.Input.Raw;
using JetBrains.Annotations;
using UIKit;
namespace Avalonia.iOS;
#nullable enable
[Adopts("UITextInputTraits")]
[Adopts("UIKeyInput")]
public partial class AvaloniaView : ITextInputMethodImpl
public partial class AvaloniaView
{
private ITextInputMethodClient? _currentClient;
public override bool CanResignFirstResponder => true;
public override bool CanBecomeFirstResponder => true;
private const string ImeLog = "IOSIME";
private Rect _cursorRect;
private TextInputOptions? _options;
[Export("hasText")]
public bool HasText
private static UIResponder? CurrentAvaloniaResponder { get; set; }
public override bool BecomeFirstResponder()
{
get
{
if (_currentClient is { } && _currentClient.SupportsSurroundingText &&
_currentClient.SurroundingText.Text.Length > 0)
{
return true;
}
return false;
}
var res = base.BecomeFirstResponder();
if (res)
CurrentAvaloniaResponder = this;
return res;
}
[Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default;
[Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; }
[Export("insertText:")]
public void InsertText(string text)
public override bool ResignFirstResponder()
{
if (KeyboardDevice.Instance is { })
{
_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
0, InputRoot, text));
}
var res = base.ResignFirstResponder();
if (res && ReferenceEquals(CurrentAvaloniaResponder, this))
CurrentAvaloniaResponder = null;
return res;
}
[Export("deleteBackward")]
public void DeleteBackward()
{
if (KeyboardDevice.Instance is { })
{
// TODO: pass this through IME infrastructure instead of emulating a backspace press
_topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None));
_topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None));
}
}
private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this);
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
_currentClient = client;
_client = client;
if (_client == null && IsDrivingText)
BecomeFirstResponder();
if (client is { })
if (_client is { })
{
BecomeFirstResponder();
}
else
{
ResignFirstResponder();
new TextInputResponder(this, _client).BecomeFirstResponder();
}
}
void ITextInputMethodImpl.SetCursorRect(Rect rect)
{
}
void ITextInputMethodImpl.SetCursorRect(Rect rect) => _cursorRect = rect;
void ITextInputMethodImpl.SetOptions(TextInputOptions options)
{
IsSecureEntry = false;
switch (options.ContentType)
{
case TextInputContentType.Normal:
KeyboardType = UIKeyboardType.Default;
break;
case TextInputContentType.Alpha:
KeyboardType = UIKeyboardType.AsciiCapable;
break;
case TextInputContentType.Digits:
KeyboardType = UIKeyboardType.PhonePad;
break;
case TextInputContentType.Pin:
KeyboardType = UIKeyboardType.NumberPad;
IsSecureEntry = true;
break;
case TextInputContentType.Number:
KeyboardType = UIKeyboardType.PhonePad;
break;
case TextInputContentType.Email:
KeyboardType = UIKeyboardType.EmailAddress;
break;
case TextInputContentType.Url:
KeyboardType = UIKeyboardType.Url;
break;
case TextInputContentType.Name:
KeyboardType = UIKeyboardType.NamePhonePad;
break;
case TextInputContentType.Password:
KeyboardType = UIKeyboardType.Default;
IsSecureEntry = true;
break;
case TextInputContentType.Social:
KeyboardType = UIKeyboardType.Twitter;
break;
case TextInputContentType.Search:
KeyboardType = UIKeyboardType.WebSearch;
break;
}
if (options.IsSensitive)
{
IsSecureEntry = true;
}
}
void ITextInputMethodImpl.SetOptions(TextInputOptions options) => _options = options;
void ITextInputMethodImpl.Reset()
{
ResignFirstResponder();
if (IsDrivingText)
BecomeFirstResponder();
}
}

25
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -19,35 +19,42 @@ using UIKit;
namespace Avalonia.iOS
{
public partial class AvaloniaView : UIView
public partial class AvaloniaView : UIView, ITextInputMethodImpl
{
internal IInputRoot InputRoot { get; private set; }
private TopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
private TouchHandler _touches;
private ITextInputMethodClient _client;
public AvaloniaView()
{
_topLevelImpl = new TopLevelImpl(this);
_touches = new TouchHandler(this, _topLevelImpl);
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
_topLevel.Prepare();
_topLevel.Renderer.Start();
var l = (CAEAGLLayer) Layer;
var l = (CAEAGLLayer)Layer;
l.ContentsScale = UIScreen.MainScreen.Scale;
l.Opaque = true;
l.DrawableProperties = new NSDictionary(
EAGLDrawableProperty.RetainedBacking, false,
EAGLDrawableProperty.ColorFormat, EAGLColorFormat.RGBA8
);
_topLevelImpl.Surfaces = new[] {new EaglLayerSurface(l)};
_topLevelImpl.Surfaces = new[] { new EaglLayerSurface(l) };
MultipleTouchEnabled = true;
AddSubviews(new UIView[] { new UIKit.UIButton(UIButtonType.InfoDark) });
}
internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
public override bool CanBecomeFirstResponder => true;
public override bool CanResignFirstResponder => true;
internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost,
ITopLevelImplWithStorageProvider
{
private readonly AvaloniaView _view;
public AvaloniaView View => _view;
@ -65,7 +72,7 @@ namespace Avalonia.iOS
}
public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor);
public void Invalidate(Rect rect)
{
@ -79,7 +86,7 @@ namespace Avalonia.iOS
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y);
public PixelPoint PointToScreen(Point point) => new PixelPoint((int) point.X, (int) point.Y);
public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y);
public void SetCursor(ICursorImpl _)
{
@ -118,7 +125,7 @@ namespace Avalonia.iOS
new AcrylicPlatformCompensationLevels();
public ITextInputMethodImpl? TextInputMethod => _view;
public INativeControlHostImpl NativeControlHost { get; }
public INativeControlHostImpl NativeControlHost { get; }
public IStorageProvider StorageProvider { get; }
}

40
src/iOS/Avalonia.iOS/CombinedSpan3.cs

@ -0,0 +1,40 @@
using System;
using Avalonia.Controls.Documents;
namespace Avalonia.iOS;
internal ref struct CombinedSpan3<T>
{
public ReadOnlySpan<T> Span1, Span2, Span3;
public CombinedSpan3(ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3)
{
Span1 = span1;
Span2 = span2;
Span3 = span3;
}
public int Length => Span1.Length + Span2.Length + Span3.Length;
void CopyFromSpan(ReadOnlySpan<T> from, ref int offset, ref Span<T> to)
{
if(to.Length == 0)
return;
if (offset < from.Length)
{
var copyNow = Math.Min(from.Length - offset, to.Length);
from.Slice(offset, copyNow).CopyTo(to);
to = to.Slice(copyNow);
offset = 0;
}
else
offset -= from.Length;
}
public void CopyTo(Span<T> to, int offset)
{
CopyFromSpan(Span1, ref offset, ref to);
CopyFromSpan(Span2, ref offset, ref to);
CopyFromSpan(Span3, ref offset, ref to);
}
}

92
src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs

@ -0,0 +1,92 @@
#nullable enable
using Avalonia.Input.TextInput;
using Foundation;
using UIKit;
namespace Avalonia.iOS;
partial class AvaloniaView
{
partial class TextInputResponder
{
[Export("autocapitalizationType")]
public UITextAutocapitalizationType AutocapitalizationType { get; private set; }
[Export("autocorrectionType")]
public UITextAutocorrectionType AutocorrectionType => UITextAutocorrectionType.Yes;
[Export("keyboardType")]
public UIKeyboardType KeyboardType =>
_view._options == null ?
UIKeyboardType.Default :
_view._options.ContentType switch
{
TextInputContentType.Alpha => UIKeyboardType.AsciiCapable,
TextInputContentType.Digits or TextInputContentType.Number => UIKeyboardType.NumberPad,
TextInputContentType.Pin => UIKeyboardType.NumberPad,
TextInputContentType.Email => UIKeyboardType.EmailAddress,
TextInputContentType.Url => UIKeyboardType.Url,
TextInputContentType.Name => UIKeyboardType.NamePhonePad,
TextInputContentType.Social => UIKeyboardType.Twitter,
TextInputContentType.Search => UIKeyboardType.WebSearch,
_ => UIKeyboardType.Default
};
[Export("keyboardAppearance")]
public UIKeyboardAppearance KeyboardAppearance => UIKeyboardAppearance.Alert;
[Export("returnKeyType")]
public UIReturnKeyType ReturnKeyType
{
get
{
if (_view._options != null)
{
return _view._options.ReturnKeyType switch
{
TextInputReturnKeyType.Done => UIReturnKeyType.Done,
TextInputReturnKeyType.Go => UIReturnKeyType.Go,
TextInputReturnKeyType.Search => UIReturnKeyType.Search,
TextInputReturnKeyType.Next => UIReturnKeyType.Next,
TextInputReturnKeyType.Return => UIReturnKeyType.Default,
TextInputReturnKeyType.Send => UIReturnKeyType.Send,
_ => _view._options.Multiline ? UIReturnKeyType.Default : UIReturnKeyType.Done
};
}
return UIReturnKeyType.Default;
}
}
[Export("enablesReturnKeyAutomatically")]
public bool EnablesReturnKeyAutomatically { get; set; }
[Export("isSecureTextEntry")] public bool IsSecureEntry =>
_view._options?.ContentType is TextInputContentType.Password or TextInputContentType.Pin
|| (_view._options?.IsSensitive ?? false);
[Export("spellCheckingType")] public UITextSpellCheckingType SpellCheckingType => UITextSpellCheckingType.Yes;
[Export("textContentType")] public NSString TextContentType { get; set; } = new NSString("text/plain");
[Export("smartQuotesType")]
public UITextSmartQuotesType SmartQuotesType { get; set; } = UITextSmartQuotesType.Default;
[Export("smartDashesType")]
public UITextSmartDashesType SmartDashesType { get; set; } = UITextSmartDashesType.Default;
[Export("smartInsertDeleteType")]
public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default;
[Export("passwordRules")] public UITextInputPasswordRules PasswordRules { get; set; } = null!;
public NSObject? WeakInputDelegate
{
get;
set;
}
NSObject IUITextInput.WeakTokenizer => _tokenizer;
}
}

491
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -0,0 +1,491 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Avalonia.Controls.Presenters;
using Foundation;
using ObjCRuntime;
using Avalonia.Input.TextInput;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Logging;
using CoreGraphics;
using UIKit;
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
namespace Avalonia.iOS;
#nullable enable
partial class AvaloniaView
{
[Adopts("UITextInput")]
[Adopts("UITextInputTraits")]
[Adopts("UIKeyInput")]
partial class TextInputResponder : UIResponder, IUITextInput
{
private class AvaloniaTextRange : UITextRange, INSCopying
{
private UITextPosition? _start;
private UITextPosition? _end;
public int StartIndex { get; }
public int EndIndex { get; }
public AvaloniaTextRange(int startIndex, int endIndex)
{
if (startIndex < 0)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (endIndex < startIndex)
throw new ArgumentOutOfRangeException(nameof(endIndex));
StartIndex = startIndex;
EndIndex = endIndex;
}
public override bool IsEmpty => StartIndex == EndIndex;
public override UITextPosition Start => _start ??= new AvaloniaTextPosition(StartIndex);
public override UITextPosition End => _end ??= new AvaloniaTextPosition(EndIndex);
public NSObject Copy(NSZone? zone)
{
return new AvaloniaTextRange(StartIndex, EndIndex);
}
}
private class AvaloniaTextPosition : UITextPosition, INSCopying
{
public AvaloniaTextPosition(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
Index = index;
}
public int Index { get; }
public NSObject Copy(NSZone? zone) => new AvaloniaTextPosition(Index);
}
public TextInputResponder(AvaloniaView view, ITextInputMethodClient client)
{
_view = view;
NextResponder = view;
_client = client;
_tokenizer = new UITextInputStringTokenizer(this);
}
public override UIResponder NextResponder { get; }
private readonly ITextInputMethodClient _client;
private int _inSurroundingTextUpdateEvent;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer;
public ITextInputMethodClient? Client => _client;
public override bool CanResignFirstResponder => true;
public override bool CanBecomeFirstResponder => true;
public override UIEditingInteractionConfiguration EditingInteractionConfiguration =>
UIEditingInteractionConfiguration.Default;
public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString());
public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode;
[DllImport("/usr/lib/libobjc.dylib")]
private static extern void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg);
private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:");
private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:");
private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:");
private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:");
private readonly AvaloniaView _view;
private string? _markedText;
private void SurroundingTextChanged(object? sender, EventArgs e)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "SurroundingTextChanged");
if (WeakInputDelegate == null)
return;
_inSurroundingTextUpdateEvent++;
try
{
objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle);
}
finally
{
_inSurroundingTextUpdateEvent--;
}
}
private void KeyPress(Key ev)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", ev);
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
RawKeyEventType.KeyDown, ev, RawInputModifiers.None));
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
RawKeyEventType.KeyUp, ev, RawInputModifiers.None));
}
private void TextInput(string text)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering text input {text}", text);
_view._topLevelImpl.Input(new RawTextInputEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, text));
}
void IUIKeyInput.InsertText(string text)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.InsertText {text}", text);
if (text == "\n")
{
KeyPress(Key.Enter);
switch (ReturnKeyType)
{
case UIReturnKeyType.Done:
case UIReturnKeyType.Go:
case UIReturnKeyType.Send:
case UIReturnKeyType.Search:
ResignFirstResponder();
break;
}
return;
}
TextInput(text);
}
void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back);
bool IUIKeyInput.HasText => true;
string IUITextInput.TextInRange(UITextRange range)
{
var r = (AvaloniaTextRange)range;
var s = _client.SurroundingText;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex);
string result = "";
if(string.IsNullOrEmpty(_markedText))
result = s.Text[r.StartIndex .. r.EndIndex];
else
{
var span = new CombinedSpan3<char>(s.Text.AsSpan().Slice(0, s.CursorOffset),
_markedText,
s.Text.AsSpan().Slice(s.CursorOffset));
var buf = new char[r.EndIndex - r.StartIndex];
span.CopyTo(buf, r.StartIndex);
result = new string(buf);
}
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "result: {res}", result);
return result;
}
void IUITextInput.ReplaceText(UITextRange range, string text)
{
var r = (AvaloniaTextRange)range;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text);
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex);
TextInput(text);
}
void IUITextInput.SetMarkedText(string markedText, NSRange selectedRange)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUIKeyInput.SetMarkedText {start} {len} {text}", selectedRange.Location,
selectedRange.Location, markedText);
_markedText = markedText;
_client.SetPreeditText(markedText);
}
void IUITextInput.UnmarkText()
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.UnmarkText");
if(_markedText == null)
return;
var commitString = _markedText;
_markedText = null;
_client.SetPreeditText(null);
if (string.IsNullOrWhiteSpace(commitString))
return;
TextInput(commitString);
}
public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition)
{
var f = (AvaloniaTextPosition)fromPosition;
var t = (AvaloniaTextPosition)toPosition;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.GetTextRange {start} {end}", f.Index, t.Index);
return new AvaloniaTextRange(f.Index, t.Index);
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset)
{
var pos = (AvaloniaTextPosition)fromPosition;
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, "IUIKeyInput.GetPosition {start} {offset}", pos.Index, (int)offset);
var res = GetPositionCore(pos, offset);
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index));
return res!;
}
private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition pos, nint offset)
{
var end = pos.Index + (int)offset;
if (end < 0)
return null!;
if (end > DocumentLength)
return null;
return new AvaloniaTextPosition(end);
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection,
nint offset)
{
var pos = (AvaloniaTextPosition)fromPosition;
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset);
var res = GetPositionCore(pos, inDirection, offset);
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index));
return res!;
}
private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition fromPosition, UITextLayoutDirection inDirection,
nint offset)
{
var f = (AvaloniaTextPosition)fromPosition;
var newPosition = f.Index;
switch (inDirection)
{
case UITextLayoutDirection.Left:
newPosition -= (int)offset;
break;
case UITextLayoutDirection.Right:
newPosition += (int)offset;
break;
}
if (newPosition < 0)
return null!;
if (newPosition > DocumentLength)
return null!;
return new AvaloniaTextPosition(newPosition);
}
NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second)
{
var f = (AvaloniaTextPosition)first;
var s = (AvaloniaTextPosition)second;
if (f.Index < s.Index)
return NSComparisonResult.Ascending;
if (f.Index > s.Index)
return NSComparisonResult.Descending;
return NSComparisonResult.Same;
}
nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition)
{
var f = (AvaloniaTextPosition)fromPosition;
var t = (AvaloniaTextPosition)toPosition;
return t.Index - f.Index;
}
UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction)
{
var r = (AvaloniaTextRange)range;
if (direction is UITextLayoutDirection.Right or UITextLayoutDirection.Down)
return r.End;
return r.Start;
}
UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction)
{
var p = (AvaloniaTextPosition)byExtendingPosition;
if (direction is UITextLayoutDirection.Left or UITextLayoutDirection.Up)
return new AvaloniaTextRange(0, p.Index);
return new AvaloniaTextRange(p.Index, DocumentLength);
}
NSWritingDirection IUITextInput.GetBaseWritingDirection(UITextPosition forPosition,
UITextStorageDirection direction)
{
return NSWritingDirection.LeftToRight;
// todo query and retyrn RTL.
}
void IUITextInput.SetBaseWritingDirectionforRange(NSWritingDirection writingDirection, UITextRange range)
{
// todo ? ignore?
}
CGRect IUITextInput.GetFirstRectForRange(UITextRange range)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetFirstRectForRange");
// TODO: Query from the input client
var r = _view._cursorRect;
return new CGRect(r.Left, r.Top, r.Width, r.Height);
}
CGRect IUITextInput.GetCaretRectForPosition(UITextPosition? position)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetCaretRectForPosition");
var rect = _client.CursorRectangle;
return new CGRect(rect.X, rect.Y, rect.Width, rect.Height);
}
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetClosestPositionToPoint");
var presenter = _client.TextViewVisual as TextPresenter;
if (presenter is { })
{
var hitResult = presenter.TextLayout.HitTestPoint(new Point(point.X, point.Y));
return new AvaloniaTextPosition(hitResult.TextPosition);
}
return null;
}
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetClosestPositionToPoint");
return new AvaloniaTextPosition(0);
}
UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetCharacterRangeAtPoint");
return new AvaloniaTextRange(0, 0);
}
UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetSelectionRect");
return new UITextSelectionRect[0];
}
[Export("textStylingAtPosition:inDirection:")]
public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction)
{
return null!;
}
UITextRange? IUITextInput.SelectedTextRange
{
get
{
return new AvaloniaTextRange(
Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset));
}
set
{
if (_inSurroundingTextUpdateEvent > 0)
return;
if (value == null)
_client.SelectInSurroundingText(_client.SurroundingText.CursorOffset,
_client.SurroundingText.CursorOffset);
else
{
var r = (AvaloniaTextRange)value;
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex);
}
}
}
NSDictionary? IUITextInput.MarkedTextStyle
{
get => null;
set {}
}
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
private int DocumentLength => _client.SurroundingText.Text.Length + (_markedText?.Length ?? 0);
UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
UITextRange IUITextInput.MarkedTextRange
{
get
{
if (string.IsNullOrWhiteSpace(_markedText))
return null!;
return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset + _markedText.Length);
}
}
public override bool BecomeFirstResponder()
{
var res = base.BecomeFirstResponder();
if (res)
{
Logger.TryGet(LogEventLevel.Debug, "IOSIME")
?.Log(null, "Became first responder");
_client.SurroundingTextChanged += SurroundingTextChanged;
CurrentAvaloniaResponder = this;
}
return res;
}
public override bool ResignFirstResponder()
{
var res = base.ResignFirstResponder();
if (res && ReferenceEquals(CurrentAvaloniaResponder, this))
{
Logger.TryGet(LogEventLevel.Debug, "IOSIME")
?.Log(null, "Resigned first responder");
_client.SurroundingTextChanged -= SurroundingTextChanged;
CurrentAvaloniaResponder = null;
}
return res;
}
}
}
Loading…
Cancel
Save