Browse Source

Merge branch 'master' into fixes/Warnings/Unused-field

pull/8786/head
Max Katz 4 years ago
committed by GitHub
parent
commit
17b67a08a7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 6
      build/SkiaSharp.props
  3. 2
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 90
      nukebuild/Build.cs
  5. 4
      nukebuild/BuildParameters.cs
  6. 57
      nukebuild/DotNetConfigHelper.cs
  7. 34
      samples/ControlCatalog/Converter/HexConverter.cs
  8. 12
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  9. 27
      samples/IntegrationTestApp/MacOSIntegration.cs
  10. 13
      samples/IntegrationTestApp/MainWindow.axaml.cs
  11. 8
      samples/IntegrationTestApp/ShowWindowTest.axaml
  12. 28
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  13. 63
      src/Avalonia.Base/Animation/Animation.cs
  14. 7
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  15. 4
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  16. 17
      src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
  17. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  18. 15
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  19. 11
      src/Avalonia.Base/Layout/LayoutHelper.cs
  20. 1
      src/Avalonia.Base/Media/DashStyle.cs
  21. 33
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  22. 4
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  23. 10
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  24. 2
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  25. 33
      src/Avalonia.Base/Rendering/DirtyVisuals.cs
  26. 35
      src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs
  27. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  28. 8
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  29. 9
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  30. 8
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  31. 4
      src/Avalonia.Controls/Calendar/Calendar.cs
  32. 10
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  33. 36
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  34. 68
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  35. 48
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  36. 56
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  37. 15
      src/Avalonia.Controls/Documents/InlineUIContainer.cs
  38. 61
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  39. 26
      src/Avalonia.Controls/RichTextBlock.cs
  40. 10
      src/Avalonia.Controls/ToggleSwitch.cs
  41. 1
      src/Avalonia.Controls/TreeView.cs
  42. 19
      src/Avalonia.Controls/TreeViewItem.cs
  43. 2
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  44. 8
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  45. 8
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  46. 6
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  47. 1
      src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
  48. 4
      src/Avalonia.Themes.Fluent/Controls/Calendar.xaml
  49. 10
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  50. 63
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  51. 4
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  52. 67
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  53. 4
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  54. 4
      src/Avalonia.Themes.Simple/Controls/Calendar.xaml
  55. 10
      src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml
  56. 52
      src/Avalonia.Themes.Simple/Controls/DatePicker.xaml
  57. 4
      src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml
  58. 58
      src/Avalonia.Themes.Simple/Controls/TimePicker.xaml
  59. 4
      src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml
  60. 3
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  61. 9
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  62. 35
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  63. 11
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  64. 34
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  65. 18
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  66. 41
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  67. 31
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  68. 12
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  69. 32
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  70. 45
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  71. 41
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  72. 2
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  73. 41
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  74. 23
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
  75. 261
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  76. 68
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  77. 7
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  78. 326
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  79. 3
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  80. 14
      src/Web/Avalonia.Web.Blazor/tsconfig.json
  81. 5
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts
  82. 40
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts
  83. 31
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  84. 35
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts
  85. 255
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts
  86. 67
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts
  87. 79
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts
  88. 169
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
  89. 16
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  90. 18
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  91. 0
      src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
  92. 40
      src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js
  93. 5
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  94. 7
      src/iOS/Avalonia.iOS/Platform.cs
  95. 64
      src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs
  96. 7
      src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs
  97. 30
      src/tools/DevGenerators/CompositionGenerator/Extensions.cs
  98. 4
      src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs
  99. 16
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  100. 1
      src/tools/DevGenerators/DevGenerators.csproj

3
.gitignore

