committed by
GitHub
79 changed files with 2667 additions and 422 deletions
@ -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; |
|
||||
} |
|
||||
@ -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> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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. |
||||
@ -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> |
||||
@ -0,0 +1,4 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
<color name="splash_background">#FFFFFF</color> |
||||
|
</resources> |
||||
@ -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> |
||||
@ -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))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend |
||||
@ -0,0 +1 @@ |
|||||
|
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend |
||||
@ -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> |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 172 KiB |
@ -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> |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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="" 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> |
||||
@ -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,4 +1,5 @@ |
|||||
<Window xmlns="https://github.com/avaloniaui" |
<Window xmlns="https://github.com/avaloniaui" |
||||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' |
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' |
||||
x:Class="Sandbox.MainWindow"> |
x:Class="Sandbox.MainWindow"> |
||||
|
<TextBox /> |
||||
</Window> |
</Window> |
||||
|
|||||
@ -1,12 +1,11 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
|
||||
using System.Text; |
|
||||
using Android.Views.InputMethods; |
using Android.Views.InputMethods; |
||||
|
using Avalonia.Android.Platform.SkiaPlatform; |
||||
|
|
||||
namespace Avalonia.Android |
namespace Avalonia.Android |
||||
{ |
{ |
||||
interface IInitEditorInfo |
internal interface IInitEditorInfo |
||||
{ |
{ |
||||
void InitEditorInfo(Action<EditorInfo> init); |
void InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,13 @@ |
|||||
|
namespace Avalonia.Input.TextInput; |
||||
|
|
||||
|
public enum TextInputReturnKeyType |
||||
|
{ |
||||
|
Default, |
||||
|
Return, |
||||
|
Done, |
||||
|
Go, |
||||
|
Send, |
||||
|
Search, |
||||
|
Next, |
||||
|
Previous |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
@ -1,5 +1,7 @@ |
|||||
export { DpiWatcher } from "./Avalonia/DpiWatcher" |
export { DpiWatcher } from "./Avalonia/DpiWatcher" |
||||
export { InputHelper } from "./Avalonia/InputHelper" |
export { InputHelper } from "./Avalonia/InputHelper" |
||||
|
export { FocusHelper } from "./Avalonia/FocusHelper" |
||||
export { NativeControlHost } from "./Avalonia/NativeControlHost" |
export { NativeControlHost } from "./Avalonia/NativeControlHost" |
||||
export { SizeWatcher } from "./Avalonia/SizeWatcher" |
export { SizeWatcher } from "./Avalonia/SizeWatcher" |
||||
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas" |
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas" |
||||
|
export { CaretHelper } from "./Avalonia/CaretHelper" |
||||
|
|||||
@ -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; |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -1,146 +1,54 @@ |
|||||
using Foundation; |
#nullable enable |
||||
using ObjCRuntime; |
|
||||
using Avalonia.Input.TextInput; |
using Avalonia.Input.TextInput; |
||||
using Avalonia.Input; |
using JetBrains.Annotations; |
||||
using Avalonia.Input.Raw; |
|
||||
using UIKit; |
using UIKit; |
||||
|
|
||||
namespace Avalonia.iOS; |
namespace Avalonia.iOS; |
||||
|
|
||||
#nullable enable |
public partial class AvaloniaView |
||||
|
|
||||
[Adopts("UITextInputTraits")] |
|
||||
[Adopts("UIKeyInput")] |
|
||||
public partial class AvaloniaView : ITextInputMethodImpl |
|
||||
{ |
{ |
||||
private ITextInputMethodClient? _currentClient; |
private const string ImeLog = "IOSIME"; |
||||
|
private Rect _cursorRect; |
||||
public override bool CanResignFirstResponder => true; |
private TextInputOptions? _options; |
||||
public override bool CanBecomeFirstResponder => true; |
|
||||
|
|
||||
[Export("hasText")] |
private static UIResponder? CurrentAvaloniaResponder { get; set; } |
||||
public bool HasText |
public override bool BecomeFirstResponder() |
||||
{ |
{ |
||||
get |
var res = base.BecomeFirstResponder(); |
||||
{ |
if (res) |
||||
if (_currentClient is { } && _currentClient.SupportsSurroundingText && |
CurrentAvaloniaResponder = this; |
||||
_currentClient.SurroundingText.Text.Length > 0) |
return res; |
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
[Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default; |
public override bool ResignFirstResponder() |
||||
|
|
||||
[Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; } |
|
||||
|
|
||||
[Export("insertText:")] |
|
||||
public void InsertText(string text) |
|
||||
{ |
{ |
||||
if (KeyboardDevice.Instance is { }) |
var res = base.ResignFirstResponder(); |
||||
{ |
if (res && ReferenceEquals(CurrentAvaloniaResponder, this)) |
||||
_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, |
CurrentAvaloniaResponder = null; |
||||
0, InputRoot, text)); |
return res; |
||||
} |
|
||||
} |
} |
||||
|
|
||||
[Export("deleteBackward")] |
private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this); |
||||
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)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) |
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) |
||||
{ |
{ |
||||
_currentClient = client; |
_client = client; |
||||
|
if (_client == null && IsDrivingText) |
||||
|
BecomeFirstResponder(); |
||||
|
|
||||
if (client is { }) |
if (_client is { }) |
||||
{ |
{ |
||||
BecomeFirstResponder(); |
new TextInputResponder(this, _client).BecomeFirstResponder(); |
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ResignFirstResponder(); |
|
||||
} |
} |
||||
} |
} |
||||
|
|
||||
void ITextInputMethodImpl.SetCursorRect(Rect rect) |
void ITextInputMethodImpl.SetCursorRect(Rect rect) => _cursorRect = rect; |
||||
{ |
|
||||
|
|
||||
} |
|
||||
|
|
||||
void ITextInputMethodImpl.SetOptions(TextInputOptions options) |
void ITextInputMethodImpl.SetOptions(TextInputOptions options) => _options = 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.Reset() |
void ITextInputMethodImpl.Reset() |
||||
{ |
{ |
||||
ResignFirstResponder(); |
if (IsDrivingText) |
||||
|
BecomeFirstResponder(); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue