Browse Source

Merge branch 'master' into colorpicker-palette-binding-fix

pull/8829/head
Max Katz 4 years ago
committed by GitHub
parent
commit
d6c9932701
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 90
      nukebuild/Build.cs
  3. 4
      nukebuild/BuildParameters.cs
  4. 57
      nukebuild/DotNetConfigHelper.cs
  5. 34
      samples/ControlCatalog/Converter/HexConverter.cs
  6. 12
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  7. 11
      src/Avalonia.Base/Layout/LayoutHelper.cs
  8. 1
      src/Avalonia.Base/Media/DashStyle.cs
  9. 33
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  10. 4
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  11. 10
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  12. 2
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  13. 33
      src/Avalonia.Base/Rendering/DirtyVisuals.cs
  14. 15
      src/Avalonia.Controls/Documents/InlineUIContainer.cs
  15. 61
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  16. 26
      src/Avalonia.Controls/RichTextBlock.cs
  17. 2
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  18. 8
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  19. 6
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  20. 3
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  21. 9
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  22. 33
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  23. 11
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  24. 34
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  25. 18
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  26. 41
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  27. 31
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  28. 12
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  29. 32
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  30. 45
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  31. 41
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  32. 2
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  33. 41
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  34. 23
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
  35. 261
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  36. 68
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  37. 7
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  38. 326
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  39. 3
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  40. 14
      src/Web/Avalonia.Web.Blazor/tsconfig.json
  41. 5
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts
  42. 40
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts
  43. 31
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  44. 35
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts
  45. 255
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts
  46. 67
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts
  47. 79
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts
  48. 172
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
  49. 16
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  50. 18
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  51. 0
      src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
  52. 40
      src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js
  53. 5
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  54. 7
      src/iOS/Avalonia.iOS/Platform.cs
  55. 4
      src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
  56. 4
      src/tools/DevGenerators/GetProcAddressInitialization.cs
  57. 5
      tests/Avalonia.Base.UnitTests/Media/PenTests.cs
  58. 27
      tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
  59. 57
      tests/Avalonia.Controls.UnitTests/BorderTests.cs
  60. 50
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

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

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>

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>

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;
@ -235,6 +244,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
{
@ -251,6 +262,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>
@ -319,6 +341,7 @@ namespace Avalonia.Controls
MaximumProperty.Changed.Subscribe(OnMaximumChanged);
MinimumProperty.Changed.Subscribe(OnMinimumChanged);
TextProperty.Changed.Subscribe(OnTextChanged);
TextConverterProperty.Changed.Subscribe(OnTextConverterChanged);
ValueProperty.Changed.Subscribe(OnValueChanged);
}
@ -485,6 +508,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.
@ -612,6 +648,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"))
{
@ -788,6 +828,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>
@ -1012,6 +1067,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)

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;

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();

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>());
}

33
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>
@ -47,10 +32,24 @@
<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" />
</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);
@ -241,10 +245,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 =>
{
@ -252,11 +259,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");
@ -306,9 +313,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();
@ -348,9 +356,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();
}
}
@ -382,6 +390,12 @@ namespace Avalonia.Web.Blazor
}
}
private void HideIme()
{
_inputHelper?.Hide();
_containerHelper?.Focus();
}
public void SetClient(ITextInputMethodClient? client)
{
if (_inputHelper is null)
@ -402,7 +416,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);
};
});
}
}

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

@ -1,141 +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,
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
@ -146,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
@ -168,14 +64,18 @@ class StorageItem {
}
public async getItems(): Promise<StorageItems> {
if (this.handle.kind !== "directory"){
return new StorageItems([]);
}
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;
}
@ -190,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());
@ -200,7 +100,7 @@ class StorageItem {
connection.close();
}
}
public async deleteBookmark(): Promise<void> {
if (!this.bookmarkId) {
return;
@ -272,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>());
}

4
src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs

@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator
).Collect();
context.RegisterSourceOutput(all, static (context, methods) =>
{
foreach (var typeGroup in methods.GroupBy(f => f.ContainingType))
foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var classBuilder = new StringBuilder();
if (typeGroup.Key.ContainingNamespace != null)
@ -91,4 +91,4 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator
}
}
}

4
src/tools/DevGenerators/GetProcAddressInitialization.cs

@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator
var all = fieldsWithAttribute.Collect();
context.RegisterSourceOutput(all, static (context, methods) =>
{
foreach (var typeGroup in methods.GroupBy(f => f.ContainingType))
foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var nextContext = 0;
var contexts = new Dictionary<string, int>();
@ -335,4 +335,4 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator
return name.ToString();
}
}
}

5
tests/Avalonia.Base.UnitTests/Media/PenTests.cs