@ -212,3 +212,6 @@ coc-settings.json
*.map
src/Web/Avalonia.Web.Blazor/wwwroot/*.js
src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js
node_modules
src/Web/Avalonia.Web.Blazor/webapp/package-lock.json
src/Web/Avalonia.Web.Blazor/wwwroot

6
build/SkiaSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.108" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1-preview.108" />
<PackageReference Include="SkiaSharp" Version="2.88.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1" />
</ItemGroup>
</Project>

2
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -91,8 +91,6 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
if(_parent != nullptr)
{
_parent->_children.remove(this);
_parent->BringToFront();
}
auto cparent = dynamic_cast<WindowImpl *>(parent);

90
nukebuild/Build.cs

@ -36,25 +36,6 @@ partial class Build : NukeBuild
{
[Solution("Avalonia.sln")] readonly Solution Solution;
static Lazy<string> MsBuildExe = new Lazy<string>(() =>
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
var msBuildDirectory = VSWhere("-latest -nologo -property installationPath -format value -prerelease").FirstOrDefault().Text;
if (!string.IsNullOrWhiteSpace(msBuildDirectory))
{
string msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\Current\Bin\MSBuild.exe");
if (!System.IO.File.Exists(msBuildExe))
msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\15.0\Bin\MSBuild.exe");
return msBuildExe;
}
return null;
}, false);
BuildParameters Parameters { get; set; }
protected override void OnBuildInitialized()
{
@ -89,25 +70,28 @@ partial class Build : NukeBuild
}
ExecWait("dotnet version:", "dotnet", "--info");
ExecWait("dotnet workloads:", "dotnet", "workload list");
Information("Processor count: " + Environment.ProcessorCount);
Information("Available RAM: " + GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 0x100000 + "MB");
}
IReadOnlyCollection<Output> MsBuildCommon(
string projectFile,
Configure<MSBuildSettings> configurator = null)
DotNetConfigHelper ApplySettingCore(DotNetConfigHelper c)
{
return MSBuild(c => c
.SetProjectFile(projectFile)
// This is required for VS2019 image on Azure Pipelines
.When(Parameters.IsRunningOnWindows &&
Parameters.IsRunningOnAzure, _ => _
.AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_11_X64")))
.AddProperty("PackageVersion", Parameters.Version)
if (Parameters.IsRunningOnAzure)
c.AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_11_X64"));
c.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", true)
.SetProcessToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal)
.Apply(configurator));
.SetVerbosity(DotNetVerbosity.Minimal);
return c;
}
DotNetBuildSettings ApplySetting(DotNetBuildSettings c, Configure<DotNetBuildSettings> configurator = null) =>
ApplySettingCore(c).Build.Apply(configurator);
DotNetPackSettings ApplySetting(DotNetPackSettings c, Configure<DotNetPackSettings> configurator = null) =>
ApplySettingCore(c).Pack.Apply(configurator);
DotNetTestSettings ApplySetting(DotNetTestSettings c, Configure<DotNetTestSettings> configurator = null) =>
ApplySettingCore(c).Test.Apply(configurator);
Target Clean => _ => _.Executes(() =>
{
@ -149,20 +133,11 @@ partial class Build : NukeBuild
Target Compile => _ => _
.DependsOn(Clean, CompileNative)
.DependsOn(CompileHtmlPreviewer)
.Executes(async () =>
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)
MsBuildCommon(Parameters.MSBuildSolution, c => c
.SetProcessArgumentConfigurator(a => a.Add("/r"))
.AddTargets("Build")
);
else
DotNetBuild(c => c
.SetProjectFile(Parameters.MSBuildSolution)
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
DotNetBuild(c => ApplySetting(c)
.SetProjectFile(Parameters.MSBuildSolution)
);
});
void RunCoreTest(string projectName)
@ -182,9 +157,8 @@ partial class Build : NukeBuild
Information($"Running for {projectName} ({fw}) ...");
DotNetTest(c => c
DotNetTest(c => ApplySetting(c)
.SetProjectFile(project)
.SetConfiguration(Parameters.Configuration)
.SetFramework(fw)
.EnableNoBuild()
.EnableNoRestore()
@ -263,19 +237,7 @@ partial class Build : NukeBuild
.Executes(() =>
{
var data = Parameters;
var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore";
var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish";
DotNetPublish(c => c
.SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj")
.EnableNoBuild()
.SetConfiguration(data.Configuration)
.AddProperty("PackageVersion", data.Version)
.AddProperty("PublishDir", pathToPublish));
Zip(data.ZipCoreArtifacts, data.BinRoot);
Zip(data.ZipNuGetArtifacts, data.NugetRoot);
Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish);
});
Target CreateIntermediateNugetPackages => _ => _
@ -283,15 +245,7 @@ partial class Build : NukeBuild
.After(RunTests)
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)
MsBuildCommon(Parameters.MSBuildSolution, c => c
.AddTargets("Pack"));
else
DotNetPack(c => c
.SetProject(Parameters.MSBuildSolution)
.SetConfiguration(Parameters.Configuration)
.AddProperty("PackageVersion", Parameters.Version));
DotNetPack(c => ApplySetting(c).SetProject(Parameters.MSBuildSolution));
});
Target CreateNugetPackages => _ => _

4
nukebuild/BuildParameters.cs

@ -51,14 +51,12 @@ public partial class Build
public AbsolutePath NugetIntermediateRoot { get; }
public AbsolutePath NugetRoot { get; }
public AbsolutePath ZipRoot { get; }
public AbsolutePath BinRoot { get; }
public AbsolutePath TestResultsRoot { get; }
public string DirSuffix { get; }
public List<string> BuildDirs { get; }
public string FileZipSuffix { get; }
public AbsolutePath ZipCoreArtifacts { get; }
public AbsolutePath ZipNuGetArtifacts { get; }
public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; }
public BuildParameters(Build b)
@ -121,14 +119,12 @@ public partial class Build
NugetRoot = ArtifactsDir / "nuget";
NugetIntermediateRoot = RootDirectory / "build-intermediate" / "nuget";
ZipRoot = ArtifactsDir / "zip";
BinRoot = ArtifactsDir / "bin";
TestResultsRoot = ArtifactsDir / "test-results";
BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList();
DirSuffix = Configuration;
FileZipSuffix = Version + ".zip";
ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix);
ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix);
ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix);
}
string GetVersion()

57
nukebuild/DotNetConfigHelper.cs

@ -0,0 +1,57 @@
using System.Globalization;
using JetBrains.Annotations;
using Nuke.Common.Tools.DotNet;
// ReSharper disable ReturnValueOfPureMethodIsNotUsed
public class DotNetConfigHelper
{
public DotNetBuildSettings Build;
public DotNetPackSettings Pack;
public DotNetTestSettings Test;
public DotNetConfigHelper(DotNetBuildSettings s)
{
Build = s;
}
public DotNetConfigHelper(DotNetPackSettings s)
{
Pack = s;
}
public DotNetConfigHelper(DotNetTestSettings s)
{
Test = s;
}
public DotNetConfigHelper AddProperty(string key, bool value) =>
AddProperty(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
public DotNetConfigHelper AddProperty(string key, string value)
{
Build = Build?.AddProperty(key, value);
Pack = Pack?.AddProperty(key, value);
Test = Test?.AddProperty(key, value);
return this;
}
public DotNetConfigHelper SetConfiguration(string configuration)
{
Build = Build?.SetConfiguration(configuration);
Pack = Pack?.SetConfiguration(configuration);
Test = Test?.SetConfiguration(configuration);
return this;
}
public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity)
{
Build = Build?.SetVerbosity(verbosity);
Pack = Pack?.SetVerbostiy(verbosity);
Test = Test?.SetVerbosity(verbosity);
return this;
}
public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s);
public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s);
public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s);
}

34
samples/ControlCatalog/Converter/HexConverter.cs

@ -0,0 +1,34 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
namespace ControlCatalog.Converter;
public class HexConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var str = value?.ToString();
if (str == null)
return AvaloniaProperty.UnsetValue;
if (int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int x))
return (decimal)x;
return AvaloniaProperty.UnsetValue;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
try
{
if (value is decimal d)
return ((int)d).ToString("X8");
return AvaloniaProperty.UnsetValue;
}
catch
{
return AvaloniaProperty.UnsetValue;
}
}
}

12
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
xmlns:converter="clr-namespace:ControlCatalog.Converter"
x:Class="ControlCatalog.Pages.NumericUpDownPage">
<StackPanel Orientation="Vertical" Spacing="4"
MaxWidth="800">
@ -97,6 +98,17 @@
</DataValidationErrors.Error>
</NumericUpDown>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="HexUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">NumericUpDown in HEX mode:</Label>
<NumericUpDown x:Name="HexUpDown" Value="0"
VerticalAlignment="Center">
<NumericUpDown.TextConverter>
<converter:HexConverter></converter:HexConverter>
</NumericUpDown.TextConverter>
</NumericUpDown>
</StackPanel>
</WrapPanel>
</StackPanel>

27
samples/IntegrationTestApp/MacOSIntegration.cs

@ -0,0 +1,27 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;
namespace IntegrationTestApp
{
public static class MacOSIntegration
{
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")]
private static extern IntPtr GetHandle(string name);
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern long Int64_objc_msgSend(IntPtr receiver, IntPtr selector);
private static readonly IntPtr s_orderedIndexSelector;
static MacOSIntegration()
{
s_orderedIndexSelector = GetHandle("orderedIndex");;
}
public static long GetOrderedIndex(Window window)
{
return Int64_objc_msgSend(window.PlatformImpl!.Handle.Handle, s_orderedIndexSelector);
}
}
}

13
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using Microsoft.CodeAnalysis;
namespace IntegrationTestApp
{
@ -63,6 +65,17 @@ namespace IntegrationTestApp
WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
};
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
// Make sure the windows have unique names and AutomationIds.
var existing = lifetime.Windows.OfType<ShowWindowTest>().Count();
if (existing > 0)
{
AutomationProperties.SetAutomationId(window, window.Name + (existing + 1));
window.Title += $" {existing + 1}";
}
}
if (size.HasValue)
{
window.Width = size.Value.Width;

8
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -3,7 +3,7 @@
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
@ -31,6 +31,10 @@
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>Fullscreen</ComboBoxItem>
</ComboBox>
<Button Name="HideButton" Grid.Row="8" Command="{Binding $parent[Window].Hide}">Hide</Button>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="Order" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/>
<Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</Window>

28
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -1,21 +1,32 @@
using System;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace IntegrationTestApp
{
public class ShowWindowTest : Window
{
private readonly DispatcherTimer? _timer;
private readonly TextBox? _orderTextBox;
public ShowWindowTest()
{
InitializeComponent();
DataContext = this;
PositionChanged += (s, e) => this.GetControl<TextBox>("Position").Text = $"{Position}";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_orderTextBox = this.GetControl<TextBox>("Order");
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
_timer.Tick += TimerOnTick;
_timer.Start();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
@ -36,5 +47,16 @@ namespace IntegrationTestApp
ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_timer?.Stop();
}
private void TimerOnTick(object? sender, EventArgs e)
{
_orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString();
}
}
}

63
src/Avalonia.Base/Animation/Animation.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -179,14 +180,14 @@ namespace Avalonia.Animation
public KeyFrames Children { get; } = new KeyFrames();
// Store values for the Animator attached properties for IAnimationSetter objects.
private static readonly Dictionary<IAnimationSetter, Type> s_animators = new Dictionary<IAnimationSetter, Type>();
private static readonly Dictionary<IAnimationSetter, (Type Type, Func<IAnimator> Factory)> s_animators = new();
/// <summary>
/// Gets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <returns>The property animator type.</returns>
public static Type? GetAnimator(IAnimationSetter setter)
public static (Type Type, Func<IAnimator> Factory)? GetAnimator(IAnimationSetter setter)
{
if (s_animators.TryGetValue(setter, out var type))
{
@ -200,24 +201,28 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter, Type value)
public static void SetAnimator(IAnimationSetter setter,
#if NET6_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)]
#endif
Type value)
{
s_animators[setter] = value;
s_animators[setter] = (value, () => (IAnimator)Activator.CreateInstance(value)!);
}
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator, Func<IAnimator> Factory)> Animators = new()
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator) ),
( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator) ),
( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator) ),
( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator) ),
( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator) ),
( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator) ),
( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator) ),
( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator) ),
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ),
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator() ),
( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator() ),
( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator() ),
( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator() ),
( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator() ),
( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator() ),
( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator() ),
( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator() ),
( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator() ),
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator() ),
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator() ),
};
/// <summary>
@ -232,18 +237,18 @@ namespace Avalonia.Animation
/// The type of the animator to instantiate.
/// </typeparam>
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator : IAnimator
where TAnimator : IAnimator, new()
{
Animators.Insert(0, (condition, typeof(TAnimator)));
Animators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator()));
}
private static Type? GetAnimatorType(AvaloniaProperty property)
private static (Type Type, Func<IAnimator> Factory)? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type) in Animators)
foreach (var (condition, type, factory) in Animators)
{
if (condition(property))
{
return type;
return (type, factory);
}
}
return null;
@ -251,7 +256,7 @@ namespace Avalonia.Animation
private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{
var handlerList = new List<(Type type, AvaloniaProperty property)>();
var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func<IAnimator>>();
var animatorKeyFrames = new List<AnimatorKeyFrame>();
var subscriptions = new List<IDisposable>();
@ -271,8 +276,10 @@ namespace Avalonia.Animation
throw new InvalidOperationException($"No animator registered for the property {setter.Property}. Add an animator to the Animation.Animators collection that matches this property to animate it.");
}
if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property));
var (type, factory) = handler.Value;
if (!handlerList.ContainsKey((type, setter.Property)))
handlerList[(type, setter.Property)] = factory;
var cue = keyframe.Cue;
@ -281,7 +288,7 @@ namespace Avalonia.Animation
cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
}
var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
var newKF = new AnimatorKeyFrame(type, factory, cue, keyframe.KeySpline);
subscriptions.Add(newKF.BindSetter(setter, control));
@ -291,10 +298,10 @@ namespace Avalonia.Animation
var newAnimatorInstances = new List<IAnimator>();
foreach (var (handlerType, property) in handlerList)
foreach (var handler in handlerList)
{
var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!;
newInstance.Property = property;
var newInstance = handler.Value();
newInstance.Property = handler.Key.Property;
newAnimatorInstances.Add(newInstance);
}

7
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@ -20,22 +20,25 @@ namespace Avalonia.Animation
}
public AnimatorKeyFrame(Type? animatorType, Cue cue)
public AnimatorKeyFrame(Type? animatorType, Func<IAnimator>? animatorFactory, Cue cue)
{
AnimatorType = animatorType;
AnimatorFactory = animatorFactory;
Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline)
public AnimatorKeyFrame(Type? animatorType, Func<IAnimator>? animatorFactory, Cue cue, KeySpline? keySpline)
{
AnimatorType = animatorType;
AnimatorFactory = animatorFactory;
Cue = cue;
KeySpline = keySpline;
}
internal bool isNeutral;
public Type? AnimatorType { get; }
public Func<IAnimator>? AnimatorFactory { get; }
public Cue Cue { get; }
public KeySpline? KeySpline { get; }
public AvaloniaProperty? Property { get; private set; }

4
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@ -171,12 +171,12 @@ namespace Avalonia.Animation.Animators
{
if (!hasStartKey)
{
_convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
_convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
}
if (!hasEndKey)
{
_convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
_convertedKeyframes.Add(new AnimatorKeyFrame(null, null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
}
}
}

17
src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs

@ -17,8 +17,7 @@ namespace Avalonia.Animation.Animators
/// </summary>
public class BaseBrushAnimator : Animator<IBrush?>
{
private static readonly List<(Func<Type, bool> Match, Type AnimatorType)> _brushAnimators =
new List<(Func<Type, bool> Match, Type AnimatorType)>();
private static readonly List<(Func<Type, bool> Match, Type AnimatorType, Func<IAnimator> AnimatorFactory)> _brushAnimators = new();
/// <summary>
/// Register an <see cref="Animator{T}"/> that handles a specific
@ -34,7 +33,7 @@ namespace Avalonia.Animation.Animators
public static void RegisterBrushAnimator<TAnimator>(Func<Type, bool> condition)
where TAnimator : IAnimator, new()
{
_brushAnimators.Insert(0, (condition, typeof(TAnimator)));
_brushAnimators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator()));
}
/// <inheritdoc/>
@ -85,14 +84,14 @@ namespace Avalonia.Animation.Animators
{
if (keyframe.Value is ISolidColorBrush solidColorBrush)
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush)
});
}
else if (keyframe.Value is IGradientBrush)
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
Value = keyframe.Value
});
@ -117,7 +116,7 @@ namespace Avalonia.Animation.Animators
{
if (keyframe.Value is ISolidColorBrush)
{
solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), keyframe.Cue, keyframe.KeySpline)
solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), () => new ISolidColorBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
Value = keyframe.Value
});
@ -137,18 +136,18 @@ namespace Avalonia.Animation.Animators
{
if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType)
{
foreach (var (match, animatorType) in _brushAnimators)
foreach (var (match, animatorType, animatorFactory) in _brushAnimators)
{
if (!match(firstKeyType))
continue;
animator = (IAnimator?)Activator.CreateInstance(animatorType);
animator = animatorFactory();
if (animator != null)
{
animator.Property = Property;
foreach (var keyframe in this)
{
animator.Add(new AnimatorKeyFrame(animatorType, keyframe.Cue, keyframe.KeySpline)
animator.Add(new AnimatorKeyFrame(animatorType, animatorFactory, keyframe.Cue, keyframe.KeySpline)
{
Value = keyframe.Value
});

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -6,6 +6,7 @@
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\*.trie" />

15
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -55,13 +55,20 @@ namespace Avalonia.Data.Core.Plugins
var methods = type.GetMethods(bindingFlags);
foreach (MethodInfo methodInfo in methods)
foreach (var methodInfo in methods)
{
if (methodInfo.Name == methodName)
{
found = methodInfo;
break;
var parameters = methodInfo.GetParameters();
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object))
{
found = methodInfo;
break;
}
else if (parameters.Length == 0)
{
found = methodInfo;
}
}
}

11
src/Avalonia.Base/Layout/LayoutHelper.cs

@ -251,6 +251,17 @@ namespace Avalonia.Layout
{
double newValue;
// Round the value to avoid FP errors. This is needed because if `value` has a floating
// point precision error (e.g. 79.333333333333343) then when it's multiplied by
// `dpiScale` and rounded up, it will be rounded up to a value one greater than it
// should be.
#if NET6_0_OR_GREATER
value = Math.Round(value, 8, MidpointRounding.ToZero);
#else
// MidpointRounding.ToZero isn't available in netstandard2.0.
value = Math.Truncate(value * 1e8) / 1e8;
#endif
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.IsOne(dpiScale))
{

1
src/Avalonia.Base/Media/DashStyle.cs

@ -35,7 +35,6 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="DashStyle"/> class.
/// </summary>
public DashStyle()
: this(null, 0)
{
}

33
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -29,6 +29,7 @@ public class CompositingRenderer : IRendererWithCompositor
private bool _queuedUpdate;
private Action _update;
private Action _invalidateScene;
private bool _updating;
internal CompositionTarget CompositionTarget;
@ -77,6 +78,8 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/>
public void AddDirty(IVisual visual)
{
if (_updating)
throw new InvalidOperationException("Visual was invalidated during the render pass");
_dirty.Add((Visual)visual);
QueueUpdate();
}
@ -84,7 +87,16 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter)
{
var res = CompositionTarget.TryHitTest(p, filter);
Func<CompositionVisual, bool>? f = null;
if (filter != null)
f = v =>
{
if (v is CompositionDrawListVisual dlv)
return filter(dlv.Visual);
return true;
};
var res = CompositionTarget.TryHitTest(p, f);
if(res == null)
yield break;
foreach(var v in res)
@ -107,6 +119,8 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/>
public void RecalculateChildren(IVisual visual)
{
if (_updating)
throw new InvalidOperationException("Visual was invalidated during the render pass");
_recalculateChildren.Add((Visual)visual);
QueueUpdate();
}
@ -191,7 +205,7 @@ public class CompositingRenderer : IRendererWithCompositor
private void InvalidateScene() =>
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
private void Update()
private void UpdateCore()
{
_queuedUpdate = false;
foreach (var visual in _dirty)
@ -240,6 +254,21 @@ public class CompositingRenderer : IRendererWithCompositor
CompositionTarget.Scaling = _root.RenderScaling;
Compositor.InvokeOnNextCommit(_invalidateScene);
}
private void Update()
{
if(_updating)
return;
_updating = true;
try
{
UpdateCore();
}
finally
{
_updating = false;
}
}
public void Resized(Size size)
{

4
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@ -54,13 +54,11 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
Visual = visual;
}
internal override bool HitTest(Point pt, Func<IVisual, bool>? filter)
internal override bool HitTest(Point pt)
{
var custom = Visual as ICustomHitTest;
if (DrawList == null && custom == null)
return false;
if (filter != null && !filter(Visual))
return false;
if (custom != null)
{
// Simulate the old behavior

10
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition
/// <param name="point"></param>
/// <param name="filter"></param>
/// <returns></returns>
public PooledList<CompositionVisual>? TryHitTest(Point point, Func<IVisual, bool>? filter)
public PooledList<CompositionVisual>? TryHitTest(Point point, Func<CompositionVisual, bool>? filter)
{
Server.Readback.NextRead();
if (Root == null)
@ -88,10 +88,14 @@ namespace Avalonia.Rendering.Composition
}
void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result,
Func<IVisual, bool>? filter)
Func<CompositionVisual, bool>? filter)
{
if (visual.Visible == false)
return;
if (filter != null && !filter(visual))
return;
if (!TryTransformTo(visual, globalPoint, out var point))
return;
@ -111,7 +115,7 @@ namespace Avalonia.Rendering.Composition
}
// Hit-test the current node
if (visual.HitTest(point, filter))
if (visual.HitTest(point))
result.Add(visual);
}

2
src/Avalonia.Base/Rendering/Composition/Visual.cs

@ -53,6 +53,6 @@ namespace Avalonia.Rendering.Composition
internal object? Tag { get; set; }
internal virtual bool HitTest(Point point, Func<IVisual, bool>? filter) => true;
internal virtual bool HitTest(Point point) => true;
}
}

33
src/Avalonia.Base/Rendering/DirtyVisuals.cs

@ -17,8 +17,7 @@ namespace Avalonia.Rendering
{
private SortedDictionary<int, List<IVisual>> _inner = new SortedDictionary<int, List<IVisual>>();
private Dictionary<IVisual, int> _index = new Dictionary<IVisual, int>();
private List<IVisual> _deferredChanges = new List<IVisual>();
private int _deferring;
private int _enumerating;
/// <summary>
/// Gets the number of dirty visuals.
@ -31,10 +30,9 @@ namespace Avalonia.Rendering
/// <param name="visual">The dirty visual.</param>
public void Add(IVisual visual)
{
if (_deferring > 0)
if (_enumerating > 0)
{
_deferredChanges.Add(visual);
return;
throw new InvalidOperationException("Visual was invalidated during a render pass");
}
var distance = visual.CalculateDistanceFromAncestor(visual.VisualRoot);
@ -65,7 +63,7 @@ namespace Avalonia.Rendering
/// </summary>
public void Clear()
{
if (_deferring > 0)
if (_enumerating > 0)
{
throw new InvalidOperationException("Cannot clear while enumerating");
}
@ -80,7 +78,7 @@ namespace Avalonia.Rendering
/// <returns>A collection of visuals.</returns>
public IEnumerator<IVisual> GetEnumerator()
{
BeginDefer();
_enumerating++;
try
{
foreach (var i in _inner)
@ -93,27 +91,10 @@ namespace Avalonia.Rendering
}
finally
{
EndDefer();
_enumerating--;
}
}
private void BeginDefer()
{
++_deferring;
}
private void EndDefer()
{
if (--_deferring > 0) return;
foreach (var visual in _deferredChanges)
{
Add(visual);
}
_deferredChanges.Clear();
}
/// <summary>
/// Gets the dirty visuals, in ascending order of distance to their root.
/// </summary>

35
src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs

@ -0,0 +1,35 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converter that will do nothing (not update bound values) when a null value is encountered.
/// This converter enables binding nullable with non-nullable properties in some scenarios.
/// </summary>
public class DoNothingForNullConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return value ?? BindingOperations.DoNothing;
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return value ?? BindingOperations.DoNothing;
}
}
}

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -11,6 +11,7 @@
<pc:ThirdComponentConverter x:Key="ThirdComponentConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
<x:Double x:Key="ColorViewComponentLabelWidth">30</x:Double>
@ -205,7 +206,7 @@
</Border>
</TabItem.Header>
<ListBox Items="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">
<ListBox.Styles>

8
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -23,10 +23,10 @@ namespace Avalonia.Controls
[PseudoClasses(":pressed", ":current", ":expanded")]
public class DataGridRowGroupHeader : TemplatedControl
{
private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton";
private const string DATAGRIDROWGROUPHEADER_indentSpacer = "IndentSpacer";
private const string DATAGRIDROWGROUPHEADER_itemCountElement = "ItemCountElement";
private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PropertyNameElement";
private const string DATAGRIDROWGROUPHEADER_expanderButton = "PART_ExpanderButton";
private const string DATAGRIDROWGROUPHEADER_indentSpacer = "PART_IndentSpacer";
private const string DATAGRIDROWGROUPHEADER_itemCountElement = "PART_ItemCountElement";
private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PART_PropertyNameElement";
private bool _areIsCheckedHandlersSuspended;
private ToggleButton _expanderButton;

9
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -53,7 +53,6 @@
<Setter Property="VerticalAlignment" Value="Center" />
</ControlTheme>
<ControlTheme x:Key="DataGridCellTextBoxTheme" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="12,0,12,0" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Style Selector="^ /template/ DataValidationErrors">
@ -410,9 +409,9 @@
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
RowDefinitions="*,Auto">
<Rectangle Name="IndentSpacer"
<Rectangle Name="PART_IndentSpacer"
Grid.Column="1" />
<ToggleButton Name="ExpanderButton"
<ToggleButton Name="PART_ExpanderButton"
Grid.Column="2"
Width="12"
Height="12"
@ -429,14 +428,14 @@
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<TextBlock Name="PropertyNameElement"
<TextBlock Name="PART_PropertyNameElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsPropertyNameVisible}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Margin="4,0,0,0"
Text="{ReflectionBinding Key}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Name="ItemCountElement"
<TextBlock Name="PART_ItemCountElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsItemCountVisible}"
Foreground="{TemplateBinding Foreground}" />

8
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -258,10 +258,10 @@
ColumnDefinitions="Auto,Auto,Auto,Auto"
RowDefinitions="Auto,*,Auto">
<Rectangle Name="IndentSpacer"
<Rectangle Name="PART_IndentSpacer"
Grid.Row="1"
Grid.Column="1" />
<ToggleButton Name="ExpanderButton"
<ToggleButton Name="PART_ExpanderButton"
Grid.Row="1"
Grid.Column="2"
Margin="2,0,0,0"
@ -277,12 +277,12 @@
Margin="0,1,0,1"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Name="PropertyNameElement"
<TextBlock Name="PART_PropertyNameElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsPropertyNameVisible}" />
<TextBlock Margin="4,0,0,0"
Text="{Binding Key}" />
<TextBlock Name="ItemCountElement"
<TextBlock Name="PART_ItemCountElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsItemCountVisible}" />
</StackPanel>

4
src/Avalonia.Controls/Calendar/Calendar.cs

@ -2109,8 +2109,8 @@ namespace Avalonia.Controls
RemovedItems = new Collection<DateTime>();
}
private const string PART_ElementRoot = "Root";
private const string PART_ElementMonth = "CalendarItem";
private const string PART_ElementRoot = "PART_Root";
private const string PART_ElementMonth = "PART_CalendarItem";
/// <summary>
/// Builds the visual tree for the

10
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -32,11 +32,11 @@ namespace Avalonia.Controls.Primitives
/// </summary>
private const int NumberOfDaysPerWeek = 7;
private const string PART_ElementHeaderButton = "HeaderButton";
private const string PART_ElementPreviousButton = "PreviousButton";
private const string PART_ElementNextButton = "NextButton";
private const string PART_ElementMonthView = "MonthView";
private const string PART_ElementYearView = "YearView";
private const string PART_ElementHeaderButton = "PART_HeaderButton";
private const string PART_ElementPreviousButton = "PART_PreviousButton";
private const string PART_ElementNextButton = "PART_NextButton";
private const string PART_ElementMonthView = "PART_MonthView";
private const string PART_ElementYearView = "PART_YearView";
private Button? _headerButton;
private Button? _nextButton;

36
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -14,15 +14,15 @@ namespace Avalonia.Controls
/// <summary>
/// A control to allow the user to select a date
/// </summary>
[TemplatePart("ButtonContentGrid", typeof(Grid))]
[TemplatePart("DayText", typeof(TextBlock))]
[TemplatePart("FirstSpacer", typeof(Rectangle))]
[TemplatePart("FlyoutButton", typeof(Button))]
[TemplatePart("MonthText", typeof(TextBlock))]
[TemplatePart("PickerPresenter", typeof(DatePickerPresenter))]
[TemplatePart("Popup", typeof(Popup))]
[TemplatePart("SecondSpacer", typeof(Rectangle))]
[TemplatePart("YearText", typeof(TextBlock))]
[TemplatePart("PART_ButtonContentGrid", typeof(Grid))]
[TemplatePart("PART_DayTextBlock", typeof(TextBlock))]
[TemplatePart("PART_FirstSpacer", typeof(Rectangle))]
[TemplatePart("PART_FlyoutButton", typeof(Button))]
[TemplatePart("PART_MonthTextBlock", typeof(TextBlock))]
[TemplatePart("PART_PickerPresenter", typeof(DatePickerPresenter))]
[TemplatePart("PART_Popup", typeof(Popup))]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle))]
[TemplatePart("PART_YearTextBlock", typeof(TextBlock))]
[PseudoClasses(":hasnodate")]
public class DatePicker : TemplatedControl
{
@ -280,15 +280,15 @@ namespace Avalonia.Controls
}
base.OnApplyTemplate(e);
_flyoutButton = e.NameScope.Find<Button>("FlyoutButton");
_dayText = e.NameScope.Find<TextBlock>("DayText");
_monthText = e.NameScope.Find<TextBlock>("MonthText");
_yearText = e.NameScope.Find<TextBlock>("YearText");
_container = e.NameScope.Find<Grid>("ButtonContentGrid");
_spacer1 = e.NameScope.Find<Rectangle>("FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("SecondSpacer");
_popup = e.NameScope.Find<Popup>("Popup");
_presenter = e.NameScope.Find<DatePickerPresenter>("PickerPresenter");
_flyoutButton = e.NameScope.Find<Button>("PART_FlyoutButton");
_dayText = e.NameScope.Find<TextBlock>("PART_DayTextBlock");
_monthText = e.NameScope.Find<TextBlock>("PART_MonthTextBlock");
_yearText = e.NameScope.Find<TextBlock>("PART_YearTextBlock");
_container = e.NameScope.Find<Grid>("PART_ButtonContentGrid");
_spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
_popup = e.NameScope.Find<Popup>("PART_Popup");
_presenter = e.NameScope.Find<DatePickerPresenter>("PART_PickerPresenter");
_areControlsAvailable = true;

68
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -13,23 +13,23 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a date for a
/// <see cref="DatePicker"/>
/// </summary>
[TemplatePart("AcceptButton", typeof(Button))]
[TemplatePart("DayDownButton", typeof(RepeatButton))]
[TemplatePart("DayHost", typeof(Panel))]
[TemplatePart("DaySelector", typeof(DateTimePickerPanel))]
[TemplatePart("DayUpButton", typeof(RepeatButton))]
[TemplatePart("DismissButton", typeof(Button))]
[TemplatePart("FirstSpacer", typeof(Rectangle))]
[TemplatePart("MonthDownButton", typeof(RepeatButton))]
[TemplatePart("MonthHost", typeof(Panel))]
[TemplatePart("MonthSelector", typeof(DateTimePickerPanel))]
[TemplatePart("MonthUpButton", typeof(RepeatButton))]
[TemplatePart("PickerContainer", typeof(Grid))]
[TemplatePart("SecondSpacer", typeof(Rectangle))]
[TemplatePart("YearDownButton", typeof(RepeatButton))]
[TemplatePart("YearHost", typeof(Panel))]
[TemplatePart("YearSelector", typeof(DateTimePickerPanel))]
[TemplatePart("YearUpButton", typeof(RepeatButton))]
[TemplatePart("PART_AcceptButton", typeof(Button))]
[TemplatePart("PART_DayDownButton", typeof(RepeatButton))]
[TemplatePart("PART_DayHost", typeof(Panel))]
[TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel))]
[TemplatePart("PART_DayUpButton", typeof(RepeatButton))]
[TemplatePart("PART_DismissButton", typeof(Button))]
[TemplatePart("PART_FirstSpacer", typeof(Rectangle))]
[TemplatePart("PART_MonthDownButton", typeof(RepeatButton))]
[TemplatePart("PART_MonthHost", typeof(Panel))]
[TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel))]
[TemplatePart("PART_MonthUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PickerContainer", typeof(Grid))]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle))]
[TemplatePart("PART_YearDownButton", typeof(RepeatButton))]
[TemplatePart("PART_YearHost", typeof(Panel))]
[TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel))]
[TemplatePart("PART_YearUpButton", typeof(RepeatButton))]
public class DatePickerPresenter : PickerPresenterBase
{
/// <summary>
@ -253,58 +253,58 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
// These are requirements, so throw if not found
_pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
_monthHost = e.NameScope.Get<Panel>("MonthHost");
_dayHost = e.NameScope.Get<Panel>("DayHost");
_yearHost = e.NameScope.Get<Panel>("YearHost");
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
_monthHost = e.NameScope.Get<Panel>("PART_MonthHost");
_dayHost = e.NameScope.Get<Panel>("PART_DayHost");
_yearHost = e.NameScope.Get<Panel>("PART_YearHost");
_monthSelector = e.NameScope.Get<DateTimePickerPanel>("MonthSelector");
_monthSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MonthSelector");
_monthSelector.SelectionChanged += OnMonthChanged;
_daySelector = e.NameScope.Get<DateTimePickerPanel>("DaySelector");
_daySelector = e.NameScope.Get<DateTimePickerPanel>("PART_DaySelector");
_daySelector.SelectionChanged += OnDayChanged;
_yearSelector = e.NameScope.Get<DateTimePickerPanel>("YearSelector");
_yearSelector = e.NameScope.Get<DateTimePickerPanel>("PART_YearSelector");
_yearSelector.SelectionChanged += OnYearChanged;
_acceptButton = e.NameScope.Get<Button>("AcceptButton");
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
_monthUpButton = e.NameScope.Find<RepeatButton>("MonthUpButton");
_monthUpButton = e.NameScope.Find<RepeatButton>("PART_MonthUpButton");
if (_monthUpButton != null)
{
_monthUpButton.Click += OnSelectorButtonClick;
}
_monthDownButton = e.NameScope.Find<RepeatButton>("MonthDownButton");
_monthDownButton = e.NameScope.Find<RepeatButton>("PART_MonthDownButton");
if (_monthDownButton != null)
{
_monthDownButton.Click += OnSelectorButtonClick;
}
_dayUpButton = e.NameScope.Find<RepeatButton>("DayUpButton");
_dayUpButton = e.NameScope.Find<RepeatButton>("PART_DayUpButton");
if (_dayUpButton != null)
{
_dayUpButton.Click += OnSelectorButtonClick;
}
_dayDownButton = e.NameScope.Find<RepeatButton>("DayDownButton");
_dayDownButton = e.NameScope.Find<RepeatButton>("PART_DayDownButton");
if (_dayDownButton != null)
{
_dayDownButton.Click += OnSelectorButtonClick;
}
_yearUpButton = e.NameScope.Find<RepeatButton>("YearUpButton");
_yearUpButton = e.NameScope.Find<RepeatButton>("PART_YearUpButton");
if (_yearUpButton != null)
{
_yearUpButton.Click += OnSelectorButtonClick;
}
_yearDownButton = e.NameScope.Find<RepeatButton>("YearDownButton");
_yearDownButton = e.NameScope.Find<RepeatButton>("PART_YearDownButton");
if (_yearDownButton != null)
{
_yearDownButton.Click += OnSelectorButtonClick;
}
_dismissButton = e.NameScope.Find<Button>("DismissButton");
_spacer1 = e.NameScope.Find<Rectangle>("FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("SecondSpacer");
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
_spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
if (_acceptButton != null)
{

48
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -12,18 +12,18 @@ namespace Avalonia.Controls
/// <summary>
/// A control to allow the user to select a time.
/// </summary>
[TemplatePart("FirstColumnDivider", typeof(Rectangle))]
[TemplatePart("FirstPickerHost", typeof(Border))]
[TemplatePart("FlyoutButton", typeof(Button))]
[TemplatePart("FlyoutButtonContentGrid", typeof(Grid))]
[TemplatePart("HourTextBlock", typeof(TextBlock))]
[TemplatePart("MinuteTextBlock", typeof(TextBlock))]
[TemplatePart("PeriodTextBlock", typeof(TextBlock))]
[TemplatePart("PickerPresenter", typeof(TimePickerPresenter))]
[TemplatePart("Popup", typeof(Popup))]
[TemplatePart("SecondColumnDivider", typeof(Rectangle))]
[TemplatePart("SecondPickerHost", typeof(Border))]
[TemplatePart("ThirdPickerHost", typeof(Border))]
[TemplatePart("PART_FirstColumnDivider", typeof(Rectangle))]
[TemplatePart("PART_FirstPickerHost", typeof(Border))]
[TemplatePart("PART_FlyoutButton", typeof(Button))]
[TemplatePart("PART_FlyoutButtonContentGrid", typeof(Grid))]
[TemplatePart("PART_HourTextBlock", typeof(TextBlock))]
[TemplatePart("PART_MinuteTextBlock", typeof(TextBlock))]
[TemplatePart("PART_PeriodTextBlock", typeof(TextBlock))]
[TemplatePart("PART_PickerPresenter", typeof(TimePickerPresenter))]
[TemplatePart("PART_Popup", typeof(Popup))]
[TemplatePart("PART_SecondColumnDivider", typeof(Rectangle))]
[TemplatePart("PART_SecondPickerHost", typeof(Border))]
[TemplatePart("PART_ThirdPickerHost", typeof(Border))]
[PseudoClasses(":hasnotime")]
public class TimePicker : TemplatedControl
{
@ -169,23 +169,23 @@ namespace Avalonia.Controls
}
base.OnApplyTemplate(e);
_flyoutButton = e.NameScope.Find<Button>("FlyoutButton");
_flyoutButton = e.NameScope.Find<Button>("PART_FlyoutButton");
_firstPickerHost = e.NameScope.Find<Border>("FirstPickerHost");
_secondPickerHost = e.NameScope.Find<Border>("SecondPickerHost");
_thirdPickerHost = e.NameScope.Find<Border>("ThirdPickerHost");
_firstPickerHost = e.NameScope.Find<Border>("PART_FirstPickerHost");
_secondPickerHost = e.NameScope.Find<Border>("PART_SecondPickerHost");
_thirdPickerHost = e.NameScope.Find<Border>("PART_ThirdPickerHost");
_hourText = e.NameScope.Find<TextBlock>("HourTextBlock");
_minuteText = e.NameScope.Find<TextBlock>("MinuteTextBlock");
_periodText = e.NameScope.Find<TextBlock>("PeriodTextBlock");
_hourText = e.NameScope.Find<TextBlock>("PART_HourTextBlock");
_minuteText = e.NameScope.Find<TextBlock>("PART_MinuteTextBlock");
_periodText = e.NameScope.Find<TextBlock>("PART_PeriodTextBlock");
_firstSplitter = e.NameScope.Find<Rectangle>("FirstColumnDivider");
_secondSplitter = e.NameScope.Find<Rectangle>("SecondColumnDivider");
_firstSplitter = e.NameScope.Find<Rectangle>("PART_FirstColumnDivider");
_secondSplitter = e.NameScope.Find<Rectangle>("PART_SecondColumnDivider");
_contentGrid = e.NameScope.Find<Grid>("FlyoutButtonContentGrid");
_contentGrid = e.NameScope.Find<Grid>("PART_FlyoutButtonContentGrid");
_popup = e.NameScope.Find<Popup>("Popup");
_presenter = e.NameScope.Find<TimePickerPresenter>("PickerPresenter");
_popup = e.NameScope.Find<Popup>("PART_Popup");
_presenter = e.NameScope.Find<TimePickerPresenter>("PART_PickerPresenter");
if (_flyoutButton != null)
_flyoutButton.Click += OnFlyoutButtonClicked;

56
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@ -11,20 +11,20 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a time. Intended for use with
/// <see cref="TimePicker"/> but can be used independently
/// </summary>
[TemplatePart("AcceptButton", typeof(Button))]
[TemplatePart("DismissButton", typeof(Button))]
[TemplatePart("HourDownButton", typeof(RepeatButton))]
[TemplatePart("HourSelector", typeof(DateTimePickerPanel))]
[TemplatePart("HourUpButton", typeof(RepeatButton))]
[TemplatePart("MinuteDownButton", typeof(RepeatButton))]
[TemplatePart("MinuteSelector", typeof(DateTimePickerPanel))]
[TemplatePart("MinuteUpButton", typeof(RepeatButton))]
[TemplatePart("PeriodDownButton", typeof(RepeatButton))]
[TemplatePart("PeriodHost", typeof(Panel))]
[TemplatePart("PeriodSelector", typeof(DateTimePickerPanel))]
[TemplatePart("PeriodUpButton", typeof(RepeatButton))]
[TemplatePart("PickerContainer", typeof(Grid))]
[TemplatePart("SecondSpacer", typeof(Rectangle))]
[TemplatePart("PART_AcceptButton", typeof(Button))]
[TemplatePart("PART_DismissButton", typeof(Button))]
[TemplatePart("PART_HourDownButton", typeof(RepeatButton))]
[TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel))]
[TemplatePart("PART_HourUpButton", typeof(RepeatButton))]
[TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))]
[TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel))]
[TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))]
[TemplatePart("PART_PeriodHost", typeof(Panel))]
[TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel))]
[TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PickerContainer", typeof(Grid))]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle))]
public class TimePickerPresenter : PickerPresenterBase
{
/// <summary>
@ -122,40 +122,40 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
_periodHost = e.NameScope.Get<Panel>("PeriodHost");
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
_periodHost = e.NameScope.Get<Panel>("PART_PeriodHost");
_hourSelector = e.NameScope.Get<DateTimePickerPanel>("HourSelector");
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>("MinuteSelector");
_periodSelector = e.NameScope.Get<DateTimePickerPanel>("PeriodSelector");
_hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector");
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
_periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector");
_spacer2 = e.NameScope.Get<Rectangle>("SecondSpacer");
_spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer");
_acceptButton = e.NameScope.Get<Button>("AcceptButton");
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
_acceptButton.Click += OnAcceptButtonClicked;
_hourUpButton = e.NameScope.Find<RepeatButton>("HourUpButton");
_hourUpButton = e.NameScope.Find<RepeatButton>("PART_HourUpButton");
if (_hourUpButton != null)
_hourUpButton.Click += OnSelectorButtonClick;
_hourDownButton = e.NameScope.Find<RepeatButton>("HourDownButton");
_hourDownButton = e.NameScope.Find<RepeatButton>("PART_HourDownButton");
if (_hourDownButton != null)
_hourDownButton.Click += OnSelectorButtonClick;
_minuteUpButton = e.NameScope.Find<RepeatButton>("MinuteUpButton");
_minuteUpButton = e.NameScope.Find<RepeatButton>("PART_MinuteUpButton");
if (_minuteUpButton != null)
_minuteUpButton.Click += OnSelectorButtonClick;
_minuteDownButton = e.NameScope.Find<RepeatButton>("MinuteDownButton");
_minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton");
if (_minuteDownButton != null)
_minuteDownButton.Click += OnSelectorButtonClick;
_periodUpButton = e.NameScope.Find<RepeatButton>("PeriodUpButton");
_periodUpButton = e.NameScope.Find<RepeatButton>("PART_PeriodUpButton");
if (_periodUpButton != null)
_periodUpButton.Click += OnSelectorButtonClick;
_periodDownButton = e.NameScope.Find<RepeatButton>("PeriodDownButton");
_periodDownButton = e.NameScope.Find<RepeatButton>("PART_PeriodDownButton");
if (_periodDownButton != null)
_periodDownButton.Click += OnSelectorButtonClick;
_dismissButton = e.NameScope.Find<Button>("DismissButton");
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
if (_dismissButton != null)
_dismissButton.Click += OnDismissButtonClicked;

15
src/Avalonia.Controls/Documents/InlineUIContainer.cs

@ -87,18 +87,7 @@ namespace Avalonia.Controls.Documents
public override TextRunProperties? Properties { get; }
public override Size Size
{
get
{
if (!Control.IsMeasureValid)
{
Control.Measure(Size.Infinity);
}
return Control.DesiredSize;
}
}
public override Size Size => Control.DesiredSize;
public override double Baseline
{
@ -118,7 +107,7 @@ namespace Avalonia.Controls.Documents
public override void Draw(DrawingContext drawingContext, Point origin)
{
Control.Arrange(new Rect(origin, Size));
//noop
}
}
}

61
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -5,6 +5,7 @@ using System.Linq;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -96,6 +97,13 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<NumericUpDown, string?>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="TextConverter"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, IValueConverter?> TextConverterProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, IValueConverter?>(nameof(TextConverter),
updown => updown.TextConverter, (o, v) => o.TextConverter = v, null, BindingMode.OneWay, false);
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
@ -125,6 +133,7 @@ namespace Avalonia.Controls
private decimal? _value;
private string? _text;
private IValueConverter? _textConverter;
private bool _internalValueSet;
private bool _clipValueToMinMax;
private bool _isSyncingTextAndValueProperties;
@ -234,6 +243,8 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
/// Note that Hex style does not work with decimal.
/// For hexadecimal display, use <see cref="TextConverter"/>.
/// </summary>
public NumberStyles ParsingNumberStyle
{
@ -250,6 +261,17 @@ namespace Avalonia.Controls
set { SetAndRaise(TextProperty, ref _text, value); }
}
/// <summary>
/// Gets or sets the custom bidirectional Text-Value converter.
/// Non-null converter overrides <see cref="ParsingNumberStyle"/>, providing finer control over
/// string representation of the underlying value.
/// </summary>
public IValueConverter? TextConverter
{
get { return _textConverter; }
set { SetAndRaise(TextConverterProperty, ref _textConverter, value); }
}
/// <summary>
/// Gets or sets the value.
/// </summary>
@ -318,6 +340,7 @@ namespace Avalonia.Controls
MaximumProperty.Changed.Subscribe(OnMaximumChanged);
MinimumProperty.Changed.Subscribe(OnMinimumChanged);
TextProperty.Changed.Subscribe(OnTextChanged);
TextConverterProperty.Changed.Subscribe(OnTextConverterChanged);
ValueProperty.Changed.Subscribe(OnValueChanged);
}
@ -484,6 +507,19 @@ namespace Avalonia.Controls
SyncTextAndValueProperties(true, Text);
}
}
/// <summary>
/// Called when the <see cref="Text"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnTextConverterChanged(IValueConverter? oldValue, IValueConverter? newValue)
{
if (IsInitialized)
{
SyncTextAndValueProperties(false, null);
}
}
/// <summary>
/// Called when the <see cref="Value"/> property value changed.
@ -611,6 +647,10 @@ namespace Avalonia.Controls
/// <returns></returns>
private string? ConvertValueToText()
{
if (TextConverter != null)
{
return TextConverter.ConvertBack(Value, typeof(string), null, CultureInfo.CurrentCulture)?.ToString();
}
//Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
if (FormatString.Contains("{0"))
{
@ -787,6 +827,21 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when the <see cref="TextConverter"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnTextConverterChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (IValueConverter?)e.OldValue;
var newValue = (IValueConverter?)e.NewValue;
upDown.OnTextConverterChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Value"/> property value changed.
/// </summary>
@ -1011,6 +1066,12 @@ namespace Avalonia.Controls
return null;
}
if (TextConverter != null)
{
var valueFromText = TextConverter.Convert(text, typeof(decimal?), null, CultureInfo.CurrentCulture);
return (decimal?)valueFromText;
}
if (IsPercent(FormatString))
{
result = ParsePercent(text, NumberFormat);

26
src/Avalonia.Controls/RichTextBlock.cs

@ -544,6 +544,32 @@ namespace Avalonia.Controls
}
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in VisualChildren)
{
if (child is Control control)
{
control.Measure(Size.Infinity);
}
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var child in VisualChildren)
{
if (child is Control control)
{
control.Arrange(new Rect(control.DesiredSize));
}
}
return base.ArrangeOverride(finalSize);
}
private string GetSelection()
{
if (!IsTextSelectionEnabled)

10
src/Avalonia.Controls/ToggleSwitch.cs

@ -9,8 +9,10 @@ namespace Avalonia.Controls
/// <summary>
/// A Toggle Switch control.
/// </summary>
[TemplatePart("MovingKnobs", typeof(Panel))]
[TemplatePart("SwitchKnob", typeof(Panel))]
[TemplatePart("PART_MovingKnobs", typeof(Panel))]
[TemplatePart("PART_OffContentPresenter", typeof(ContentPresenter))]
[TemplatePart("PART_OnContentPresenter", typeof(ContentPresenter))]
[TemplatePart("PART_SwitchKnob", typeof(Panel))]
[PseudoClasses(":dragging")]
public class ToggleSwitch : ToggleButton
{
@ -163,8 +165,8 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_switchKnob = e.NameScope.Find<Panel>("SwitchKnob");
_knobsPanel = e.NameScope.Get<Panel>("MovingKnobs");
_switchKnob = e.NameScope.Find<Panel>("PART_SwitchKnob");
_knobsPanel = e.NameScope.Get<Panel>("PART_MovingKnobs");
_knobsPanel.PointerPressed += KnobsPanel_PointerPressed;
_knobsPanel.PointerReleased += KnobsPanel_PointerReleased;

1
src/Avalonia.Controls/TreeView.cs

@ -495,6 +495,7 @@ namespace Avalonia.Controls
break;
case NavigationDirection.Down:
case NavigationDirection.Right:
if (from?.IsExpanded == true && intoChildren && from.ItemCount > 0)
{
result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0)!;

19
src/Avalonia.Controls/TreeViewItem.cs

@ -157,17 +157,26 @@ namespace Avalonia.Controls
switch (e.Key)
{
case Key.Right:
if (Items != null && Items.Cast<object>().Any())
if (Items != null && Items.Cast<object>().Any() && !IsExpanded)
{
IsExpanded = true;
e.Handled = true;
}
e.Handled = true;
break;
case Key.Left:
IsExpanded = false;
e.Handled = true;
if (Items is not null && Items.Cast<object>().Any() && IsExpanded)
{
if (IsFocused)
{
IsExpanded = false;
}
else
{
FocusManager.Instance?.Focus(this, NavigationMethod.Directional);
}
e.Handled = true;
}
break;
}
}

2
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -61,7 +61,7 @@ namespace Avalonia.Controls.Utils
_backgroundGeometryCache = null;
}
if (boundRect.Width != 0 && innerRect.Height != 0)
if (boundRect.Width != 0 && boundRect.Height != 0)
{
var borderGeometryKeypoints =
new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false);

8
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -168,8 +168,12 @@ namespace Avalonia.DesignerSupport.Remote
var entryPoint = asm.EntryPoint;
if (entryPoint == null)
throw Die($"Assembly {args.AppPath} doesn't have an entry point");
var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty<Type>(), null);
var builderMethod = entryPoint.DeclaringType.GetMethod(
BuilderMethodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy,
null,
Array.Empty<Type>(),
null);
if (builderMethod == null)
throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
Design.IsDesignMode = true;

8
src/Avalonia.Dialogs/ManagedFileChooser.cs

@ -10,8 +10,8 @@ using Avalonia.LogicalTree;
namespace Avalonia.Dialogs
{
[TemplatePart("QuickLinks", typeof(Control))]
[TemplatePart("Files", typeof(ListBox))]
[TemplatePart("PART_QuickLinks", typeof(Control))]
[TemplatePart("PART_Files", typeof(ListBox))]
public class ManagedFileChooser : TemplatedControl
{
private Control _quickLinksRoot;
@ -90,8 +90,8 @@ namespace Avalonia.Dialogs
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_quickLinksRoot = e.NameScope.Get<Control>("QuickLinks");
_filesView = e.NameScope.Get<ListBox>("Files");
_quickLinksRoot = e.NameScope.Get<Control>("PART_QuickLinks");
_filesView = e.NameScope.Get<ListBox>("PART_Files");
}
}
}

6
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -72,7 +72,7 @@ namespace Avalonia.FreeDesktop
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task ?? Array.Empty<string>();
return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).AbsolutePath))).ToList();
return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
@ -96,7 +96,7 @@ namespace Avalonia.FreeDesktop
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task;
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).AbsolutePath : null;
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null;
if (path is null)
{
@ -126,7 +126,7 @@ namespace Avalonia.FreeDesktop
var uris = await tsc.Task ?? Array.Empty<string>();
return uris
.Select(path => new Uri(path).AbsolutePath)
.Select(path => new Uri(path).LocalPath)
// WSL2 freedesktop allows to select files as well in directory picker, filter it out.
.Where(Directory.Exists)
.Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList();

1
src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml

@ -65,6 +65,7 @@
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource RepeatButtonBackgroundDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource RepeatButtonForegroundDisabled}" />
</Style>
</ControlTheme>

4
src/Avalonia.Themes.Fluent/Controls/Calendar.xaml

@ -23,11 +23,11 @@
<Setter Property="Template">
<ControlTemplate>
<StackPanel
Name="Root"
Name="PART_Root"
HorizontalAlignment="Center"
ClipToBounds="True">
<CalendarItem
Name="CalendarItem"
Name="PART_CalendarItem"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

10
src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml

@ -79,12 +79,12 @@
-->
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="40,*" MinWidth="294">
<Grid ColumnDefinitions="5*,*,*">
<Button Name="HeaderButton"
<Button Name="PART_HeaderButton"
Theme="{StaticResource FluentCalendarButton}"
Foreground="{TemplateBinding Foreground}"
Padding="12,0,0,0"
HorizontalContentAlignment="Left" />
<Button Name="PreviousButton"
<Button Name="PART_PreviousButton"
Grid.Column="1"
Theme="{StaticResource FluentCalendarButton}"
Foreground="{TemplateBinding Foreground}"
@ -97,7 +97,7 @@
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
<Button Name="NextButton"
<Button Name="PART_NextButton"
Grid.Column="2"
Theme="{StaticResource FluentCalendarButton}"
Foreground="{TemplateBinding Foreground}"
@ -113,7 +113,7 @@
</Grid>
<!--Border below is used only for MonthView but it can't be moved inside of Grid because CalendarItem expects it to be empty and it will cause side-effects-->
<Border Name="BackgroundLayer" Background="{TemplateBinding BorderBrush}" Margin="0,38,0,0" IsVisible="{ReflectionBinding #MonthView.IsVisible}" Grid.Row="1" />
<Grid Name="MonthView" Grid.Row="1" IsVisible="False" MinHeight="290">
<Grid Name="PART_MonthView" Grid.Row="1" IsVisible="False" MinHeight="290">
<Grid.RowDefinitions>
<!--This should always be the week day names??-->
<RowDefinition Height="38" />
@ -134,7 +134,7 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
<Grid Name="YearView"
<Grid Name="PART_YearView"
Background="{TemplateBinding BorderBrush}"
MinHeight="290"
Grid.Row="1"

63
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@ -56,18 +56,11 @@
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushPointerOver}"/>
<Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPointerOver}"/>
<Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundPointerOver}"/>
</Style>
<Style Selector="^:pressed">
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushPressed}"/>
<Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPressed}"/>
<Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundPressed}"/>
</Style>
@ -101,7 +94,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Top"/>
<Button Name="FlyoutButton"
<Button Name="PART_FlyoutButton"
Grid.Row="1"
Theme="{StaticResource FluentDatePickerFlyoutButton}"
Foreground="{TemplateBinding Foreground}"
@ -115,28 +108,28 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
TemplatedControl.IsTemplateFocusTarget="True">
<Grid Name="ButtonContentGrid" ColumnDefinitions="78*,Auto,132*,Auto,78*">
<TextBlock Name="DayText" Text="day" HorizontalAlignment="Center"
<Grid Name="PART_ButtonContentGrid" ColumnDefinitions="78*,Auto,132*,Auto,78*">
<TextBlock Name="PART_DayTextBlock" Text="day" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
<TextBlock Name="MonthText" Text="month" TextAlignment="Left"
<TextBlock Name="PART_MonthTextBlock" Text="month" TextAlignment="Left"
Padding="{DynamicResource DatePickerHostMonthPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
<TextBlock Name="YearText" Text="year" HorizontalAlignment="Center"
<TextBlock Name="PART_YearTextBlock" Text="year" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
<Rectangle x:Name="FirstSpacer"
<Rectangle x:Name="PART_FirstSpacer"
Fill="{DynamicResource DatePickerSpacerFill}"
HorizontalAlignment="Center"
Width="1"
Grid.Column="1" />
<Rectangle x:Name="SecondSpacer"
<Rectangle x:Name="PART_SecondSpacer"
Fill="{DynamicResource DatePickerSpacerFill}"
HorizontalAlignment="Center"
Width="1"
@ -144,10 +137,10 @@
</Grid>
</Button>
<Popup Name="Popup" WindowManagerAddShadowHint="False"
<Popup Name="PART_Popup" WindowManagerAddShadowHint="False"
IsLightDismissEnabled="True" PlacementTarget="{TemplateBinding}"
PlacementMode="Bottom">
<DatePickerPresenter Name="PickerPresenter" />
<DatePickerPresenter Name="PART_PickerPresenter" />
</Popup>
</Grid>
@ -188,62 +181,62 @@
Padding="{DynamicResource DateTimeFlyoutBorderPadding}"
MaxHeight="398" CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="ContentRoot" RowDefinitions="*,Auto">
<Grid Name="PickerContainer">
<Grid Name="PART_PickerContainer">
<Grid.Styles>
<Style Selector="DateTimePickerPanel > ListBoxItem">
<Setter Property="Theme" Value="{StaticResource FluentDateTimePickerItem}" />
</Style>
</Grid.Styles>
<!--Column Definitions set in code, ignore here-->
<Panel Name="MonthHost">
<Panel Name="PART_MonthHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="MonthSelector"
<DateTimePickerPanel Name="PART_MonthSelector"
PanelType="Month"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="MonthUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="MonthDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
<RepeatButton Name="PART_MonthUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PART_MonthDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
</Panel>
<Panel Name="DayHost">
<Panel Name="PART_DayHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="DaySelector"
<DateTimePickerPanel Name="PART_DaySelector"
PanelType="Day"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="DayUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="DayDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
<RepeatButton Name="PART_DayUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PART_DayDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
</Panel>
<Panel Name="YearHost">
<Panel Name="PART_YearHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="YearSelector"
<DateTimePickerPanel Name="PART_YearSelector"
PanelType="Year"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
ShouldLoop="False" />
</ScrollViewer>
<RepeatButton Name="YearUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="YearDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
<RepeatButton Name="PART_YearUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PART_YearDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
</Panel>
<Rectangle Name="HighlightRect" IsHitTestVisible="False" ZIndex="-1"
Fill="{DynamicResource DatePickerFlyoutPresenterHighlightFill}"
Grid.Column="0" Grid.ColumnSpan="5" VerticalAlignment="Center"
Height="{DynamicResource DatePickerFlyoutPresenterHighlightHeight}" />
<Rectangle Name="FirstSpacer"
<Rectangle Name="PART_FirstSpacer"
Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}"
HorizontalAlignment="Center"
Width="{DynamicResource DatePickerSpacerThemeWidth}"
Grid.Column="1" />
<Rectangle Name="SecondSpacer"
<Rectangle Name="PART_SecondSpacer"
Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}"
HorizontalAlignment="Center"
Width="{DynamicResource DatePickerSpacerThemeWidth}"
Grid.Column="3" />
</Grid>
<Grid Name="AcceptDismissGrid"
Grid.Row="1"
ColumnDefinitions="*,*">
@ -251,7 +244,7 @@
VerticalAlignment="Top"
Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}"
Grid.ColumnSpan="2"/>
<Button Name="AcceptButton"
<Button Name="PART_AcceptButton"
Grid.Column="0"
Theme="{StaticResource FluentDateTimePickerButton}"
Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
@ -262,8 +255,8 @@
StrokeThickness="0.75"
Data="M0.5,8.5 5,13.5 15.5,3" />
</Button>
<Button Name="DismissButton"
Grid.Column="1"
<Button Name="PART_DismissButton"
Grid.Column="1"
Theme="{StaticResource FluentDateTimePickerButton}"
Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
HorizontalAlignment="Stretch"

4
src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml

@ -134,7 +134,7 @@
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<ListBox x:Name="QuickLinks" DockPanel.Dock="Left" Items="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False">
<ListBox x:Name="PART_QuickLinks" DockPanel.Dock="Left" Items="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
@ -213,7 +213,7 @@
<GridSplitter Grid.Column="7" />
<TextBlock Grid.Column="8" Text="Size" />
</Grid>
<ListBox x:Name="Files"
<ListBox x:Name="PART_Files"
VirtualizationMode="Simple"
Items="{Binding Items}"
Margin="0 5"

67
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -55,18 +55,11 @@
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushPointerOver}"/>
<Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPointerOver}"/>
<Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundPointerOver}"/>
</Style>
<Style Selector="^:pressed">
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushPressed}"/>
<Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPressed}"/>
<Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundPressed}"/>
</Style>
@ -101,7 +94,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<Button x:Name="FlyoutButton"
<Button x:Name="PART_FlyoutButton"
Grid.Row="1"
Theme="{StaticResource FluentTimePickerFlyoutButton}"
Foreground="{TemplateBinding Foreground}"
@ -115,13 +108,13 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Top">
<Grid Name="FlyoutButtonContentGrid">
<Grid Name="PART_FlyoutButtonContentGrid">
<!--Ignore col defs here, set in code-->
<Border x:Name="FirstPickerHost"
<Border x:Name="PART_FirstPickerHost"
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="HourTextBlock"
<TextBlock x:Name="PART_HourTextBlock"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
@ -129,17 +122,17 @@
FontSize="{TemplateBinding FontSize}" />
</Border>
<Rectangle Name="FirstColumnDivider"
<Rectangle Name="PART_FirstColumnDivider"
Fill="{DynamicResource TimePickerSpacerFill}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="1" />
<Border x:Name="SecondPickerHost"
<Border x:Name="PART_SecondPickerHost"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="MinuteTextBlock"
<TextBlock x:Name="PART_MinuteTextBlock"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
@ -147,17 +140,17 @@
FontSize="{TemplateBinding FontSize}"/>
</Border>
<Rectangle Name="SecondColumnDivider"
<Rectangle Name="PART_SecondColumnDivider"
Fill="{DynamicResource TimePickerSpacerFill}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="3" />
<Border x:Name="ThirdPickerHost"
<Border x:Name="PART_ThirdPickerHost"
Grid.Column="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="PeriodTextBlock"
<TextBlock x:Name="PART_PeriodTextBlock"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
@ -167,12 +160,12 @@
</Grid>
</Button>
<Popup Name="Popup"
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
IsLightDismissEnabled="True"
PlacementTarget="{TemplateBinding}"
PlacementMode="Bottom">
<TimePickerPresenter Name="PickerPresenter" />
<TimePickerPresenter Name="PART_PickerPresenter" />
</Popup>
</Grid>
@ -216,42 +209,42 @@
Padding="{DynamicResource DateTimeFlyoutBorderPadding}"
MaxHeight="398">
<Grid Name="ContentPanel" RowDefinitions="*,Auto">
<Grid Name="PickerContainer">
<Grid Name="PART_PickerContainer">
<!--Ignore col defs here, set in code-->
<Panel Name="HourHost" Grid.Column="0">
<Panel Name="PART_HourHost" Grid.Column="0">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="HourSelector"
<DateTimePickerPanel Name="PART_HourSelector"
PanelType="Hour"
ShouldLoop="True"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
</ScrollViewer>
<RepeatButton Name="HourUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="HourDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
<RepeatButton Name="PART_HourUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PART_HourDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
</Panel>
<Panel Name="MinuteHost" Grid.Column="2">
<Panel Name="PART_MinuteHost" Grid.Column="2">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="MinuteSelector"
<DateTimePickerPanel Name="PART_MinuteSelector"
PanelType="Minute"
ShouldLoop="True"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
</ScrollViewer>
<RepeatButton Name="MinuteUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="MinuteDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
<RepeatButton Name="PART_MinuteUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PART_MinuteDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
</Panel>
<Panel Name="PeriodHost" Grid.Column="4">
<Panel Name="PART_PeriodHost" Grid.Column="4">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="PeriodSelector"
<DateTimePickerPanel Name="PART_PeriodSelector"
PanelType="TimePeriod"
ShouldLoop="False"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
</ScrollViewer>
<RepeatButton Name="PeriodUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PeriodDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
<RepeatButton Name="PART_PeriodUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
<RepeatButton Name="PART_PeriodDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
</Panel>
<Rectangle x:Name="HighlightRect" ZIndex="-1"
@ -260,12 +253,12 @@
Grid.ColumnSpan="5"
VerticalAlignment="Center"
Height="{DynamicResource TimePickerFlyoutPresenterHighlightHeight}" />
<Rectangle Name="FirstSpacer"
<Rectangle Name="PART_FirstSpacer"
Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="1" />
<Rectangle Name="SecondSpacer"
<Rectangle Name="PART_SecondSpacer"
Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
@ -279,7 +272,7 @@
VerticalAlignment="Top"
Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
Grid.ColumnSpan="2"/>
<Button Name="AcceptButton"
<Button Name="PART_AcceptButton"
Grid.Column="0"
Theme="{StaticResource FluentDateTimePickerButton}"
Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
@ -290,8 +283,8 @@
StrokeThickness="0.75"
Data="M0.5,8.5 5,13.5 15.5,3" />
</Button>
<Button Name="DismissButton"
Grid.Column="1"
<Button Name="PART_DismissButton"
Grid.Column="1"
Theme="{StaticResource FluentDateTimePickerButton}"
Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
FontSize="16"

4
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@ -106,13 +106,13 @@
CornerRadius="10" />
<Canvas
x:Name="SwitchKnob"
x:Name="PART_SwitchKnob"
Grid.Row="1"
Width="20"
Height="20"
HorizontalAlignment="Left">
<Grid x:Name="MovingKnobs" Width="20" Height="20">
<Grid x:Name="PART_MovingKnobs" Width="20" Height="20">
<Ellipse
x:Name="SwitchKnobOn"
Fill="{DynamicResource ToggleSwitchKnobFillOn}"

4
src/Avalonia.Themes.Simple/Controls/Calendar.xaml

@ -16,10 +16,10 @@
<Setter Property="HeaderBackground" Value="{DynamicResource ThemeAccentBrush2}" />
<Setter Property="Template">
<ControlTemplate>
<StackPanel Name="Root"
<StackPanel Name="PART_Root"
HorizontalAlignment="Center"
ClipToBounds="True">
<CalendarItem Name="CalendarItem"
<CalendarItem Name="PART_CalendarItem"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

10
src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml

@ -65,7 +65,7 @@
Fill="{TemplateBinding HeaderBackground}"
Stretch="Fill" />
<Button Name="PreviousButton"
<Button Name="PART_PreviousButton"
HorizontalAlignment="Left"
Classes="CalendarHeader CalendarNavigation"
IsVisible="False">
@ -80,7 +80,7 @@
</Button>
<Button Name="HeaderButton"
<Button Name="PART_HeaderButton"
Grid.Column="1"
Padding="1,5,1,9"
HorizontalAlignment="Center"
@ -89,7 +89,7 @@
FontSize="10.5"
FontWeight="Bold" />
<Button Name="NextButton"
<Button Name="PART_NextButton"
Grid.Column="2"
HorizontalAlignment="Right"
Classes="CalendarHeader CalendarNavigation"
@ -105,7 +105,7 @@
</Button>
<Grid Name="MonthView"
<Grid Name="PART_MonthView"
Grid.Row="1"
Grid.ColumnSpan="3"
Margin="6,-1,6,6"
@ -130,7 +130,7 @@
</Grid.ColumnDefinitions>
</Grid>
<Grid Name="YearView"
<Grid Name="PART_YearView"
Grid.Row="1"
Grid.ColumnSpan="3"
Margin="6,-3,7,6"

52
src/Avalonia.Themes.Simple/Controls/DatePicker.xaml

@ -102,7 +102,7 @@
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
<Button Name="FlyoutButton"
<Button Name="PART_FlyoutButton"
Grid.Row="1"
MinWidth="{DynamicResource DatePickerThemeMinWidth}"
MaxWidth="{DynamicResource DatePickerThemeMaxWidth}"
@ -116,35 +116,35 @@
IsEnabled="{TemplateBinding IsEnabled}"
TemplatedControl.IsTemplateFocusTarget="True"
Theme="{StaticResource SimpleDatePickerFlyoutButton}">
<Grid Name="ButtonContentGrid"
<Grid Name="PART_ButtonContentGrid"
ColumnDefinitions="78*,Auto,132*,Auto,78*">
<TextBlock Name="DayText"
<TextBlock Name="PART_DayTextBlock"
Padding="{DynamicResource DatePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="day" />
<TextBlock Name="MonthText"
<TextBlock Name="PART_MonthTextBlock"
Padding="{DynamicResource DatePickerHostMonthPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="month"
TextAlignment="Left" />
<TextBlock Name="YearText"
<TextBlock Name="PART_YearTextBlock"
Padding="{DynamicResource DatePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Text="year" />
<Rectangle x:Name="FirstSpacer"
<Rectangle x:Name="PART_FirstSpacer"
Grid.Column="1"
Width="1"
HorizontalAlignment="Center"
Fill="{DynamicResource DatePickerSpacerFill}" />
<Rectangle x:Name="SecondSpacer"
<Rectangle x:Name="PART_SecondSpacer"
Grid.Column="3"
Width="1"
HorizontalAlignment="Center"
@ -152,12 +152,12 @@
</Grid>
</Button>
<Popup Name="Popup"
<Popup Name="PART_Popup"
IsLightDismissEnabled="True"
PlacementMode="Bottom"
PlacementTarget="{TemplateBinding}"
WindowManagerAddShadowHint="False">
<DatePickerPresenter Name="PickerPresenter" />
<DatePickerPresenter Name="PART_PickerPresenter" />
</Popup>
</Grid>
@ -197,50 +197,50 @@
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="ContentRoot"
RowDefinitions="*,Auto">
<Grid Name="PickerContainer">
<Grid Name="PART_PickerContainer">
<Grid.Styles>
<Style Selector="DateTimePickerPanel > ListBoxItem">
<Setter Property="Theme" Value="{StaticResource SimpleDateTimePickerItem}" />
</Style>
</Grid.Styles>
<!-- Column Definitions set in code, ignore here -->
<Panel Name="MonthHost">
<Panel Name="PART_MonthHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="MonthSelector"
<DateTimePickerPanel Name="PART_MonthSelector"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
PanelType="Month"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="MonthUpButton"
<RepeatButton Name="PART_MonthUpButton"
Theme="{StaticResource SimpleDateTimePickerUpButton}" />
<RepeatButton Name="MonthDownButton"
<RepeatButton Name="PART_MonthDownButton"
Theme="{StaticResource SimpleDateTimePickerDownButton}" />
</Panel>
<Panel Name="DayHost">
<Panel Name="PART_DayHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="DaySelector"
<DateTimePickerPanel Name="PART_DaySelector"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
PanelType="Day"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="DayUpButton"
<RepeatButton Name="PART_DayUpButton"
Theme="{StaticResource SimpleDateTimePickerUpButton}" />
<RepeatButton Name="DayDownButton"
<RepeatButton Name="PART_DayDownButton"
Theme="{StaticResource SimpleDateTimePickerDownButton}" />
</Panel>
<Panel Name="YearHost">
<Panel Name="PART_YearHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="YearSelector"
<DateTimePickerPanel Name="PART_YearSelector"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
PanelType="Year"
ShouldLoop="False" />
</ScrollViewer>
<RepeatButton Name="YearUpButton"
<RepeatButton Name="PART_YearUpButton"
Theme="{StaticResource SimpleDateTimePickerUpButton}" />
<RepeatButton Name="YearDownButton"
<RepeatButton Name="PART_YearDownButton"
Theme="{StaticResource SimpleDateTimePickerDownButton}" />
</Panel>
<Rectangle Name="HighlightRect"
@ -255,12 +255,12 @@
Color="{DynamicResource ThemeAccentColor}" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Name="FirstSpacer"
<Rectangle Name="PART_FirstSpacer"
Grid.Column="1"
Width="{DynamicResource DatePickerSpacerThemeWidth}"
HorizontalAlignment="Center"
Fill="{DynamicResource ThemeControlMidHighBrush}" />
<Rectangle Name="SecondSpacer"
<Rectangle Name="PART_SecondSpacer"
Grid.Column="3"
Width="{DynamicResource DatePickerSpacerThemeWidth}"
HorizontalAlignment="Center"
@ -274,7 +274,7 @@
Height="{DynamicResource DatePickerSpacerThemeWidth}"
VerticalAlignment="Top"
Fill="{DynamicResource ThemeControlMidHighBrush}" />
<Button Name="AcceptButton"
<Button Name="PART_AcceptButton"
Grid.Column="0"
Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
HorizontalAlignment="Stretch"
@ -285,7 +285,7 @@
StrokeLineCap="Round"
StrokeThickness="0.75" />
</Button>
<Button Name="DismissButton"
<Button Name="PART_DismissButton"
Grid.Column="1"
Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
HorizontalAlignment="Stretch"

4
src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml

@ -94,7 +94,7 @@
Text="{Binding FileName}"
Watermark="File name" />
<ListBox x:Name="QuickLinks"
<ListBox x:Name="PART_QuickLinks"
Margin="0,0,5,5"
BorderBrush="Transparent"
DockPanel.Dock="Left"
@ -143,7 +143,7 @@
<TextBlock Grid.Column="8"
Text="Size" />
</Grid>
<ListBox x:Name="Files"
<ListBox x:Name="PART_Files"
Margin="0,5"
Items="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"

58
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@ -102,7 +102,7 @@
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{DynamicResource ThemeForegroundColor}" />
<Button x:Name="FlyoutButton"
<Button x:Name="PART_FlyoutButton"
Grid.Row="1"
MinWidth="{DynamicResource TimePickerThemeMinWidth}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
@ -116,13 +116,13 @@
IsEnabled="{TemplateBinding IsEnabled}"
Theme="{StaticResource SimpleTimePickerFlyoutButton}">
<Grid Name="FlyoutButtonContentGrid">
<Grid Name="PART_FlyoutButtonContentGrid">
<!-- Ignore col defs here, set in code -->
<Border x:Name="FirstPickerHost"
<Border x:Name="PART_FirstPickerHost"
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="HourTextBlock"
<TextBlock x:Name="PART_HourTextBlock"
Padding="{DynamicResource TimePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"
@ -130,17 +130,17 @@
FontWeight="{TemplateBinding FontWeight}" />
</Border>
<Rectangle Name="FirstColumnDivider"
<Rectangle Name="PART_FirstColumnDivider"
Grid.Column="1"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
HorizontalAlignment="Center"
Fill="{DynamicResource TimePickerSpacerFill}" />
<Border x:Name="SecondPickerHost"
<Border x:Name="PART_SecondPickerHost"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="MinuteTextBlock"
<TextBlock x:Name="PART_MinuteTextBlock"
Padding="{DynamicResource TimePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"
@ -148,17 +148,17 @@
FontWeight="{TemplateBinding FontWeight}" />
</Border>
<Rectangle Name="SecondColumnDivider"
<Rectangle Name="PART_SecondColumnDivider"
Grid.Column="3"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
HorizontalAlignment="Center"
Fill="{DynamicResource TimePickerSpacerFill}" />
<Border x:Name="ThirdPickerHost"
<Border x:Name="PART_ThirdPickerHost"
Grid.Column="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock x:Name="PeriodTextBlock"
<TextBlock x:Name="PART_PeriodTextBlock"
Padding="{DynamicResource TimePickerHostPadding}"
HorizontalAlignment="Center"
FontFamily="{TemplateBinding FontFamily}"
@ -168,12 +168,12 @@
</Grid>
</Button>
<Popup Name="Popup"
<Popup Name="PART_Popup"
IsLightDismissEnabled="True"
PlacementMode="Bottom"
PlacementTarget="{TemplateBinding}"
WindowManagerAddShadowHint="False">
<TimePickerPresenter Name="PickerPresenter" />
<TimePickerPresenter Name="PART_PickerPresenter" />
</Popup>
</Grid>
@ -214,50 +214,50 @@
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="ContentPanel"
RowDefinitions="*,Auto">
<Grid Name="PickerContainer">
<Grid Name="PART_PickerContainer">
<!-- Ignore col defs here, set in code -->
<Panel Name="HourHost"
<Panel Name="PART_HourHost"
Grid.Column="0">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="HourSelector"
<DateTimePickerPanel Name="PART_HourSelector"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
PanelType="Hour"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="HourUpButton"
<RepeatButton Name="PART_HourUpButton"
Theme="{StaticResource SimpleDateTimePickerUpButton}" />
<RepeatButton Name="HourDownButton"
<RepeatButton Name="PART_HourDownButton"
Theme="{StaticResource SimpleDateTimePickerDownButton}" />
</Panel>
<Panel Name="MinuteHost"
<Panel Name="PART_MinuteHost"
Grid.Column="2">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="MinuteSelector"
<DateTimePickerPanel Name="PART_MinuteSelector"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
PanelType="Minute"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="MinuteUpButton"
<RepeatButton Name="PART_MinuteUpButton"
Theme="{StaticResource SimpleDateTimePickerUpButton}" />
<RepeatButton Name="MinuteDownButton"
<RepeatButton Name="PART_MinuteDownButton"
Theme="{StaticResource SimpleDateTimePickerDownButton}" />
</Panel>
<Panel Name="PeriodHost"
<Panel Name="PART_PeriodHost"
Grid.Column="4">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="PeriodSelector"
<DateTimePickerPanel Name="PART_PeriodSelector"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
PanelType="TimePeriod"
ShouldLoop="False" />
</ScrollViewer>
<RepeatButton Name="PeriodUpButton"
<RepeatButton Name="PART_PeriodUpButton"
Theme="{StaticResource SimpleDateTimePickerUpButton}" />
<RepeatButton Name="PeriodDownButton"
<RepeatButton Name="PART_PeriodDownButton"
Theme="{StaticResource SimpleDateTimePickerDownButton}" />
</Panel>
@ -272,12 +272,12 @@
Color="{DynamicResource ThemeAccentColor}" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Name="FirstSpacer"
<Rectangle Name="PART_FirstSpacer"
Grid.Column="1"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
HorizontalAlignment="Center"
Fill="{DynamicResource ThemeControlMidHighBrush}" />
<Rectangle Name="SecondSpacer"
<Rectangle Name="PART_SecondSpacer"
Grid.Column="3"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
HorizontalAlignment="Center"
@ -291,7 +291,7 @@
Height="{DynamicResource TimePickerSpacerThemeWidth}"
VerticalAlignment="Top"
Fill="{DynamicResource ThemeControlMidHighBrush}" />
<Button Name="AcceptButton"
<Button Name="PART_AcceptButton"
Grid.Column="0"
Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
HorizontalAlignment="Stretch"
@ -302,7 +302,7 @@
StrokeLineCap="Round"
StrokeThickness="0.75" />
</Button>
<Button Name="DismissButton"
<Button Name="PART_DismissButton"
Grid.Column="1"
Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
HorizontalAlignment="Stretch"

4
src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml

@ -149,11 +149,11 @@
BorderBrush="{DynamicResource ToggleSwitchStrokeOn}"
BorderThickness="{DynamicResource ToggleSwitchOnStrokeThickness}" />
<Canvas x:Name="SwitchKnob" Grid.Row="1"
<Canvas x:Name="PART_SwitchKnob" Grid.Row="1"
HorizontalAlignment="Left"
Width="20" Height="20">
<Grid x:Name="MovingKnobs"
<Grid x:Name="PART_MovingKnobs"
Width="20" Height="20">
<Grid.Transitions>
<Transitions>

3
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@ -7,6 +7,7 @@ using Avalonia.LinuxFramebuffer.Input;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
namespace Avalonia.LinuxFramebuffer
{
@ -32,7 +33,7 @@ namespace Avalonia.LinuxFramebuffer
{
var factory = AvaloniaLocator.Current.GetService<IRendererFactory>();
var renderLoop = AvaloniaLocator.Current.GetService<IRenderLoop>();
return factory?.Create(root, renderLoop) ?? new DeferredRenderer(root, renderLoop);
return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor);
}
public void Dispose()

9
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -15,6 +15,7 @@ using Avalonia.LinuxFramebuffer.Output;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using JetBrains.Annotations;
@ -26,6 +27,10 @@ namespace Avalonia.LinuxFramebuffer
private static readonly Stopwatch St = Stopwatch.StartNew();
internal static uint Timestamp => (uint)St.ElapsedTicks;
public static InternalPlatformThreadingInterface Threading;
internal static Compositor Compositor { get; private set; }
LinuxFramebufferPlatform(IOutputBackend backend)
{
_fb = backend;
@ -48,6 +53,10 @@ namespace Avalonia.LinuxFramebuffer
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
}

35
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@ -8,31 +8,16 @@
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<PropertyGroup>
<TypescriptOutDir>wwwroot</TypescriptOutDir>
<TypeScriptNoEmitOnError>true</TypeScriptNoEmitOnError>
<TypeScriptNoImplicitReturns>true</TypeScriptNoImplicitReturns>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<TypeScriptRemoveComments>false</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TypeScriptRemoveComments>true</TypeScriptRemoveComments>
<TypeScriptSourceMap>false</TypeScriptSourceMap>
</PropertyGroup>
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<Content Include="*.props">
<Pack>true</Pack>
@ -45,12 +30,26 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.7.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.5.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
<Exec Command="npm install" WorkingDirectory="webapp" />
<!-- Write the stamp file, so incremental builds work -->
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" />
</Target>
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
<Exec Command="npm run build" WorkingDirectory="webapp" />
</Target>
</Project>

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

@ -1,7 +1,11 @@
<div id="container" class="avalonia-container" tabindex="0" oncontextmenu="return false;"
<div id="container" @ref="_containerElement"
class="avalonia-container"
tabindex="0" oncontextmenu="return false;"
@onwheel="OnWheel"
@onkeydown="OnKeyDown"
@onkeydown:preventDefault="true"
@onkeyup="OnKeyUp"
@onkeyup:preventDefault="true"
@onpointerdown="OnPointerDown"
@onpointerup="OnPointerUp"
@onpointermove="OnPointerMove"
@ -15,7 +19,10 @@
<input id="inputElement" @ref="_inputElement" type="text" @oninput="OnInput"
onpaste="return false;"
oncopy="return false;"
oncut="return false;"/>
oncut="return false;"
@onkeydown:preventDefault="true"
@onkeyup:preventDefault="true"
autocapitalize="none"/>
</div>
<style>

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

@ -6,6 +6,7 @@ using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Web.Blazor.Interop;
using Avalonia.Web.Blazor.Interop.Storage;
@ -28,12 +29,15 @@ namespace Avalonia.Web.Blazor
private SizeWatcherInterop? _sizeWatcher = null;
private DpiWatcherInterop? _dpiWatcher = null;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
private AvaloniaModule? _avaloniaModule = null;
private InputHelperInterop? _inputHelper = null;
private InputHelperInterop? _canvasHelper = null;
private InputHelperInterop? _containerHelper = null;
private NativeControlHostInterop? _nativeControlHost = null;
private StorageProviderInterop? _storageProvider = null;
private ElementReference _htmlCanvas;
private ElementReference _inputElement;
private ElementReference _containerElement;
private ElementReference _nativeControlsContainer;
private double _dpi = 1;
private SKSize _canvasSize = new (100, 100);
@ -240,10 +244,13 @@ namespace Avalonia.Web.Blazor
{
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
_avaloniaModule = await AvaloniaModule.ImportAsync(Js);
_inputHelper.Hide();
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
_canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas);
_containerHelper = new InputHelperInterop(_avaloniaModule, _containerElement);
HideIme();
_canvasHelper.SetCursor("default");
_topLevelImpl.SetCssCursor = x =>
{
@ -251,11 +258,11 @@ namespace Avalonia.Web.Blazor
_canvasHelper.SetCursor(x); //windows
};
_nativeControlHost = await NativeControlHostInterop.ImportAsync(Js, _nativeControlsContainer);
_nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer);
_storageProvider = await StorageProviderInterop.ImportAsync(Js);
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame);
Console.WriteLine("Interop created");
@ -303,9 +310,10 @@ namespace Avalonia.Web.Blazor
{
_interop.RequestAnimationFrame(true);
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
_sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged);
_dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged);
_sizeWatcher.Start();
_topLevel.Prepare();
_topLevel.Renderer.Start();
@ -345,9 +353,9 @@ namespace Avalonia.Web.Blazor
// We also don't want to have it as a meaningful public API.
// Therefore we have InternalsVisibleTo hack here.
if (_topLevel.Renderer is DeferredRenderer dr)
if (_topLevel.Renderer is CompositingRenderer dr)
{
dr.Render(true);
dr.CompositionTarget.ImmediateUIThreadRender();
}
}
@ -379,6 +387,12 @@ namespace Avalonia.Web.Blazor
}
}
private void HideIme()
{
_inputHelper?.Hide();
_containerHelper?.Focus();
}
public void SetClient(ITextInputMethodClient? client)
{
if (_inputHelper is null)
@ -399,7 +413,7 @@ namespace Avalonia.Web.Blazor
else
{
_inputElementFocused = false;
_inputHelper.Hide();
HideIme();
}
}

18
src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs

@ -0,0 +1,18 @@
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class AvaloniaModule : JSModuleInterop
{
private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/avalonia.js")
{
}
public static async Task<AvaloniaModule> ImportAsync(IJSRuntime js)
{
var interop = new AvaloniaModule(js);
await interop.ImportAsync();
return interop;
}
}
}

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

@ -1,43 +1,29 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class DpiWatcherInterop : JSModuleInterop
internal class DpiWatcherInterop : IDisposable
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/DpiWatcher.js";
private const string StartSymbol = "DpiWatcher.start";
private const string StopSymbol = "DpiWatcher.stop";
private const string GetDpiSymbol = "DpiWatcher.getDpi";
private static DpiWatcherInterop? instance;
private event Action<double>? callbacksEvent;
private readonly FloatFloatActionHelper callbackHelper;
private readonly FloatFloatActionHelper _callbackHelper;
private readonly AvaloniaModule _module;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null)
public DpiWatcherInterop(AvaloniaModule module, Action<double>? callback = null)
{
var interop = Get(js);
await interop.ImportAsync();
if (callback != null)
interop.Subscribe(callback);
return interop;
}
_module = module;
_callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
public static DpiWatcherInterop Get(IJSRuntime js) =>
instance ??= new DpiWatcherInterop(js);
private DpiWatcherInterop(IJSRuntime js)
: base(js, JsFilename)
{
callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
if (callback != null)
Subscribe(callback);
}
protected override void OnDisposingModule() =>
Stop();
public void Dispose() => Stop();
public void Subscribe(Action<double> callback)
{
@ -65,9 +51,9 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference != null)
return GetDpi();
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return Invoke<double>(StartSymbol, callbackReference);
return _module.Invoke<double>(StartSymbol, callbackReference);
}
private void Stop()
@ -75,13 +61,12 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference == null)
return;
Invoke(StopSymbol);
_module.Invoke(StopSymbol);
callbackReference?.Dispose();
callbackReference = null;
}
public double GetDpi() =>
Invoke<double>(GetDpiSymbol);
public double GetDpi() => _module.Invoke<double>(GetDpiSymbol);
}
}

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

@ -1,41 +1,32 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class InputHelperInterop : JSModuleInterop
internal class InputHelperInterop
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js";
private const string ClearSymbol = "InputHelper.clear";
private const string FocusSymbol = "InputHelper.focus";
private const string SetCursorSymbol = "InputHelper.setCursor";
private const string HideSymbol = "InputHelper.hide";
private const string ShowSymbol = "InputHelper.show";
private readonly ElementReference inputElement;
private readonly AvaloniaModule _module;
private readonly ElementReference _inputElement;
public static async Task<InputHelperInterop> ImportAsync(IJSRuntime js, ElementReference element)
public InputHelperInterop(AvaloniaModule module, ElementReference inputElement)
{
var interop = new InputHelperInterop(js, element);
await interop.ImportAsync();
return interop;
_module = module;
_inputElement = inputElement;
}
public InputHelperInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
inputElement = element;
}
public void Clear() => Invoke(ClearSymbol, inputElement);
public void Clear() => _module.Invoke(ClearSymbol, _inputElement);
public void Focus() => Invoke(FocusSymbol, inputElement);
public void Focus() => _module.Invoke(FocusSymbol, _inputElement);
public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind);
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind);
public void Hide() => Invoke(HideSymbol, inputElement);
public void Hide() => _module.Invoke(HideSymbol, _inputElement);
public void Show() => Invoke(ShowSymbol, inputElement);
public void Show() => _module.Invoke(ShowSymbol, _inputElement);
}
}

12
src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs

@ -1,6 +1,4 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
@ -31,16 +29,16 @@ namespace Avalonia.Web.Blazor.Interop
protected IJSUnmarshalledObjectReference Module =>
module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first.");
protected void Invoke(string identifier, params object?[]? args) =>
internal void Invoke(string identifier, params object?[]? args) =>
Module.InvokeVoid(identifier, args);
protected TValue Invoke<TValue>(string identifier, params object?[]? args) =>
internal TValue Invoke<TValue>(string identifier, params object?[]? args) =>
Module.Invoke<TValue>(identifier, args);
protected ValueTask InvokeAsync(string identifier, params object?[]? args) =>
internal ValueTask InvokeAsync(string identifier, params object?[]? args) =>
Module.InvokeVoidAsync(identifier, args);
protected ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
internal ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
Module.InvokeAsync<TValue>(identifier, args);
protected virtual void OnDisposingModule() { }

32
src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs

@ -1,5 +1,4 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
@ -10,31 +9,24 @@ using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl
internal class NativeControlHostInterop : INativeControlHostImpl
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js";
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild";
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment";
private const string GetReferenceSymbol = "NativeControlHost.GetReference";
private readonly ElementReference hostElement;
private readonly AvaloniaModule _module;
private readonly ElementReference _hostElement;
public static async Task<NativeControlHostInterop> ImportAsync(IJSRuntime js, ElementReference element)
public NativeControlHostInterop(AvaloniaModule module, ElementReference element)
{
var interop = new NativeControlHostInterop(js, element);
await interop.ImportAsync();
return interop;
}
public NativeControlHostInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
hostElement = element;
_module = module;
_hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
var element = _module.Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
return new JSObjectControlHandle(element);
}
@ -43,9 +35,9 @@ namespace Avalonia.Web.Blazor.Interop
Attachment? a = null;
try
{
using var hostElementJsReference = Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, hostElement);
using var hostElementJsReference = _module.Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, _hostElement);
var child = create(new JSObjectControlHandle(hostElementJsReference));
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
a = new Attachment(attachmenetReference, child);
@ -62,7 +54,7 @@ namespace Avalonia.Web.Blazor.Interop
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
@ -111,7 +103,7 @@ namespace Avalonia.Web.Blazor.Interop
}
else
{
_native.InvokeVoid(AttachToSymbol, host.hostElement);
_native.InvokeVoid(AttachToSymbol, host._hostElement);
}
_attachedTo = host;
}

45
src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs

@ -4,7 +4,7 @@ using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SKHtmlCanvasInterop : JSModuleInterop
internal class SKHtmlCanvasInterop : IDisposable
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js";
private const string InitGLSymbol = "SKHtmlCanvas.initGL";
@ -14,39 +14,32 @@ namespace Avalonia.Web.Blazor.Interop
private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize";
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
private readonly ElementReference htmlCanvas;
private readonly string htmlElementId;
private readonly ActionHelper callbackHelper;
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlCanvas;
private readonly string _htmlElementId;
private readonly ActionHelper _callbackHelper;
private DotNetObjectReference<ActionHelper>? callbackReference;
public static async Task<SKHtmlCanvasInterop> ImportAsync(IJSRuntime js, ElementReference element, Action callback)
public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback)
{
var interop = new SKHtmlCanvasInterop(js, element, callback);
await interop.ImportAsync();
return interop;
}
public SKHtmlCanvasInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback)
: base(js, JsFilename)
{
htmlCanvas = element;
htmlElementId = element.Id;
_module = module;
_htmlCanvas = element;
_htmlElementId = element.Id;
callbackHelper = new ActionHelper(renderFrameCallback);
_callbackHelper = new ActionHelper(renderFrameCallback);
}
protected override void OnDisposingModule() =>
Deinit();
public void Dispose() => Deinit();
public GLInfo InitGL()
{
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference);
return _module.Invoke<GLInfo>(InitGLSymbol, _htmlCanvas, _htmlElementId, callbackReference);
}
public bool InitRaster()
@ -54,9 +47,9 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference);
return _module.Invoke<bool>(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference);
}
public void Deinit()
@ -64,19 +57,19 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference == null)
return;
Invoke(DeinitSymbol, htmlElementId);
_module.Invoke(DeinitSymbol, _htmlElementId);
callbackReference?.Dispose();
}
public void RequestAnimationFrame(bool enableRenderLoop) =>
Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop);
_module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop);
public void SetCanvasSize(int rawWidth, int rawHeight) =>
Invoke(SetCanvasSizeSymbol, htmlCanvas, rawWidth, rawHeight);
_module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight);
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
_module.Invoke(PutImageDataSymbol, _htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
}

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

@ -1,50 +1,39 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SizeWatcherInterop : JSModuleInterop
internal class SizeWatcherInterop : IDisposable
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SizeWatcher.js";
private const string ObserveSymbol = "SizeWatcher.observe";
private const string UnobserveSymbol = "SizeWatcher.unobserve";
private readonly ElementReference htmlElement;
private readonly string htmlElementId;
private readonly FloatFloatActionHelper callbackHelper;
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlElement;
private readonly string _htmlElementId;
private readonly FloatFloatActionHelper _callbackHelper;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback)
public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action<SKSize> callback)
{
var interop = new SizeWatcherInterop(js, element, callback);
await interop.ImportAsync();
interop.Start();
return interop;
_module = module;
_htmlElement = element;
_htmlElementId = element.Id;
_callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
}
public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action<SKSize> callback)
: base(js, JsFilename)
{
htmlElement = element;
htmlElementId = element.Id;
callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
}
protected override void OnDisposingModule() =>
Stop();
public void Dispose() => Stop();
public void Start()
{
if (callbackReference != null)
return;
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
_module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference);
}
public void Stop()
@ -52,7 +41,7 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference == null)
return;
Invoke(UnobserveSymbol, htmlElementId);
_module.Invoke(UnobserveSymbol, _htmlElementId);
callbackReference?.Dispose();
callbackReference = null;

2
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@ -12,7 +12,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/StorageProvider.js";
private const string JsFilename = "./_content/Avalonia.Web.Blazor/avaloniaStorage.js";
private const string PickerCancelMessage = "The user aborted a request";
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js)

41
src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts

@ -1,41 +0,0 @@

export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback: DotNet.DotNetObjectReference;
public static getDpi() {
return window.devicePixelRatio;
}
public static start(callback: DotNet.DotNetObjectReference): number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop() {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId);
DpiWatcher.callback = undefined;
}
static update() {
if (!DpiWatcher.callback)
return;
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
}
}
}

23
src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts

@ -1,23 +0,0 @@

export class InputHelper {
public static clear (inputElement: HTMLInputElement){
inputElement.value = "";
}
public static focus (inputElement: HTMLInputElement){
inputElement.focus();
inputElement.setSelectionRange(0, 0);
}
public static setCursor (inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static hide (inputElement: HTMLInputElement){
inputElement.style.display = 'none';
}
public static show (inputElement: HTMLInputElement){
inputElement.style.display = 'block';
}
}

261
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts

@ -1,261 +0,0 @@
// aliases for emscripten
declare let GL: any;
declare let GLctx: WebGLRenderingContext;
declare let Module: EmscriptenModule;
// container for gl info
type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
// alias for a potential skia html canvas
type SKHtmlCanvasElement = {
SKHtmlCanvas: SKHtmlCanvas
} & HTMLCanvasElement
export class SKHtmlCanvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObjectReference;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth: number;
newHeight: number;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
var view = SKHtmlCanvas.init(true, element, elementId, callback);
if (!view || !view.glInfo)
return null;
return view.glInfo;
}
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean {
var view = SKHtmlCanvas.init(false, element, elementId, callback);
if (!view)
return false;
return true;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas {
var htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}
if (!SKHtmlCanvas.elements)
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
SKHtmlCanvas.elements[elementId] = element;
const view = new SKHtmlCanvas(useGL, element, callback);
htmlCanvas.SKHtmlCanvas = view;
return view;
}
public static deinit(elementId: string) {
if (!elementId)
return;
const element = SKHtmlCanvas.elements[elementId];
SKHtmlCanvas.elements.delete(elementId);
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.deinit();
htmlCanvas.SKHtmlCanvas = undefined;
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number)
{
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
}
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
}
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
}
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) {
this.htmlCanvas = element;
this.renderFrameCallback = callback;
if (useGL) {
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return null;
}
// make current
GL.makeContextCurrent(ctx);
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.glInfo = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};
}
}
public deinit() {
this.setEnableRenderLoop(false);
}
public setCanvasSize(width: number, height: number)
{
this.newWidth = width;
this.newHeight = height;
if(this.htmlCanvas.width != this.newWidth)
{
this.htmlCanvas.width = this.newWidth;
}
if(this.htmlCanvas.height != this.newHeight)
{
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public requestAnimationFrame(renderLoop?: boolean) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if(this.htmlCanvas.width != this.newWidth)
{
this.htmlCanvas.width = this.newWidth;
}
if(this.htmlCanvas.height != this.newHeight)
{
this.htmlCanvas.height = this.newHeight;
}
this.renderFrameCallback.invokeMethod('Invoke');
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
}
public setEnableRenderLoop(enable: boolean) {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}
public putImageData(pData: number, width: number, height: number): boolean {
if (this.glInfo || !pData || width <= 0 || width <= 0)
return false;
var ctx = this.htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}
// make sure the canvas is scaled correctly for the drawing
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);
return true;
}
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 0,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1,
};
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}
return ctx;
}
}

68
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts

@ -1,68 +0,0 @@

type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
type SizeWatcherInstance = {
callback: DotNet.DotNetObjectReference;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) {
if (!element || !callback)
return;
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback: callback
};
SizeWatcher.elements[elementId] = element;
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
return;
//console.info('Removing size watcher observation...');
const element = SizeWatcher.elements[elementId];
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
static init() {
if (SizeWatcher.observer)
return;
//console.info('Starting size watcher...');
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element) {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
return;
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
}
}

7
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts

@ -1,7 +0,0 @@
declare namespace DotNet {
interface DotNetObjectReference extends DotNet.DotNetObject {
_id: number;
dispose();
}
}

326
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts

@ -1,326 +0,0 @@
// Type definitions for Emscripten 1.39.16
// Project: https://emscripten.org
// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
// Periklis Tsirakidis <https://github.com/periklis>
// Bumsik Kim <https://github.com/kbumsik>
// Louis DeScioli <https://github.com/lourd>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.2
/** Other WebAssembly declarations, for compatibility with older versions of Typescript */
declare namespace WebAssembly {
interface Module {}
}
declare namespace Emscripten {
interface FileSystemType {}
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
type JSType = 'number' | 'string' | 'array' | 'boolean';
type TypeCompatibleWithC = number | string | any[] | boolean;
type CIntType = 'i8' | 'i16' | 'i32' | 'i64';
type CFloatType = 'float' | 'double';
type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*';
type CType = CIntType | CFloatType | CPointerType;
type WebAssemblyImports = Array<{
name: string;
kind: string;
}>;
type WebAssemblyExports = Array<{
module: string;
name: string;
kind: string;
}>;
interface CCallOpts {
async?: boolean | undefined;
}
}
interface EmscriptenModule {
print(str: string): void;
printErr(str: string): void;
arguments: string[];
environment: Emscripten.EnvironmentType;
preInit: Array<{ (): void }>;
preRun: Array<{ (): void }>;
postRun: Array<{ (): void }>;
onAbort: { (what: any): void };
onRuntimeInitialized: { (): void };
preinitializedWebGLContext: WebGLRenderingContext;
noInitialRun: boolean;
noExitRuntime: boolean;
logReadFiles: boolean;
filePackagePrefixURL: string;
wasmBinary: ArrayBuffer;
destroy(object: object): void;
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
instantiateWasm(
imports: Emscripten.WebAssemblyImports,
successCallback: (module: WebAssembly.Module) => void,
): Emscripten.WebAssemblyExports;
locateFile(url: string, scriptDirectory: string): string;
onCustomMessage(event: MessageEvent): void;
// USE_TYPED_ARRAYS == 1
HEAP: Int32Array;
IHEAP: Int32Array;
FHEAP: Float64Array;
// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;
TOTAL_STACK: number;
TOTAL_MEMORY: number;
FAST_MEMORY: number;
addOnPreRun(cb: () => any): void;
addOnInit(cb: () => any): void;
addOnPreMain(cb: () => any): void;
addOnExit(cb: () => any): void;
addOnPostRun(cb: () => any): void;
preloadedImages: any;
preloadedAudios: any;
_malloc(size: number): number;
_free(ptr: number): void;
}
/**
* A factory function is generated when setting the `MODULARIZE` build option
* to `1` in your Emscripten build. It return a Promise that resolves to an
* initialized, ready-to-call `EmscriptenModule` instance.
*
* By default, the factory function will be named `Module`. It's recommended to
* use the `EXPORT_ES6` option, in which the factory function will be the
* default export. If used without `EXPORT_ES6`, the factory function will be a
* global variable. You can rename the variable using the `EXPORT_NAME` build
* option. It's left to you to declare any global variables as needed in your
* application's types.
* @param moduleOverrides Default properties for the initialized module.
*/
type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = (
moduleOverrides?: Partial<T>,
) => Promise<T>;
declare namespace FS {
interface Lookup {
path: string;
node: FSNode;
}
interface FSStream {}
interface FSNode {}
interface ErrnoError {}
let ignorePermissions: boolean;
let trackingDelegate: any;
let tracking: any;
let genericErrors: any;
//
// paths
//
function lookupPath(path: string, opts: any): Lookup;
function getPath(node: FSNode): string;
//
// nodes
//
function isFile(mode: number): boolean;
function isDir(mode: number): boolean;
function isLink(mode: number): boolean;
function isChrdev(mode: number): boolean;
function isBlkdev(mode: number): boolean;
function isFIFO(mode: number): boolean;
function isSocket(mode: number): boolean;
//
// devices
//
function major(dev: number): number;
function minor(dev: number): number;
function makedev(ma: number, mi: number): number;
function registerDevice(dev: number, ops: any): void;
//
// core
//
function syncfs(populate: boolean, callback: (e: any) => any): void;
function syncfs(callback: (e: any) => any, populate?: boolean): void;
function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
function unmount(mountpoint: string): void;
function mkdir(path: string, mode?: number): any;
function mkdev(path: string, mode?: number, dev?: number): any;
function symlink(oldpath: string, newpath: string): any;
function rename(old_path: string, new_path: string): void;
function rmdir(path: string): void;
function readdir(path: string): any;
function unlink(path: string): void;
function readlink(path: string): string;
function stat(path: string, dontFollow?: boolean): any;
function lstat(path: string): any;
function chmod(path: string, mode: number, dontFollow?: boolean): void;
function lchmod(path: string, mode: number): void;
function fchmod(fd: number, mode: number): void;
function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
function lchown(path: string, uid: number, gid: number): void;
function fchown(fd: number, uid: number, gid: number): void;
function truncate(path: string, len: number): void;
function ftruncate(fd: number, len: number): void;
function utime(path: string, atime: number, mtime: number): void;
function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream;
function close(stream: FSStream): void;
function llseek(stream: FSStream, offset: number, whence: number): any;
function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
function write(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position?: number,
canOwn?: boolean,
): number;
function allocate(stream: FSStream, offset: number, length: number): void;
function mmap(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position: number,
prot: number,
flags: number,
): any;
function ioctl(stream: FSStream, cmd: any, arg: any): any;
function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array;
function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string;
function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array;
function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void;
//
// module-level FS code
//
function cwd(): string;
function chdir(path: string): void;
function init(
input: null | (() => number | null),
output: null | ((c: number) => any),
error: null | ((c: number) => any),
): void;
function createLazyFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
): FSNode;
function createPreloadedFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
onload?: () => void,
onerror?: () => void,
dontCreateFile?: boolean,
canOwn?: boolean,
): void;
function createDataFile(
parent: string | FSNode,
name: string,
data: ArrayBufferView,
canRead: boolean,
canWrite: boolean,
canOwn: boolean,
): FSNode;
}
declare var MEMFS: Emscripten.FileSystemType;
declare var NODEFS: Emscripten.FileSystemType;
declare var IDBFS: Emscripten.FileSystemType;
// Below runtime function/variable declarations are exportable by
// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
// EmscriptenModule interface to add runtime functions.
//
// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
// You can access ccall() via Module["ccall"]. In this case, you should
// extend EmscriptenModule to pass the compiler check like the following:
//
// interface YourOwnEmscriptenModule extends EmscriptenModule {
// ccall: typeof ccall;
// }
//
// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
declare function ccall(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
args: Emscripten.TypeCompatibleWithC[],
opts?: Emscripten.CCallOpts,
): any;
declare function cwrap(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
opts?: Emscripten.CCallOpts,
): (...args: any[]) => any;
declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void;
declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number;
declare function allocate(
slab: number[] | ArrayBufferView | number,
types: Emscripten.CType | Emscripten.CType[],
allocator: number,
ptr?: number,
): number;
declare function stackAlloc(size: number): number;
declare function stackSave(): number;
declare function stackRestore(ptr: number): void;
declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string;
declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF8(str: string): number;
declare function allocateUTF8(str: string): number;
declare function allocateUTF8OnStack(str: string): number;
declare function UTF16ToString(ptr: number): string;
declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF16(str: string): number;
declare function UTF32ToString(ptr: number): string;
declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF32(str: string): number;
declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
declare function intArrayToString(array: number[]): string;
declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function writeArrayToMemory(array: number[], buffer: number): void;
declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function addRunDependency(id: any): void;
declare function removeRunDependency(id: any): void;
declare function addFunction(func: (...args: any[]) => any, signature?: string): number;
declare function removeFunction(funcPtr: number): void;
declare var ALLOC_NORMAL: number;
declare var ALLOC_STACK: number;
declare var ALLOC_STATIC: number;
declare var ALLOC_DYNAMIC: number;
declare var ALLOC_NONE: number;

3
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@ -7,6 +7,7 @@ using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Web.Blazor.Interop;
using SkiaSharp;
@ -146,7 +147,7 @@ namespace Avalonia.Web.Blazor
public IRenderer CreateRenderer(IRenderRoot root)
{
var loop = AvaloniaLocator.Current.GetRequiredService<IRenderLoop>();
return new DeferredRenderer(root, loop);
return new CompositingRenderer(root, new Compositor(loop, null));
}
public void Invalidate(Rect rect)

14
src/Web/Avalonia.Web.Blazor/tsconfig.json

@ -1,14 +0,0 @@
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "ES2020",
"module": "ES2020",
"outDir": "wwwroot"
},
"exclude": [
"node_modules"
]
}

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

@ -0,0 +1,5 @@
export { DpiWatcher } from "./DpiWatcher"
export { InputHelper } from "./InputHelper"
export { NativeControlHost } from "./NativeControlHost"
export { SizeWatcher } from "./SizeWatcher"
export { SKHtmlCanvas } from "./SKHtmlCanvas"

40
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts

@ -0,0 +1,40 @@
export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback?: DotNet.DotNetObject;
public static getDpi() {
return window.devicePixelRatio;
}
public static start(callback: DotNet.DotNetObject): number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop() {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId);
DpiWatcher.callback = undefined;
}
static update() {
if (!DpiWatcher.callback)
return;
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
}
}
}

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

@ -0,0 +1,31 @@
export class InputHelper {
public static clear(inputElement: HTMLInputElement) {
inputElement.value = "";
}
public static isInputElement( element : HTMLInputElement | HTMLElement ) : element is HTMLInputElement {
return ( element as HTMLInputElement).setSelectionRange !== undefined;
}
public static focus(inputElement: HTMLElement) {
inputElement.focus();
if(this.isInputElement(inputElement))
{
(inputElement as HTMLInputElement).setSelectionRange(0,0);
}
}
public static setCursor(inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static hide(inputElement: HTMLInputElement) {
inputElement.style.display = 'none';
}
public static show(inputElement: HTMLInputElement) {
inputElement.style.display = 'block';
}
}

35
src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts → src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts

@ -14,10 +14,9 @@
}
}
class NativeControlHostTopLevelAttachment
{
_child: HTMLElement;
_host: HTMLElement;
class NativeControlHostTopLevelAttachment {
_child?: HTMLElement;
_host?: HTMLElement;
InitializeWithChildHandle(child: HTMLElement) {
this._child = child;
@ -25,32 +24,38 @@ class NativeControlHostTopLevelAttachment
}
AttachTo(host: HTMLElement): void {
if (this._host) {
if (this._host && this._child) {
this._host.removeChild(this._child);
}
this._host = host;
if (this._host) {
if (this._host && this._child) {
this._host.appendChild(this._child);
}
}
ShowInBounds(x: number, y: number, width: number, height: number): void {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
if (this._child) {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
}
}
HideWithSize(width: number, height: number): void {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
if (this._child) {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
}
}
ReleaseChild(): void {
this._child = null;
if (this._child) {
this._child = undefined;
}
}
}

255
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts

@ -0,0 +1,255 @@
// aliases for emscripten
declare let GL: any;
declare let GLctx: WebGLRenderingContext;
declare let Module: EmscriptenModule;
// container for gl info
type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
// alias for a potential skia html canvas
type SKHtmlCanvasElement = {
SKHtmlCanvas: SKHtmlCanvas | undefined
} & HTMLCanvasElement
export class SKHtmlCanvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo?: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObject;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth?: number;
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null {
var view = SKHtmlCanvas.init(true, element, elementId, callback);
if (!view || !view.glInfo)
return null;
return view.glInfo;
}
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean {
var view = SKHtmlCanvas.init(false, element, elementId, callback);
if (!view)
return false;
return true;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null {
var htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}
if (!SKHtmlCanvas.elements)
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
SKHtmlCanvas.elements.set(elementId, element);
const view = new SKHtmlCanvas(useGL, element, callback);
htmlCanvas.SKHtmlCanvas = view;
return view;
}
public static deinit(elementId: string) {
if (!elementId)
return;
const element = SKHtmlCanvas.elements.get(elementId);
SKHtmlCanvas.elements.delete(elementId);
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.deinit();
htmlCanvas.SKHtmlCanvas = undefined;
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
}
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
}
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
}
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) {
this.htmlCanvas = element;
this.renderFrameCallback = callback;
if (useGL) {
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return;
}
// make current
GL.makeContextCurrent(ctx);
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.glInfo = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};
}
}
public deinit() {
this.setEnableRenderLoop(false);
}
public setCanvasSize(width: number, height: number) {
this.newWidth = width;
this.newHeight = height;
if (this.htmlCanvas.width != this.newWidth) {
this.htmlCanvas.width = this.newWidth;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public requestAnimationFrame(renderLoop?: boolean) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if (this.htmlCanvas.width != this.newWidth) {
this.htmlCanvas.width = this.newWidth || 0;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight || 0;
}
this.renderFrameCallback.invokeMethod('Invoke');
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
}
public setEnableRenderLoop(enable: boolean) {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}
public putImageData(pData: number, width: number, height: number): boolean {
if (this.glInfo || !pData || width <= 0 || width <= 0)
return false;
var ctx = this.htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}
// make sure the canvas is scaled correctly for the drawing
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);
return true;
}
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 0,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1,
};
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}
return ctx;
}
}

67
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts

@ -0,0 +1,67 @@
type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
type SizeWatcherInstance = {
callback: DotNet.DotNetObject;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) {
if (!element || !callback)
return;
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback: callback
};
SizeWatcher.elements.set(elementId, element);
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
return;
//console.info('Removing size watcher observation...');
const element = SizeWatcher.elements.get(elementId)!;
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
static init() {
if (SizeWatcher.observer)
return;
//console.info('Starting size watcher...');
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element) {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
return;
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
}
}

79
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts

@ -0,0 +1,79 @@
class InnerDbConnection {
constructor(private database: IDBDatabase) { }
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
const tx = this.database.transaction(store, mode);
return tx.objectStore(store);
}
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.put(obj, key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public get(store: string, key: IDBValidKey): any {
const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => {
const response = os.get(key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public delete(store: string, key: IDBValidKey): Promise<void> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.delete(key);
response.onsuccess = () => {
resolve();
};
response.onerror = () => {
reject(response.error);
};
});
}
public close() {
this.database.close();
}
}
export class IndexedDbWrapper {
constructor(private databaseName: string, private objectStores: [string]) {
}
public connect(): Promise<InnerDbConnection> {
const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => {
const db = (<IDBRequest<IDBDatabase>>event.target).result;
this.objectStores.forEach(store => {
db.createObjectStore(store);
});
};
return new Promise((resolve, reject) => {
conn.onsuccess = event => {
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
};
conn.onerror = event => {
reject((<IDBRequest<IDBDatabase>>event.target).error);
};
});
}
}

169
src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts → src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts

@ -1,142 +1,23 @@
// As we don't have proper package managing for Avalonia.Web project, declare types manually
declare global {
interface FileSystemWritableFileStream {
write(position: number, data: BufferSource | Blob | string): Promise<void>;
truncate(size: number): Promise<void>;
close(): Promise<void>;
}
type PermissionsMode = "read" | "readwrite";
interface FileSystemFileHandle {
name: string,
kind: "file" | "directory",
getFile(): Promise<File>;
createWritable(options?: { keepExistingData?: boolean }): Promise<FileSystemWritableFileStream>;
queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
import { IndexedDbWrapper } from "./IndexedDbWrapper";
entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>;
}
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemFileHandle;
interface FilePickerAcceptType {
description: string,
// mime -> ext[] array
accept: { [mime: string]: string | string[] }
}
interface FilePickerOptions {
types?: FilePickerAcceptType[],
excludeAcceptAllOption: boolean,
id?: string,
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemHandle;
interface OpenFilePickerOptions {
startIn?: StartInDirectory
}
interface OpenFilePickerOptions extends FilePickerOptions {
multiple: boolean
}
interface SaveFilePickerOptions extends FilePickerOptions {
suggestedName?: string
}
interface DirectoryPickerOptions {
id?: string,
interface SaveFilePickerOptions {
startIn?: StartInDirectory
}
interface Window {
showOpenFilePicker: (options: OpenFilePickerOptions) => Promise<FileSystemFileHandle[]>;
showSaveFilePicker: (options: SaveFilePickerOptions) => Promise<FileSystemFileHandle>;
showDirectoryPicker: (options: DirectoryPickerOptions) => Promise<FileSystemFileHandle>;
}
}
// TODO move to another file and use import
class IndexedDbWrapper {
constructor(private databaseName: string, private objectStores: [ string ]) {
}
public connect(): Promise<InnerDbConnection> {
const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => {
const db = (<IDBRequest<IDBDatabase>>event.target).result;
this.objectStores.forEach(store => {
db.createObjectStore(store);
});
}
return new Promise((resolve, reject) => {
conn.onsuccess = event => {
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
}
conn.onerror = event => {
reject((<IDBRequest<IDBDatabase>>event.target).error);
}
});
}
}
class InnerDbConnection {
constructor(private database: IDBDatabase) { }
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
const tx = this.database.transaction(store, mode);
return tx.objectStore(store);
}
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.put(obj, key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public get(store: string, key: IDBValidKey): any {
const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => {
const response = os.get(key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public delete(store: string, key: IDBValidKey): Promise<void> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.delete(key);
response.onsuccess = () => {
resolve();
};
response.onerror = () => {
reject(response.error);
};
});
}
public close() {
this.database.close();
}
}
const fileBookmarksStore: string = "fileBookmarks";
const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
fileBookmarksStore
])
]);
class StorageItem {
constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { }
constructor(public handle: FileSystemHandle, private bookmarkId?: string) { }
public getName(): string {
return this.handle.name
@ -147,21 +28,35 @@ class StorageItem {
}
public async openRead(): Promise<Blob> {
if (!(this.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await this.verityPermissions('read');
return await this.handle.getFile();
const file = await this.handle.getFile();
return file;
}
public async openWrite(): Promise<FileSystemWritableFileStream> {
if (!(this.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await this.verityPermissions('readwrite');
return await this.handle.createWritable({ keepExistingData: true });
}
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> {
const file = this.handle.getFile && await this.handle.getFile();
return file && {
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> {
const file = this.handle instanceof FileSystemFileHandle
&& await this.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
@ -174,13 +69,13 @@ class StorageItem {
}
const items: StorageItem[] = [];
for await (const [key, value] of this.handle.entries()) {
for await (const [key, value] of (this.handle as any).entries()) {
items.push(new StorageItem(value));
}
return new StorageItems(items);
}
private async verityPermissions(mode: PermissionsMode): Promise<void | never> {
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> {
if (await this.handle.queryPermission({ mode }) === 'granted') {
return;
}
@ -195,7 +90,7 @@ class StorageItem {
if (this.bookmarkId) {
return this.bookmarkId;
}
const connection = await avaloniaDb.connect();
try {
const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId());
@ -205,7 +100,7 @@ class StorageItem {
connection.close();
}
}
public async deleteBookmark(): Promise<void> {
if (!this.bookmarkId) {
return;
@ -277,7 +172,7 @@ export class StorageProvider {
};
const handles = await window.showOpenFilePicker(options);
return new StorageItems(handles.map(handle => new StorageItem(handle)));
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle)));
}
public static async saveFileDialog(

16
src/Web/Avalonia.Web.Blazor/webapp/package.json

@ -0,0 +1,16 @@
{
"name": "avalonia.web",
"scripts": {
"dist": "cross-env NODE_ENV=production webpack",
"build": "cross-env NODE_ENV=development webpack"
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"cross-env": "^7.0.3",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
}
}

18
src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES6",
"strict": true,
"sourceMap": true,
"outDir": "../wwwroot",
"removeComments": true,
"noEmitOnError": true,
"lib": [
"dom",
"ES6",
"esnext.asynciterable"
]
},
"exclude": [
"node_modules"
]
}

0
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts → src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts

40
src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js

@ -0,0 +1,40 @@
const path = require('path');
const prod = process.env.NODE_ENV == 'production';
module.exports = {
mode: prod ? "production" : "development",
devtool: 'source-map',
target: ["web", "es2020"],
entry: {
avalonia: './modules/Avalonia/Avalonia.ts',
avaloniaStorage: {
import: './modules/Storage/StorageProvider.ts',
dependOn: 'avalonia',
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../wwwroot'),
library: {
type: 'module',
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
optimization: {
minimize: false
},
experiments: {
outputModule: true,
}
};

5
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -10,6 +10,7 @@ using Avalonia.iOS.Storage;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using CoreAnimation;
using Foundation;
using ObjCRuntime;
@ -63,8 +64,8 @@ namespace Avalonia.iOS
// No-op
}
public IRenderer CreateRenderer(IRenderRoot root) => new DeferredRenderer(root,
AvaloniaLocator.Current.GetService<IRenderLoop>());
public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor);
public void Invalidate(Rect rect)
{

7
src/iOS/Avalonia.iOS/Platform.cs

@ -6,6 +6,7 @@ using Avalonia.Input.Platform;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
namespace Avalonia
{
@ -26,6 +27,8 @@ namespace Avalonia.iOS
{
public static EaglFeature GlFeature;
public static DisplayLinkTimer Timer;
internal static Compositor Compositor { get; private set; }
class PlatformSettings : IPlatformSettings
{
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickSize"/>
@ -57,6 +60,10 @@ namespace Avalonia.iOS
.Bind<IRenderTimer>().ToConstant(Timer)
.Bind<IPlatformThreadingInterface>().ToConstant(new PlatformThreadingInterface())
.Bind<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
}

64
src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs

@ -0,0 +1,64 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace DevAnalyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OnPropertyChangedOverrideAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AVADEV2001";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"Missing invoke base.OnPropertyChanged",
"Method '{0}' do not invoke base.{0}",
"Potential issue",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The OnPropertyChanged of the base class was not invoked in the override method declaration, which could lead to unwanted behavior.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
}
private static void AnalyzeMethod(SymbolAnalysisContext context)
{
if (context.Symbol is IMethodSymbol currentMethod
&& currentMethod.Name == "OnPropertyChanged"
&& currentMethod.OverriddenMethod is IMethodSymbol originalMethod)
{
var declaration = currentMethod.DeclaringSyntaxReferences.FirstOrDefault()
?.GetSyntax(context.CancellationToken);
if (declaration is not null && context.Compilation.GetSemanticModel(declaration!.SyntaxTree) is { } semanticModel)
{
if (declaration.SyntaxTree.TryGetRoot(out var root))
{
var baseInvocations = root.DescendantNodes().OfType<BaseExpressionSyntax>();
if (baseInvocations.Any())
{
foreach (var baseInvocation in baseInvocations)
{
var parent = baseInvocation.Parent;
var targetSymbol = semanticModel.GetSymbolInfo(parent, context.CancellationToken);
if (SymbolEqualityComparer.Default.Equals(targetSymbol.Symbol, originalMethod))
{
return;
}
}
}
context.ReportDiagnostic(Diagnostic.Create(Rule, currentMethod.Locations[0], currentMethod.Name));
}
}
}
}
}
}

7
src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs

@ -11,8 +11,9 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
{
var schema =
context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml"));
var configs = schema.Select((t, _) =>
(GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString())));
var configs = schema.Select((t, _) => t.GetText())
.Where(source => source is not null)
.Select((source, _) => (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(source!.ToString())));
context.RegisterSourceOutput(configs, (spc, config) =>
{
var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config);
@ -20,4 +21,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
});
}
}
}
}

30
src/tools/DevGenerators/CompositionGenerator/Extensions.cs

@ -7,49 +7,47 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
{
public static class Extensions
{
public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers)
public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[]? modifiers)
{
if (modifiers == null)
if (modifiers is null)
return cl;
return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
}
public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers)
public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[]? modifiers)
{
if (modifiers == null)
if (modifiers is null)
return cl;
return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
}
public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers)
public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[]? modifiers)
{
if (modifiers == null)
if (modifiers is null)
return cl;
return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
}
public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers)
public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[]? modifiers)
{
if (modifiers == null)
if (modifiers is null)
return cl;
return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
}
public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers)
public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[]? modifiers)
{
if (modifiers == null)
if (modifiers is null)
return cl;
return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
}
public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers)
public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[]? modifiers)
{
if (modifiers == null)
if (modifiers is null)
return cl;
return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
}
public static string WithLowerFirst(this string s)
{
@ -58,7 +56,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
return char.ToLowerInvariant(s[0]) + s.Substring(1);
}
public static ExpressionSyntax MemberAccess(params string[] identifiers)
public static ExpressionSyntax MemberAccess(params string[]? identifiers)
{
if (identifiers == null || identifiers.Length == 0)
throw new ArgumentException();
@ -96,4 +94,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
? s.Substring(prefix.Length)
: s;
}
}
}

4
src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs

@ -54,7 +54,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) =>
DeclareField(type, name, null, modifiers);
FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer,
FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax? initializer,
params SyntaxKind[] modifiers) =>
FieldDeclaration(
VariableDeclaration(ParseTypeName(type),
@ -63,4 +63,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
.WithSemicolonToken(Semicolon())
.WithModifiers(TokenList(modifiers.Select(x => Token(x))));
}
}
}

16
src/tools/DevGenerators/CompositionGenerator/Generator.cs

@ -37,8 +37,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject";
string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet";
string ServerName(string? c) => c != null ? ("Server" + c) : "ServerObject";
string ChangesName(string? c) => c != null ? (c + "Changes") : "ChangeSet";
string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields";
string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name;
string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst();
@ -298,10 +298,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
if(cl.Implements.Count > 0)
foreach (var impl in cl.Implements)
{
client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name))));
client = client.WithBaseList(client.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.Name))));
if (impl.ServerName != null)
server = server.WithBaseList(
server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
client = client.AddMembers(
ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;"));
@ -527,7 +527,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
body = body.AddStatements(
ParseStatement("return base.GetPropertyForAnimation(name);"));
var method = ((MethodDeclarationSyntax) ParseMemberDeclaration(
$"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}"))
$"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")!)
.WithBody(body);
return cl.AddMembers(method);
@ -540,7 +540,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
body = body.AddStatements(
ParseStatement("return base.GetCompositionProperty(name);"));
var method = ((MethodDeclarationSyntax)ParseMemberDeclaration(
$"public override CompositionProperty? GetCompositionProperty(string name){{}}"))
$"public override CompositionProperty? GetCompositionProperty(string name){{}}")!)
.WithBody(body);
return cl.AddMembers(method);
@ -559,11 +559,11 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
);
return cl.AddMembers(
((MethodDeclarationSyntax) ParseMemberDeclaration(
"internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}"))
"internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")!)
.WithBody(body));
}
}
}
}

1
src/tools/DevGenerators/DevGenerators.csproj

@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save