diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 80be694741..1d03118993 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 2d5bb6bec9..2a807b8cdf 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -4,7 +4,10 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; using System.Xml.Linq; +using Avalonia; using Avalonia.Web; +using Avalonia.Web.Blazor; +using ControlCatalog; //using SkiaSharp; internal partial class Program @@ -15,24 +18,11 @@ internal partial class Program private static void Main(string[] args) { - Console.WriteLine("Hello, Browser!"); - - Console.WriteLine(); - - foreach(var arg in args) - { - Console.WriteLine(arg); - } - - var div = GetElementById("out"); - Console.WriteLine("got div"); - - var canvas = AvaloniaRuntime.CreateCanvas(div); - - Console.WriteLine("Created canvas"); - - AvaloniaRuntime.Foo(canvas); + BuildAvaloniaApp().UseBrowserWasm(); } + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure(); } public partial class MyClass diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index 51b1d70173..13f311ba53 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -14,6 +14,11 @@ + + + + + true diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index e5c315af4c..2580f52666 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -1,11 +1,23 @@ using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; +using Avalonia.Web.Blazor; namespace Avalonia.Web; + public partial class AvaloniaRuntime { + + + public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] @@ -14,19 +26,26 @@ public partial class AvaloniaRuntime [JSExport] internal static void StartAvaloniaView(JSObject canvas) { - InterceptGLObject(); + // setup, get gl context... + + } + + public static GLInfo InitialiseGL (JSObject canvas) + { + InterceptGLObject(); + var info = InitGL(canvas, "testCanvas"); - + var glInfo = new GLInfo( - info.GetPropertyAsInt32("context"), - (uint)info.GetPropertyAsInt32("fboId"), - info.GetPropertyAsInt32("stencil"), - info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), info.GetPropertyAsInt32("depth")); - Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); + return glInfo; } [JSImport("Canvas.createCanvas", "avalonia.ts")] @@ -36,7 +55,7 @@ public partial class AvaloniaRuntime public static partial void Foo(JSObject canvas); [JSImport("Canvas.initGL", "avalonia.ts")] - internal static partial JSObject InitGL(JSObject canvas, string canvasId); + private static partial JSObject InitGL(JSObject canvas, string canvasId); [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs new file mode 100644 index 0000000000..ec462893df --- /dev/null +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; +using Avalonia.Input.TextInput; +using Avalonia.Platform.Storage; +using Avalonia.Rendering.Composition; +using Avalonia.Web.Blazor; +using SkiaSharp; +using static Avalonia.Web.AvaloniaRuntime; + +namespace Avalonia.Web +{ + public partial class AvaloniaView : ITextInputMethodImpl + { + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject GetElementById(string id); + + private readonly RazorViewTopLevelImpl _topLevelImpl; + private EmbeddableControlRoot _topLevel; + + // Interop + /*private SKHtmlCanvasInterop? _interop = null; + private SizeWatcherInterop? _sizeWatcher = null; + private DpiWatcherInterop? _dpiWatcher = null;*/ + private 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); + + private GRContext? _context; + private GRGlInterface? _glInterface; + private const SKColorType ColorType = SKColorType.Rgba8888; + + private bool _useGL; + private bool _inputElementFocused; + + public AvaloniaView() + { + Console.WriteLine("In AvaloniaView"); + var div = GetElementById("out"); + Console.WriteLine("got div"); + + var canvas = AvaloniaRuntime.CreateCanvas(div); + canvas.SetProperty("id", "mycanvas"); + + AvaloniaRuntime.Foo(canvas); + + _topLevelImpl = new RazorViewTopLevelImpl(this); + + _topLevel = new EmbeddableControlRoot(_topLevelImpl); + + Console.WriteLine("created toplevel"); + + _topLevel.Prepare(); + + Console.WriteLine("Prepped"); + + _topLevel.Renderer.Start(); + + + + + //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); + + + var skiaOptions = AvaloniaLocator.Current.GetService(); + _useGL = skiaOptions?.CustomGpuFactory != null; + + if (_useGL) + { + _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas); + Console.WriteLine("jsglinfo created - init gl"); + } + else + { + throw new NotImplementedException(); + //var rasterInitialized = _interop.InitRaster(); + //Console.WriteLine("raster initialized: {0}", rasterInitialized); + } + + if (_useGL) + { + // 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(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); + Console.WriteLine("glcontext created and resource limit set"); + } + + _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize(100, 100), 1, GRSurfaceOrigin.BottomLeft) }; + } + else + { + //_topLevelImpl.SetSurface(ColorType, + // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + } + + // _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + Threading.Dispatcher.UIThread.Post(async () => + { + //_interop.RequestAnimationFrame(true); + + // _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); + // _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); + + //_sizeWatcher.Start(); + }); + } + + internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + //_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + } + + public Control? Content + { + get => (Control)_topLevel.Content!; + set => _topLevel.Content = value; + } + + public bool KeyPreventDefault { get; set; } + + internal INativeControlHostImpl GetNativeControlHostImpl() + { + throw new NotImplementedException(); + //return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); + } + + internal IStorageProvider GetStorageProvider() + { + throw new NotImplementedException(); + //return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); + } + + private void OnRenderFrame() + { + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) + { + Console.WriteLine("nothing to render"); + return; + } + + ManualTriggerRenderTimer.Instance.RaiseTick(); + } + + private void ForceBlit() + { + // Note: this is technically a hack, but it's a kinda unique use case when + // we want to blit the previous frame + // renderer doesn't have much control over the render target + // we render on the UI thread + // We also don't want to have it as a meaningful public API. + // Therefore we have InternalsVisibleTo hack here. + + if (_topLevel.Renderer is CompositingRenderer dr) + { + //dr.CompositionTarget.ImmediateUIThreadRender(); + } + } + + private void OnDpiChanged(double newDpi) + { + if (Math.Abs(_dpi - newDpi) > 0.0001) + { + _dpi = newDpi; + + //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); + } + } + + private void OnSizeChanged(SKSize newSize) + { + if (_canvasSize != newSize) + { + _canvasSize = newSize; + + //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); + } + } + + public void SetClient(ITextInputMethodClient? client) + { + + } + + public void SetCursorRect(Rect rect) + { + } + + public void SetOptions(TextInputOptions options) + { + } + + public void Reset() + { + + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs new file mode 100644 index 0000000000..04c1117510 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs @@ -0,0 +1,46 @@ +using System.Runtime.InteropServices.JavaScript; +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media; + +namespace Avalonia.Web.Blazor +{ + public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + { + public AvaloniaView? View; + + public Control? MainView + { + get => View!.Content; + set => View!.Content = value; + } + } + + public static partial class WebAppBuilder + { + + + public static T UseBrowserWasm( + this T builder) + where T : AppBuilderBase, new() + { + Console.WriteLine("In UseBrowserWasm"); + var lifetime = new BlazorSingleViewLifetime(); + + return builder + .UseWindowingSubsystem(BlazorWindowingPlatform.Register) + .UseSkia() + .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }) + .AfterSetup(b => + { + var view = new AvaloniaView(); + lifetime.View = view; + Console.WriteLine("After setup"); + + }) + .SetupWithLifetime(lifetime); + + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web/BlazorSkiaGpu.cs new file mode 100644 index 0000000000..7191a00a66 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaGpu.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Avalonia.Skia; + +namespace Avalonia.Web.Blazor +{ + public class BlazorSkiaGpu : ISkiaGpu + { + public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable 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; + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs new file mode 100644 index 0000000000..0c53825131 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs @@ -0,0 +1,37 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.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(); + + _surface.Dispose(); + } + + public GRContext GrContext { get; } + + public SKSurface SkSurface => _surface; + + public double ScaleFactor { get; } + + public GRSurfaceOrigin SurfaceOrigin { get; } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..fa6a39f210 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs @@ -0,0 +1,39 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.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 => _blazorSkiaSurface.Size != _size; + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs new file mode 100644 index 0000000000..a1d58cdda0 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs @@ -0,0 +1,88 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + { + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + + private FramebufferData? _fbData; + private readonly Action _blitCallback; + private readonly Action _onDisposeAction; + + public BlazorSkiaRasterSurface( + SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + ColorType = colorType; + Size = size; + Scaling = scaling; + _blitCallback = blitCallback; + _onDisposeAction = Blit; + } + + public void Dispose() + { + _fbData?.Dispose(); + _fbData = null; + } + + public ILockedFramebuffer Lock() + { + var bytesPerPixel = 4; // TODO: derive from ColorType + var dpi = Scaling * 96.0; + var width = (int)(Size.Width * Scaling); + var height = (int)(Size.Height * Scaling); + + if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) + { + _fbData?.Dispose(); + _fbData = new FramebufferData(width, height, bytesPerPixel); + } + + var pixelFormat = ColorType.ToPixelFormat(); + var data = _fbData.Value; + return new LockedFramebuffer( + data.Address, data.Size, data.RowBytes, + new Vector(dpi, dpi), pixelFormat, _onDisposeAction); + } + + private void Blit() + { + if (_fbData != null) + { + var data = _fbData.Value; + _blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); + } + } + + private readonly struct FramebufferData + { + public PixelSize Size { get; } + + public int RowBytes { get; } + + public IntPtr Address { get; } + + public FramebufferData(int width, int height, int bytesPerPixel) + { + Size = new PixelSize(width, height); + RowBytes = width * bytesPerPixel; + Address = Marshal.AllocHGlobal(width * height * bytesPerPixel); + } + + public void Dispose() + { + Marshal.FreeHGlobal(Address); + } + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web/BlazorSkiaSurface.cs new file mode 100644 index 0000000000..30dcddc66a --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaSurface.cs @@ -0,0 +1,30 @@ +using SkiaSharp; +using static Avalonia.Web.AvaloniaRuntime; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaSurface : IBlazorSkiaSurface + { + public BlazorSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + { + Context = context; + GlInfo = glInfo; + ColorType = colorType; + Size = size; + Scaling = scaling; + Origin = origin; + } + + 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 GLInfo GlInfo { get; set; } + } +} diff --git a/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs new file mode 100644 index 0000000000..5463893e27 --- /dev/null +++ b/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Web.Blazor +{ + internal interface IBlazorSkiaSurface + { + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + } +} diff --git a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs new file mode 100644 index 0000000000..505b978a91 --- /dev/null +++ b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs @@ -0,0 +1,18 @@ +using System; +using System.Diagnostics; +using Avalonia.Rendering; + +namespace Avalonia.Web.Blazor +{ + public class ManualTriggerRenderTimer : IRenderTimer + { + private static readonly Stopwatch s_sw = Stopwatch.StartNew(); + + public static ManualTriggerRenderTimer Instance { get; } = new(); + + public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); + + public event Action? Tick; + public bool RunsInBackground => false; + } +} diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs new file mode 100644 index 0000000000..82f4064e1a --- /dev/null +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using SkiaSharp; +using static Avalonia.Web.AvaloniaRuntime; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + { + private Size _clientSize; + private IBlazorSkiaSurface? _currentSurface; + private IInputRoot? _inputRoot; + private readonly Stopwatch _sw = Stopwatch.StartNew(); + private readonly AvaloniaView _avaloniaView; + private readonly TouchDevice _touchDevice; + private readonly PenDevice _penDevice; + //private string _currentCursor = CssCursor.Default; + + public RazorViewTopLevelImpl(AvaloniaView avaloniaView) + { + Surfaces = Enumerable.Empty(); + _avaloniaView = avaloniaView; + TransparencyLevel = WindowTransparencyLevel.None; + AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); + _touchDevice = new TouchDevice(); + _penDevice = new PenDevice(); + } + + public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; + + + + + public void SetClientSize(SKSize size, double dpi) + { + var newSize = new Size(size.Width, size.Height); + + if (Math.Abs(RenderScaling - dpi) > 0.0001) + { + if (_currentSurface is { }) + { + _currentSurface.Scaling = dpi; + } + + ScalingChanged?.Invoke(dpi); + } + + 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 RawPointerEvent( + RawPointerEventType eventType, string pointerType, + RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) + { + if (_inputRoot is { } + && Input is { } input) + { + var device = GetPointerDevice(pointerType); + var args = device is TouchDevice ? + new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) : + new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers) + { + RawPointerId = touchPointId + }; + + input.Invoke(args); + } + } + + private IPointerDevice GetPointerDevice(string pointerType) + { + return pointerType switch + { + "touch" => _touchDevice, + "pen" => _penDevice, + _ => MouseDevice + }; + } + + public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); + } + } + + public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) + { + /*if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) + { + if (_inputRoot is { }) + { + var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); + + Input?.Invoke(args); + + return args.Handled; + } + } + else if (Keycodes.KeyCodes.TryGetValue(key, out avkey)) + { + if (_inputRoot is { }) + { + var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); + + Input?.Invoke(args); + + return args.Handled; + } + }*/ + + return false; + } + + 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.GetRequiredService(); + return new CompositingRenderer(root, new Compositor(loop, null)); + } + + 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) + { + /* var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; + if (_currentCursor != val) + { + SetCssCursor?.Invoke(val); + _currentCursor = val; + }*/ + } + + public IPopupImpl? CreatePopup() + { + return null; + } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + + } + + public Size ClientSize => _clientSize; + public Size? FrameSize => null; + public double RenderScaling => _currentSurface?.Scaling ?? 1; + + public IEnumerable Surfaces { get; set; } + + public Action? SetCssCursor { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } + public Action? 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 => _avaloniaView; + + public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); + public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); + } +} diff --git a/src/Web/Avalonia.Web/WinStubs.cs b/src/Web/Avalonia.Web/WinStubs.cs new file mode 100644 index 0000000000..e41f6ca29c --- /dev/null +++ b/src/Web/Avalonia.Web/WinStubs.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.IO; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + internal class IconLoaderStub : IPlatformIconLoader + { + private 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(); + } + + internal class ScreenStub : IScreenImpl + { + public int ScreenCount => 1; + + public IReadOnlyList AllScreens { get; } = + new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + + public Screen? ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen? ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen? ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } + } +} diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs new file mode 100644 index 0000000000..4d52f54690 --- /dev/null +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -0,0 +1,110 @@ +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; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + { + private bool _signaled; + private static KeyboardDevice? s_keyboard; + + public IWindowImpl CreateWindow() => throw new NotSupportedException(); + + IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() + { + throw new NotImplementedException(); + } + + public ITrayIconImpl? CreateTrayIcon() + { + return null; + } + + public static KeyboardDevice Keyboard => s_keyboard ?? + throw new InvalidOperationException("123 BlazorWindowingPlatform not registered."); + + public static void Register() + { + Console.WriteLine("Registering windowing"); + + var instance = new BlazorWindowingPlatform(); + s_keyboard = new KeyboardDevice(); + AvaloniaLocator.CurrentMutable + //.Bind().ToSingleton() + //.Bind().ToSingleton() + .Bind().ToConstant(s_keyboard) + .Bind().ToConstant(instance) + .Bind().ToConstant(instance) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(ManualTriggerRenderTimer.Instance) + .Bind().ToConstant(instance) + .Bind().ToSingleton() + .Bind().ToSingleton(); + } + + public Size DoubleClickSize { get; } = new Size(2, 2); + + public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + + public Size TouchDoubleClickSize => new Size(16, 16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; + public void RunLoop(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + { + return GetRuntimePlatform() + .StartSystemTimer(interval, () => + { + Dispatcher.UIThread.RunJobs(priority); + tick(); + }); + } + + public void Signal(DispatcherPriority priority) + { + if (_signaled) + return; + + _signaled = true; + + IDisposable? disp = null; + + disp = GetRuntimePlatform() + .StartSystemTimer(TimeSpan.FromMilliseconds(1), + () => + { + _signaled = false; + disp?.Dispose(); + + Signaled?.Invoke(null); + }); + } + + public bool CurrentThreadIsLoopThread + { + get + { + return true; // Blazor is single threaded. + } + } + + public event Action? Signaled; + + private static IRuntimePlatform GetRuntimePlatform() + { + return AvaloniaLocator.Current.GetRequiredService(); + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index 61e7b71797..bb97d0c9b7 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -11,9 +11,4 @@ export class AvaloniaRuntime { Canvas }); } - - createAvaloniaView(element: HTMLDivElement): void { - const canvas = document.createElement("canvas"); - element.appendChild(canvas); - } } diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js index 896ad807ff..8c2721c600 100644 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -109,10 +109,6 @@ var AvaloniaRuntime = class { Canvas }); } - createAvaloniaView(element) { - const canvas = document.createElement("canvas"); - element.appendChild(canvas); - } }; // modules/avalonia.ts