66 changed files with 3473 additions and 29 deletions
@ -0,0 +1,10 @@ |
|||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> |
|||
<Found Context="routeData"> |
|||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> |
|||
</Found> |
|||
<NotFound> |
|||
<LayoutView Layout="@typeof(MainLayout)"> |
|||
<p>Sorry, there's nothing at this address.</p> |
|||
</LayoutView> |
|||
</NotFound> |
|||
</Router> |
|||
@ -0,0 +1,16 @@ |
|||
using Avalonia.Blazor; |
|||
|
|||
namespace ControlCatalog.Web; |
|||
|
|||
public partial class App |
|||
{ |
|||
protected override void OnParametersSet() |
|||
{ |
|||
base.OnParametersSet(); |
|||
|
|||
using (AvaloniaBlazor.Lock()) |
|||
{ |
|||
BlazorSingleViewLifetimeExtensions.SetupWithBlazorSingleViewLifetime<ControlCatalog.App>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> |
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<!-- In debug, make builds faster by reducing optimizations --> |
|||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'"> |
|||
<WasmNativeStrip>false</WasmNativeStrip> |
|||
<EmccCompileOptimizationFlag>-O1</EmccCompileOptimizationFlag> |
|||
<RunAOTCompilation>false</RunAOTCompilation> |
|||
<Optimize>true</Optimize> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> |
|||
<Optimize>true</Optimize> |
|||
<WasmNativeStrip>true</WasmNativeStrip> |
|||
<EmccCompileOptimizationFlag>-O2</EmccCompileOptimizationFlag> |
|||
<RunAOTCompilation>false</RunAOTCompilation> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" /> |
|||
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.155" /> |
|||
<PackageReference Include="Avalonia" Version="0.10.11-rc.1" /> |
|||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.155" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<NativeFileReference Include="libHarfBuzzSharp.a" /> |
|||
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\libSkiaSharp.a" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Web\Avalonia.Blazor\Avalonia.Blazor.csproj" /> |
|||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,5 @@ |
|||
@page "/" |
|||
|
|||
@using Avalonia.Blazor |
|||
|
|||
<AvaloniaView /> |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using ControlCatalog.Web; |
|||
|
|||
public class Program |
|||
{ |
|||
public static async Task Main(string[] args) |
|||
{ |
|||
await CreateHostBuilder(args).Build().RunAsync(); |
|||
} |
|||
|
|||
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) |
|||
{ |
|||
var builder = WebAssemblyHostBuilder.CreateDefault(args); |
|||
|
|||
builder.RootComponents.Add<App>("#app"); |
|||
|
|||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,30 @@ |
|||
{ |
|||
"iisSettings": { |
|||
"windowsAuthentication": false, |
|||
"anonymousAuthentication": true, |
|||
"iisExpress": { |
|||
"applicationUrl": "http://localhost:13961", |
|||
"sslPort": 44319 |
|||
} |
|||
}, |
|||
"profiles": { |
|||
"ControlCatalog.Web - IIS Express": { |
|||
"commandName": "IISExpress", |
|||
"launchBrowser": true, |
|||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
}, |
|||
"ControlCatalog.Web": { |
|||
"commandName": "Project", |
|||
"dotnetRunMessages": "true", |
|||
"launchBrowser": true, |
|||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
|||
"applicationUrl": "https://localhost:5001;http://localhost:5000", |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
@inherits LayoutComponentBase |
|||
|
|||
<div class="page"> |
|||
<div class="main"> |
|||
@Body |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,70 @@ |
|||
.page { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.main { |
|||
flex: 1; |
|||
} |
|||
|
|||
.sidebar { |
|||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); |
|||
} |
|||
|
|||
.top-row { |
|||
background-color: #f7f7f7; |
|||
border-bottom: 1px solid #d6d5d5; |
|||
justify-content: flex-end; |
|||
height: 3.5rem; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.top-row ::deep a, .top-row .btn-link { |
|||
white-space: nowrap; |
|||
margin-left: 1.5rem; |
|||
} |
|||
|
|||
.top-row a:first-child { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
@media (max-width: 640.98px) { |
|||
.top-row:not(.auth) { |
|||
display: none; |
|||
} |
|||
|
|||
.top-row.auth { |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.top-row a, .top-row .btn-link { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
@media (min-width: 641px) { |
|||
.page { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.sidebar { |
|||
width: 250px; |
|||
height: 100vh; |
|||
position: sticky; |
|||
top: 0; |
|||
} |
|||
|
|||
.top-row { |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.main > div { |
|||
padding-left: 2rem !important; |
|||
padding-right: 1.5rem !important; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<div class="top-row pl-4 navbar navbar-dark"> |
|||
<a class="navbar-brand" href="">SkiaSharp on Blazor</a> |
|||
<button class="navbar-toggler" @onclick="ToggleNavMenu"> |
|||
<span class="navbar-toggler-icon"></span> |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> |
|||
<ul class="nav flex-column"> |
|||
<li class="nav-item px-3"> |
|||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> |
|||
<span class="oi oi-home" aria-hidden="true"></span> Raster Canvas |
|||
</NavLink> |
|||
</li> |
|||
<li class="nav-item px-3"> |
|||
<NavLink class="nav-link" href="gpu"> |
|||
<span class="oi oi-plus" aria-hidden="true"></span> GPU Canvas |
|||
</NavLink> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
|
|||
@code { |
|||
private bool collapseNavMenu = true; |
|||
|
|||
private string? NavMenuCssClass => |
|||
collapseNavMenu ? "collapse" : null; |
|||
|
|||
private void ToggleNavMenu() |
|||
{ |
|||
collapseNavMenu = !collapseNavMenu; |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
.navbar-toggler { |
|||
background-color: rgba(255, 255, 255, 0.1); |
|||
} |
|||
|
|||
.top-row { |
|||
height: 3.5rem; |
|||
background-color: rgba(0,0,0,0.4); |
|||
} |
|||
|
|||
.navbar-brand { |
|||
font-size: 1.1rem; |
|||
} |
|||
|
|||
.oi { |
|||
width: 2rem; |
|||
font-size: 1.1rem; |
|||
vertical-align: text-top; |
|||
top: -2px; |
|||
} |
|||
|
|||
.nav-item { |
|||
font-size: 0.9rem; |
|||
padding-bottom: 0.5rem; |
|||
} |
|||
|
|||
.nav-item:first-of-type { |
|||
padding-top: 1rem; |
|||
} |
|||
|
|||
.nav-item:last-of-type { |
|||
padding-bottom: 1rem; |
|||
} |
|||
|
|||
.nav-item ::deep a { |
|||
color: #d7d7d7; |
|||
border-radius: 4px; |
|||
height: 3rem; |
|||
display: flex; |
|||
align-items: center; |
|||
line-height: 3rem; |
|||
} |
|||
|
|||
.nav-item ::deep a.active { |
|||
background-color: rgba(255,255,255,0.25); |
|||
color: white; |
|||
} |
|||
|
|||
.nav-item ::deep a:hover { |
|||
background-color: rgba(255,255,255,0.1); |
|||
color: white; |
|||
} |
|||
|
|||
@media (min-width: 641px) { |
|||
.navbar-toggler { |
|||
display: none; |
|||
} |
|||
|
|||
.collapse { |
|||
/* Never collapse the sidebar for wide screens */ |
|||
display: block; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
@using System.Net.Http |
|||
@using System.Net.Http.Json |
|||
@using Microsoft.AspNetCore.Components.Forms |
|||
@using Microsoft.AspNetCore.Components.Routing |
|||
@using Microsoft.AspNetCore.Components.Web |
|||
@using Microsoft.AspNetCore.Components.Web.Virtualization |
|||
@using Microsoft.AspNetCore.Components.WebAssembly.Http |
|||
@using Microsoft.JSInterop |
|||
@using ControlCatalog.Web |
|||
@using ControlCatalog.Web.Shared |
|||
@using SkiaSharp |
|||
Binary file not shown.
@ -0,0 +1,91 @@ |
|||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); |
|||
|
|||
html, body { |
|||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; |
|||
margin: 0; |
|||
height: 100vh; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
a, .btn-link { |
|||
color: #0366d6; |
|||
} |
|||
|
|||
.btn-primary { |
|||
color: #fff; |
|||
background-color: #1b6ec2; |
|||
border-color: #1861ac; |
|||
} |
|||
|
|||
.content { |
|||
padding-top: 1.1rem; |
|||
} |
|||
|
|||
.valid.modified:not([type=checkbox]) { |
|||
outline: 1px solid #26b050; |
|||
} |
|||
|
|||
.invalid { |
|||
outline: 1px solid red; |
|||
} |
|||
|
|||
.validation-message { |
|||
color: red; |
|||
} |
|||
|
|||
#blazor-error-ui { |
|||
background: lightyellow; |
|||
bottom: 0; |
|||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); |
|||
display: none; |
|||
left: 0; |
|||
padding: 0.6rem 1.25rem 0.7rem 1.25rem; |
|||
position: fixed; |
|||
width: 100%; |
|||
z-index: 1000; |
|||
} |
|||
|
|||
#blazor-error-ui .dismiss { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
right: 0.75rem; |
|||
top: 0.5rem; |
|||
} |
|||
|
|||
.canvas-container { |
|||
opacity:1; |
|||
background-color:#ccc; |
|||
position:fixed; |
|||
width:100%; |
|||
height:100%; |
|||
top:0px; |
|||
left:0px; |
|||
z-index:500; |
|||
} |
|||
|
|||
canvas |
|||
{ |
|||
opacity:1; |
|||
background-color:#ccc; |
|||
position:fixed; |
|||
width:100%; |
|||
height:100%; |
|||
top:0px; |
|||
left:0px; |
|||
z-index:500; |
|||
} |
|||
|
|||
#app, .page { |
|||
height: 100%; |
|||
} |
|||
|
|||
.overlay{ |
|||
opacity:0.0; |
|||
background-color:#ccc; |
|||
position:fixed; |
|||
width:100vw; |
|||
height:100vh; |
|||
top:0px; |
|||
left:0px; |
|||
z-index:1000; |
|||
} |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@ -0,0 +1,43 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> |
|||
<title>Avalonia Sample</title> |
|||
<base href="/" /> |
|||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> |
|||
<link href="css/app.css" rel="stylesheet" /> |
|||
</head> |
|||
|
|||
<body> |
|||
<div id="app">Powered by Avalonia</div> |
|||
|
|||
<div id="blazor-error-ui"> |
|||
An unhandled error has occurred. |
|||
<a href="" class="reload">Reload</a> |
|||
<a class="dismiss">🗙</a> |
|||
</div> |
|||
<script src="js/app.js"></script> |
|||
<script src="_framework/blazor.webassembly.js"></script> |
|||
</body> |
|||
|
|||
<script> |
|||
clearInput = () => { |
|||
document.getElementById("inputBox").value = ""; |
|||
} |
|||
|
|||
focusInput = () => { |
|||
document.getElementById("inputBox").focus(); |
|||
} |
|||
|
|||
finishInput = () => { |
|||
document.getElementById("container").focus(); |
|||
}; |
|||
|
|||
setCursor = (kind) => { |
|||
document.getElementById("inputBox").style.cursor = kind; |
|||
}; |
|||
</script> |
|||
|
|||
</html> |
|||
@ -0,0 +1 @@ |
|||
|
|||
@ -0,0 +1,398 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
class AssetLoader : IAssetLoader |
|||
{ |
|||
private const string AvaloniaResourceName = "!AvaloniaResources"; |
|||
private static readonly Dictionary<string, AssemblyDescriptor> AssemblyNameCache |
|||
= new Dictionary<string, AssemblyDescriptor>(); |
|||
|
|||
private AssemblyDescriptor _defaultResmAssembly; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AssetLoader"/> class.
|
|||
/// </summary>
|
|||
/// <param name="assembly">
|
|||
/// The default assembly from which to load resm: assets for which no assembly is specified.
|
|||
/// </param>
|
|||
public AssetLoader(Assembly assembly = null) |
|||
{ |
|||
if (assembly == null) |
|||
assembly = Assembly.GetEntryAssembly(); |
|||
if (assembly != null) |
|||
_defaultResmAssembly = new AssemblyDescriptor(assembly); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the default assembly from which to load assets for which no assembly is specified.
|
|||
/// </summary>
|
|||
/// <param name="assembly">The default assembly.</param>
|
|||
public void SetDefaultAssembly(Assembly assembly) |
|||
{ |
|||
_defaultResmAssembly = new AssemblyDescriptor(assembly); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks if an asset with the specified URI exists.
|
|||
/// </summary>
|
|||
/// <param name="uri">The URI.</param>
|
|||
/// <param name="baseUri">
|
|||
/// A base URI to use if <paramref name="uri"/> is relative.
|
|||
/// </param>
|
|||
/// <returns>True if the asset could be found; otherwise false.</returns>
|
|||
public bool Exists(Uri uri, Uri baseUri = null) |
|||
{ |
|||
return GetAsset(uri, baseUri) != null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Opens the asset with the requested URI.
|
|||
/// </summary>
|
|||
/// <param name="uri">The URI.</param>
|
|||
/// <param name="baseUri">
|
|||
/// A base URI to use if <paramref name="uri"/> is relative.
|
|||
/// </param>
|
|||
/// <returns>A stream containing the asset contents.</returns>
|
|||
/// <exception cref="FileNotFoundException">
|
|||
/// The asset could not be found.
|
|||
/// </exception>
|
|||
public Stream Open(Uri uri, Uri baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; |
|||
|
|||
/// <summary>
|
|||
/// Opens the asset with the requested URI and returns the asset stream and the
|
|||
/// assembly containing the asset.
|
|||
/// </summary>
|
|||
/// <param name="uri">The URI.</param>
|
|||
/// <param name="baseUri">
|
|||
/// A base URI to use if <paramref name="uri"/> is relative.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The stream containing the resource contents together with the assembly.
|
|||
/// </returns>
|
|||
/// <exception cref="FileNotFoundException">
|
|||
/// The asset could not be found.
|
|||
/// </exception>
|
|||
public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri baseUri = null) |
|||
{ |
|||
var asset = GetAsset(uri, baseUri); |
|||
|
|||
if (asset == null) |
|||
{ |
|||
throw new FileNotFoundException($"The resource {uri} could not be found."); |
|||
} |
|||
|
|||
return (asset.GetStream(), asset.Assembly); |
|||
} |
|||
|
|||
public Assembly GetAssembly(Uri uri, Uri baseUri) |
|||
{ |
|||
if (!uri.IsAbsoluteUri && baseUri != null) |
|||
uri = new Uri(baseUri, uri); |
|||
return GetAssembly(uri).Assembly; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets all assets of a folder and subfolders that match specified uri.
|
|||
/// </summary>
|
|||
/// <param name="uri">The URI.</param>
|
|||
/// <param name="baseUri">Base URI that is used if <paramref name="uri"/> is relative.</param>
|
|||
/// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
|
|||
public IEnumerable<Uri> GetAssets(Uri uri, Uri baseUri) |
|||
{ |
|||
if (uri.IsAbsoluteUri && uri.Scheme == "resm") |
|||
{ |
|||
var assembly = GetAssembly(uri); |
|||
|
|||
return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath)) |
|||
.Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? |
|||
Enumerable.Empty<Uri>(); |
|||
} |
|||
|
|||
uri = EnsureAbsolute(uri, baseUri); |
|||
if (uri.Scheme == "avares") |
|||
{ |
|||
var (asm, path) = GetResAsmAndPath(uri); |
|||
if (asm == null) |
|||
{ |
|||
throw new ArgumentException( |
|||
"No default assembly, entry assembly or explicit assembly specified; " + |
|||
"don't know where to look up for the resource, try specifying assembly explicitly."); |
|||
} |
|||
|
|||
if (asm?.AvaloniaResources == null) |
|||
return Enumerable.Empty<Uri>(); |
|||
path = path.TrimEnd('/') + '/'; |
|||
return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) |
|||
.Select(x => new Uri($"avares://{asm.Name}{x.Key}")); |
|||
} |
|||
|
|||
return Enumerable.Empty<Uri>(); |
|||
} |
|||
|
|||
private Uri EnsureAbsolute(Uri uri, Uri baseUri) |
|||
{ |
|||
if (uri.IsAbsoluteUri) |
|||
return uri; |
|||
if(baseUri == null) |
|||
throw new ArgumentException($"Relative uri {uri} without base url"); |
|||
if (!baseUri.IsAbsoluteUri) |
|||
throw new ArgumentException($"Base uri {baseUri} is relative"); |
|||
if (baseUri.Scheme == "resm") |
|||
throw new ArgumentException( |
|||
$"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); |
|||
return new Uri(baseUri, uri); |
|||
} |
|||
|
|||
private IAssetDescriptor GetAsset(Uri uri, Uri baseUri) |
|||
{ |
|||
if (uri.IsAbsoluteUri && uri.Scheme == "resm") |
|||
{ |
|||
var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; |
|||
|
|||
if (asm == null) |
|||
{ |
|||
throw new ArgumentException( |
|||
"No default assembly, entry assembly or explicit assembly specified; " + |
|||
"don't know where to look up for the resource, try specifying assembly explicitly."); |
|||
} |
|||
|
|||
IAssetDescriptor rv; |
|||
|
|||
var resourceKey = uri.AbsolutePath; |
|||
asm.Resources.TryGetValue(resourceKey, out rv); |
|||
return rv; |
|||
} |
|||
|
|||
uri = EnsureAbsolute(uri, baseUri); |
|||
|
|||
if (uri.Scheme == "avares") |
|||
{ |
|||
var (asm, path) = GetResAsmAndPath(uri); |
|||
if (asm.AvaloniaResources == null) |
|||
return null; |
|||
asm.AvaloniaResources.TryGetValue(path, out var desc); |
|||
return desc; |
|||
} |
|||
|
|||
throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); |
|||
} |
|||
|
|||
private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) |
|||
{ |
|||
var asm = GetAssembly(uri.Authority); |
|||
return (asm, uri.AbsolutePath); |
|||
} |
|||
|
|||
private AssemblyDescriptor GetAssembly(Uri uri) |
|||
{ |
|||
if (uri != null) |
|||
{ |
|||
if (!uri.IsAbsoluteUri) |
|||
return null; |
|||
if (uri.Scheme == "avares") |
|||
return GetResAsmAndPath(uri).asm; |
|||
|
|||
if (uri.Scheme == "resm") |
|||
{ |
|||
var qs = ParseQueryString(uri); |
|||
string assemblyName; |
|||
|
|||
if (qs.TryGetValue("assembly", out assemblyName)) |
|||
{ |
|||
return GetAssembly(assemblyName); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private AssemblyDescriptor GetAssembly(string name) |
|||
{ |
|||
if (name == null) |
|||
throw new ArgumentNullException(nameof(name)); |
|||
|
|||
AssemblyDescriptor rv; |
|||
if (!AssemblyNameCache.TryGetValue(name, out rv)) |
|||
{ |
|||
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); |
|||
var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); |
|||
if (match != null) |
|||
{ |
|||
AssemblyNameCache[name] = rv = new AssemblyDescriptor(match); |
|||
} |
|||
else |
|||
{ |
|||
// iOS does not support loading assemblies dynamically!
|
|||
//
|
|||
#if __IOS__
|
|||
throw new InvalidOperationException( |
|||
$"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); |
|||
#else
|
|||
AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); |
|||
#endif
|
|||
} |
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
private Dictionary<string, string> ParseQueryString(Uri uri) |
|||
{ |
|||
return uri.Query.TrimStart('?') |
|||
.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) |
|||
.Select(p => p.Split('=')) |
|||
.ToDictionary(p => p[0], p => p[1]); |
|||
} |
|||
|
|||
private interface IAssetDescriptor |
|||
{ |
|||
Stream GetStream(); |
|||
Assembly Assembly { get; } |
|||
} |
|||
|
|||
private class AssemblyResourceDescriptor : IAssetDescriptor |
|||
{ |
|||
private readonly Assembly _asm; |
|||
private readonly string _name; |
|||
|
|||
public AssemblyResourceDescriptor(Assembly asm, string name) |
|||
{ |
|||
_asm = asm; |
|||
_name = name; |
|||
} |
|||
|
|||
public Stream GetStream() |
|||
{ |
|||
return _asm.GetManifestResourceStream(_name); |
|||
} |
|||
|
|||
public Assembly Assembly => _asm; |
|||
} |
|||
|
|||
private class AvaloniaResourceDescriptor : IAssetDescriptor |
|||
{ |
|||
private readonly int _offset; |
|||
private readonly int _length; |
|||
public Assembly Assembly { get; } |
|||
|
|||
public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) |
|||
{ |
|||
_offset = offset; |
|||
_length = length; |
|||
Assembly = asm; |
|||
} |
|||
|
|||
public Stream GetStream() |
|||
{ |
|||
return new SlicedStream(Assembly.GetManifestResourceStream(AvaloniaResourceName), _offset, _length); |
|||
} |
|||
} |
|||
|
|||
class SlicedStream : Stream |
|||
{ |
|||
private readonly Stream _baseStream; |
|||
private readonly int _from; |
|||
|
|||
public SlicedStream(Stream baseStream, int from, int length) |
|||
{ |
|||
Length = length; |
|||
_baseStream = baseStream; |
|||
_from = from; |
|||
_baseStream.Position = from; |
|||
} |
|||
public override void Flush() |
|||
{ |
|||
} |
|||
|
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); |
|||
} |
|||
|
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
if (origin == SeekOrigin.Begin) |
|||
Position = offset; |
|||
if (origin == SeekOrigin.End) |
|||
Position = _from + Length + offset; |
|||
if (origin == SeekOrigin.Current) |
|||
Position = Position + offset; |
|||
return Position; |
|||
} |
|||
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
|
|||
public override bool CanRead => true; |
|||
public override bool CanSeek => _baseStream.CanRead; |
|||
public override bool CanWrite => false; |
|||
public override long Length { get; } |
|||
public override long Position |
|||
{ |
|||
get => _baseStream.Position - _from; |
|||
set => _baseStream.Position = value + _from; |
|||
} |
|||
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (disposing) |
|||
_baseStream.Dispose(); |
|||
} |
|||
|
|||
public override void Close() => _baseStream.Close(); |
|||
} |
|||
|
|||
private class AssemblyDescriptor |
|||
{ |
|||
public AssemblyDescriptor(Assembly assembly) |
|||
{ |
|||
Assembly = assembly; |
|||
|
|||
if (assembly != null) |
|||
{ |
|||
Resources = assembly.GetManifestResourceNames() |
|||
.ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); |
|||
Name = assembly.GetName().Name; |
|||
using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) |
|||
{ |
|||
if (resources != null) |
|||
{ |
|||
Resources.Remove(AvaloniaResourceName); |
|||
|
|||
var indexLength = new BinaryReader(resources).ReadInt32(); |
|||
var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); |
|||
var baseOffset = indexLength + 4; |
|||
AvaloniaResources = index.ToDictionary(r => "/" + r.Path.TrimStart('/'), r => (IAssetDescriptor) |
|||
new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public Assembly Assembly { get; } |
|||
public Dictionary<string, IAssetDescriptor> Resources { get; } |
|||
public Dictionary<string, IAssetDescriptor> AvaloniaResources { get; } |
|||
public string Name { get; } |
|||
} |
|||
|
|||
public static void RegisterResUriParsers() |
|||
{ |
|||
if (!UriParser.IsKnownScheme("avares")) |
|||
UriParser.Register(new GenericUriParser( |
|||
GenericUriParserOptions.GenericAuthority | |
|||
GenericUriParserOptions.NoUserInfo | |
|||
GenericUriParserOptions.NoPort | |
|||
GenericUriParserOptions.NoQuery | |
|||
GenericUriParserOptions.NoFragment), "avares", -1); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<ImplicitUsings>enable</ImplicitUsings> |
|||
</PropertyGroup> |
|||
|
|||
|
|||
<ItemGroup> |
|||
<SupportedPlatform Include="browser" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" /> |
|||
<PackageReference Include="Avalonia" Version="0.10.11-rc.1" /> |
|||
<PackageReference Include="Avalonia.Skia" Version="0.10.11-rc.1" /> |
|||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.5.2" PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public static class AvaloniaBlazor |
|||
{ |
|||
public static IDisposable Lock() => BlazorWindowingPlatform.Lock(); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public class AvaloniaBlazorAppBuilder : AppBuilderBase<AvaloniaBlazorAppBuilder> |
|||
{ |
|||
public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices) |
|||
: base(platform, platformServices) |
|||
{ |
|||
} |
|||
|
|||
public AvaloniaBlazorAppBuilder() : base(BlazorRuntimePlatform.Instance, BlazorRuntimePlatform.RegisterServices) |
|||
{ |
|||
UseWindowingSubsystem(BlazorWindowingPlatform.Register); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
<div id="container" class="avalonia-container" tabindex="0" oncontextmenu="return false;" |
|||
onmousemove="@OnMouseMove" |
|||
onmousedown="@OnMouseDown" |
|||
onmouseup="@OnMouseUp" |
|||
onwheel="@OnWheel" |
|||
onkeydown="@OnKeyDown" |
|||
onkeyup="@OnKeyUp"> |
|||
|
|||
<canvas @ref="_htmlCanvas" @attributes="AdditionalAttributes"/> |
|||
|
|||
<input id="inputBox" |
|||
class="overlay" |
|||
type="text" |
|||
oninput="@OnInput"/> |
|||
</div> |
|||
@ -0,0 +1,277 @@ |
|||
using Avalonia.Blazor.Interop; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Controls.Embedding; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Input.TextInput; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.AspNetCore.Components.Web; |
|||
using Microsoft.JSInterop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public partial class AvaloniaView : ITextInputMethodImpl |
|||
{ |
|||
private readonly RazorViewTopLevelImpl _topLevelImpl; |
|||
private EmbeddableControlRoot _topLevel; |
|||
|
|||
// Interop
|
|||
private SKHtmlCanvasInterop _interop = null!; |
|||
private SizeWatcherInterop _sizeWatcher = null!; |
|||
private DpiWatcherInterop _dpiWatcher = null!; |
|||
private SKHtmlCanvasInterop.GLInfo _jsGlInfo = null!; |
|||
private ElementReference _htmlCanvas; |
|||
private double _dpi; |
|||
private SKSize _canvasSize; |
|||
|
|||
private GRContext? _context; |
|||
private GRGlInterface? _glInterface; |
|||
private const SKColorType ColorType = SKColorType.Rgba8888; |
|||
|
|||
private bool _initialised; |
|||
|
|||
[Inject] IJSRuntime Js { get; set; } = null!; |
|||
|
|||
public AvaloniaView() |
|||
{ |
|||
_topLevelImpl = new RazorViewTopLevelImpl(this); |
|||
|
|||
_topLevel = new EmbeddableControlRoot(_topLevelImpl); |
|||
|
|||
if (Application.Current.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) |
|||
{ |
|||
_topLevel.Content = lifetime.MainView; |
|||
}; |
|||
} |
|||
|
|||
void OnMouseMove(MouseEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawMouseEvent(RawPointerEventType.Move, new Point(e.ClientX, e.ClientY), |
|||
RawInputModifiers.None); |
|||
} |
|||
|
|||
void OnMouseUp(MouseEventArgs e) |
|||
{ |
|||
RawPointerEventType type = default; |
|||
|
|||
switch (e.Button) |
|||
{ |
|||
case 0: |
|||
type = RawPointerEventType.LeftButtonUp; |
|||
break; |
|||
|
|||
case 1: |
|||
type = RawPointerEventType.MiddleButtonUp; |
|||
break; |
|||
|
|||
case 2: |
|||
type = RawPointerEventType.RightButtonUp; |
|||
break; |
|||
} |
|||
|
|||
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e)); |
|||
} |
|||
|
|||
void OnMouseDown(MouseEventArgs e) |
|||
{ |
|||
RawPointerEventType type = default; |
|||
|
|||
switch (e.Button) |
|||
{ |
|||
case 0: |
|||
type = RawPointerEventType.LeftButtonDown; |
|||
break; |
|||
|
|||
case 1: |
|||
type = RawPointerEventType.MiddleButtonDown; |
|||
break; |
|||
|
|||
case 2: |
|||
type = RawPointerEventType.RightButtonDown; |
|||
break; |
|||
} |
|||
|
|||
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e)); |
|||
} |
|||
|
|||
void OnWheel(WheelEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawMouseWheelEvent(new Point(e.ClientX, e.ClientY), |
|||
new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e)); |
|||
} |
|||
|
|||
static RawInputModifiers GetModifiers(WheelEventArgs e) |
|||
{ |
|||
RawInputModifiers modifiers = RawInputModifiers.None; |
|||
|
|||
if (e.CtrlKey) modifiers |= RawInputModifiers.Control; |
|||
if (e.AltKey) modifiers |= RawInputModifiers.Alt; |
|||
if (e.ShiftKey) modifiers |= RawInputModifiers.Shift; |
|||
if (e.MetaKey) modifiers |= RawInputModifiers.Meta; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
static RawInputModifiers GetModifiers(MouseEventArgs e) |
|||
{ |
|||
RawInputModifiers modifiers = RawInputModifiers.None; |
|||
|
|||
if (e.CtrlKey) modifiers |= RawInputModifiers.Control; |
|||
if (e.AltKey) modifiers |= RawInputModifiers.Alt; |
|||
if (e.ShiftKey) modifiers |= RawInputModifiers.Shift; |
|||
if (e.MetaKey) modifiers |= RawInputModifiers.Meta; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
static RawInputModifiers GetModifiers(KeyboardEventArgs e) |
|||
{ |
|||
RawInputModifiers modifiers = RawInputModifiers.None; |
|||
|
|||
if (e.CtrlKey) modifiers |= RawInputModifiers.Control; |
|||
if (e.AltKey) modifiers |= RawInputModifiers.Alt; |
|||
if (e.ShiftKey) modifiers |= RawInputModifiers.Shift; |
|||
if (e.MetaKey) modifiers |= RawInputModifiers.Meta; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
void OnKeyDown(KeyboardEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Key, GetModifiers(e)); |
|||
} |
|||
|
|||
void OnKeyUp(KeyboardEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, GetModifiers(e)); |
|||
} |
|||
|
|||
void OnInput(ChangeEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawTextEvent(e.Value.ToString()); |
|||
|
|||
Js.InvokeVoidAsync("clearInput"); |
|||
} |
|||
|
|||
[Parameter(CaptureUnmatchedValues = true)] |
|||
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } |
|||
|
|||
|
|||
|
|||
protected override async Task OnAfterRenderAsync(bool firstRender) |
|||
{ |
|||
if (firstRender) |
|||
{ |
|||
Avalonia.Threading.Dispatcher.UIThread.Post(async () => |
|||
{ |
|||
await Js.InvokeVoidAsync("setCursor", "default"); |
|||
|
|||
Console.WriteLine("starting html canvas setup"); |
|||
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); |
|||
|
|||
Console.WriteLine("Interop created"); |
|||
_jsGlInfo = _interop.InitGL(); |
|||
|
|||
Console.WriteLine("jsglinfo created - init gl"); |
|||
|
|||
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); |
|||
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); |
|||
|
|||
Console.WriteLine("watchers created."); |
|||
|
|||
// create the SkiaSharp context
|
|||
if (_context == null) |
|||
{ |
|||
Console.WriteLine("create glcontext"); |
|||
_glInterface = GRGlInterface.Create(); |
|||
_context = GRContext.CreateGl(_glInterface); |
|||
|
|||
|
|||
// bump the default resource cache limit
|
|||
_context.SetResourceCacheLimit(256 * 1024 * 1024); |
|||
Console.WriteLine("glcontext created and resource limit set"); |
|||
} |
|||
|
|||
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, |
|||
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); |
|||
|
|||
_initialised = true; |
|||
|
|||
await Task.Delay(250); // without this we get some kind of initialisation error with gl
|
|||
_topLevel.Prepare(); |
|||
|
|||
_topLevel.Renderer.Start(); |
|||
Invalidate(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private void OnRenderFrame() |
|||
{ |
|||
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) |
|||
{ |
|||
Console.WriteLine("nothing to render"); |
|||
return; |
|||
} |
|||
|
|||
ManualTriggerRenderTimer.Instance.RaiseTick(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_dpiWatcher.Unsubscribe(OnDpiChanged); |
|||
_sizeWatcher.Dispose(); |
|||
_interop.Dispose(); |
|||
} |
|||
|
|||
private void OnDpiChanged(double newDpi) |
|||
{ |
|||
_dpi = newDpi; |
|||
|
|||
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
|||
|
|||
Invalidate(); |
|||
} |
|||
|
|||
private void OnSizeChanged(SKSize newSize) |
|||
{ |
|||
_canvasSize = newSize; |
|||
|
|||
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
|||
|
|||
Invalidate(); |
|||
} |
|||
|
|||
public void Invalidate() |
|||
{ |
|||
if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) |
|||
{ |
|||
Console.WriteLine("invalidate ignored"); |
|||
return; |
|||
} |
|||
|
|||
_interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
|||
} |
|||
|
|||
public void SetActive(bool active) |
|||
{ |
|||
Console.WriteLine("focus input box."); |
|||
Js.InvokeVoidAsync("clearInput"); |
|||
Js.InvokeVoidAsync("focusInput"); |
|||
} |
|||
|
|||
public void SetCursorRect(Rect rect) |
|||
{ |
|||
} |
|||
|
|||
public void SetOptions(TextInputOptionsQueryEventArgs options) |
|||
{ |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
Js.InvokeVoidAsync("clearInput"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
class BlazorRuntimePlatform : IRuntimePlatform |
|||
{ |
|||
public static IRuntimePlatform Instance = new BlazorRuntimePlatform(); |
|||
public IDisposable StartSystemTimer(TimeSpan interval, Action tick) |
|||
{ |
|||
return new Timer(_ => tick(), null, interval, interval); |
|||
} |
|||
|
|||
public RuntimePlatformInfo GetRuntimeInfo() |
|||
{ |
|||
return new RuntimePlatformInfo |
|||
{ |
|||
IsDesktop = false, |
|||
IsMobile = false, |
|||
IsMono = true, |
|||
IsUnix = false, |
|||
IsCoreClr = false, |
|||
IsDotNetFramework = false |
|||
}; |
|||
} |
|||
|
|||
class BasicBlob : IUnmanagedBlob |
|||
{ |
|||
private IntPtr _data; |
|||
public BasicBlob(int size) |
|||
{ |
|||
_data = Marshal.AllocHGlobal(size); |
|||
Size = size; |
|||
} |
|||
public void Dispose() |
|||
{ |
|||
if (_data != IntPtr.Zero) |
|||
Marshal.FreeHGlobal(_data); |
|||
_data = IntPtr.Zero; |
|||
} |
|||
|
|||
public IntPtr Address => _data; |
|||
public int Size { get; } |
|||
public bool IsDisposed => _data == IntPtr.Zero; |
|||
} |
|||
|
|||
public IUnmanagedBlob AllocBlob(int size) |
|||
{ |
|||
return new BasicBlob(size); |
|||
} |
|||
|
|||
public static void RegisterServices(AvaloniaBlazorAppBuilder builder) |
|||
{ |
|||
AssetLoader.RegisterResUriParsers(); |
|||
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>().ToConstant(Instance); |
|||
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new AssetLoader()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using Avalonia.Blazor; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
public Control MainView { get; set; } |
|||
} |
|||
|
|||
public static class BlazorSingleViewLifetimeExtensions |
|||
{ |
|||
|
|||
|
|||
public static AvaloniaBlazorAppBuilder SetupWithBlazorSingleViewLifetime<TApp>() |
|||
where TApp : Application, new() |
|||
{ |
|||
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>() |
|||
.UseSkia() |
|||
.With(new SkiaOptions() { CustomGpuFactory = () => new BlazorSkiaGpu() }) |
|||
.SetupWithLifetime(new BlazorSingleViewLifetime()); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia; |
|||
using Avalonia.Skia; |
|||
|
|||
namespace Avalonia.Blazor; |
|||
|
|||
public class BlazorSkiaGpu : ISkiaGpu |
|||
{ |
|||
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces) |
|||
{ |
|||
foreach (var surface in surfaces) |
|||
{ |
|||
if (surface is BlazorSkiaSurface blazorSkiaSurface) |
|||
{ |
|||
return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor; |
|||
|
|||
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession |
|||
{ |
|||
private readonly SKSurface _surface; |
|||
|
|||
|
|||
public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) |
|||
{ |
|||
_surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); |
|||
|
|||
GrContext = blazorSkiaSurface.Context; |
|||
|
|||
ScaleFactor = blazorSkiaSurface.Scaling; |
|||
|
|||
SurfaceOrigin = blazorSkiaSurface.Origin; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_surface.Flush(); |
|||
|
|||
GrContext.Flush(); |
|||
|
|||
_surface.Dispose(); |
|||
} |
|||
|
|||
public GRContext GrContext { get; } |
|||
|
|||
public SKSurface SkSurface => _surface; |
|||
|
|||
public double ScaleFactor { get; } |
|||
|
|||
public GRSurfaceOrigin SurfaceOrigin { get; } |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using Avalonia; |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor; |
|||
|
|||
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
|||
{ |
|||
private readonly GRBackendRenderTarget _renderTarget; |
|||
private readonly BlazorSkiaSurface _blazorSkiaSurface; |
|||
private readonly PixelSize _size; |
|||
|
|||
public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) |
|||
{ |
|||
_size = blazorSkiaSurface.Size; |
|||
|
|||
var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); |
|||
{ |
|||
_blazorSkiaSurface = blazorSkiaSurface; |
|||
_renderTarget = new GRBackendRenderTarget( |
|||
(int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), |
|||
(int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), |
|||
blazorSkiaSurface.GlInfo.Samples, |
|||
blazorSkiaSurface.GlInfo.Stencils, glFbInfo); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_renderTarget.Dispose(); |
|||
} |
|||
|
|||
public ISkiaGpuRenderSession BeginRenderingSession() |
|||
{ |
|||
return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); |
|||
} |
|||
|
|||
public bool IsCorrupted |
|||
{ |
|||
get |
|||
{ |
|||
var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height; |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Avalonia; |
|||
using Avalonia.Blazor.Interop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor; |
|||
|
|||
internal class BlazorSkiaSurface |
|||
{ |
|||
public SKColorType ColorType { get; set; } |
|||
|
|||
public PixelSize Size { get; set; } |
|||
|
|||
public GRContext Context { get; set; } |
|||
|
|||
public GRSurfaceOrigin Origin { get; set; } |
|||
|
|||
public double Scaling { get; set; } |
|||
|
|||
public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public class CustomFontManagerImpl : IFontManagerImpl |
|||
{ |
|||
private readonly Typeface[] _customTypefaces; |
|||
private readonly string _defaultFamilyName; |
|||
|
|||
private readonly Typeface _defaultTypeface = |
|||
new Typeface("avares://Avalonia.Blazor/Assets#Noto Mono"); |
|||
private readonly Typeface _italicTypeface = |
|||
new Typeface("avares://Avalonia.Blazor/Assets#Noto Sans"); |
|||
private readonly Typeface _emojiTypeface = |
|||
new Typeface("avares://Avalonia.Blazor/Assets#Twitter Color Emoji"); |
|||
|
|||
public CustomFontManagerImpl() |
|||
{ |
|||
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; |
|||
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; |
|||
} |
|||
|
|||
public string GetDefaultFontFamilyName() |
|||
{ |
|||
return _defaultFamilyName; |
|||
} |
|||
|
|||
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) |
|||
{ |
|||
return _customTypefaces.Select(x => x.FontFamily.Name); |
|||
} |
|||
|
|||
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; |
|||
|
|||
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, |
|||
CultureInfo culture, out Typeface typeface) |
|||
{ |
|||
foreach (var customTypeface in _customTypefaces) |
|||
{ |
|||
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
typeface = _defaultTypeface; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) |
|||
{ |
|||
SKTypeface skTypeface; |
|||
|
|||
switch (typeface.FontFamily.Name) |
|||
{ |
|||
case "Twitter Color Emoji": |
|||
{ |
|||
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily); |
|||
skTypeface = typefaceCollection.Get(typeface); |
|||
break; |
|||
} |
|||
case "Noto Sans": |
|||
{ |
|||
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); |
|||
skTypeface = typefaceCollection.Get(typeface); |
|||
break; |
|||
} |
|||
default: |
|||
{ |
|||
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); |
|||
skTypeface = typefaceCollection.Get(_defaultTypeface); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return new GlyphTypefaceImpl(skTypeface); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
// This class provides an example of how JavaScript functionality can be wrapped
|
|||
// in a .NET class for easy consumption. The associated JavaScript module is
|
|||
// loaded on demand when first needed.
|
|||
//
|
|||
// This class can be registered as scoped DI service and then injected into Blazor
|
|||
// components for use.
|
|||
|
|||
public class ExampleJsInterop : IAsyncDisposable |
|||
{ |
|||
private readonly Lazy<Task<IJSObjectReference>> moduleTask; |
|||
|
|||
public ExampleJsInterop(IJSRuntime jsRuntime) |
|||
{ |
|||
moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>( |
|||
"import", "./_content//exampleJsInterop.js").AsTask()); |
|||
} |
|||
|
|||
public async ValueTask<string> Prompt(string message) |
|||
{ |
|||
var module = await moduleTask.Value; |
|||
return await module.InvokeAsync<string>("showPrompt", message); |
|||
} |
|||
|
|||
public async ValueTask DisposeAsync() |
|||
{ |
|||
if (moduleTask.IsValueCreated) |
|||
{ |
|||
var module = await moduleTask.Value; |
|||
await module.DisposeAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Blazor.Interop |
|||
{ |
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class ActionHelper |
|||
{ |
|||
private readonly Action action; |
|||
|
|||
public ActionHelper(Action action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke() => action?.Invoke(); |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Blazor.Interop |
|||
{ |
|||
internal class DpiWatcherInterop : JSModuleInterop |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.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 DotNetObjectReference<FloatFloatActionHelper>? callbackReference; |
|||
|
|||
public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null) |
|||
{ |
|||
var interop = Get(js); |
|||
await interop.ImportAsync(); |
|||
if (callback != null) |
|||
interop.Subscribe(callback); |
|||
return interop; |
|||
} |
|||
|
|||
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)); |
|||
} |
|||
|
|||
protected override void OnDisposingModule() => |
|||
Stop(); |
|||
|
|||
public void Subscribe(Action<double> callback) |
|||
{ |
|||
var shouldStart = callbacksEvent == null; |
|||
|
|||
callbacksEvent += callback; |
|||
|
|||
var dpi = shouldStart |
|||
? Start() |
|||
: GetDpi(); |
|||
|
|||
callback(dpi); |
|||
} |
|||
|
|||
public void Unsubscribe(Action<double> callback) |
|||
{ |
|||
callbacksEvent -= callback; |
|||
|
|||
if (callbacksEvent == null) |
|||
Stop(); |
|||
} |
|||
|
|||
private double Start() |
|||
{ |
|||
if (callbackReference != null) |
|||
return GetDpi(); |
|||
|
|||
callbackReference = DotNetObjectReference.Create(callbackHelper); |
|||
|
|||
return Invoke<double>(StartSymbol, callbackReference); |
|||
} |
|||
|
|||
private void Stop() |
|||
{ |
|||
if (callbackReference == null) |
|||
return; |
|||
|
|||
Invoke(StopSymbol); |
|||
|
|||
callbackReference?.Dispose(); |
|||
callbackReference = null; |
|||
} |
|||
|
|||
public double GetDpi() => |
|||
Invoke<double>(GetDpiSymbol); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Blazor.Interop |
|||
{ |
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class FloatFloatActionHelper |
|||
{ |
|||
private readonly Action<float, float> action; |
|||
|
|||
public FloatFloatActionHelper(Action<float, float> action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke(float width, float height) => action?.Invoke(width, height); |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Blazor.Interop |
|||
{ |
|||
internal class JSModuleInterop : IDisposable |
|||
{ |
|||
private readonly Task<IJSUnmarshalledObjectReference> moduleTask; |
|||
private IJSUnmarshalledObjectReference? module; |
|||
|
|||
public JSModuleInterop(IJSRuntime js, string filename) |
|||
{ |
|||
if (js is not IJSInProcessRuntime) |
|||
throw new NotSupportedException("SkiaSharp currently only works on Web Assembly."); |
|||
|
|||
moduleTask = js.InvokeAsync<IJSUnmarshalledObjectReference>("import", filename).AsTask(); |
|||
} |
|||
|
|||
public async Task ImportAsync() |
|||
{ |
|||
module = await moduleTask; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
OnDisposingModule(); |
|||
Module.Dispose(); |
|||
} |
|||
|
|||
protected IJSUnmarshalledObjectReference Module => |
|||
module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first."); |
|||
|
|||
protected void Invoke(string identifier, params object?[]? args) => |
|||
Module.InvokeVoid(identifier, args); |
|||
|
|||
protected TValue Invoke<TValue>(string identifier, params object?[]? args) => |
|||
Module.Invoke<TValue>(identifier, args); |
|||
|
|||
protected virtual void OnDisposingModule() { } |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor.Interop |
|||
{ |
|||
internal class SKHtmlCanvasInterop : JSModuleInterop |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.Blazor/SKHtmlCanvas.js"; |
|||
private const string InitGLSymbol = "SKHtmlCanvas.initGL"; |
|||
private const string InitRasterSymbol = "SKHtmlCanvas.initRaster"; |
|||
private const string DeinitSymbol = "SKHtmlCanvas.deinit"; |
|||
private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame"; |
|||
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData"; |
|||
|
|||
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) |
|||
{ |
|||
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; |
|||
|
|||
callbackHelper = new ActionHelper(renderFrameCallback); |
|||
} |
|||
|
|||
protected override void OnDisposingModule() => |
|||
Deinit(); |
|||
|
|||
public GLInfo InitGL() |
|||
{ |
|||
if (callbackReference != null) |
|||
throw new InvalidOperationException("Unable to initialize the same canvas more than once."); |
|||
|
|||
callbackReference = DotNetObjectReference.Create(callbackHelper); |
|||
|
|||
return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference); |
|||
} |
|||
|
|||
public bool InitRaster() |
|||
{ |
|||
if (callbackReference != null) |
|||
throw new InvalidOperationException("Unable to initialize the same canvas more than once."); |
|||
|
|||
callbackReference = DotNetObjectReference.Create(callbackHelper); |
|||
|
|||
return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference); |
|||
} |
|||
|
|||
public void Deinit() |
|||
{ |
|||
if (callbackReference == null) |
|||
return; |
|||
|
|||
Invoke(DeinitSymbol, htmlElementId); |
|||
|
|||
callbackReference?.Dispose(); |
|||
} |
|||
|
|||
public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) => |
|||
Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight); |
|||
|
|||
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) => |
|||
Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); |
|||
|
|||
public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor.Interop |
|||
{ |
|||
internal class SizeWatcherInterop : JSModuleInterop |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.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 DotNetObjectReference<FloatFloatActionHelper>? callbackReference; |
|||
|
|||
public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback) |
|||
{ |
|||
var interop = new SizeWatcherInterop(js, element, callback); |
|||
await interop.ImportAsync(); |
|||
interop.Start(); |
|||
return interop; |
|||
} |
|||
|
|||
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 Start() |
|||
{ |
|||
if (callbackReference != null) |
|||
return; |
|||
|
|||
callbackReference = DotNetObjectReference.Create(callbackHelper); |
|||
|
|||
Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference); |
|||
} |
|||
|
|||
public void Stop() |
|||
{ |
|||
if (callbackReference == null) |
|||
return; |
|||
|
|||
Invoke(UnobserveSymbol, htmlElementId); |
|||
|
|||
callbackReference?.Dispose(); |
|||
callbackReference = null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Blazor; |
|||
|
|||
internal static class Keycodes |
|||
{ |
|||
public static Dictionary<string, Key> KeyCodes = new() |
|||
{ |
|||
{ "Escape", Key.Escape }, |
|||
{ "Digit1", Key.D1 }, |
|||
{ "Digit2", Key.D2 }, |
|||
{ "Digit3", Key.D3 }, |
|||
{ "Digit4", Key.D4 }, |
|||
{ "Digit5", Key.D5 }, |
|||
{ "Digit6", Key.D6 }, |
|||
{ "Digit7", Key.D7 }, |
|||
{ "Digit8", Key.D8 }, |
|||
{ "Digit9", Key.D9 }, |
|||
{ "Digit0", Key.D0 }, |
|||
{ "Minus", Key.OemMinus }, |
|||
//{ "Equal" , Key. },
|
|||
{ "Backspace", Key.Back }, |
|||
{ "Tab", Key.Tab }, |
|||
{ "KeyQ", Key.Q }, |
|||
{ "KeyW", Key.W }, |
|||
{ "KeyE", Key.E }, |
|||
{ "KeyR", Key.R }, |
|||
{ "KeyT", Key.T }, |
|||
{ "KeyY", Key.Y }, |
|||
{ "KeyU", Key.U }, |
|||
{ "KeyI", Key.I }, |
|||
{ "KeyO", Key.O }, |
|||
{ "KeyP", Key.P }, |
|||
{ "BracketLeft", Key.OemOpenBrackets }, |
|||
{ "BracketRight", Key.OemCloseBrackets }, |
|||
{ "Enter", Key.Enter }, |
|||
{ "ControlLeft", Key.LeftCtrl }, |
|||
{ "KeyA", Key.A }, |
|||
{ "KeyS", Key.S }, |
|||
{ "KeyD", Key.D }, |
|||
{ "KeyF", Key.F }, |
|||
{ "KeyG", Key.G }, |
|||
{ "KeyH", Key.H }, |
|||
{ "KeyJ", Key.J }, |
|||
{ "KeyK", Key.K }, |
|||
{ "KeyL", Key.L }, |
|||
{ "Semicolon", Key.OemSemicolon }, |
|||
{ "Quote", Key.OemQuotes }, |
|||
//{ "Backquote" , Key. },
|
|||
{ "ShiftLeft", Key.LeftShift }, |
|||
{ "Backslash", Key.OemBackslash }, |
|||
{ "KeyZ", Key.Z }, |
|||
{ "KeyX", Key.X }, |
|||
{ "KeyC", Key.C }, |
|||
{ "KeyV", Key.V }, |
|||
{ "KeyB", Key.B }, |
|||
{ "KeyN", Key.N }, |
|||
{ "KeyM", Key.M }, |
|||
{ "Comma", Key.OemComma }, |
|||
{ "Period", Key.OemPeriod }, |
|||
//{ "Slash" , Key. },
|
|||
{ "ShiftRight", Key.RightShift }, |
|||
{ "NumpadMultiply", Key.Multiply }, |
|||
{ "AltLeft", Key.LeftAlt }, |
|||
{ "Space", Key.Space }, |
|||
{ "CapsLock", Key.CapsLock }, |
|||
{ "F1", Key.F1 }, |
|||
{ "F2", Key.F2 }, |
|||
{ "F3", Key.F3 }, |
|||
{ "F4", Key.F4 }, |
|||
{ "F5", Key.F5 }, |
|||
{ "F6", Key.F6 }, |
|||
{ "F7", Key.F7 }, |
|||
{ "F8", Key.F8 }, |
|||
{ "F9", Key.F9 }, |
|||
{ "F10", Key.F10 }, |
|||
{ "NumLock", Key.NumLock }, |
|||
{ "ScrollLock", Key.Scroll }, |
|||
{ "Numpad7", Key.NumPad7 }, |
|||
{ "Numpad8", Key.NumPad8 }, |
|||
{ "Numpad9", Key.NumPad9 }, |
|||
{ "NumpadSubtract", Key.Subtract }, |
|||
{ "Numpad4", Key.NumPad4 }, |
|||
{ "Numpad5", Key.NumPad5 }, |
|||
{ "Numpad6", Key.NumPad6 }, |
|||
{ "NumpadAdd", Key.Add }, |
|||
{ "Numpad1", Key.NumPad1 }, |
|||
{ "Numpad2", Key.NumPad2 }, |
|||
{ "Numpad3", Key.NumPad3 }, |
|||
{ "Numpad0", Key.NumPad0 }, |
|||
{ "NumpadDecimal", Key.Decimal }, |
|||
{ "Unidentified", Key.NoName }, |
|||
//{ "IntlBackslash" , Key.bac },
|
|||
{ "F11", Key.F11 }, |
|||
{ "F12", Key.F12 }, |
|||
//{ "IntlRo" , Key.Ro },
|
|||
//{ "Unidentified" , Key. },
|
|||
{ "Convert", Key.ImeConvert }, |
|||
{ "KanaMode", Key.KanaMode }, |
|||
{ "NonConvert", Key.ImeNonConvert }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "NumpadEnter", Key.Enter }, |
|||
{ "ControlRight", Key.RightCtrl }, |
|||
{ "NumpadDivide", Key.Divide }, |
|||
{ "PrintScreen", Key.PrintScreen }, |
|||
{ "AltRight", Key.RightAlt }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "Home", Key.Home }, |
|||
{ "ArrowUp", Key.Up }, |
|||
{ "PageUp", Key.PageUp }, |
|||
{ "ArrowLeft", Key.Left }, |
|||
{ "ArrowRight", Key.Right }, |
|||
{ "End", Key.End }, |
|||
{ "ArrowDown", Key.Down }, |
|||
{ "PageDown", Key.PageDown }, |
|||
{ "Insert", Key.Insert }, |
|||
{ "Delete", Key.Delete }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "AudioVolumeMute", Key.VolumeMute }, |
|||
{ "AudioVolumeDown", Key.VolumeDown }, |
|||
{ "AudioVolumeUp", Key.VolumeUp }, |
|||
//{ "NumpadEqual" , Key. },
|
|||
{ "Pause", Key.Pause }, |
|||
{ "NumpadComma", Key.OemComma } |
|||
}; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using Avalonia.Rendering; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public class ManualTriggerRenderTimer : IRenderTimer |
|||
{ |
|||
private static readonly Stopwatch _sw = Stopwatch.StartNew(); |
|||
|
|||
public static ManualTriggerRenderTimer Instance { get; } = new ManualTriggerRenderTimer(); |
|||
|
|||
public void RaiseTick() => Tick?.Invoke(_sw.Elapsed); |
|||
|
|||
public event Action<TimeSpan> Tick; |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using Avalonia; |
|||
using Avalonia.Blazor; |
|||
using Avalonia.Blazor.Interop; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor; |
|||
|
|||
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod |
|||
{ |
|||
private Size _clientSize; |
|||
private BlazorSkiaSurface? _currentSurface; |
|||
private IInputRoot? _inputRoot; |
|||
private Stopwatch _sw = Stopwatch.StartNew(); |
|||
private readonly ITextInputMethodImpl _textInputMethod; |
|||
|
|||
public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod) |
|||
{ |
|||
_textInputMethod = textInputMethod; |
|||
} |
|||
|
|||
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; |
|||
|
|||
|
|||
internal void SetSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling) |
|||
{ |
|||
_currentSurface = new BlazorSkiaSurface |
|||
{ |
|||
Context = context, |
|||
GlInfo = glInfo, |
|||
ColorType = colorType, |
|||
Size = size, |
|||
Scaling = scaling, |
|||
Origin = GRSurfaceOrigin.BottomLeft |
|||
}; |
|||
} |
|||
|
|||
public void SetClientSize(SKSize size, double dpi) |
|||
{ |
|||
var newSize = new Size(size.Width, size.Height); |
|||
|
|||
if (newSize != _clientSize) |
|||
{ |
|||
_clientSize = newSize; |
|||
|
|||
if (_currentSurface is { }) |
|||
{ |
|||
_currentSurface.Size = new PixelSize((int)(size.Width), (int)(size.Height)); |
|||
} |
|||
|
|||
Resized?.Invoke(newSize, PlatformResizeReason.User); |
|||
} |
|||
} |
|||
|
|||
public void RawMouseEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, _inputRoot, type, p, modifiers)); |
|||
} |
|||
} |
|||
|
|||
public void RawMouseWheelEvent( Point p, Vector v, RawInputModifiers modifiers) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); |
|||
} |
|||
} |
|||
|
|||
public void RawKeyboardEvent (RawKeyEventType type, string key, RawInputModifiers modifiers) |
|||
{ |
|||
if (Keycodes.KeyCodes.TryGetValue(key, out var avkey)) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void RawTextEvent(string text) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) |
|||
{ |
|||
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>(); |
|||
|
|||
return new DeferredRenderer(root, loop); |
|||
} |
|||
|
|||
public void Invalidate(Rect rect) |
|||
{ |
|||
//Console.WriteLine("invalidate rect called");
|
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) |
|||
{ |
|||
_inputRoot = inputRoot; |
|||
} |
|||
|
|||
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y); |
|||
|
|||
public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y); |
|||
|
|||
public void SetCursor(ICursorImpl cursor) |
|||
{ |
|||
// nop
|
|||
|
|||
} |
|||
|
|||
public IPopupImpl CreatePopup() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Size ClientSize => _clientSize; |
|||
public Size? FrameSize => null; |
|||
public double RenderScaling => 1; |
|||
|
|||
public IEnumerable<object> Surfaces => new[] { _currentSurface }; |
|||
|
|||
internal BlazorSkiaSurface Surface => _currentSurface; |
|||
|
|||
public Action<RawInputEventArgs> Input { get; set; } |
|||
public Action<Rect> Paint { get; set; } |
|||
public Action<Size, PlatformResizeReason> Resized { get; set; } |
|||
public Action<double> ScalingChanged { get; set; } |
|||
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } |
|||
public Action Closed { get; set; } |
|||
public Action LostFocus { get; set; } |
|||
public IMouseDevice MouseDevice { get; } = new MouseDevice(); |
|||
|
|||
public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; |
|||
public WindowTransparencyLevel TransparencyLevel { get; } |
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } |
|||
|
|||
public ITextInputMethodImpl TextInputMethod => _textInputMethod; |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Media; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
internal class SKTypefaceCollection |
|||
{ |
|||
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces = |
|||
new ConcurrentDictionary<Typeface, SKTypeface>(); |
|||
|
|||
public void AddTypeface(Typeface key, SKTypeface typeface) |
|||
{ |
|||
_typefaces.TryAdd(key, typeface); |
|||
} |
|||
|
|||
public SKTypeface Get(Typeface typeface) |
|||
{ |
|||
return GetNearestMatch(_typefaces, typeface); |
|||
} |
|||
|
|||
private static SKTypeface GetNearestMatch(IDictionary<Typeface, SKTypeface> typefaces, Typeface key) |
|||
{ |
|||
if (typefaces.TryGetValue(key, out var typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
|
|||
var initialWeight = (int)key.Weight; |
|||
|
|||
var weight = (int)key.Weight; |
|||
|
|||
weight -= weight % 50; // make sure we start at a full weight
|
|||
|
|||
for (var i = 0; i < 2; i++) |
|||
{ |
|||
for (var j = 0; j < initialWeight; j += 50) |
|||
{ |
|||
if (weight - j >= 100) |
|||
{ |
|||
if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
} |
|||
|
|||
if (weight + j > 900) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) |
|||
{ |
|||
return typeface; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//Nothing was found so we try to get a regular typeface.
|
|||
return typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface) ? typeface : null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Platform; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
internal static class SKTypefaceCollectionCache |
|||
{ |
|||
private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections; |
|||
|
|||
static SKTypefaceCollectionCache() |
|||
{ |
|||
s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the or add typeface collection.
|
|||
/// </summary>
|
|||
/// <param name="fontFamily">The font family.</param>
|
|||
/// <returns></returns>
|
|||
public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) |
|||
{ |
|||
return s_cachedCollections.GetOrAdd(fontFamily, x => CreateCustomFontCollection(fontFamily)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the custom font collection.
|
|||
/// </summary>
|
|||
/// <param name="fontFamily">The font family.</param>
|
|||
/// <returns></returns>
|
|||
private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) |
|||
{ |
|||
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); |
|||
|
|||
var typeFaceCollection = new SKTypefaceCollection(); |
|||
|
|||
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>(); |
|||
|
|||
foreach (var asset in fontAssets) |
|||
{ |
|||
var assetStream = assetLoader.Open(asset); |
|||
|
|||
if (assetStream == null) |
|||
throw new InvalidOperationException("Asset could not be loaded."); |
|||
|
|||
var typeface = SKTypeface.FromStream(assetStream); |
|||
|
|||
if (typeface == null) |
|||
throw new InvalidOperationException("Typeface could not be loaded."); |
|||
|
|||
if (typeface.FamilyName != fontFamily.Name) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), |
|||
(FontWeight)typeface.FontWeight); |
|||
|
|||
typeFaceCollection.AddTypeface(key, typeface); |
|||
} |
|||
|
|||
return typeFaceCollection; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
class ClipboardStub : IClipboard |
|||
{ |
|||
public Task<string> GetTextAsync() => Task.FromResult(""); |
|||
|
|||
public Task SetTextAsync(string text) => Task.CompletedTask; |
|||
|
|||
public Task ClearAsync() => Task.CompletedTask; |
|||
|
|||
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; |
|||
|
|||
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>()); |
|||
|
|||
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(null); |
|||
} |
|||
|
|||
class CursorStub : ICursorImpl |
|||
{ |
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
class CursorFactoryStub : ICursorFactory |
|||
{ |
|||
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) |
|||
{ |
|||
return new CursorStub(); |
|||
} |
|||
|
|||
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) |
|||
{ |
|||
return new CursorStub(); |
|||
} |
|||
} |
|||
|
|||
class IconLoaderStub : IPlatformIconLoader |
|||
{ |
|||
class IconStub : IWindowIconImpl |
|||
{ |
|||
public void Save(Stream outputStream) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public IWindowIconImpl LoadIcon(string fileName) => new IconStub(); |
|||
|
|||
public IWindowIconImpl LoadIcon(Stream stream) => new IconStub(); |
|||
|
|||
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub(); |
|||
} |
|||
|
|||
class SystemDialogsStub : ISystemDialogImpl |
|||
{ |
|||
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) => |
|||
Task.FromResult((string[]) null); |
|||
|
|||
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) => |
|||
Task.FromResult((string) null); |
|||
} |
|||
|
|||
class ScreenStub : IScreenImpl |
|||
{ |
|||
public int ScreenCount => 1; |
|||
|
|||
public IReadOnlyList<Screen> AllScreens { get; } = |
|||
new Screen[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Blazor |
|||
{ |
|||
public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface |
|||
{ |
|||
private static object _uiLock = new object(); |
|||
private static object _syncRootLock = new object(); |
|||
private bool _signaled = false; |
|||
private static int _uiThreadId = -1; |
|||
private static int _lockNesting = 0; |
|||
|
|||
public IWindowImpl CreateWindow() => throw new System.NotSupportedException(); |
|||
|
|||
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public ITrayIconImpl CreateTrayIcon() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public static KeyboardDevice Keyboard { get; private set; } |
|||
|
|||
public static void Register() |
|||
{ |
|||
var instance = new BlazorWindowingPlatform(); |
|||
Keyboard = new KeyboardDevice(); |
|||
AvaloniaLocator.CurrentMutable |
|||
.Bind<IClipboard>().ToSingleton<ClipboardStub>() |
|||
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>() |
|||
.Bind<IKeyboardDevice>().ToConstant(Keyboard) |
|||
.Bind<IPlatformSettings>().ToConstant(instance) |
|||
.Bind<IPlatformThreadingInterface>().ToConstant(instance) |
|||
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) |
|||
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance) |
|||
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>() |
|||
.Bind<IWindowingPlatform>().ToConstant(instance) |
|||
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>() |
|||
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>(); |
|||
} |
|||
|
|||
public Size DoubleClickSize { get; } = new Size(2, 2); |
|||
|
|||
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); |
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
return AvaloniaLocator.Current.GetService<IRuntimePlatform>() |
|||
.StartSystemTimer(interval, () => |
|||
{ |
|||
using (Lock()) |
|||
{ |
|||
Dispatcher.UIThread.RunJobs(priority); |
|||
tick(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void Signal(DispatcherPriority priority) |
|||
{ |
|||
lock (_syncRootLock) |
|||
{ |
|||
if(_signaled) |
|||
return; |
|||
_signaled = true; |
|||
IDisposable disp = null; |
|||
disp = AvaloniaLocator.Current.GetService<IRuntimePlatform>() |
|||
.StartSystemTimer(TimeSpan.FromMilliseconds(1), |
|||
() => |
|||
{ |
|||
lock (_syncRootLock) |
|||
{ |
|||
_signaled = false; |
|||
disp.Dispose(); |
|||
} |
|||
|
|||
using (Lock()) |
|||
Signaled?.Invoke(null); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread |
|||
{ |
|||
get |
|||
{ |
|||
return true; // Blazor is single threaded.
|
|||
} |
|||
} |
|||
|
|||
public event Action<DispatcherPriority?> Signaled; |
|||
|
|||
class LockDisposable : IDisposable |
|||
{ |
|||
public void Dispose() |
|||
{ |
|||
lock (_syncRootLock) |
|||
{ |
|||
_lockNesting--; |
|||
if (_lockNesting == 0) |
|||
_uiThreadId = -1; |
|||
} |
|||
|
|||
Monitor.Exit(_uiLock); |
|||
} |
|||
} |
|||
|
|||
public static IDisposable Lock() |
|||
{ |
|||
Monitor.Enter(_uiLock); |
|||
lock (_syncRootLock) |
|||
{ |
|||
_lockNesting++; |
|||
_uiThreadId = Thread.CurrentThread.ManagedThreadId; |
|||
} |
|||
|
|||
return new LockDisposable(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
@using Microsoft.AspNetCore.Components.Web |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"name": "Avalonia.Blazor", |
|||
"lockfileVersion": 2, |
|||
"requires": true, |
|||
"packages": {} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"noImplicitAny": false, |
|||
"noEmitOnError": true, |
|||
"removeComments": false, |
|||
"sourceMap": true, |
|||
"target": "ES2020", |
|||
"module": "ES2020", |
|||
"outDir": "wwwroot" |
|||
}, |
|||
"exclude": [ |
|||
"node_modules" |
|||
] |
|||
} |
|||
@ -0,0 +1 @@ |
|||
*.js |
|||
@ -0,0 +1 @@ |
|||
{"version":3,"file":"DpiWatcher.js","sourceRoot":"","sources":["DpiWatcher.ts"],"names":[],"mappings":"AACA,MAAM,OAAO,UAAU;IAKf,MAAM,CAAC,MAAM;QACnB,OAAO,MAAM,CAAC,gBAAgB,CAAC;IAChC,CAAC;IAEM,MAAM,CAAC,KAAK,CAAC,QAAsC;QACzD,wEAAwE;QAExE,UAAU,CAAC,OAAO,GAAG,MAAM,CAAC,gBAAgB,CAAC;QAC7C,UAAU,CAAC,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjE,UAAU,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAE/B,OAAO,UAAU,CAAC,OAAO,CAAC;IAC3B,CAAC;IAEM,MAAM,CAAC,IAAI;QACjB,mFAAmF;QAEnF,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAEzC,UAAU,CAAC,QAAQ,GAAG,SAAS,CAAC;IACjC,CAAC;IAED,MAAM,CAAC,MAAM;QACZ,IAAI,CAAC,UAAU,CAAC,QAAQ;YACvB,OAAO;QAER,MAAM,UAAU,GAAG,MAAM,CAAC,gBAAgB,CAAC;QAC3C,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC;QACnC,UAAU,CAAC,OAAO,GAAG,UAAU,CAAC;QAEhC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,KAAK,EAAE;YAC3C,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;SAChE;IACF,CAAC;CACD"} |
|||
@ -0,0 +1,41 @@ |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,225 @@ |
|||
// 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; |
|||
|
|||
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, width?: number, height?: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, 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 requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) { |
|||
// optionally update the render loop
|
|||
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) |
|||
this.setEnableRenderLoop(renderLoop); |
|||
|
|||
// make sure the canvas is scaled correctly for the drawing
|
|||
if (width && height) { |
|||
this.htmlCanvas.width = width; |
|||
this.htmlCanvas.height = height; |
|||
} |
|||
|
|||
// 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); |
|||
} |
|||
|
|||
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: 1, |
|||
premultipliedAlpha: 1, |
|||
preserveDrawingBuffer: 0, |
|||
preferLowPowerToHighPerformance: 0, |
|||
failIfMajorPerformanceCaveat: 0, |
|||
majorVersion: 2, |
|||
minorVersion: 0, |
|||
enableExtensionsByDefault: 1, |
|||
explicitSwapControl: 0, |
|||
renderViaOffscreenBackBuffer: 0, |
|||
}; |
|||
|
|||
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; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
{"version":3,"file":"SizeWatcher.js","sourceRoot":"","sources":["SizeWatcher.ts"],"names":[],"mappings":"AASA,MAAM,OAAO,WAAW;IAIhB,MAAM,CAAC,OAAO,CAAC,OAAoB,EAAE,SAAiB,EAAE,QAAsC;QACpG,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;YACxB,OAAO;QAER,mFAAmF;QAEnF,WAAW,CAAC,IAAI,EAAE,CAAC;QAEnB,MAAM,cAAc,GAAG,OAA6B,CAAC;QACrD,cAAc,CAAC,WAAW,GAAG;YAC5B,QAAQ,EAAE,QAAQ;SAClB,CAAC;QAEF,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC;QAC1C,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAEtC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAEM,MAAM,CAAC,SAAS,CAAC,SAAiB;QACxC,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,CAAC,QAAQ;YACtC,OAAO;QAER,uDAAuD;QAEvD,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAEhD,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,CAAC,IAAI;QACV,IAAI,WAAW,CAAC,QAAQ;YACvB,OAAO;QAER,2CAA2C;QAE3C,WAAW,CAAC,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAC;QACtD,WAAW,CAAC,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE;YACrD,KAAK,IAAI,KAAK,IAAI,OAAO,EAAE;gBAC1B,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;aACjC;QACF,CAAC,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,MAAM,CAAC,OAAgB;QAC7B,MAAM,cAAc,GAAG,OAA6B,CAAC;QACrD,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,CAAC;QAE5C,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAClC,OAAO;QAER,OAAO,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5F,CAAC;CACD"} |
|||
@ -0,0 +1,68 @@ |
|||
|
|||
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); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 378 B |
Loading…
Reference in new issue