@ -53,7 +53,10 @@ namespace Avalonia.Base.UnitTests.Media
var raised = false;
target.Invalidated += (s, e) => raised = true;
dashes.Dashes.Add(0.3);
dashes.Dashes = new AvaloniaList<double>
{
0.3
};
Assert.True(raised);
}

27
tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs

@ -432,6 +432,33 @@ public class CompositorHitTestingTests : CompositorTestsBase
s.AssertHitTest(new Point(5, 10), null, targetRectangle);
}
}
[Fact]
public void HitTest_Filter_Should_Filter_Out_Children()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Border child, parent;
s.TopLevel.Content = parent = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = child = new Border
{
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
}
};
s.AssertHitTest(new Point(100, 100), null, child, parent);
s.AssertHitTest(new Point(100, 100), v => v != parent);
}
}
private IDisposable TestApplication()
{

57
tests/Avalonia.Controls.UnitTests/BorderTests.cs

@ -78,7 +78,7 @@ namespace Avalonia.Controls.UnitTests
}
};
var root = CreatedRoot(1.5, target);
var root = CreateRoot(1.5, target);
root.LayoutManager.ExecuteInitialLayoutPass();
@ -101,7 +101,7 @@ namespace Avalonia.Controls.UnitTests
}
};
var root = CreatedRoot(1.5, target);
var root = CreateRoot(1.5, target);
root.LayoutManager.ExecuteInitialLayoutPass();
@ -111,7 +111,58 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(104, 104), target.DesiredSize);
}
private static TestRoot CreatedRoot(
[Fact]
public void Measure_Arranges_Child_To_Rounded_BorderThickness()
{
Canvas child;
var target = new Border
{
BorderThickness = new Thickness(1),
Width = 82,
Height = 82,
Child = child = new Canvas(),
};
var root = CreateRoot(1.5, target);
root.LayoutManager.ExecuteInitialLayoutPass();
// - 1 pixel border thickness is rounded up to 1.3333; for both sides it is 2.6666
// - Size of 82 needs no rounding
// - Minus border thickness, space for child is 82 - 2.6666 = 79.3333
Assert.Equal(1.3333, child.Bounds.Left, 3);
Assert.Equal(1.3333, child.Bounds.Top, 3);
Assert.Equal(79.3333, child.Bounds.Width, 3);
Assert.Equal(79.3333, child.Bounds.Height, 3);
}
[Fact]
public void Measure_Arranges_Child_With_Rounded_Margin()
{
Border child;
var target = new Border
{
Width = 220,
Height = 220,
Child = child = new Border
{
Margin = new Thickness(0, 25, 25, 25),
},
};
var root = CreateRoot(1.5, target);
root.LayoutManager.ExecuteInitialLayoutPass();
// - 25 margin gets rounded up to 25.3333
// - Size of 220 needs no rounding
Assert.Equal(0, child.Bounds.Left, 3);
Assert.Equal(25.3333, child.Bounds.Top, 3);
Assert.Equal(194.6666, child.Bounds.Width, 3);
Assert.Equal(169.3333, child.Bounds.Height, 3);
}
private static TestRoot CreateRoot(
double scaling,
Control child,
Size? constraint = null)

50
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

@ -1,3 +1,5 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Data;
@ -5,12 +7,49 @@ using Avalonia.Diagnostics;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class ControlTemplateTests : XamlTestBase
{
[Fact]
public void StyledProperties_Should_Be_Set_In_The_ControlTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:controls=""using:Avalonia.Markup.Xaml.UnitTests.Xaml"">
<Button>
<Button.Template>
<ControlTemplate>
<controls:ListBoxHierachyLine>
<controls:ListBoxHierachyLine.LineDashStyle>
<DashStyle Dashes=""2,2"" Offset=""1"" />
</controls:ListBoxHierachyLine.LineDashStyle>
</controls:ListBoxHierachyLine>
</ControlTemplate>
</Button.Template>
</Button>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)window.Content;
window.ApplyTemplate();
button.ApplyTemplate();
var listBoxHierarchyLine = button.GetVisualChildren().ElementAt(0) as ListBoxHierachyLine;
Assert.Equal(1, listBoxHierarchyLine.LineDashStyle.Offset);
Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes.Count);
Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes[0]);
Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes[1]);
}
}
[Fact]
public void Inline_ControlTemplate_Styled_Values_Are_Set_With_Style_Priority()
{
@ -270,4 +309,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("Bar", bar.Name);
}
}
public class ListBoxHierachyLine : Panel
{
public static readonly StyledProperty<DashStyle> LineDashStyleProperty =
AvaloniaProperty.Register<ListBoxHierachyLine, DashStyle>(nameof(LineDashStyle));
public DashStyle LineDashStyle
{
get => GetValue(LineDashStyleProperty);
set => SetValue(LineDashStyleProperty, value);
}
}
}

Loading…
Cancel
Save