136 changed files with 6201 additions and 1451 deletions
@ -0,0 +1,14 @@ |
|||
<Project> |
|||
|
|||
<PropertyGroup> |
|||
<EnforceExtendedAnalyzerRules Condition="'$(EnforceExtendedAnalyzerRules)' == ''">true</EnforceExtendedAnalyzerRules> |
|||
<IsRoslynComponent Condition="'$(IsRoslynComponent)' == ''">true</IsRoslynComponent> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.5.0" PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,56 @@ |
|||
# Building Local NuGet Packages |
|||
|
|||
To build NuGet packages, one can use the `CreateNugetPackages` target: |
|||
|
|||
Windows |
|||
|
|||
``` |
|||
.\build.ps1 CreateNugetPackages |
|||
``` |
|||
|
|||
Linux/macOS |
|||
|
|||
``` |
|||
./build.sh CreateNugetPackages |
|||
``` |
|||
|
|||
Or if you have Nuke's [dotnet global tool](https://nuke.build/docs/getting-started/installation/) installed: |
|||
|
|||
``` |
|||
nuke CreateNugetPackages |
|||
``` |
|||
|
|||
The produced NuGet packages will be placed in the `artifacts\nuget` directory. |
|||
|
|||
> [!NOTE] |
|||
> The rest of this document will assume that you have the Nuke global tool installed, as the invocation is the same on all platforms. You can always replace `nuke` in the instructions below with the `build` script relvant to your platform. |
|||
|
|||
By default the packages will be built in debug configuration. To build in relase configuration add the `--configuration` parameter, e.g.: |
|||
|
|||
``` |
|||
nuke CreateNugetPackages --configuration Release |
|||
``` |
|||
|
|||
To configure the version of the built packages, add the `--force-nuget-version` parameter, e.g.: |
|||
|
|||
``` |
|||
nuke CreateNugetPackages --force-nuget-version 11.4.0 |
|||
``` |
|||
|
|||
## Building to the Local Cache |
|||
|
|||
Building packages with the `CreateNugetPackages` target has a few gotchas: |
|||
|
|||
- One needs to set up a local nuget feed to consume the packages |
|||
- When building on an operating system other than macOS, the Avalonia.Native package will not be built, resulting in a NuGet error when trying to use Avalonia.Desktop |
|||
- It's easy to introduce versioning problems |
|||
|
|||
For these reasons, it is possible to build Avalonia directly to your machine's NuGet cache using the `BuildToNuGetCache` target: |
|||
|
|||
```bash |
|||
nuke --target BuildToNuGetCache --configuration Release |
|||
``` |
|||
|
|||
This command will generate nuget packages and push them into your local NuGet cache (usually `~/.nuget/packages`) with a version of `9999.0.0-localbuild`. |
|||
|
|||
Each time local changes are made to Avalonia, running this command again will replace the old packages and reset the cache, meaning that the changes should be picked up automatically by msbuild. |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<ProvisioningType>manual</ProvisioningType> |
|||
<TargetFramework>$(AvsCurrentMacCatalystTargetFramework)</TargetFramework> |
|||
<!-- Used to support Desktop Mode Idiom, min supported version is 13.1, which supports iPad scaling. --> |
|||
<SupportedOSPlatformVersion>14.0</SupportedOSPlatformVersion> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" /> |
|||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> |
|||
<None Include="Info.Catalyst.plist"> |
|||
<LogicalName>Info.plist</LogicalName> |
|||
</None> |
|||
</ItemGroup> |
|||
<PropertyGroup> |
|||
<UseInterpreter>true</UseInterpreter> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -0,0 +1,21 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<ProvisioningType>manual</ProvisioningType> |
|||
<TargetFramework>$(AvsCurrentTvOSTargetFramework)</TargetFramework> |
|||
<SupportedOSPlatformVersion>$(AvsMinSupportedTvOSVersion)</SupportedOSPlatformVersion> |
|||
<!-- To run this in the simulator, you need to use the x64 architecture, |
|||
since SkiaSharp only bundles native libraries for x64 for the tvOS Simulator --> |
|||
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">tvossimulator-x64</RuntimeIdentifier> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" /> |
|||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> |
|||
<None Include="Info.tvOS.plist"> |
|||
<LogicalName>Info.plist</LogicalName> |
|||
</None> |
|||
</ItemGroup> |
|||
<PropertyGroup> |
|||
<UseInterpreter>true</UseInterpreter> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -0,0 +1,42 @@ |
|||
<?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.Catalyst</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>UIDeviceFamily</key> |
|||
<array> |
|||
<integer>6</integer> |
|||
</array> |
|||
<key>UIRequiredDeviceCapabilities</key> |
|||
<array> |
|||
<string>arm64</string> |
|||
</array> |
|||
<key>UILaunchStoryboardName</key> |
|||
<string>LaunchScreen</string> |
|||
<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>com.apple.security.files.user-selected.read-write</key> |
|||
<true/> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,40 @@ |
|||
<?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.tvOS</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>UIDeviceFamily</key> |
|||
<array> |
|||
<integer>3</integer> |
|||
</array> |
|||
<key>UIRequiredDeviceCapabilities</key> |
|||
<array> |
|||
<string>arm64</string> |
|||
</array> |
|||
<key>UILaunchStoryboardName</key> |
|||
<string>LaunchScreen</string> |
|||
<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> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,5 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TextTestApp.App" RequestedThemeVariant="Light"> |
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,21 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace TextTestApp |
|||
{ |
|||
public partial class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |
|||
desktop.MainWindow = new MainWindow(); |
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Layout; |
|||
|
|||
namespace TextTestApp |
|||
{ |
|||
public class GridRow : Grid |
|||
{ |
|||
protected override void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
base.ChildrenChanged(sender, e); |
|||
|
|||
while (Children.Count > ColumnDefinitions.Count) |
|||
ColumnDefinitions.Add(new ColumnDefinition { SharedSizeGroup = "c" + ColumnDefinitions.Count }); |
|||
|
|||
for (int i = 0; i < Children.Count; i++) |
|||
{ |
|||
SetColumn(Children[i], i); |
|||
if (Children[i] is Layoutable l) |
|||
l.VerticalAlignment = VerticalAlignment.Center; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,705 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Globalization; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Documents; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.TextFormatting; |
|||
|
|||
namespace TextTestApp |
|||
{ |
|||
public class InteractiveLineControl : Control |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Text" /> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string?> TextProperty = |
|||
TextBlock.TextProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Background"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<IBrush?> BackgroundProperty = |
|||
Border.BackgroundProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
public static readonly StyledProperty<IBrush?> ExtentStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(ExtentStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> BaselineStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BaselineStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> TextBoundsStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(TextBoundsStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> RunBoundsStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(RunBoundsStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> NextHitStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(NextHitStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> BackspaceHitStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BackspaceHitStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> PreviousHitStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(PreviousHitStroke)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> DistanceStrokeProperty = |
|||
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(DistanceStroke)); |
|||
|
|||
public IBrush? ExtentStroke |
|||
{ |
|||
get => GetValue(ExtentStrokeProperty); |
|||
set => SetValue(ExtentStrokeProperty, value); |
|||
} |
|||
public IBrush? BaselineStroke |
|||
{ |
|||
get => GetValue(BaselineStrokeProperty); |
|||
set => SetValue(BaselineStrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? TextBoundsStroke |
|||
{ |
|||
get => GetValue(TextBoundsStrokeProperty); |
|||
set => SetValue(TextBoundsStrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? RunBoundsStroke |
|||
{ |
|||
get => GetValue(RunBoundsStrokeProperty); |
|||
set => SetValue(RunBoundsStrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? NextHitStroke |
|||
{ |
|||
get => GetValue(NextHitStrokeProperty); |
|||
set => SetValue(NextHitStrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? BackspaceHitStroke |
|||
{ |
|||
get => GetValue(BackspaceHitStrokeProperty); |
|||
set => SetValue(BackspaceHitStrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? PreviousHitStroke |
|||
{ |
|||
get => GetValue(PreviousHitStrokeProperty); |
|||
set => SetValue(PreviousHitStrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? DistanceStroke |
|||
{ |
|||
get => GetValue(DistanceStrokeProperty); |
|||
set => SetValue(DistanceStrokeProperty, value); |
|||
} |
|||
|
|||
private IPen? _extentPen; |
|||
protected IPen ExtentPen => _extentPen ??= new Pen(ExtentStroke, dashStyle: DashStyle.Dash); |
|||
|
|||
private IPen? _baselinePen; |
|||
protected IPen BaselinePen => _baselinePen ??= new Pen(BaselineStroke); |
|||
|
|||
private IPen? _textBoundsPen; |
|||
protected IPen TextBoundsPen => _textBoundsPen ??= new Pen(TextBoundsStroke); |
|||
|
|||
private IPen? _runBoundsPen; |
|||
protected IPen RunBoundsPen => _runBoundsPen ??= new Pen(RunBoundsStroke, dashStyle: DashStyle.Dash); |
|||
|
|||
private IPen? _nextHitPen; |
|||
protected IPen NextHitPen => _nextHitPen ??= new Pen(NextHitStroke); |
|||
|
|||
private IPen? _previousHitPen; |
|||
protected IPen PreviousHitPen => _previousHitPen ??= new Pen(PreviousHitStroke); |
|||
|
|||
private IPen? _backspaceHitPen; |
|||
protected IPen BackspaceHitPen => _backspaceHitPen ??= new Pen(BackspaceHitStroke); |
|||
|
|||
private IPen? _distancePen; |
|||
protected IPen DistancePen => _distancePen ??= new Pen(DistanceStroke); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the text to draw.
|
|||
/// </summary>
|
|||
public string? Text |
|||
{ |
|||
get => GetValue(TextProperty); |
|||
set => SetValue(TextProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a brush used to paint the control's background.
|
|||
/// </summary>
|
|||
public IBrush? Background |
|||
{ |
|||
get => GetValue(BackgroundProperty); |
|||
set => SetValue(BackgroundProperty, value); |
|||
} |
|||
|
|||
// TextRunProperties
|
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FontFamily"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<FontFamily> FontFamilyProperty = |
|||
TextElement.FontFamilyProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FontFeaturesProperty"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty = |
|||
TextElement.FontFeaturesProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FontSize"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<double> FontSizeProperty = |
|||
TextElement.FontSizeProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FontStyle"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<FontStyle> FontStyleProperty = |
|||
TextElement.FontStyleProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FontWeight"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<FontWeight> FontWeightProperty = |
|||
TextElement.FontWeightProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="FontWeight"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<FontStretch> FontStretchProperty = |
|||
TextElement.FontStretchProperty.AddOwner<InteractiveLineControl>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the font family used to draw the control's text.
|
|||
/// </summary>
|
|||
public FontFamily FontFamily |
|||
{ |
|||
get => GetValue(FontFamilyProperty); |
|||
set => SetValue(FontFamilyProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the font features turned on/off.
|
|||
/// </summary>
|
|||
public FontFeatureCollection? FontFeatures |
|||
{ |
|||
get => GetValue(FontFeaturesProperty); |
|||
set => SetValue(FontFeaturesProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the size of the control's text in points.
|
|||
/// </summary>
|
|||
public double FontSize |
|||
{ |
|||
get => GetValue(FontSizeProperty); |
|||
set => SetValue(FontSizeProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the font style used to draw the control's text.
|
|||
/// </summary>
|
|||
public FontStyle FontStyle |
|||
{ |
|||
get => GetValue(FontStyleProperty); |
|||
set => SetValue(FontStyleProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the font weight used to draw the control's text.
|
|||
/// </summary>
|
|||
public FontWeight FontWeight |
|||
{ |
|||
get => GetValue(FontWeightProperty); |
|||
set => SetValue(FontWeightProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the font stretch used to draw the control's text.
|
|||
/// </summary>
|
|||
public FontStretch FontStretch |
|||
{ |
|||
get => GetValue(FontStretchProperty); |
|||
set => SetValue(FontStretchProperty, value); |
|||
} |
|||
|
|||
private GenericTextRunProperties? _textRunProperties; |
|||
public GenericTextRunProperties TextRunProperties |
|||
{ |
|||
get |
|||
{ |
|||
return _textRunProperties ??= CreateTextRunProperties(); |
|||
} |
|||
set |
|||
{ |
|||
if (value == null) |
|||
throw new ArgumentNullException(nameof(value)); |
|||
|
|||
_textRunProperties = value; |
|||
SetCurrentValue(FontFamilyProperty, value.Typeface.FontFamily); |
|||
SetCurrentValue(FontFeaturesProperty, value.FontFeatures); |
|||
SetCurrentValue(FontSizeProperty, value.FontRenderingEmSize); |
|||
SetCurrentValue(FontStyleProperty, value.Typeface.Style); |
|||
SetCurrentValue(FontWeightProperty, value.Typeface.Weight); |
|||
SetCurrentValue(FontStretchProperty, value.Typeface.Stretch); |
|||
} |
|||
} |
|||
|
|||
private GenericTextRunProperties CreateTextRunProperties() |
|||
{ |
|||
Typeface typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); |
|||
return new GenericTextRunProperties(typeface, FontFeatures, FontSize, |
|||
textDecorations: null, |
|||
foregroundBrush: Brushes.Black, |
|||
backgroundBrush: null, |
|||
baselineAlignment: BaselineAlignment.Baseline, |
|||
cultureInfo: null); |
|||
} |
|||
|
|||
// TextParagraphProperties
|
|||
|
|||
private GenericTextParagraphProperties? _textParagraphProperties; |
|||
public GenericTextParagraphProperties TextParagraphProperties |
|||
{ |
|||
get |
|||
{ |
|||
return _textParagraphProperties ??= CreateTextParagraphProperties(); |
|||
} |
|||
set |
|||
{ |
|||
if (value == null) |
|||
throw new ArgumentNullException(nameof(value)); |
|||
|
|||
_textParagraphProperties = null; |
|||
SetCurrentValue(FlowDirectionProperty, value.FlowDirection); |
|||
} |
|||
} |
|||
|
|||
private GenericTextParagraphProperties CreateTextParagraphProperties() |
|||
{ |
|||
return new GenericTextParagraphProperties( |
|||
FlowDirection, |
|||
TextAlignment.Start, |
|||
firstLineInParagraph: false, |
|||
alwaysCollapsible: false, |
|||
TextRunProperties, |
|||
textWrapping: TextWrapping.NoWrap, |
|||
lineHeight: 0, |
|||
indent: 0, |
|||
letterSpacing: 0); |
|||
} |
|||
|
|||
private readonly ITextSource _textSource; |
|||
private class TextSource : ITextSource |
|||
{ |
|||
private readonly InteractiveLineControl _owner; |
|||
|
|||
public TextSource(InteractiveLineControl owner) |
|||
{ |
|||
_owner = owner; |
|||
} |
|||
|
|||
public TextRun? GetTextRun(int textSourceIndex) |
|||
{ |
|||
string text = _owner.Text ?? string.Empty; |
|||
|
|||
if (textSourceIndex < 0 || textSourceIndex >= text.Length) |
|||
return null; |
|||
|
|||
return new TextCharacters(text, _owner.TextRunProperties); |
|||
} |
|||
} |
|||
|
|||
private TextLine? _textLine; |
|||
public TextLine? TextLine => _textLine ??= TextFormatter.Current.FormatLine(_textSource, 0, Bounds.Size.Width, TextParagraphProperties); |
|||
|
|||
private TextLayout? _textLayout; |
|||
public TextLayout TextLayout => _textLayout ??= new TextLayout(_textSource, TextParagraphProperties); |
|||
|
|||
private Size? _textLineSize; |
|||
protected Size TextLineSize => _textLineSize ??= TextLine is { } textLine ? new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height) : default; |
|||
|
|||
private Size? _inkSize; |
|||
protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace + textLine.OverhangTrailing, textLine.Extent) : default; |
|||
|
|||
public event EventHandler? TextLineChanged; |
|||
|
|||
public InteractiveLineControl() |
|||
{ |
|||
_textSource = new TextSource(this); |
|||
|
|||
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased); |
|||
RenderOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias); |
|||
} |
|||
|
|||
private void InvalidateTextRunProperties() |
|||
{ |
|||
_textRunProperties = null; |
|||
InvalidateTextParagraphProperties(); |
|||
} |
|||
|
|||
private void InvalidateTextParagraphProperties() |
|||
{ |
|||
_textParagraphProperties = null; |
|||
InvalidateTextLine(); |
|||
} |
|||
|
|||
private void InvalidateTextLine() |
|||
{ |
|||
_textLayout = null; |
|||
_textLine = null; |
|||
_textLineSize = null; |
|||
_inkSize = null; |
|||
InvalidateMeasure(); |
|||
InvalidateVisual(); |
|||
|
|||
TextLineChanged?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
switch (change.Property.Name) |
|||
{ |
|||
case nameof(FontFamily): |
|||
case nameof(FontSize): |
|||
InvalidateTextRunProperties(); |
|||
break; |
|||
|
|||
case nameof(FontStyle): |
|||
case nameof(FontWeight): |
|||
case nameof(FontStretch): |
|||
InvalidateTextRunProperties(); |
|||
break; |
|||
|
|||
case nameof(FlowDirection): |
|||
InvalidateTextParagraphProperties(); |
|||
break; |
|||
|
|||
case nameof(Text): |
|||
InvalidateTextLine(); |
|||
break; |
|||
|
|||
case nameof(BaselineStroke): |
|||
_baselinePen = null; |
|||
InvalidateVisual(); |
|||
break; |
|||
|
|||
case nameof(TextBoundsStroke): |
|||
_textBoundsPen = null; |
|||
InvalidateVisual(); |
|||
break; |
|||
|
|||
case nameof(RunBoundsStroke): |
|||
_runBoundsPen = null; |
|||
InvalidateVisual(); |
|||
break; |
|||
|
|||
case nameof(NextHitStroke): |
|||
_nextHitPen = null; |
|||
InvalidateVisual(); |
|||
break; |
|||
|
|||
case nameof(PreviousHitStroke): |
|||
_previousHitPen = null; |
|||
InvalidateVisual(); |
|||
break; |
|||
|
|||
case nameof(BackspaceHitStroke): |
|||
_backspaceHitPen = null; |
|||
InvalidateVisual(); |
|||
break; |
|||
} |
|||
|
|||
base.OnPropertyChanged(change); |
|||
} |
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
if (TextLine == null) |
|||
return default; |
|||
|
|||
return new Size(Math.Max(TextLineSize.Width, InkSize.Width), Math.Max(TextLineSize.Height, InkSize.Height)); |
|||
} |
|||
|
|||
private const double VerticalSpacing = 5; |
|||
private const double HorizontalSpacing = 5; |
|||
private const double ArrowSize = 5; |
|||
|
|||
private Dictionary<string, FormattedText> _labelsCache = new(); |
|||
protected FormattedText GetOrCreateLabel(string label, IBrush brush, bool disableCache = false) |
|||
{ |
|||
if (_labelsCache.TryGetValue(label, out var text)) |
|||
return text; |
|||
|
|||
text = new FormattedText(label, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, Typeface.Default, 8, brush); |
|||
|
|||
if (!disableCache) |
|||
_labelsCache[label] = text; |
|||
|
|||
return text; |
|||
} |
|||
|
|||
private Rect _inkRenderBounds; |
|||
private Rect _lineRenderBounds; |
|||
|
|||
public Rect InkRenderBounds => _inkRenderBounds; |
|||
public Rect LineRenderBounds => _lineRenderBounds; |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
TextLine? textLine = TextLine; |
|||
if (textLine == null) |
|||
return; |
|||
|
|||
// overhang leading should be negative when extending (e.g. for j) WPF: "When the leading alignment point comes before the leading drawn pixel, the value is negative." - docs wrong but values correct
|
|||
// overhang trailing should be negative when extending (e.g. for f) WPF: "The OverhangTrailing value will be positive when the trailing drawn pixel comes before the trailing alignment point."
|
|||
// overhang after should be negative when inside (e.g. for x) WPF: "The value is positive if the bottommost drawn pixel goes below the line bottom, and is negative if it is within (on or above) the line."
|
|||
// => we want overhang before to be negative when inside (e.g. for x)
|
|||
|
|||
double overhangBefore = textLine.Extent - textLine.OverhangAfter - textLine.Height; |
|||
Rect inkBounds = new Rect(new Point(textLine.OverhangLeading, -overhangBefore), InkSize); |
|||
Rect lineBounds = new Rect(new Point(0, 0), TextLineSize); |
|||
|
|||
if (inkBounds.Left < 0) |
|||
lineBounds = lineBounds.Translate(new Vector(-inkBounds.Left, 0)); |
|||
|
|||
if (inkBounds.Top < 0) |
|||
lineBounds = lineBounds.Translate(new Vector(0, -inkBounds.Top)); |
|||
|
|||
_inkRenderBounds = inkBounds; |
|||
_lineRenderBounds = lineBounds; |
|||
|
|||
Rect bounds = new Rect(0, 0, Math.Max(inkBounds.Right, lineBounds.Right), Math.Max(inkBounds.Bottom, lineBounds.Bottom)); |
|||
double labelX = bounds.Right + HorizontalSpacing; |
|||
|
|||
if (Background is IBrush background) |
|||
context.FillRectangle(background, lineBounds); |
|||
|
|||
if (ExtentStroke != null) |
|||
{ |
|||
context.DrawRectangle(ExtentPen, inkBounds); |
|||
RenderLabel(context, nameof(textLine.Extent), ExtentStroke, labelX, inkBounds.Top); |
|||
} |
|||
|
|||
using (context.PushTransform(Matrix.CreateTranslation(lineBounds.Left, lineBounds.Top))) |
|||
{ |
|||
labelX -= lineBounds.Left; // labels to ignore horizontal transform
|
|||
|
|||
if (BaselineStroke != null) |
|||
{ |
|||
RenderFontLine(context, textLine.Baseline, lineBounds.Width, BaselinePen); // no other lines currently available in Avalonia
|
|||
RenderLabel(context, nameof(textLine.Baseline), BaselineStroke, labelX, textLine.Baseline); |
|||
} |
|||
|
|||
textLine.Draw(context, lineOrigin: default); |
|||
|
|||
var runBoundsStroke = RunBoundsStroke; |
|||
if (TextBoundsStroke != null || runBoundsStroke != null) |
|||
{ |
|||
IReadOnlyList<TextBounds> textBounds = textLine.GetTextBounds(textLine.FirstTextSourceIndex, textLine.Length); |
|||
foreach (var textBound in textBounds) |
|||
{ |
|||
if (runBoundsStroke != null) |
|||
{ |
|||
var runBounds = textBound.TextRunBounds; |
|||
foreach (var runBound in runBounds) |
|||
context.DrawRectangle(RunBoundsPen, runBound.Rectangle); |
|||
} |
|||
|
|||
context.DrawRectangle(TextBoundsPen, textBound.Rectangle); |
|||
} |
|||
} |
|||
|
|||
double y = inkBounds.Bottom - lineBounds.Top + VerticalSpacing * 2; |
|||
|
|||
if (NextHitStroke != null) |
|||
{ |
|||
RenderHits(context, NextHitPen, textLine, textLine.GetNextCaretCharacterHit, new CharacterHit(0), ref y); |
|||
RenderLabel(context, nameof(textLine.GetNextCaretCharacterHit), NextHitStroke, labelX, y); |
|||
y += VerticalSpacing * 2; |
|||
} |
|||
|
|||
if (PreviousHitStroke != null) |
|||
{ |
|||
RenderLabel(context, nameof(textLine.GetPreviousCaretCharacterHit), PreviousHitStroke, labelX, y); |
|||
RenderHits(context, PreviousHitPen, textLine, textLine.GetPreviousCaretCharacterHit, new CharacterHit(textLine.Length), ref y); |
|||
y += VerticalSpacing * 2; |
|||
} |
|||
|
|||
if (BackspaceHitStroke != null) |
|||
{ |
|||
RenderLabel(context, nameof(textLine.GetBackspaceCaretCharacterHit), BackspaceHitStroke, labelX, y); |
|||
RenderHits(context, BackspaceHitPen, textLine, textLine.GetBackspaceCaretCharacterHit, new CharacterHit(textLine.Length), ref y); |
|||
y += VerticalSpacing * 2; |
|||
} |
|||
|
|||
if (DistanceStroke != null) |
|||
{ |
|||
y += VerticalSpacing; |
|||
|
|||
var label = RenderLabel(context, nameof(textLine.GetDistanceFromCharacterHit), DistanceStroke, 0, y); |
|||
y += label.Height; |
|||
|
|||
for (int i = 0; i < textLine.Length; i++) |
|||
{ |
|||
var hit = new CharacterHit(i); |
|||
CharacterHit prevHit = default, nextHit = default; |
|||
|
|||
double leftLabelX = -HorizontalSpacing; |
|||
|
|||
// we want z-order to be previous, next, distance
|
|||
// but labels need to be ordered next, distance, previous
|
|||
if (NextHitStroke != null) |
|||
{ |
|||
nextHit = textLine.GetNextCaretCharacterHit(hit); |
|||
var nextLabel = RenderLabel(context, $" > {nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}", NextHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true); |
|||
leftLabelX -= nextLabel.WidthIncludingTrailingWhitespace; |
|||
} |
|||
|
|||
if (PreviousHitStroke != null) |
|||
{ |
|||
prevHit = textLine.GetPreviousCaretCharacterHit(hit); |
|||
var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex, 0)); |
|||
var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex + prevHit.TrailingLength, 0)); |
|||
RenderHorizontalPoint(context, x1, x2, y, PreviousHitPen, ArrowSize); |
|||
} |
|||
|
|||
if (NextHitStroke != null) |
|||
{ |
|||
var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex, 0)); |
|||
var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex + nextHit.TrailingLength, 0)); |
|||
RenderHorizontalPoint(context, x1, x2, y, NextHitPen, ArrowSize); |
|||
} |
|||
|
|||
label = RenderLabel(context, $"[{i}]", DistanceStroke, leftLabelX, y, TextAlignment.Right); |
|||
leftLabelX -= label.WidthIncludingTrailingWhitespace; |
|||
|
|||
if (PreviousHitStroke != null) |
|||
RenderLabel(context, $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength} < ", PreviousHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true); |
|||
|
|||
double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(i)); |
|||
RenderHorizontalBar(context, 0, distance, y, DistancePen, ArrowSize); |
|||
//RenderLabel(context, distance.ToString("F2"), DistanceStroke, distance + HorizontalSpacing, y, disableCache: true);
|
|||
|
|||
y += label.Height; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[return: NotNullIfNotNull("brush")] |
|||
private FormattedText? RenderLabel(DrawingContext context, string label, IBrush? brush, double x, double y, TextAlignment alignment = TextAlignment.Left, bool disableCache = false) |
|||
{ |
|||
if (brush == null) |
|||
return null; |
|||
|
|||
var text = GetOrCreateLabel(label, brush, disableCache); |
|||
|
|||
if (alignment == TextAlignment.Right) |
|||
context.DrawText(text, new Point(x - text.WidthIncludingTrailingWhitespace, y - text.Height / 2)); |
|||
else |
|||
context.DrawText(text, new Point(x, y - text.Height / 2)); |
|||
|
|||
return text; |
|||
} |
|||
|
|||
private void RenderHits(DrawingContext context, IPen hitPen, TextLine textLine, Func<CharacterHit, CharacterHit> nextHit, CharacterHit startingHit, ref double y) |
|||
{ |
|||
CharacterHit lastHit = startingHit; |
|||
double lastX = textLine.GetDistanceFromCharacterHit(lastHit); |
|||
double lastDirection = 0; |
|||
y -= VerticalSpacing; // we always start with adding one below
|
|||
|
|||
while (true) |
|||
{ |
|||
CharacterHit hit = nextHit(lastHit); |
|||
if (hit == lastHit) |
|||
break; |
|||
|
|||
double x = textLine.GetDistanceFromCharacterHit(hit); |
|||
double direction = Math.Sign(x - lastX); |
|||
|
|||
if (direction == 0 || lastDirection != direction) |
|||
y += VerticalSpacing; |
|||
|
|||
if (direction == 0) |
|||
RenderPoint(context, x, y, hitPen, ArrowSize); |
|||
else |
|||
RenderHorizontalArrow(context, lastX, x, y, hitPen, ArrowSize); |
|||
|
|||
lastX = x; |
|||
lastHit = hit; |
|||
lastDirection = direction; |
|||
} |
|||
} |
|||
|
|||
private void RenderPoint(DrawingContext context, double x, double y, IPen pen, double arrowHeight) |
|||
{ |
|||
context.DrawEllipse(pen.Brush, pen, new Point(x, y), ArrowSize / 2, ArrowSize / 2); |
|||
} |
|||
|
|||
private void RenderHorizontalPoint(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size) |
|||
{ |
|||
PathGeometry startCap = new PathGeometry(); |
|||
PathFigure startFigure = new PathFigure(); |
|||
startFigure.StartPoint = new Point(xStart, y - size / 2); |
|||
startFigure.IsClosed = true; |
|||
startFigure.IsFilled = true; |
|||
startFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xStart, y + size / 2), SweepDirection = SweepDirection.CounterClockwise }); |
|||
startCap.Figures!.Add(startFigure); |
|||
|
|||
context.DrawGeometry(pen.Brush, pen, startCap); |
|||
|
|||
PathGeometry endCap = new PathGeometry(); |
|||
PathFigure endFigure = new PathFigure(); |
|||
endFigure.StartPoint = new Point(xEnd, y - size / 2); |
|||
endFigure.IsClosed = true; |
|||
endFigure.IsFilled = false; |
|||
endFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xEnd, y + size / 2), SweepDirection = SweepDirection.Clockwise }); |
|||
endCap.Figures!.Add(endFigure); |
|||
|
|||
context.DrawGeometry(pen.Brush, pen, endCap); |
|||
} |
|||
|
|||
private void RenderHorizontalArrow(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size) |
|||
{ |
|||
context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y)); |
|||
context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
|
|||
|
|||
if (xEnd >= xStart) |
|||
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry( |
|||
[ |
|||
new Point(xEnd - size, y - size / 2), |
|||
new Point(xEnd - size, y + size/2), |
|||
new Point(xEnd, y) |
|||
], isFilled: true)); |
|||
else |
|||
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry( |
|||
[ |
|||
new Point(xEnd + size, y - size / 2), |
|||
new Point(xEnd + size, y + size/2), |
|||
new Point(xEnd, y) |
|||
], isFilled: true)); |
|||
} |
|||
private void RenderHorizontalBar(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size) |
|||
{ |
|||
context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y)); |
|||
context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
|
|||
context.DrawLine(pen, new Point(xEnd, y - size / 2), new Point(xEnd, y + size / 2)); // end cap
|
|||
} |
|||
|
|||
private void RenderFontLine(DrawingContext context, double y, double width, IPen pen) |
|||
{ |
|||
context.DrawLine(pen, new Point(0, y), new Point(width, y)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:local="clr-namespace:TextTestApp" |
|||
x:Class="TextTestApp.MainWindow" |
|||
Title="Text Test App" Width="700" Height="700"> |
|||
|
|||
<DockPanel> |
|||
<Border DockPanel.Dock="Bottom" Background="WhiteSmoke" BorderThickness="0,1,0,0" BorderBrush="Silver" Padding="2"> |
|||
<DockPanel> |
|||
<ToggleSwitch Name="_hitRangeToggle" DockPanel.Dock="Right" OnContent="HitTestTextRange" OffContent="HitTestTextPosition" IsCheckedChanged="OnHitTestMethodChanged" /> |
|||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> |
|||
<TextBlock Text="HitTestPoint:" Margin="5,0" /> |
|||
<TextBlock Name="_coordinates" MinWidth="120" /> |
|||
<Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" /> |
|||
<TextBlock Text="TextPosition:" Margin="5,0" /> |
|||
<TextBlock Name="_hit" MinWidth="60" /> |
|||
<Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" /> |
|||
</StackPanel> |
|||
</DockPanel> |
|||
</Border> |
|||
|
|||
<DockPanel DockPanel.Dock="Top" Margin="5"> |
|||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right"> |
|||
<Label Content="_Font:" Target="{Binding ElementName=_font}" VerticalAlignment="Center" Margin="5,0,0,0" /> |
|||
<ComboBox Name="_font" ItemsSource="{Binding SystemFonts, Source={x:Static FontManager.Current}}" /> |
|||
<Label Content="_Size:" Target="{Binding ElementName=_size}" VerticalAlignment="Center" Margin="5,0,0,0" /> |
|||
<TextBox Name="_size" VerticalAlignment="Center" Text="64" /> |
|||
<Button VerticalAlignment="Center" Click="OnNewWindowClick" ToolTip.Tip="New window" Margin="5,0,0,0">+</Button> |
|||
</StackPanel> |
|||
|
|||
<Label Content="_Text:" Target="{Binding ElementName=_text}" VerticalAlignment="Center"/> |
|||
<TextBox Name="_text" Text="Hello!" VerticalAlignment="Center" /> |
|||
</DockPanel> |
|||
|
|||
<Grid RowDefinitions="*,5,*"> |
|||
<local:InteractiveLineControl Name="_rendering" DockPanel.Dock="Top" Margin="16" HorizontalAlignment="Center" |
|||
|
|||
Text="{Binding Text, ElementName=_text}" |
|||
FontFamily="{Binding SelectedValue, ElementName=_font}" |
|||
FontSize="{Binding Text, ElementName=_size}" |
|||
Background="BlanchedAlmond" |
|||
ExtentStroke="Black" |
|||
BaselineStroke="Blue" |
|||
TextBoundsStroke="Goldenrod" |
|||
RunBoundsStroke="Gold" |
|||
NextHitStroke="Green" |
|||
PreviousHitStroke="Blue" |
|||
BackspaceHitStroke="Red" |
|||
DistanceStroke="Black" |
|||
|
|||
PointerMoved="OnPointerMoved" |
|||
/> |
|||
|
|||
<GridSplitter Grid.Row="1" /> |
|||
|
|||
<TabControl Grid.Row="2" DockPanel.Dock="Bottom" Background="White" BorderBrush="Whitesmoke" BorderThickness="0,1,0,0"> |
|||
<TabItem Header="Shaped Buffer"> |
|||
<ListBox Name="_buffer" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionMode="Multiple" SelectionChanged="OnBufferSelectionChanged" Background="Transparent"> |
|||
<ListBox.Styles> |
|||
<Style Selector="ListBoxItem"> |
|||
<Setter Property="Padding" Value="0"/> |
|||
<Setter Property="Background" Value="White" /> |
|||
</Style> |
|||
</ListBox.Styles> |
|||
<Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1"> |
|||
<local:GridRow ColumnSpacing="10"> |
|||
<TextBlock Text="" /> |
|||
<TextBlock Text="Index" /> |
|||
<TextBlock Text="Characters" /> |
|||
<TextBlock Text="Codepoints" /> |
|||
<TextBlock Text="Glyph" /> |
|||
<TextBlock Text="Glyph ID" /> |
|||
<TextBlock Text="Advance" /> |
|||
<TextBlock Text="Offset" /> |
|||
<TextBlock Text="Ink Bounds" /> |
|||
</local:GridRow> |
|||
</Border> |
|||
</ListBox> |
|||
</TabItem> |
|||
<TabItem Header="Character Hits"> |
|||
<ListBox Name="_hits" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionChanged="OnHitsSelectionChanged" Background="Transparent"> |
|||
<ListBox.Styles> |
|||
<Style Selector="ListBoxItem"> |
|||
<Setter Property="Padding" Value="0"/> |
|||
<Setter Property="Background" Value="White" /> |
|||
</Style> |
|||
</ListBox.Styles> |
|||
<Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1"> |
|||
<local:GridRow ColumnSpacing="10"> |
|||
<TextBlock Text="" /> |
|||
<TextBlock Text="Backspace Hit" /> |
|||
<TextBlock Text="Previous Hit" /> |
|||
<TextBlock Text="Index" /> |
|||
<TextBlock Text="Next Hit" /> |
|||
<TextBlock Text="Codepoint" /> |
|||
<TextBlock Text="Character" /> |
|||
<TextBlock Text="Distance" /> |
|||
</local:GridRow> |
|||
</Border> |
|||
</ListBox> |
|||
</TabItem> |
|||
</TabControl> |
|||
</Grid> |
|||
</DockPanel> |
|||
</Window> |
|||
@ -0,0 +1,340 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.TextFormatting; |
|||
|
|||
namespace TextTestApp |
|||
{ |
|||
public partial class MainWindow : Window |
|||
{ |
|||
private SelectionAdorner? _selectionAdorner; |
|||
|
|||
public MainWindow() |
|||
{ |
|||
InitializeComponent(); |
|||
|
|||
_selectionAdorner = new(); |
|||
_selectionAdorner.Stroke = Brushes.Red; |
|||
_selectionAdorner.Fill = new SolidColorBrush(Colors.LightSkyBlue, 0.25); |
|||
_selectionAdorner.IsHitTestVisible = false; |
|||
AdornerLayer.SetIsClipEnabled(_selectionAdorner, false); |
|||
AdornerLayer.SetAdorner(_rendering, _selectionAdorner); |
|||
|
|||
_rendering.TextLineChanged += OnShapeBufferChanged; |
|||
OnShapeBufferChanged(); |
|||
} |
|||
|
|||
private void OnNewWindowClick(object? sender, RoutedEventArgs e) |
|||
{ |
|||
MainWindow win = new MainWindow(); |
|||
win.Show(); |
|||
} |
|||
|
|||
protected override void OnKeyDown(KeyEventArgs e) |
|||
{ |
|||
if (e.Key == Key.F5) |
|||
{ |
|||
_rendering.InvalidateVisual(); |
|||
OnShapeBufferChanged(); |
|||
e.Handled = true; |
|||
} |
|||
else if (e.Key == Key.Escape) |
|||
{ |
|||
if (_hits.IsKeyboardFocusWithin && _hits.SelectedIndex != -1) |
|||
{ |
|||
_hits.SelectedIndex = -1; |
|||
e.Handled = true; |
|||
} |
|||
else if (_buffer.IsKeyboardFocusWithin && _buffer.SelectedIndex != -1) |
|||
{ |
|||
_buffer.SelectedIndex = -1; |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
base.OnKeyDown(e); |
|||
} |
|||
|
|||
private void OnShapeBufferChanged(object? sender, EventArgs e) => OnShapeBufferChanged(); |
|||
private void OnShapeBufferChanged() |
|||
{ |
|||
if (_selectionAdorner == null) |
|||
return; |
|||
|
|||
ListBuffers(); |
|||
ListHits(); |
|||
|
|||
Rect bounds = _rendering.LineRenderBounds; |
|||
_selectionAdorner!.Transform = Matrix.CreateTranslation(bounds.X, bounds.Y); |
|||
} |
|||
|
|||
private void ListBuffers() |
|||
{ |
|||
for (int i = _buffer.ItemCount - 1; i >= 1; i--) |
|||
_buffer.Items.RemoveAt(i); |
|||
|
|||
TextLine? textLine = _rendering.TextLine; |
|||
if (textLine == null) |
|||
return; |
|||
|
|||
double currentX = _rendering.LineRenderBounds.Left; |
|||
foreach (TextRun run in textLine.TextRuns) |
|||
{ |
|||
if (run is ShapedTextRun shapedRun) |
|||
{ |
|||
_buffer.Items.Add(new TextBlock |
|||
{ |
|||
Text = $"{run.GetType().Name}: Bidi = {shapedRun.BidiLevel}, Font = {shapedRun.ShapedBuffer.GlyphTypeface.FamilyName}", |
|||
FontWeight = FontWeight.Bold, |
|||
Padding = new Thickness(10, 0), |
|||
Tag = run, |
|||
}); |
|||
|
|||
ListBuffer(textLine, shapedRun, ref currentX); |
|||
} |
|||
else |
|||
_buffer.Items.Add(new TextBlock |
|||
{ |
|||
Text = run.GetType().Name, |
|||
FontWeight = FontWeight.Bold, |
|||
Padding = new Thickness(10, 0), |
|||
Tag = run |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private void ListHits() |
|||
{ |
|||
for (int i = _hits.ItemCount - 1; i >= 1; i--) |
|||
_hits.Items.RemoveAt(i); |
|||
|
|||
TextLine? textLine = _rendering.TextLine; |
|||
if (textLine == null) |
|||
return; |
|||
|
|||
for (int i = 0; i < textLine.Length; i++) |
|||
{ |
|||
string? clusterText = _rendering.Text!.Substring(i, 1); |
|||
string? clusterHex = ToHex(clusterText); |
|||
|
|||
var hit = new CharacterHit(i); |
|||
var prevHit = textLine.GetPreviousCaretCharacterHit(hit); |
|||
var nextHit = textLine.GetNextCaretCharacterHit(hit); |
|||
var bkspHit = textLine.GetBackspaceCaretCharacterHit(hit); |
|||
|
|||
GridRow row = new GridRow { ColumnSpacing = 10 }; |
|||
row.Children.Add(new Control()); |
|||
row.Children.Add(new TextBlock { Text = $"{bkspHit.FirstCharacterIndex}+{bkspHit.TrailingLength}" }); |
|||
row.Children.Add(new TextBlock { Text = $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength}" }); |
|||
row.Children.Add(new TextBlock { Text = i.ToString(), FontWeight = FontWeight.Bold }); |
|||
row.Children.Add(new TextBlock { Text = $"{nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}" }); |
|||
row.Children.Add(new TextBlock { Text = clusterHex }); |
|||
row.Children.Add(new TextBlock { Text = clusterText }); |
|||
row.Children.Add(new TextBlock { Text = textLine.GetDistanceFromCharacterHit(hit).ToString() }); |
|||
row.Tag = i; |
|||
|
|||
_hits.Items.Add(row); |
|||
} |
|||
} |
|||
|
|||
private static readonly IBrush TransparentAliceBlue = new SolidColorBrush(0x0F0188FF); |
|||
private static readonly IBrush TransparentAntiqueWhite = new SolidColorBrush(0x28DF8000); |
|||
private void ListBuffer(TextLine textLine, ShapedTextRun shapedRun, ref double currentX) |
|||
{ |
|||
ShapedBuffer buffer = shapedRun.ShapedBuffer; |
|||
|
|||
int lastClusterStart = -1; |
|||
bool oddCluster = false; |
|||
|
|||
IReadOnlyList<GlyphInfo> glyphInfos = buffer; |
|||
|
|||
currentX += shapedRun.GlyphRun.BaselineOrigin.X; |
|||
for (var i = 0; i < glyphInfos.Count; i++) |
|||
{ |
|||
GlyphInfo info = glyphInfos[i]; |
|||
int clusterStart = info.GlyphCluster; |
|||
int clusterLength = FindClusterLenghtAt(i); |
|||
string? clusterText = _rendering.Text!.Substring(clusterStart, clusterLength); |
|||
string? clusterHex = ToHex(clusterText); |
|||
|
|||
Border border = new Border(); |
|||
if (clusterStart == lastClusterStart) |
|||
{ |
|||
clusterText = clusterHex = null; |
|||
} |
|||
else |
|||
{ |
|||
oddCluster = !oddCluster; |
|||
lastClusterStart = clusterStart; |
|||
} |
|||
border.Background = oddCluster ? TransparentAliceBlue : TransparentAntiqueWhite; |
|||
|
|||
|
|||
GridRow row = new GridRow { ColumnSpacing = 10 }; |
|||
row.Children.Add(new Control()); |
|||
row.Children.Add(new TextBlock { Text = clusterStart.ToString() }); |
|||
row.Children.Add(new TextBlock { Text = clusterText }); |
|||
row.Children.Add(new TextBlock { Text = clusterHex, TextWrapping = TextWrapping.Wrap }); |
|||
row.Children.Add(new Image { Source = CreateGlyphDrawing(shapedRun.GlyphRun.GlyphTypeface, FontSize, info), Margin = new Thickness(2) }); |
|||
row.Children.Add(new TextBlock { Text = info.GlyphIndex.ToString() }); |
|||
row.Children.Add(new TextBlock { Text = info.GlyphAdvance.ToString() }); |
|||
row.Children.Add(new TextBlock { Text = info.GlyphOffset.ToString() }); |
|||
|
|||
Geometry glyph = GetGlyphOutline(shapedRun.GlyphRun.GlyphTypeface, shapedRun.GlyphRun.FontRenderingEmSize, info); |
|||
Rect glyphBounds = glyph.Bounds; |
|||
Rect offsetBounds = glyphBounds.Translate(new Vector(currentX + info.GlyphOffset.X, info.GlyphOffset.Y)); |
|||
|
|||
TextBlock boundsBlock = new TextBlock { Text = offsetBounds.ToString() }; |
|||
ToolTip.SetTip(boundsBlock, "Origin bounds: " + glyphBounds); |
|||
row.Children.Add(boundsBlock); |
|||
|
|||
border.Child = row; |
|||
border.Tag = offsetBounds; |
|||
_buffer.Items.Add(border); |
|||
|
|||
currentX += glyphInfos[i].GlyphAdvance; |
|||
} |
|||
|
|||
int FindClusterLenghtAt(int index) |
|||
{ |
|||
int cluster = glyphInfos[index].GlyphCluster; |
|||
if (shapedRun.BidiLevel % 2 == 0) |
|||
{ |
|||
while (++index < glyphInfos.Count) |
|||
if (glyphInfos[index].GlyphCluster != cluster) |
|||
return glyphInfos[index].GlyphCluster - cluster; |
|||
|
|||
return shapedRun.Length + glyphInfos[0].GlyphCluster - cluster; |
|||
} |
|||
else |
|||
{ |
|||
while (--index >= 0) |
|||
if (glyphInfos[index].GlyphCluster != cluster) |
|||
return glyphInfos[index].GlyphCluster - cluster; |
|||
|
|||
return shapedRun.Length + glyphInfos[glyphInfos.Count - 1].GlyphCluster - cluster; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private IImage CreateGlyphDrawing(IGlyphTypeface glyphTypeface, double emSize, GlyphInfo info) |
|||
{ |
|||
return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } }; |
|||
} |
|||
|
|||
private Geometry GetGlyphOutline(IGlyphTypeface typeface, double emSize, GlyphInfo info) |
|||
{ |
|||
// substitute for GlyphTypeface.GetGlyphOutline
|
|||
return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry(); |
|||
} |
|||
|
|||
private void OnPointerMoved(object sender, PointerEventArgs e) |
|||
{ |
|||
InteractiveLineControl lineControl = (InteractiveLineControl)sender; |
|||
TextLayout textLayout = lineControl.TextLayout; |
|||
Rect lineBounds = lineControl.LineRenderBounds; |
|||
|
|||
PointerPoint pointerPoint = e.GetCurrentPoint(lineControl); |
|||
Point point = new Point(pointerPoint.Position.X - lineBounds.Left, pointerPoint.Position.Y - lineBounds.Top); |
|||
_coordinates.Text = $"{pointerPoint.Position.X:F4}, {pointerPoint.Position.Y:F4}"; |
|||
|
|||
TextHitTestResult textHit = textLayout.HitTestPoint(point); |
|||
_hit.Text = $"{textHit.TextPosition} ({textHit.CharacterHit.FirstCharacterIndex}+{textHit.CharacterHit.TrailingLength})"; |
|||
if (textHit.IsTrailing) |
|||
_hit.Text += " T"; |
|||
|
|||
if (textHit.IsInside) |
|||
{ |
|||
_hits.SelectedIndex = textHit.TextPosition + 1; // header
|
|||
} |
|||
else |
|||
_hits.SelectedIndex = -1; |
|||
} |
|||
|
|||
private void OnHitTestMethodChanged(object? sender, RoutedEventArgs e) |
|||
{ |
|||
_hits.SelectionMode = _hitRangeToggle.IsChecked == true ? SelectionMode.Multiple : SelectionMode.Single; |
|||
} |
|||
|
|||
private void OnHitsSelectionChanged(object? sender, SelectionChangedEventArgs e) |
|||
{ |
|||
if (_selectionAdorner == null) |
|||
return; |
|||
|
|||
List<Rect> rectangles = new List<Rect>(); |
|||
TextLayout textLayout = _rendering.TextLayout; |
|||
|
|||
if (_hitRangeToggle.IsChecked == true) |
|||
{ |
|||
// collect continuous selected indices
|
|||
List<(int start, int length)> selections = new(1); |
|||
|
|||
int[] indices = _hits.Selection.SelectedIndexes.ToArray(); |
|||
Array.Sort(indices); |
|||
|
|||
int currentIndex = -1; |
|||
int currentLength = 0; |
|||
for (int i = 0; i < indices.Length; i++) |
|||
if (_hits.Items[indices[i]] is Control { Tag: int index }) |
|||
{ |
|||
if (index == currentIndex + currentLength) |
|||
{ |
|||
currentLength++; |
|||
} |
|||
else |
|||
{ |
|||
if (currentLength > 0) |
|||
selections.Add((currentIndex, currentLength)); |
|||
|
|||
currentIndex = index; |
|||
currentLength = 1; |
|||
} |
|||
} |
|||
|
|||
if (currentLength > 0) |
|||
selections.Add((currentIndex, currentLength)); |
|||
|
|||
foreach (var selection in selections) |
|||
{ |
|||
var selectionRectangles = textLayout.HitTestTextRange(selection.start, selection.length); |
|||
rectangles.AddRange(selectionRectangles); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (_hits.SelectedItem is Control { Tag: int index }) |
|||
{ |
|||
Rect rect = textLayout.HitTestTextPosition(index); |
|||
rectangles.Add(rect); |
|||
} |
|||
} |
|||
|
|||
_selectionAdorner.Rectangles = rectangles; |
|||
} |
|||
|
|||
private void OnBufferSelectionChanged(object? sender, SelectionChangedEventArgs e) |
|||
{ |
|||
List<Rect> rectangles = new List<Rect>(_buffer.Selection.Count); |
|||
|
|||
foreach (var row in _buffer.SelectedItems) |
|||
if (row is Control { Tag: Rect rect }) |
|||
rectangles.Add(rect); |
|||
|
|||
_selectionAdorner.Rectangles = rectangles; |
|||
} |
|||
|
|||
private static string ToHex(string s) |
|||
{ |
|||
if (string.IsNullOrEmpty(s)) |
|||
return s; |
|||
|
|||
return string.Join(" ", s.Select(c => ((int)c).ToString("X4"))); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System; |
|||
using Avalonia; |
|||
|
|||
namespace TextTestApp |
|||
{ |
|||
static class Program |
|||
{ |
|||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
|||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
|||
// yet and stuff might break.
|
|||
[STAThread] |
|||
public static void Main(string[] args) |
|||
{ |
|||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
} |
|||
|
|||
// Avalonia configuration, don't remove; also used by visual designer.
|
|||
public static AppBuilder BuildAvaloniaApp() |
|||
{ |
|||
return AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
|
|||
namespace TextTestApp |
|||
{ |
|||
public class SelectionAdorner : Control |
|||
{ |
|||
public static readonly StyledProperty<IBrush?> FillProperty = |
|||
AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Fill)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> StrokeProperty = |
|||
AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Stroke)); |
|||
|
|||
public static readonly StyledProperty<Matrix> TransformProperty = |
|||
AvaloniaProperty.Register<SelectionAdorner, Matrix>(nameof(Transform), Matrix.Identity); |
|||
|
|||
public Matrix Transform |
|||
{ |
|||
get => this.GetValue(TransformProperty); |
|||
set => SetValue(TransformProperty, value); |
|||
} |
|||
|
|||
public IBrush? Stroke |
|||
{ |
|||
get => GetValue(StrokeProperty); |
|||
set => SetValue(StrokeProperty, value); |
|||
} |
|||
|
|||
public IBrush? Fill |
|||
{ |
|||
get => GetValue(FillProperty); |
|||
set => SetValue(FillProperty, value); |
|||
} |
|||
|
|||
private IList<Rect>? _rectangles; |
|||
public IList<Rect>? Rectangles |
|||
{ |
|||
get => _rectangles; |
|||
set |
|||
{ |
|||
_rectangles = value; |
|||
InvalidateVisual(); |
|||
} |
|||
} |
|||
|
|||
public SelectionAdorner() |
|||
{ |
|||
AffectsRender<SelectionAdorner>(FillProperty, StrokeProperty, TransformProperty); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
var rectangles = Rectangles; |
|||
if (rectangles == null) |
|||
return; |
|||
|
|||
using (context.PushTransform(Transform)) |
|||
{ |
|||
Pen pen = new Pen(Stroke, 1); |
|||
for (int i = 0; i < rectangles.Count; i++) |
|||
{ |
|||
Rect rectangle = rectangles[i]; |
|||
Rect normalized = rectangle.Width < 0 ? new Rect(rectangle.TopRight, rectangle.BottomLeft) : rectangle; |
|||
|
|||
if (rectangles[i].Width == 0) |
|||
context.DrawLine(pen, rectangle.TopLeft, rectangle.BottomRight); |
|||
else |
|||
context.DrawRectangle(Fill, pen, normalized); |
|||
|
|||
RenderCue(context, pen, rectangle.TopLeft, 5, isFilled: true); |
|||
RenderCue(context, pen, rectangle.TopRight, 5, isFilled: false); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void RenderCue(DrawingContext context, IPen pen, Point p, double size, bool isFilled) |
|||
{ |
|||
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry( |
|||
[ |
|||
new Point(p.X - size / 2, p.Y - size), |
|||
new Point(p.X + size / 2, p.Y - size), |
|||
new Point(p.X, p.Y), |
|||
new Point(p.X - size / 2, p.Y - size), |
|||
], isFilled)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework> |
|||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch> |
|||
<ApplicationManifest>app.manifest</ApplicationManifest> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\build\SourceGenerators.props" /> |
|||
|
|||
</Project> |
|||
@ -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,67 @@ |
|||
using System; |
|||
using Android.Views; |
|||
using Android.Views.InputMethods; |
|||
using Avalonia.Android.Platform.SkiaPlatform; |
|||
|
|||
namespace Avalonia.Android |
|||
{ |
|||
public partial class AvaloniaView : IInitEditorInfo |
|||
{ |
|||
private Func<TopLevelImpl, EditorInfo, IInputConnection>? _initEditorInfo; |
|||
|
|||
public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs) |
|||
{ |
|||
return _initEditorInfo?.Invoke(_view, outAttrs!)!; |
|||
} |
|||
|
|||
void IInitEditorInfo.InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init) |
|||
{ |
|||
_initEditorInfo = init; |
|||
} |
|||
|
|||
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, global::Android.Graphics.Rect? previouslyFocusedRect) |
|||
{ |
|||
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect); |
|||
_accessHelper?.OnFocusChanged(gainFocus, (int)direction, previouslyFocusedRect); |
|||
} |
|||
|
|||
protected override bool DispatchHoverEvent(MotionEvent? e) |
|||
{ |
|||
return _accessHelper?.DispatchHoverEvent(e!) == true || base.DispatchHoverEvent(e); |
|||
} |
|||
|
|||
protected override bool DispatchGenericPointerEvent(MotionEvent? e) |
|||
{ |
|||
var result = _view.PointerHelper.DispatchMotionEvent(e, out var callBase); |
|||
|
|||
var baseResult = callBase && base.DispatchGenericPointerEvent(e); |
|||
|
|||
return result ?? baseResult; |
|||
} |
|||
|
|||
public override bool DispatchTouchEvent(MotionEvent? e) |
|||
{ |
|||
var result = _view.PointerHelper.DispatchMotionEvent(e, out var callBase); |
|||
var baseResult = callBase && base.DispatchTouchEvent(e); |
|||
|
|||
if(result == true) |
|||
{ |
|||
// Request focus for this view
|
|||
RequestFocus(); |
|||
} |
|||
|
|||
return result ?? baseResult; |
|||
} |
|||
|
|||
public override bool DispatchKeyEvent(KeyEvent? e) |
|||
{ |
|||
var res = _view.KeyboardHelper.DispatchKeyEvent(e, out var callBase); |
|||
if (res == false) |
|||
callBase = _accessHelper?.DispatchKeyEvent(e!) == false && callBase; |
|||
|
|||
var baseResult = callBase && base.DispatchKeyEvent(e); |
|||
|
|||
return res ?? baseResult; |
|||
} |
|||
} |
|||
} |
|||
@ -1,40 +1,130 @@ |
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
public class DispatcherPriorityAwaitable : INotifyCompletion |
|||
/// <summary>
|
|||
/// A simple awaitable type that will return a DispatcherPriorityAwaiter.
|
|||
/// </summary>
|
|||
public struct DispatcherPriorityAwaitable |
|||
{ |
|||
private readonly Dispatcher _dispatcher; |
|||
private protected readonly Task Task; |
|||
private readonly Task? _task; |
|||
private readonly DispatcherPriority _priority; |
|||
|
|||
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority) |
|||
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task? task, DispatcherPriority priority) |
|||
{ |
|||
_dispatcher = dispatcher; |
|||
Task = task; |
|||
_task = task; |
|||
_priority = priority; |
|||
} |
|||
|
|||
public void OnCompleted(Action continuation) => |
|||
Task.ContinueWith(_ => _dispatcher.Post(continuation, _priority)); |
|||
|
|||
public bool IsCompleted => Task.IsCompleted; |
|||
public DispatcherPriorityAwaiter GetAwaiter() => new(_dispatcher, _task, _priority); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is returned from DispatcherPriorityAwaitable.GetAwaiter()
|
|||
/// </remarks>
|
|||
public struct DispatcherPriorityAwaiter : INotifyCompletion |
|||
{ |
|||
private readonly Dispatcher _dispatcher; |
|||
private readonly Task? _task; |
|||
private readonly DispatcherPriority _priority; |
|||
|
|||
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task? task, DispatcherPriority priority) |
|||
{ |
|||
_dispatcher = dispatcher; |
|||
_task = task; |
|||
_priority = priority; |
|||
} |
|||
|
|||
public void OnCompleted(Action continuation) |
|||
{ |
|||
if(_task == null || _task.IsCompleted) |
|||
_dispatcher.Post(continuation, _priority); |
|||
else |
|||
{ |
|||
var self = this; |
|||
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => |
|||
{ |
|||
self._dispatcher.Post(continuation, self._priority); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This always returns false since continuation is requested to be queued to a dispatcher queue
|
|||
/// </summary>
|
|||
public bool IsCompleted => false; |
|||
|
|||
public void GetResult() |
|||
{ |
|||
if (_task != null) |
|||
_task.GetAwaiter().GetResult(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A simple awaitable type that will return a DispatcherPriorityAwaiter<T>.
|
|||
/// </summary>
|
|||
public struct DispatcherPriorityAwaitable<T> |
|||
{ |
|||
private readonly Dispatcher _dispatcher; |
|||
private readonly Task<T> _task; |
|||
private readonly DispatcherPriority _priority; |
|||
|
|||
public void GetResult() => Task.GetAwaiter().GetResult(); |
|||
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) |
|||
{ |
|||
_dispatcher = dispatcher; |
|||
_task = task; |
|||
_priority = priority; |
|||
} |
|||
|
|||
public DispatcherPriorityAwaitable GetAwaiter() => this; |
|||
public DispatcherPriorityAwaiter<T> GetAwaiter() => new(_dispatcher, _task, _priority); |
|||
} |
|||
|
|||
public sealed class DispatcherPriorityAwaitable<T> : DispatcherPriorityAwaitable |
|||
/// <summary>
|
|||
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is returned from DispatcherPriorityAwaitable<T>.GetAwaiter()
|
|||
/// </remarks>
|
|||
public struct DispatcherPriorityAwaiter<T> : INotifyCompletion |
|||
{ |
|||
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) : base( |
|||
dispatcher, task, priority) |
|||
private readonly Dispatcher _dispatcher; |
|||
private readonly Task<T> _task; |
|||
private readonly DispatcherPriority _priority; |
|||
|
|||
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) |
|||
{ |
|||
_dispatcher = dispatcher; |
|||
_task = task; |
|||
_priority = priority; |
|||
} |
|||
|
|||
public new T GetResult() => ((Task<T>)Task).GetAwaiter().GetResult(); |
|||
public void OnCompleted(Action continuation) |
|||
{ |
|||
if(_task.IsCompleted) |
|||
_dispatcher.Post(continuation, _priority); |
|||
else |
|||
{ |
|||
var self = this; |
|||
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => |
|||
{ |
|||
self._dispatcher.Post(continuation, self._priority); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This always returns false since continuation is requested to be queued to a dispatcher queue
|
|||
/// </summary>
|
|||
public bool IsCompleted => false; |
|||
|
|||
public new DispatcherPriorityAwaitable<T> GetAwaiter() => this; |
|||
} |
|||
public void GetResult() => _task.GetAwaiter().GetResult(); |
|||
} |
|||
@ -1,9 +1,8 @@ |
|||
using System.Collections.Generic; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface ICodeGenerator |
|||
{ |
|||
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names); |
|||
string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names); |
|||
} |
|||
|
|||
@ -1,6 +1,8 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface IGlobPattern |
|||
internal interface IGlobPattern : IEquatable<IGlobPattern> |
|||
{ |
|||
bool Matches(string str); |
|||
} |
|||
|
|||
@ -1,11 +1,46 @@ |
|||
using System.Collections.Immutable; |
|||
using System.Threading; |
|||
using XamlX.Ast; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface IViewResolver |
|||
{ |
|||
ResolvedView? ResolveView(string xaml); |
|||
ResolvedViewDocument? ResolveView(string xaml, CancellationToken cancellationToken); |
|||
} |
|||
|
|||
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); |
|||
internal record ResolvedViewInfo(string ClassName, string Namespace) |
|||
{ |
|||
public string FullName => $"{Namespace}.{ClassName}"; |
|||
public override string ToString() => FullName; |
|||
} |
|||
|
|||
internal record ResolvedViewDocument(string ClassName, string Namespace, XamlDocument Xaml) |
|||
: ResolvedViewInfo(ClassName, Namespace); |
|||
|
|||
internal record ResolvedXmlView( |
|||
string ClassName, |
|||
string Namespace, |
|||
EquatableList<ResolvedXmlName> XmlNames) |
|||
: ResolvedViewInfo(ClassName, Namespace) |
|||
{ |
|||
public ResolvedXmlView(ResolvedViewInfo info, EquatableList<ResolvedXmlName> xmlNames) |
|||
: this(info.ClassName, info.Namespace, xmlNames) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
internal record ResolvedView( |
|||
string ClassName, |
|||
string Namespace, |
|||
bool IsWindow, |
|||
EquatableList<ResolvedName> Names) |
|||
: ResolvedViewInfo(ClassName, Namespace) |
|||
{ |
|||
public ResolvedView(ResolvedViewInfo info, bool isWindow, EquatableList<ResolvedName> names) |
|||
: this(info.ClassName, info.Namespace, isWindow, names) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
|
|||
namespace Avalonia.Generators.Common; |
|||
|
|||
// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#pipeline-model-design
|
|||
// With minor modification to use ReadOnlyCollection instead of List
|
|||
internal class EquatableList<T>(IList<T> collection) |
|||
: ReadOnlyCollection<T>(collection), IEquatable<EquatableList<T>> |
|||
{ |
|||
public bool Equals(EquatableList<T>? other) |
|||
{ |
|||
// If the other list is null or a different size, they're not equal
|
|||
if (other is null || Count != other.Count) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Compare each pair of elements for equality
|
|||
for (int i = 0; i < Count; i++) |
|||
{ |
|||
if (!EqualityComparer<T>.Default.Equals(this[i], other[i])) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// If we got this far, the lists are equal
|
|||
return true; |
|||
} |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return Equals(obj as EquatableList<T>); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
var hash = 0; |
|||
for (var i = 0; i < Count; i++) |
|||
{ |
|||
hash ^= this[i]?.GetHashCode() ?? 0; |
|||
} |
|||
return hash; |
|||
} |
|||
|
|||
public static bool operator ==(EquatableList<T>? list1, EquatableList<T>? list2) |
|||
{ |
|||
return ReferenceEquals(list1, list2) |
|||
|| list1 is not null && list2 is not null && list1.Equals(list2); |
|||
} |
|||
|
|||
public static bool operator !=(EquatableList<T>? list1, EquatableList<T>? list2) |
|||
{ |
|||
return !(list1 == list2); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue