From e8719e018db82a6998900e61bea3f11470eade9f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:24:53 +0200 Subject: [PATCH 001/103] Fix GlyphRun.GetTralingWhitespaceLengthRightToLeft --- src/Avalonia.Base/Media/GlyphRun.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index cae7a8fe75..f207b3c636 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -786,14 +786,15 @@ namespace Avalonia.Media var clusterLength = 1; - while (i - 1 >= 0) + var j = i; + + while (j - 1 >= 0) { - var nextCluster = GlyphClusters[i - 1]; + var nextCluster = GlyphClusters[--j]; if (currentCluster == nextCluster) { - clusterLength++; - i--; + clusterLength++; continue; } @@ -808,7 +809,7 @@ namespace Avalonia.Media trailingWhitespaceLength += clusterLength; - glyphCount++; + glyphCount += clusterLength; } } From 0856cfaff985c15614b3bd647cf827b021a0444a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:25:12 +0200 Subject: [PATCH 002/103] Remove redundant comment --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index fa1ab6fd29..7495956cd2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -266,10 +266,6 @@ namespace Avalonia.Media.TextFormatting { offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } - //else - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); From 678620422df2e6bea1b9c7e3832a998559af2083 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 19 Aug 2022 15:59:51 +0200 Subject: [PATCH 003/103] More RTL hit testing fixes --- .../Media/TextFormatting/TextLineImpl.cs | 249 ++++++++---------- .../Media/TextFormatting/TextRunBounds.cs | 2 +- .../Documents/InlineCollection.cs | 14 +- .../Media/TextFormatting/TextLineTests.cs | 83 +++++- 4 files changed, 202 insertions(+), 146 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7495956cd2..aba8008fb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; - if(collapsingProperties is null) + if (collapsingProperties is null) { return this; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[i]; - if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; currentPosition += currentRun.TextSourceLength; @@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if(j > _textRuns.Count - 1) + if (j > _textRuns.Count - 1) { break; } currentRun = _textRuns[j]; - if(currentDistance + currentRun.Size.Width <= distance) + if (currentDistance + currentRun.Size.Width <= distance) { currentDistance += currentRun.Size.Width; currentPosition -= currentRun.TextSourceLength; @@ -322,11 +322,11 @@ namespace Avalonia.Media.TextFormatting continue; } - + break; } - if(i > index) + if (i > index) { while (i >= index) { @@ -350,7 +350,7 @@ namespace Avalonia.Media.TextFormatting } } - if (currentPosition + currentRun.TextSourceLength >= characterIndex && + if (currentPosition + currentRun.TextSourceLength >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return Math.Max(0, currentDistance + distance); @@ -530,6 +530,8 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = Rect.Empty; + TextRunBounds lastRunBounds = default; + for (var index = 0; index < TextRuns.Count; index++) { if (TextRuns[index] is not DrawableTextRun currentRun) @@ -539,53 +541,93 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - var runWidth = 0.0; - TextRunBounds? currentRunBounds = null; var currentShapedRun = currentRun as ShapedTextCharacters; + TextRunBounds currentRunBounds; + + double combinedWidth; + + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = index; - startX += currentShapedRun.Size.Width; + var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) { - var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { break; } - startX += nextShapedRun.Size.Width; - rightToLeftIndex++; + + rightToLeftWidth += nextShapedRun.Size.Width; + + if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength) + { + break; + } + + currentShapedRun = nextShapedRun; } - if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) + startX = startX + rightToLeftWidth; + + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + remainingLength -= currentRunBounds.Length; + currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; + endX = currentRunBounds.Rectangle.Right; + startX = currentRunBounds.Rectangle.Left; + + var rightToLeftRunBounds = new List { currentRunBounds }; + + for (int i = rightToLeftIndex - 1; i >= index; i--) { - startX = currentRunBounds!.Rectangle.Left; - endX = currentRunBounds.Rectangle.Right; + currentShapedRun = TextRuns[i] as ShapedTextCharacters; + + if(currentShapedRun == null) + { + continue; + } - runWidth = currentRunBounds.Rectangle.Width; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + rightToLeftRunBounds.Insert(0, currentRunBounds); + + remainingLength -= currentRunBounds.Length; + startX = currentRunBounds.Rectangle.Left; + + currentPosition += currentRunBounds.Length; } + combinedWidth = endX - startX; + + currentRect = new Rect(startX, 0, combinedWidth, Height); + currentDirection = FlowDirection.RightToLeft; + + if (!MathUtilities.IsZero(combinedWidth)) + { + result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); + } + + startX = endX; } else { if (currentShapedRun != null) { - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); currentPosition += offset; @@ -661,43 +703,46 @@ namespace Avalonia.Media.TextFormatting characterLength = NewLineLength; } - runWidth = endX - startX; - currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + combinedWidth = endX - startX; + + currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); currentPosition += characterLength; remainingLength -= characterLength; - } - if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + startX = endX; + + if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + { + currentRect = currentRect.WithWidth(currentWidth + combinedWidth); - var textBounds = result[result.Count - 1]; + var textBounds = result[result.Count - 1]; - textBounds.Rectangle = currentRect; + textBounds.Rectangle = currentRect; - textBounds.TextRunBounds.Add(currentRunBounds!); - } - else - { - currentRect = currentRunBounds!.Rectangle; + textBounds.TextRunBounds.Add(currentRunBounds); + } + else + { + currentRect = currentRunBounds.Rectangle; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + } } + + lastRunBounds = currentRunBounds; } - currentWidth += runWidth; - + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) { break; } - startX = endX; lastDirection = currentDirection; } @@ -852,105 +897,45 @@ namespace Avalonia.Media.TextFormatting return result; } - private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) + private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) { - textRunBounds = null; + var startX = endX; - for (var index = runIndex; index >= 0; index--) - { - if (TextRuns[index] is not DrawableTextRun currentRun) - { - continue; - } + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += offset; - currentPosition += currentRun.TextSourceLength; + var startIndex = currentRun.Text.Start + offset; - continue; - } + double startOffset; + double endOffset; - var characterLength = 0; - var endX = startX; - - if (currentRun is ShapedTextCharacters currentShapedRun) - { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; - - var startIndex = currentRun.Text.Start + offset; - double startOffset; - double endOffset; - - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - } - - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; - - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - } - else - { - if (currentPosition + currentRun.TextSourceLength <= characterIndex) - { - endX -= currentRun.Size.Width; - } + startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; - characterLength = currentRun.TextSourceLength; - } - } + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } - - var runWidth = endX - startX; - - remainingLength -= characterLength; - - currentPosition += characterLength; - - textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + if (endX < startX) + { + (endX, startX) = (startX, endX); + } - return true; + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - return false; + var runWidth = endX - startX; + + return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) @@ -1532,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; - if(textAlignment == TextAlignment.Justify) + if (textAlignment == TextAlignment.Justify) { textAlignment = TextAlignment.Start; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index 91150160ed..bdc7a1ca89 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public sealed class TextRunBounds + public readonly struct TextRunBounds { /// /// Constructing TextRunBounds diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 565ed75ad9..15b4688809 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents private void AddText(string text) { - if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) + if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) { textBlock._text += text; } @@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents { foreach (var child in this) { - ((ISetLogicalParent)child).SetParent(parent); + var oldParent = child.Parent; + + if (oldParent != parent) + { + if (oldParent != null) + { + ((ISetLogicalParent)child).SetParent(null); + } + + ((ISetLogicalParent)child).SetParent(parent); + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d744ede87d..251c850fc8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -597,21 +597,82 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(1, textBounds.Count); + Assert.Equal(2, textBounds.Count); - Assert.Equal(144.0234375, textBounds[0].Rectangle.Width); + Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(1, textBounds.Count); + Assert.Equal(3, textBounds.Count); - Assert.Equal(216.03515625, textBounds[0].Rectangle.Width); + Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(1, textBounds.Count); + Assert.Equal(4, textBounds.Count); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_GetTextRange() + { + var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textRuns = textLine.TextRuns.Cast().ToList(); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width); + var lineWidth = textLine.WidthIncludingTrailingWhitespace; + + var textBounds = textLine.GetTextBounds(0, text.Length); + + TextBounds lastBounds = null; + + var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList(); + + Assert.Equal(textRuns.Count, runBounds.Count); + + for (var i = 0; i < textRuns.Count; i++) + { + var run = textRuns[i]; + var bounds = runBounds[i]; + + Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run, bounds.TextRun); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + } + + for (var i = 0; i < textBounds.Count; i++) + { + var currentBounds = textBounds[i]; + + if (lastBounds != null) + { + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + } + + var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + + lastBounds = currentBounds; + } + + var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(lineWidth, sumOfBoundsWidth); } } @@ -779,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); - Assert.Equal(1, textBounds.Count); + Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -789,8 +850,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); @@ -799,8 +860,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } From 1f05ee41bbccbf17de73e5da68d32054b1b61e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 04:42:58 +0100 Subject: [PATCH 004/103] fallback from CenterOwner to CenterScreen when owner is null --- src/Avalonia.Controls/Window.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 03c66aff2b..db161968b0 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -830,10 +830,11 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - Owner is Window ownerWindow && - ownerWindow.WindowState == WindowState.Minimized) + (Owner is null || + (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + ) { - // If startup location is CenterOwner, but owner is minimized then fall back + // If startup location is CenterOwner, but owner is null or minimized then fall back // to CenterScreen. This behavior is consistent with WPF. startupLocation = WindowStartupLocation.CenterScreen; } From 541b2e74dae3bf2d653be0b0ff45ad2f4eb5a9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:03:32 +0100 Subject: [PATCH 005/103] Use owner argument instead of Owner property --- src/Avalonia.Controls/Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index db161968b0..674b47f0d8 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -830,8 +830,8 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - (Owner is null || - (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + (owner is null || + (owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { // If startup location is CenterOwner, but owner is null or minimized then fall back From fa78c1c886be76f52adfd06f3fecc1ab7099c3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:07:01 +0100 Subject: [PATCH 006/103] Revert original check to the Owner property --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 674b47f0d8..a7f82fc26f 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -831,7 +831,7 @@ namespace Avalonia.Controls if (startupLocation == WindowStartupLocation.CenterOwner && (owner is null || - (owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) + (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { // If startup location is CenterOwner, but owner is null or minimized then fall back From e3238ed20d47b3cc5d1efd7c38b0f73d7fa419b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:09:19 +0100 Subject: [PATCH 007/103] Remove useless check, it's always true --- src/Avalonia.Controls/Window.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a7f82fc26f..f0369c416c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -869,14 +869,11 @@ namespace Avalonia.Controls } else if (startupLocation == WindowStartupLocation.CenterOwner) { - if (owner != null) - { - var ownerSize = owner.FrameSize ?? owner.ClientSize; - var ownerRect = new PixelRect( - owner.Position, - PixelSize.FromSize(ownerSize, scaling)); - Position = ownerRect.CenterRect(rect).Position; - } + var ownerSize = owner.FrameSize ?? owner.ClientSize; + var ownerRect = new PixelRect( + owner.Position, + PixelSize.FromSize(ownerSize, scaling)); + Position = ownerRect.CenterRect(rect).Position; } } From 80edb2d1a4f5e5dc9357d31acb7d9120eff96549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Tue, 20 Sep 2022 05:12:29 +0100 Subject: [PATCH 008/103] Minor improvement to reuse parent and spare a property get --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f0369c416c..b1d50cf430 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -668,7 +668,7 @@ namespace Avalonia.Controls Owner = parent; parent?.AddChild(this, false); - SetWindowStartupLocation(Owner?.PlatformImpl); + SetWindowStartupLocation(parent?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); From 6d05bbbc35b45546b66f10992e79cf19fa571357 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 11:48:15 +0100 Subject: [PATCH 009/103] add initial work towards blazorless wasm --- Avalonia.sln | 18 +- global.json | 4 - .../Avalonia.Web.Sample.csproj | 24 + src/Web/Avalonia.Web.Sample/Program.cs | 27 + src/Web/Avalonia.Web.Sample/index.html | 20 + src/Web/Avalonia.Web.Sample/main.js | 24 + .../runtimeconfig.template.json | 11 + src/Web/Avalonia.Web/Avalonia.Web.csproj | 41 ++ src/Web/Avalonia.Web/Avalonia.Web.targets | 6 + src/Web/Avalonia.Web/AvaloniaRuntime.cs | 51 ++ src/Web/Avalonia.Web/webapp/build.js | 16 + .../Avalonia.Web/webapp/modules/avalonia.ts | 7 + .../webapp/modules/avalonia/canvas.ts | 119 ++++ .../webapp/modules/avalonia/runtime.ts | 23 + .../Avalonia.Web/webapp/modules/storage.ts | 5 + src/Web/Avalonia.Web/webapp/package-lock.json | 634 ++++++++++++++++++ src/Web/Avalonia.Web/webapp/package.json | 13 + src/Web/Avalonia.Web/webapp/tsconfig.json | 19 + src/Web/Avalonia.Web/webapp/types/dotnet.d.ts | 249 +++++++ src/Web/Avalonia.Web/wwwroot/avalonia.js | 122 ++++ src/Web/Avalonia.Web/wwwroot/storage.js | 10 + 21 files changed, 1437 insertions(+), 6 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj create mode 100644 src/Web/Avalonia.Web.Sample/Program.cs create mode 100644 src/Web/Avalonia.Web.Sample/index.html create mode 100644 src/Web/Avalonia.Web.Sample/main.js create mode 100644 src/Web/Avalonia.Web.Sample/runtimeconfig.template.json create mode 100644 src/Web/Avalonia.Web/Avalonia.Web.csproj create mode 100644 src/Web/Avalonia.Web/Avalonia.Web.targets create mode 100644 src/Web/Avalonia.Web/AvaloniaRuntime.cs create mode 100644 src/Web/Avalonia.Web/webapp/build.js create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/storage.ts create mode 100644 src/Web/Avalonia.Web/webapp/package-lock.json create mode 100644 src/Web/Avalonia.Web/webapp/package.json create mode 100644 src/Web/Avalonia.Web/webapp/tsconfig.json create mode 100644 src/Web/Avalonia.Web/webapp/types/dotnet.d.ts create mode 100644 src/Web/Avalonia.Web/wwwroot/avalonia.js create mode 100644 src/Web/Avalonia.Web/wwwroot/storage.js diff --git a/Avalonia.sln b/Avalonia.sln index 35b6b2108a..228052c2d5 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -212,7 +212,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPick EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web", "src\Web\Avalonia.Web\Avalonia.Web.csproj", "{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Sample", "src\Web\Avalonia.Web.Sample\Avalonia.Web.Sample.csproj", "{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -502,6 +506,14 @@ Global {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.Build.0 = Release|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -545,6 +557,7 @@ Global {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -559,7 +572,8 @@ Global {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/global.json b/global.json index a6792b05c7..44d4e10dbf 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,4 @@ { - "sdk": { - "version": "6.0.202", - "rollForward": "latestFeature" - }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "3.0.22", diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj new file mode 100644 index 0000000000..8f5912a16f --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -0,0 +1,24 @@ + + + net7.0 + browser-wasm + main.js + Exe + true + true + + + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs new file mode 100644 index 0000000000..b880d61ab5 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using Avalonia.Web; +//using SkiaSharp; + +internal class Program +{ + private static void Main(string[] args) + { + Console.WriteLine("Hello, Browser!"); + + AvaloniaRuntime.Init(); + } +} + +public partial class MyClass +{ + [JSExport] + internal static async Task TestDynamicModule() + { + await JSHost.ImportAsync("storage.ts", "./storage.js"); + var fileApiSupported = AvaloniaRuntime.IsFileApiSupported(); + + Console.WriteLine("DynamicModule result: " + fileApiSupported); + } +} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html new file mode 100644 index 0000000000..25b92798ad --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -0,0 +1,20 @@ + + + + + + + Avalonia.Web.Sample + + + + + + + + +
+ + + + diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js new file mode 100644 index 0000000000..9973b469c7 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnet } from './dotnet.js' +import { createAvaloniaRuntime } from './avalonia.js' + +const is_browser = typeof window != "undefined"; +if (!is_browser) throw new Error(`Expected to be running in a browser`); + +const dotnetRuntime = await dotnet + .withDiagnosticTracing(false) + .withApplicationArgumentsFromQuery() + .create(); + +const avaloniaRuntime = await createAvaloniaRuntime(dotnetRuntime); + +const outDiv = document.getElementById("out"); +avaloniaRuntime.createAvaloniaView(outDiv); + +const config = dotnetRuntime.getConfig(); +const exports = await dotnetRuntime.getAssemblyExports(config.mainAssemblyName); +await exports.MyClass.TestDynamicModule(); + +await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json b/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json new file mode 100644 index 0000000000..8f0557352c --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json @@ -0,0 +1,11 @@ +{ + "wasmHostProperties": { + "perHostConfig": [ + { + "name": "browser", + "html-path": "index.html", + "Host": "browser" + } + ] + } +} diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj new file mode 100644 index 0000000000..51b1d70173 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -0,0 +1,41 @@ + + + net7.0 + enable + true + + + + + + + + + + + + + + true + build\ + + + true + build\;buildTransitive\ + + + + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets new file mode 100644 index 0000000000..97c8c29ee4 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs new file mode 100644 index 0000000000..c8ff75a77f --- /dev/null +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Avalonia.Web; + +public partial class AvaloniaRuntime +{ + public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + + [JSExport] + internal static void StartAvaloniaView(JSObject canvas) + { + Init(); + // setup, get gl context... + var info = InitGL(canvas, "testCanvas"); + + + /* var glInfo = new GLInfo( + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("depth")); + + Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}");*/ + } + + public static void Init () + { + if (false) + { + SkiaSharp.GRGlInterface.Create(); + } + } + + [JSImport("Canvas.Foo", "avalonia.ts")] + internal static partial void Foo(JSObject canvas); + + [JSImport("Canvas.initGL", "avalonia.ts")] + internal static partial JSObject InitGL(JSObject canvas, string canvasId); + + [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] + public static partial bool IsFileApiSupported(); + + //[DllImport("__Internal")] + //public + +} + diff --git a/src/Web/Avalonia.Web/webapp/build.js b/src/Web/Avalonia.Web/webapp/build.js new file mode 100644 index 0000000000..32f79b0709 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/build.js @@ -0,0 +1,16 @@ +require("esbuild").build({ + entryPoints: [ + "./modules/avalonia.ts", + "./modules/storage.ts" + ], + outdir: "../wwwroot", + bundle: true, + minify: false, + format: "esm", + target: "es2016", + platform: "browser", + sourcemap: "linked", + loader: {".ts": "ts"} + }) + .then(() => console.log("⚡ Done")) + .catch(() => process.exit(1)); \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts new file mode 100644 index 0000000000..bf9ef48772 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -0,0 +1,7 @@ +import { RuntimeAPI } from "../types/dotnet"; +import { AvaloniaRuntime } from "./avalonia/runtime"; + +export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { + const dotnetAssembly = await api.getAssemblyExports("Avalonia.Web.dll"); + return new AvaloniaRuntime(dotnetAssembly, api); +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts new file mode 100644 index 0000000000..c8acf5e428 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -0,0 +1,119 @@ +declare let GL: any; +declare let GLctx: WebGLRenderingContext; +declare let Module: EmscriptenModule; + +type SKGLViewInfo = { + context: WebGLRenderingContext | WebGL2RenderingContext | undefined; + fboId: number; + stencil: number; + sample: number; + depth: number; +} + +type SKHtmlCanvasElement = { + SKHtmlCanvas: Canvas | undefined +} & HTMLCanvasElement + +export class Canvas { + static elements: Map; + + //htmlCanvas: HTMLCanvasElement; + glInfo?: SKGLViewInfo; + //renderFrameCallback: DotNet.DotNetObject; + renderLoopEnabled: boolean = false; + renderLoopRequest: number = 0; + newWidth?: number; + newHeight?: number; + + public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { + console.log("inside initGL"); + var view = Canvas.init(true, element, elementId); + if (!view || !view.glInfo) + return null; + + return view.glInfo; + } + + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string): Canvas | null { + var htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas) { + console.error(`No canvas element was provided.`); + return null; + } + + if (!Canvas.elements) + Canvas.elements = new Map(); + Canvas.elements.set(elementId, element); + + const view = new Canvas(useGL, element); + + htmlCanvas.SKHtmlCanvas = view; + + return view; + } + + + public constructor(useGL: boolean, element: HTMLCanvasElement) { + //this.htmlCanvas = element; + //this.renderFrameCallback = callback; + + if (useGL) { + const ctx = Canvas.createWebGLContext(element); + if (!ctx) { + console.error(`Failed to create WebGL context: err ${ctx}`); + return; + } + + // make current + GL.makeContextCurrent(ctx); + + // read values + const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { + context: ctx, + fboId: fbo ? fbo.id : 0, + stencil: GLctx.getParameter(GLctx.STENCIL_BITS), + sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) + depth: GLctx.getParameter(GLctx.DEPTH_BITS), + }; + } + } + + + static Foo(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext("2d")!; + ctx.fillStyle = "#FF0000"; + ctx.fillRect(0, 0, 150, 75); + } + + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { + const contextAttributes = { + alpha: 1, + depth: 1, + stencil: 8, + antialias: 0, + premultipliedAlpha: 1, + preserveDrawingBuffer: 0, + preferLowPowerToHighPerformance: 0, + failIfMajorPerformanceCaveat: 0, + majorVersion: 2, + minorVersion: 0, + enableExtensionsByDefault: 1, + explicitSwapControl: 0, + renderViaOffscreenBackBuffer: 1, + }; + + + var context = htmlCanvas.getContext("webgl2", contextAttributes); + + 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; + } +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts new file mode 100644 index 0000000000..bd1989e935 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -0,0 +1,23 @@ +import { RuntimeAPI } from "../../types/dotnet"; + +import { Canvas } from "./canvas"; + +export class AvaloniaRuntime { + constructor( + private dotnetAssembly: any, + api: RuntimeAPI + ) { + api.setModuleImports("avalonia.ts", { + Canvas + }); + } + + createAvaloniaView(element: HTMLDivElement): void { + const canvas = document.createElement("canvas"); + element.appendChild(canvas); + + + + this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); + } +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/storage.ts b/src/Web/Avalonia.Web/webapp/modules/storage.ts new file mode 100644 index 0000000000..7b6cd816de --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage.ts @@ -0,0 +1,5 @@ +export class StorageProvider { + static isFileApiSupported(): boolean { + return (globalThis as any).showOpenFilePicker !== undefined; + } +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/package-lock.json b/src/Web/Avalonia.Web/webapp/package-lock.json new file mode 100644 index 0000000000..5df9471195 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/package-lock.json @@ -0,0 +1,634 @@ +{ + "name": "avalonia.web", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "avalonia.web", + "devDependencies": { + "@types/emscripten": "^1.39.6", + "@types/wicg-file-system-access": "^2020.9.5", + "esbuild": "^0.15.7", + "typescript": "^4.7.4" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", + "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", + "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/emscripten": { + "version": "1.39.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", + "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", + "dev": true + }, + "node_modules/@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.9.tgz", + "integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.9", + "@esbuild/linux-loong64": "0.15.9", + "esbuild-android-64": "0.15.9", + "esbuild-android-arm64": "0.15.9", + "esbuild-darwin-64": "0.15.9", + "esbuild-darwin-arm64": "0.15.9", + "esbuild-freebsd-64": "0.15.9", + "esbuild-freebsd-arm64": "0.15.9", + "esbuild-linux-32": "0.15.9", + "esbuild-linux-64": "0.15.9", + "esbuild-linux-arm": "0.15.9", + "esbuild-linux-arm64": "0.15.9", + "esbuild-linux-mips64le": "0.15.9", + "esbuild-linux-ppc64le": "0.15.9", + "esbuild-linux-riscv64": "0.15.9", + "esbuild-linux-s390x": "0.15.9", + "esbuild-netbsd-64": "0.15.9", + "esbuild-openbsd-64": "0.15.9", + "esbuild-sunos-64": "0.15.9", + "esbuild-windows-32": "0.15.9", + "esbuild-windows-64": "0.15.9", + "esbuild-windows-arm64": "0.15.9" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.9.tgz", + "integrity": "sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.9.tgz", + "integrity": "sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.9.tgz", + "integrity": "sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.9.tgz", + "integrity": "sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.9.tgz", + "integrity": "sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.9.tgz", + "integrity": "sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.9.tgz", + "integrity": "sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.9.tgz", + "integrity": "sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.9.tgz", + "integrity": "sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.9.tgz", + "integrity": "sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.9.tgz", + "integrity": "sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.9.tgz", + "integrity": "sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.9.tgz", + "integrity": "sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.9.tgz", + "integrity": "sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.9.tgz", + "integrity": "sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.9.tgz", + "integrity": "sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.9.tgz", + "integrity": "sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.9.tgz", + "integrity": "sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.9.tgz", + "integrity": "sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.9.tgz", + "integrity": "sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/typescript": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", + "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", + "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", + "dev": true, + "optional": true + }, + "@types/emscripten": { + "version": "1.39.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", + "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", + "dev": true + }, + "@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", + "dev": true + }, + "esbuild": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.9.tgz", + "integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.15.9", + "@esbuild/linux-loong64": "0.15.9", + "esbuild-android-64": "0.15.9", + "esbuild-android-arm64": "0.15.9", + "esbuild-darwin-64": "0.15.9", + "esbuild-darwin-arm64": "0.15.9", + "esbuild-freebsd-64": "0.15.9", + "esbuild-freebsd-arm64": "0.15.9", + "esbuild-linux-32": "0.15.9", + "esbuild-linux-64": "0.15.9", + "esbuild-linux-arm": "0.15.9", + "esbuild-linux-arm64": "0.15.9", + "esbuild-linux-mips64le": "0.15.9", + "esbuild-linux-ppc64le": "0.15.9", + "esbuild-linux-riscv64": "0.15.9", + "esbuild-linux-s390x": "0.15.9", + "esbuild-netbsd-64": "0.15.9", + "esbuild-openbsd-64": "0.15.9", + "esbuild-sunos-64": "0.15.9", + "esbuild-windows-32": "0.15.9", + "esbuild-windows-64": "0.15.9", + "esbuild-windows-arm64": "0.15.9" + } + }, + "esbuild-android-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.9.tgz", + "integrity": "sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.9.tgz", + "integrity": "sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.9.tgz", + "integrity": "sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.9.tgz", + "integrity": "sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.9.tgz", + "integrity": "sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.9.tgz", + "integrity": "sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.9.tgz", + "integrity": "sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.9.tgz", + "integrity": "sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.9.tgz", + "integrity": "sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.9.tgz", + "integrity": "sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.9.tgz", + "integrity": "sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.9.tgz", + "integrity": "sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.9.tgz", + "integrity": "sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.9.tgz", + "integrity": "sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.9.tgz", + "integrity": "sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.9.tgz", + "integrity": "sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.9.tgz", + "integrity": "sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.9.tgz", + "integrity": "sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.9.tgz", + "integrity": "sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.9.tgz", + "integrity": "sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==", + "dev": true, + "optional": true + }, + "typescript": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "dev": true + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/package.json b/src/Web/Avalonia.Web/webapp/package.json new file mode 100644 index 0000000000..ca029ce534 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/package.json @@ -0,0 +1,13 @@ +{ + "name": "avalonia.web", + "scripts": { + "prebuild": "node ./node_modules/typescript/bin/tsc -noEmit", + "build": "node build.js" + }, + "devDependencies": { + "@types/emscripten": "^1.39.6", + "@types/wicg-file-system-access": "^2020.9.5", + "typescript": "^4.7.4", + "esbuild": "^0.15.7" + } +} diff --git a/src/Web/Avalonia.Web/webapp/tsconfig.json b/src/Web/Avalonia.Web/webapp/tsconfig.json new file mode 100644 index 0000000000..ad0e727150 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "es2020", + "strict": true, + "sourceMap": true, + "noEmitOnError": true, + "isolatedModules": true, // we need it for esbuild + "lib": [ + "dom", + "es2016", + "esnext.asynciterable" + ] + }, + "exclude": [ + "node_modules" + ] + } + \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts new file mode 100644 index 0000000000..bc2068cb59 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts @@ -0,0 +1,249 @@ +// See https://raw.githubusercontent.com/dotnet/runtime/main/src/mono/wasm/runtime/dotnet.d.ts + +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. +//! +//! This is generated file, see src/mono/wasm/runtime/rollup.config.js + +//! This is not considered public API with backward compatibility guarantees. + +interface DotnetHostBuilder { + withConfig(config: MonoConfig): DotnetHostBuilder; + withConfigSrc(configSrc: string): DotnetHostBuilder; + withApplicationArguments(...args: string[]): DotnetHostBuilder; + withEnvironmentVariable(name: string, value: string): DotnetHostBuilder; + withEnvironmentVariables(variables: { + [i: string]: string; + }): DotnetHostBuilder; + withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder; + withDiagnosticTracing(enabled: boolean): DotnetHostBuilder; + withDebugging(level: number): DotnetHostBuilder; + withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; + withApplicationArgumentsFromQuery(): DotnetHostBuilder; + create(): Promise; + run(): Promise; +} + +declare interface NativePointer { + __brandNativePointer: "NativePointer"; +} +declare interface VoidPtr extends NativePointer { + __brand: "VoidPtr"; +} +declare interface CharPtr extends NativePointer { + __brand: "CharPtr"; +} +declare interface Int32Ptr extends NativePointer { + __brand: "Int32Ptr"; +} +declare interface EmscriptenModule { + HEAP8: Int8Array; + HEAP16: Int16Array; + HEAP32: Int32Array; + HEAPU8: Uint8Array; + HEAPU16: Uint16Array; + HEAPU32: Uint32Array; + HEAPF32: Float32Array; + HEAPF64: Float64Array; + _malloc(size: number): VoidPtr; + _free(ptr: VoidPtr): void; + print(message: string): void; + printErr(message: string): void; + ccall(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T; + cwrap(ident: string, returnType: string, argTypes?: string[], opts?: any): T; + cwrap(ident: string, ...args: any[]): T; + setValue(ptr: VoidPtr, value: number, type: string, noSafe?: number | boolean): void; + setValue(ptr: Int32Ptr, value: number, type: string, noSafe?: number | boolean): void; + getValue(ptr: number, type: string, noSafe?: number | boolean): number; + UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string; + UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; + FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; + FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; + FS_readFile(filename: string, opts: any): any; + removeRunDependency(id: string): void; + addRunDependency(id: string): void; + stackSave(): VoidPtr; + stackRestore(stack: VoidPtr): void; + stackAlloc(size: number): VoidPtr; + ready: Promise; + instantiateWasm?: InstantiateWasmCallBack; + preInit?: (() => any)[] | (() => any); + preRun?: (() => any)[] | (() => any); + onRuntimeInitialized?: () => any; + postRun?: (() => any)[] | (() => any); + onAbort?: { + (error: any): void; + }; +} +declare type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void; +declare type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any; +declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; + +declare type MonoConfig = { + /** + * The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. + */ + assemblyRootFolder?: string; + /** + * A list of assets to load along with the runtime. + */ + assets?: AssetEntry[]; + /** + * Additional search locations for assets. + */ + remoteSources?: string[]; + /** + * It will not fail the startup is .pdb files can't be downloaded + */ + ignorePdbLoadErrors?: boolean; + /** + * We are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome. The default value is 16. + */ + maxParallelDownloads?: number; + /** + * Name of the assembly with main entrypoint + */ + mainAssemblyName?: string; + /** + * Configures the runtime's globalization mode + */ + globalizationMode?: GlobalizationMode; + /** + * debugLevel > 0 enables debugging and sets the debug log level to debugLevel + * debugLevel == 0 disables debugging and enables interpreter optimizations + * debugLevel < 0 enabled debugging and disables debug logging. + */ + debugLevel?: number; + /** + * Enables diagnostic log messages during startup + */ + diagnosticTracing?: boolean; + /** + * Dictionary-style Object containing environment variables + */ + environmentVariables?: { + [i: string]: string; + }; + /** + * initial number of workers to add to the emscripten pthread pool + */ + pthreadPoolSize?: number; +}; +interface ResourceRequest { + name: string; + behavior: AssetBehaviours; + resolvedUrl?: string; + hash?: string; +} +interface LoadingResource { + name: string; + url: string; + response: Promise; +} +interface AssetEntry extends ResourceRequest { + /** + * If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded. + */ + virtualPath?: string; + /** + * Culture code + */ + culture?: string; + /** + * If true, an attempt will be made to load the asset from each location in MonoConfig.remoteSources. + */ + loadRemote?: boolean; + /** + * If true, the runtime startup would not fail if the asset download was not successful. + */ + isOptional?: boolean; + /** + * If provided, runtime doesn't have to fetch the data. + * Runtime would set the buffer to null after instantiation to free the memory. + */ + buffer?: ArrayBuffer; + /** + * It's metadata + fetch-like Promise + * If provided, the runtime doesn't have to initiate the download. It would just await the response. + */ + pendingDownload?: LoadingResource; +} +declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads"; +declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". +"invariant" | // operate in invariant globalization mode. +"auto"; +declare type DotnetModuleConfig = { + disableDotnet6Compatibility?: boolean; + config?: MonoConfig; + configSrc?: string; + onConfigLoaded?: (config: MonoConfig) => void | Promise; + onDotnetReady?: () => void | Promise; + imports?: any; + exports?: string[]; + downloadResource?: (request: ResourceRequest) => LoadingResource | undefined; +} & Partial; +declare type APIType = { + runMain: (mainAssemblyName: string, args: string[]) => Promise; + runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise; + setEnvironmentVariable: (name: string, value: string) => void; + getAssemblyExports(assemblyName: string): Promise; + setModuleImports(moduleName: string, moduleImports: any): void; + getConfig: () => MonoConfig; + setHeapB32: (offset: NativePointer, value: number | boolean) => void; + setHeapU8: (offset: NativePointer, value: number) => void; + setHeapU16: (offset: NativePointer, value: number) => void; + setHeapU32: (offset: NativePointer, value: NativePointer | number) => void; + setHeapI8: (offset: NativePointer, value: number) => void; + setHeapI16: (offset: NativePointer, value: number) => void; + setHeapI32: (offset: NativePointer, value: number) => void; + setHeapI52: (offset: NativePointer, value: number) => void; + setHeapU52: (offset: NativePointer, value: number) => void; + setHeapI64Big: (offset: NativePointer, value: bigint) => void; + setHeapF32: (offset: NativePointer, value: number) => void; + setHeapF64: (offset: NativePointer, value: number) => void; + getHeapB32: (offset: NativePointer) => boolean; + getHeapU8: (offset: NativePointer) => number; + getHeapU16: (offset: NativePointer) => number; + getHeapU32: (offset: NativePointer) => number; + getHeapI8: (offset: NativePointer) => number; + getHeapI16: (offset: NativePointer) => number; + getHeapI32: (offset: NativePointer) => number; + getHeapI52: (offset: NativePointer) => number; + getHeapU52: (offset: NativePointer) => number; + getHeapI64Big: (offset: NativePointer) => bigint; + getHeapF32: (offset: NativePointer) => number; + getHeapF64: (offset: NativePointer) => number; +}; +declare type RuntimeAPI = { + /** + * @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts + */ + MONO: any; + /** + * @deprecated Please use API object instead. See also BINDINGType in dotnet-legacy.d.ts + */ + BINDING: any; + INTERNAL: any; + Module: EmscriptenModule; + runtimeId: number; + runtimeBuildInfo: { + productVersion: string; + gitHash: string; + buildConfiguration: string; + }; +} & APIType; +declare type ModuleAPI = { + dotnet: DotnetHostBuilder; + exit: (code: number, reason?: any) => void; +}; +declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)): Promise; +declare type CreateDotnetRuntimeType = typeof createDotnetRuntime; + +declare global { + function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined; +} + +declare const dotnet: ModuleAPI["dotnet"]; +declare const exit: ModuleAPI["exit"]; + +export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; \ No newline at end of file diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js new file mode 100644 index 0000000000..8a44381e52 --- /dev/null +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -0,0 +1,122 @@ +var __async = (__this, __arguments, generator) => { + return new Promise((resolve, reject) => { + var fulfilled = (value) => { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + }; + var rejected = (value) => { + try { + step(generator.throw(value)); + } catch (e) { + reject(e); + } + }; + var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); + step((generator = generator.apply(__this, __arguments)).next()); + }); +}; + +// modules/avalonia/canvas.ts +var Canvas = class { + constructor(useGL, element) { + this.renderLoopEnabled = false; + this.renderLoopRequest = 0; + if (useGL) { + const ctx = Canvas.createWebGLContext(element); + if (!ctx) { + console.error(`Failed to create WebGL context: err ${ctx}`); + return; + } + GL.makeContextCurrent(ctx); + const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { + context: ctx, + fboId: fbo ? fbo.id : 0, + stencil: GLctx.getParameter(GLctx.STENCIL_BITS), + sample: 0, + depth: GLctx.getParameter(GLctx.DEPTH_BITS) + }; + } + } + static initGL(element, elementId) { + console.log("inside initGL"); + var view = Canvas.init(true, element, elementId); + if (!view || !view.glInfo) + return null; + return view.glInfo; + } + static init(useGL, element, elementId) { + var htmlCanvas = element; + if (!htmlCanvas) { + console.error(`No canvas element was provided.`); + return null; + } + if (!Canvas.elements) + Canvas.elements = /* @__PURE__ */ new Map(); + Canvas.elements.set(elementId, element); + const view = new Canvas(useGL, element); + htmlCanvas.SKHtmlCanvas = view; + return view; + } + static Foo(canvas) { + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#FF0000"; + ctx.fillRect(0, 0, 150, 75); + } + static createWebGLContext(htmlCanvas) { + const contextAttributes = { + alpha: 1, + depth: 1, + stencil: 8, + antialias: 0, + premultipliedAlpha: 1, + preserveDrawingBuffer: 0, + preferLowPowerToHighPerformance: 0, + failIfMajorPerformanceCaveat: 0, + majorVersion: 2, + minorVersion: 0, + enableExtensionsByDefault: 1, + explicitSwapControl: 0, + renderViaOffscreenBackBuffer: 1 + }; + var context = htmlCanvas.getContext("webgl2", contextAttributes); + let ctx = 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; + } +}; + +// modules/avalonia/runtime.ts +var AvaloniaRuntime = class { + constructor(dotnetAssembly, api) { + this.dotnetAssembly = dotnetAssembly; + api.setModuleImports("avalonia.ts", { + Canvas + }); + } + createAvaloniaView(element) { + const canvas = document.createElement("canvas"); + element.appendChild(canvas); + this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); + } +}; + +// modules/avalonia.ts +function createAvaloniaRuntime(api) { + return __async(this, null, function* () { + const dotnetAssembly = yield api.getAssemblyExports("Avalonia.Web.dll"); + return new AvaloniaRuntime(dotnetAssembly, api); + }); +} +export { + createAvaloniaRuntime +}; +//# sourceMappingURL=avalonia.js.map diff --git a/src/Web/Avalonia.Web/wwwroot/storage.js b/src/Web/Avalonia.Web/wwwroot/storage.js new file mode 100644 index 0000000000..a2042aaa11 --- /dev/null +++ b/src/Web/Avalonia.Web/wwwroot/storage.js @@ -0,0 +1,10 @@ +// modules/storage.ts +var StorageProvider = class { + static isFileApiSupported() { + return globalThis.showOpenFilePicker !== void 0; + } +}; +export { + StorageProvider +}; +//# sourceMappingURL=storage.js.map From 08336ae4b3c5fb11783355e59c14a438d2f333cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 17:54:30 +0100 Subject: [PATCH 010/103] add js emscripten injection. --- .../Avalonia.Web.Sample/Avalonia.Web.Sample.csproj | 5 ++++- src/Web/Avalonia.Web.Sample/Program.cs | 13 +++++++++++++ src/Web/Avalonia.Web.Sample/interop.js | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Web/Avalonia.Web.Sample/interop.js diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 8f5912a16f..80be694741 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -1,4 +1,4 @@ - + net7.0 browser-wasm @@ -6,6 +6,9 @@ Exe true true + true + -sVERBOSE + --js-library="C:\Users\User\repos\Avalonia\src\Web\Avalonia.Web.Sample\interop.js" diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index b880d61ab5..93516e65ed 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; using Avalonia.Web; @@ -6,10 +7,22 @@ using Avalonia.Web; internal class Program { + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject example_initialize(); private static void Main(string[] args) { Console.WriteLine("Hello, Browser!"); + example_initialize(); + + Console.WriteLine(); + + + foreach(var arg in args) + { + Console.WriteLine(arg); + } AvaloniaRuntime.Init(); } } diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web.Sample/interop.js new file mode 100644 index 0000000000..2358c357d3 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/interop.js @@ -0,0 +1,13 @@ +var LibraryExample = { + // Internal functions + $EXAMPLE: { + internal_func: function () { + } + }, + example_initialize: function () { + window["avalonia-helper-GL"] = GL + } +} + +autoAddDeps(LibraryExample, '$EXAMPLE') +mergeInto(LibraryManager.library, LibraryExample) From e3e0f3caef6d430ebeb67ba991ca9954bd12b6da Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 18:46:13 +0100 Subject: [PATCH 011/103] working webgl and GRGl context initialisation. --- src/Web/Avalonia.Web.Sample/Program.cs | 5 +---- src/Web/Avalonia.Web.Sample/interop.js | 2 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 5 ++++- .../Avalonia.Web/webapp/modules/avalonia/canvas.ts | 13 +++++++------ src/Web/Avalonia.Web/wwwroot/avalonia.js | 4 +++- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 93516e65ed..3ad6db15dc 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -8,14 +8,11 @@ using Avalonia.Web; internal class Program { - [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - static extern JSObject example_initialize(); + private static void Main(string[] args) { Console.WriteLine("Hello, Browser!"); - example_initialize(); - Console.WriteLine(); diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web.Sample/interop.js index 2358c357d3..b75212e7a7 100644 --- a/src/Web/Avalonia.Web.Sample/interop.js +++ b/src/Web/Avalonia.Web.Sample/interop.js @@ -5,7 +5,7 @@ var LibraryExample = { } }, example_initialize: function () { - window["avalonia-helper-GL"] = GL + globalThis.AvaloniaGL = GL } } diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index c8ff75a77f..d5bcd7dd98 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -9,10 +9,13 @@ public partial class AvaloniaRuntime { public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject example_initialize(); + [JSExport] internal static void StartAvaloniaView(JSObject canvas) { - Init(); + example_initialize(); // setup, get gl context... var info = InitGL(canvas, "testCanvas"); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index c8acf5e428..2e1443074d 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -1,6 +1,4 @@ -declare let GL: any; -declare let GLctx: WebGLRenderingContext; -declare let Module: EmscriptenModule; +declare let Module: EmscriptenModule; type SKGLViewInfo = { context: WebGLRenderingContext | WebGL2RenderingContext | undefined; @@ -64,9 +62,13 @@ export class Canvas { return; } + var GL = (globalThis as any).AvaloniaGL; + // make current GL.makeContextCurrent(ctx); + var GLctx = GL.currentContext.GLctx as WebGLRenderingContext; + // read values const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); this.glInfo = { @@ -103,8 +105,7 @@ export class Canvas { renderViaOffscreenBackBuffer: 1, }; - - var context = htmlCanvas.getContext("webgl2", contextAttributes); + var GL = (globalThis as any).AvaloniaGL; let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); if (!ctx && contextAttributes.majorVersion > 1) { @@ -116,4 +117,4 @@ export class Canvas { return ctx; } -} \ No newline at end of file +} diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js index 8a44381e52..94c5e9d254 100644 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -30,7 +30,9 @@ var Canvas = class { console.error(`Failed to create WebGL context: err ${ctx}`); return; } + var GL = globalThis.AvaloniaGL; GL.makeContextCurrent(ctx); + var GLctx = GL.currentContext.GLctx; const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); this.glInfo = { context: ctx, @@ -82,7 +84,7 @@ var Canvas = class { explicitSwapControl: 0, renderViaOffscreenBackBuffer: 1 }; - var context = htmlCanvas.getContext("webgl2", contextAttributes); + var GL = globalThis.AvaloniaGL; let ctx = GL.createContext(htmlCanvas, contextAttributes); if (!ctx && contextAttributes.majorVersion > 1) { console.warn("Falling back to WebGL 1.0"); From 43017d159029d5903c446140eb40b58a726b0535 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 18:50:58 +0100 Subject: [PATCH 012/103] remove and rename code. --- src/Web/Avalonia.Web.Sample/Program.cs | 2 +- src/Web/Avalonia.Web.Sample/interop.js | 2 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 20 ++++---------------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 3ad6db15dc..18a593c9e8 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -14,12 +14,12 @@ internal class Program Console.WriteLine("Hello, Browser!"); Console.WriteLine(); - foreach(var arg in args) { Console.WriteLine(arg); } + AvaloniaRuntime.Init(); } } diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web.Sample/interop.js index b75212e7a7..c7ae3a56c7 100644 --- a/src/Web/Avalonia.Web.Sample/interop.js +++ b/src/Web/Avalonia.Web.Sample/interop.js @@ -4,7 +4,7 @@ var LibraryExample = { internal_func: function () { } }, - example_initialize: function () { + InterceptGLObject: function () { globalThis.AvaloniaGL = GL } } diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index d5bcd7dd98..0d21ae096d 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Avalonia.Web; @@ -10,32 +9,24 @@ public partial class AvaloniaRuntime public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - static extern JSObject example_initialize(); + static extern JSObject InterceptGLObject(); [JSExport] internal static void StartAvaloniaView(JSObject canvas) { - example_initialize(); + InterceptGLObject(); // setup, get gl context... var info = InitGL(canvas, "testCanvas"); - /* var glInfo = new GLInfo( + var glInfo = new GLInfo( info.GetPropertyAsInt32("context"), (uint)info.GetPropertyAsInt32("fboId"), info.GetPropertyAsInt32("stencil"), info.GetPropertyAsInt32("sample"), info.GetPropertyAsInt32("depth")); - Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}");*/ - } - - public static void Init () - { - if (false) - { - SkiaSharp.GRGlInterface.Create(); - } + Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); } [JSImport("Canvas.Foo", "avalonia.ts")] @@ -47,8 +38,5 @@ public partial class AvaloniaRuntime [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); - //[DllImport("__Internal")] - //public - } From 39aa7f513c744809e5cf9930c76aa98ae1eee80a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 19:33:31 +0100 Subject: [PATCH 013/103] move initialisation to managed side. --- src/Web/Avalonia.Web.Sample/Program.cs | 16 +++++++++++++--- src/Web/Avalonia.Web.Sample/main.js | 10 +--------- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 3 +++ .../webapp/modules/avalonia/canvas.ts | 8 ++++++++ .../webapp/modules/avalonia/runtime.ts | 6 +----- src/Web/Avalonia.Web/wwwroot/avalonia.js | 6 +++++- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 18a593c9e8..e03a48ac5e 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,14 +1,18 @@ using System; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; +using System.Xml.Linq; using Avalonia.Web; //using SkiaSharp; -internal class Program +internal partial class Program { - + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject GetElementById(string id); + private static void Main(string[] args) { Console.WriteLine("Hello, Browser!"); @@ -20,7 +24,13 @@ internal class Program Console.WriteLine(arg); } - AvaloniaRuntime.Init(); + var div = GetElementById("out"); + Console.WriteLine("got div"); + + var canvas = AvaloniaRuntime.CreateCanvas(div); + + Console.WriteLine("Created canvas"); + } } diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js index 9973b469c7..9d90db8bd2 100644 --- a/src/Web/Avalonia.Web.Sample/main.js +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { createAvaloniaRuntime } from './avalonia.js' const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,13 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -const avaloniaRuntime = await createAvaloniaRuntime(dotnetRuntime); - -const outDiv = document.getElementById("out"); -avaloniaRuntime.createAvaloniaView(outDiv); - const config = dotnetRuntime.getConfig(); -const exports = await dotnetRuntime.getAssemblyExports(config.mainAssemblyName); -await exports.MyClass.TestDynamicModule(); -await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); \ No newline at end of file +await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 0d21ae096d..d6f4b2dae1 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -29,6 +29,9 @@ public partial class AvaloniaRuntime Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); } + [JSImport("Canvas.createCanvas", "avalonia.js")] + public static partial JSObject CreateCanvas(JSObject container); + [JSImport("Canvas.Foo", "avalonia.ts")] internal static partial void Foo(JSObject canvas); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 2e1443074d..ad39d58a8c 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -23,6 +23,14 @@ export class Canvas { newWidth?: number; newHeight?: number; + public static createCanvas(element: HTMLDivElement): HTMLCanvasElement { + var canvas = document.createElement("canvas"); + + element.appendChild(canvas); + + return canvas; + } + public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { console.log("inside initGL"); var view = Canvas.init(true, element, elementId); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index bd1989e935..61e7b71797 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -15,9 +15,5 @@ export class AvaloniaRuntime { createAvaloniaView(element: HTMLDivElement): void { const canvas = document.createElement("canvas"); element.appendChild(canvas); - - - - this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); } -} \ No newline at end of file +} diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js index 94c5e9d254..896ad807ff 100644 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -43,6 +43,11 @@ var Canvas = class { }; } } + static createCanvas(element) { + var canvas = document.createElement("canvas"); + element.appendChild(canvas); + return canvas; + } static initGL(element, elementId) { console.log("inside initGL"); var view = Canvas.init(true, element, elementId); @@ -107,7 +112,6 @@ var AvaloniaRuntime = class { createAvaloniaView(element) { const canvas = document.createElement("canvas"); element.appendChild(canvas); - this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); } }; From 8efd4bb04d4b2d19a77e31825d8a570b7fa414f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 15:48:51 -0400 Subject: [PATCH 014/103] Fix --- src/Web/Avalonia.Web.Sample/Program.cs | 3 ++- src/Web/Avalonia.Web.Sample/main.js | 3 +++ src/Web/Avalonia.Web/AvaloniaRuntime.cs | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index e03a48ac5e..2d5bb6bec9 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -30,7 +30,8 @@ internal partial class Program var canvas = AvaloniaRuntime.CreateCanvas(div); Console.WriteLine("Created canvas"); - + + AvaloniaRuntime.Foo(canvas); } } diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js index 9d90db8bd2..3683aea181 100644 --- a/src/Web/Avalonia.Web.Sample/main.js +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' +import { createAvaloniaRuntime } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -11,6 +12,8 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); +await createAvaloniaRuntime(dotnetRuntime); + const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index d6f4b2dae1..e5c315af4c 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -29,11 +29,11 @@ public partial class AvaloniaRuntime Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); } - [JSImport("Canvas.createCanvas", "avalonia.js")] + [JSImport("Canvas.createCanvas", "avalonia.ts")] public static partial JSObject CreateCanvas(JSObject container); [JSImport("Canvas.Foo", "avalonia.ts")] - internal static partial void Foo(JSObject canvas); + public static partial void Foo(JSObject canvas); [JSImport("Canvas.initGL", "avalonia.ts")] internal static partial JSObject InitGL(JSObject canvas, string canvasId); From 2a67b7950b79667e1bbe53ab6988b4de5158da25 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 23:18:47 +0100 Subject: [PATCH 015/103] temporarilty trim down control catalog. --- samples/ControlCatalog/MainView.xaml | 217 +----------------------- samples/ControlCatalog/MainView.xaml.cs | 99 +---------- 2 files changed, 4 insertions(+), 312 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7f5a191519..15cdb93896 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,218 +4,7 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - None - BorderOnly - Full - - - - - FluentLight - FluentDark - SimpleLight - SimpleDark - - - - - None - Transparent - Blur - AcrylicBlur - Mica - - - - - LeftToRight - RightToLeft - - - - - - - - + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 7133ddaa6a..52dbd25c8c 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -18,104 +18,7 @@ namespace ControlCatalog { AvaloniaXamlLoader.Load(this); - var sideBar = this.Get("Sidebar"); - - if (AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo().IsDesktop == true) - { - var tabItems = (sideBar.Items as IList); - tabItems?.Add(new TabItem() - { - Header = "Screens", - Content = new ScreenPage() - }); - - } - - var themes = this.Get("Themes"); - themes.SelectionChanged += (sender, e) => - { - if (themes.SelectedItem is CatalogTheme theme) - { - var themeStyle = Application.Current!.Styles[0]; - if (theme == CatalogTheme.FluentLight) - { - if (App.Fluent.Mode != FluentThemeMode.Light) - { - App.Fluent.Mode = FluentThemeMode.Light; - } - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.ColorPickerFluent; - Application.Current.Styles[2] = App.DataGridFluent; - } - else if (theme == CatalogTheme.FluentDark) - { - - if (App.Fluent.Mode != FluentThemeMode.Dark) - { - App.Fluent.Mode = FluentThemeMode.Dark; - } - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.ColorPickerFluent; - Application.Current.Styles[2] = App.DataGridFluent; - } - else if (theme == CatalogTheme.SimpleLight) - { - App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; - Application.Current.Styles[0] = App.SimpleLight; - Application.Current.Styles[1] = App.ColorPickerSimple; - Application.Current.Styles[2] = App.DataGridSimple; - } - else if (theme == CatalogTheme.SimpleDark) - { - App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; - Application.Current.Styles[0] = App.SimpleDark; - Application.Current.Styles[1] = App.ColorPickerSimple; - Application.Current.Styles[2] = App.DataGridSimple; - } - } - }; - - var flowDirections = this.Get("FlowDirection"); - flowDirections.SelectionChanged += (sender, e) => - { - if (flowDirections.SelectedItem is FlowDirection flowDirection) - { - this.FlowDirection = flowDirection; - } - }; - - var decorations = this.Get("Decorations"); - decorations.SelectionChanged += (sender, e) => - { - if (VisualRoot is Window window - && decorations.SelectedItem is SystemDecorations systemDecorations) - { - window.SystemDecorations = systemDecorations; - } - }; - - var transparencyLevels = this.Get("TransparencyLevels"); - IDisposable? backgroundSetter = null, paneBackgroundSetter = null; - transparencyLevels.SelectionChanged += (sender, e) => - { - backgroundSetter?.Dispose(); - paneBackgroundSetter?.Dispose(); - if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected - && selected != WindowTransparencyLevel.None) - { - var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5); - backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); - paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); - } - }; - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - var decorations = this.Get("Decorations"); - if (VisualRoot is Window window) - decorations.SelectedIndex = (int)window.SystemDecorations; + Console.WriteLine("Main view loaded"); } } } From 25878fa83855bf68d82d70e74e41f784cc20a02c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 23:19:11 +0100 Subject: [PATCH 016/103] partially implement wasm backend initialisation. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web.Sample/Program.cs | 24 +- src/Web/Avalonia.Web/Avalonia.Web.csproj | 5 + src/Web/Avalonia.Web/AvaloniaRuntime.cs | 35 ++- src/Web/Avalonia.Web/AvaloniaView.cs | 236 ++++++++++++++++++ .../Avalonia.Web/BlazorSingleViewLifetime.cs | 46 ++++ src/Web/Avalonia.Web/BlazorSkiaGpu.cs | 26 ++ .../BlazorSkiaGpuRenderSession.cs | 37 +++ .../Avalonia.Web/BlazorSkiaGpuRenderTarget.cs | 39 +++ .../Avalonia.Web/BlazorSkiaRasterSurface.cs | 88 +++++++ src/Web/Avalonia.Web/BlazorSkiaSurface.cs | 30 +++ src/Web/Avalonia.Web/IBlazorSkiaSurface.cs | 9 + .../Avalonia.Web/ManualTriggerRenderTimer.cs | 18 ++ src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 217 ++++++++++++++++ src/Web/Avalonia.Web/WinStubs.cs | 52 ++++ src/Web/Avalonia.Web/WindowingPlatform.cs | 110 ++++++++ .../webapp/modules/avalonia/runtime.ts | 5 - src/Web/Avalonia.Web/wwwroot/avalonia.js | 4 - 18 files changed, 948 insertions(+), 34 deletions(-) create mode 100644 src/Web/Avalonia.Web/AvaloniaView.cs create mode 100644 src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpu.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaSurface.cs create mode 100644 src/Web/Avalonia.Web/IBlazorSkiaSurface.cs create mode 100644 src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs create mode 100644 src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs create mode 100644 src/Web/Avalonia.Web/WinStubs.cs create mode 100644 src/Web/Avalonia.Web/WindowingPlatform.cs 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 From 94278e5c3a52071b820b0a5d5c2c03d295fe9ffd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 23:23:58 +0100 Subject: [PATCH 017/103] successful gl canvas init. --- src/Web/Avalonia.Web/AvaloniaView.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index ec462893df..f7af35d2b0 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -60,8 +60,6 @@ namespace Avalonia.Web var canvas = AvaloniaRuntime.CreateCanvas(div); canvas.SetProperty("id", "mycanvas"); - AvaloniaRuntime.Foo(canvas); - _topLevelImpl = new RazorViewTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); From 4b5885fab6f9662996fef949af75b2e8ffdd2764 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:01:30 -0400 Subject: [PATCH 018/103] Add initial InputHelper --- src/Web/Avalonia.Web.Sample/index.html | 2 +- src/Web/Avalonia.Web/AvaloniaView.cs | 8 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 46 +++++++ src/Web/Avalonia.Web/Keycodes.cs | 129 ++++++++++++++++++ src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 11 +- .../webapp/modules/avalonia/input.ts | 107 +++++++++++++++ .../webapp/modules/avalonia/runtime.ts | 4 +- 7 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 src/Web/Avalonia.Web/Interop/InputHelper.cs create mode 100644 src/Web/Avalonia.Web/Keycodes.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 25b92798ad..518abd4a15 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -13,7 +13,7 @@ -
+
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index f7af35d2b0..54652bc7e2 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -13,6 +13,8 @@ using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Web.Blazor; +using Avalonia.Web.Interop; + using SkiaSharp; using static Avalonia.Web.AvaloniaRuntime; @@ -72,8 +74,10 @@ namespace Avalonia.Web _topLevel.Renderer.Start(); - - + InputHelper.SubscribeKeyboardEvents( + div, + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs new file mode 100644 index 0000000000..6856a021f2 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class InputHelper +{ + //[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] + //public static partial void SubscribePointerEvents( + // JSObject htmlElement, + // [JSMarshalAs>] + // Func wheel); + + [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] + public static partial void SubscribeKeyboardEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func keyDown, + [JSMarshalAs>] + Func keyUp); + + [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] + public static partial void SubscribeInputEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func input); + + + [JSImport("InputHelper.clearInput", "avalonia.ts")] + public static partial void ClearInputElement(JSObject htmlElement); + + [JSImport("InputHelper.isInputElement", "avalonia.ts")] + public static partial void IsInputElement(JSObject htmlElement); + + [JSImport("InputHelper.focusElement", "avalonia.ts")] + public static partial void FocusElement(JSObject htmlElement); + + [JSImport("InputHelper.setCursor", "avalonia.ts")] + public static partial void SetCursor(JSObject htmlElement, string kind); + + [JSImport("InputHelper.hide", "avalonia.ts")] + public static partial void HideElement(JSObject htmlElement); + + [JSImport("InputHelper.show", "avalonia.ts")] + public static partial void ShowElement(JSObject htmlElement); +} diff --git a/src/Web/Avalonia.Web/Keycodes.cs b/src/Web/Avalonia.Web/Keycodes.cs new file mode 100644 index 0000000000..d1185f6e45 --- /dev/null +++ b/src/Web/Avalonia.Web/Keycodes.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; + +using Avalonia.Input; + +namespace Avalonia.Web +{ + internal static class Keycodes + { + public static Dictionary 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 } + }; + } +} diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs index 82f4064e1a..2bcc36e687 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -110,14 +110,15 @@ namespace Avalonia.Web.Blazor public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) { - /*if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) + Console.WriteLine($"{type} {code} {key} {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; } } @@ -126,12 +127,12 @@ namespace Avalonia.Web.Blazor if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); return args.Handled; } - }*/ + } return false; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts new file mode 100644 index 0000000000..a6734c8ce0 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -0,0 +1,107 @@ +enum RawInputModifiers +{ + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, + KeyboardMask = Alt | Control | Shift | Meta, + + PenInverted = 512, + PenEraser = 1024, + PenBarrelButton = 2048 +} + +export class InputHelper { + public static subscribeKeyboardEvents( + element: HTMLInputElement, + keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + ) { + const keyDownHandler = (args: Event) => { + const keyArgs = args; + if (keyDownCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + args.preventDefault(); + } + }; + element.addEventListener("keydown", keyDownHandler); + + const keyUpHandler = (args: Event) => { + const keyArgs = args; + if (keyUpCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + args.preventDefault(); + } + }; + element.addEventListener("keyup", keyUpHandler); + + return () => { + element.removeEventListener("keydown", keyDownHandler); + element.removeEventListener("keyup", keyUpHandler); + }; + } + + public static subscribeInputEvents( + element: HTMLInputElement, + inputCallback: (value: string) => boolean + ) { + const inputHandler = (args: Event) => { + if (inputCallback((args as any).value)) { + args.preventDefault(); + } + }; + element.addEventListener("input", inputHandler); + + return () => { + element.removeEventListener("input", inputHandler); + }; + } + + public static clearInput(inputElement: HTMLInputElement) { + inputElement.value = ""; + } + + public static isInputElement(element: HTMLInputElement | HTMLElement): element is HTMLInputElement { + return (element as HTMLInputElement).setSelectionRange !== undefined; + } + + public static focusElement(inputElement: HTMLElement) { + inputElement.focus(); + + if (this.isInputElement(inputElement)) { + (inputElement as HTMLInputElement).setSelectionRange(0, 0); + } + } + + public static setCursor(inputElement: HTMLInputElement, kind: string) { + inputElement.style.cursor = kind; + } + + public static hide(inputElement: HTMLInputElement) { + inputElement.style.display = 'none'; + } + + public static show(inputElement: HTMLInputElement) { + inputElement.style.display = 'block'; + } + + private static getModifiers(args: KeyboardEvent): RawInputModifiers { + var modifiers = RawInputModifiers.None; + + if (args.ctrlKey) + modifiers |= RawInputModifiers.Control; + if (args.altKey) + modifiers |= RawInputModifiers.Alt; + if (args.shiftKey) + modifiers |= RawInputModifiers.Shift; + if (args.metaKey) + modifiers |= RawInputModifiers.Meta; + + return modifiers; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index bb97d0c9b7..2f154aeb3f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -1,6 +1,7 @@ import { RuntimeAPI } from "../../types/dotnet"; import { Canvas } from "./canvas"; +import { InputHelper } from "./input"; export class AvaloniaRuntime { constructor( @@ -8,7 +9,8 @@ export class AvaloniaRuntime { api: RuntimeAPI ) { api.setModuleImports("avalonia.ts", { - Canvas + Canvas, + InputHelper }); } } From 8402e6b5f2f22e2bd571b9d9fe782d1bb5cc1373 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:01:52 -0400 Subject: [PATCH 019/103] Delete generated files --- src/Web/Avalonia.Web/wwwroot/avalonia.js | 124 ----------------------- src/Web/Avalonia.Web/wwwroot/storage.js | 10 -- 2 files changed, 134 deletions(-) delete mode 100644 src/Web/Avalonia.Web/wwwroot/avalonia.js delete mode 100644 src/Web/Avalonia.Web/wwwroot/storage.js diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js deleted file mode 100644 index 8c2721c600..0000000000 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ /dev/null @@ -1,124 +0,0 @@ -var __async = (__this, __arguments, generator) => { - return new Promise((resolve, reject) => { - var fulfilled = (value) => { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - }; - var rejected = (value) => { - try { - step(generator.throw(value)); - } catch (e) { - reject(e); - } - }; - var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); - step((generator = generator.apply(__this, __arguments)).next()); - }); -}; - -// modules/avalonia/canvas.ts -var Canvas = class { - constructor(useGL, element) { - this.renderLoopEnabled = false; - this.renderLoopRequest = 0; - if (useGL) { - const ctx = Canvas.createWebGLContext(element); - if (!ctx) { - console.error(`Failed to create WebGL context: err ${ctx}`); - return; - } - var GL = globalThis.AvaloniaGL; - GL.makeContextCurrent(ctx); - var GLctx = GL.currentContext.GLctx; - const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); - this.glInfo = { - context: ctx, - fboId: fbo ? fbo.id : 0, - stencil: GLctx.getParameter(GLctx.STENCIL_BITS), - sample: 0, - depth: GLctx.getParameter(GLctx.DEPTH_BITS) - }; - } - } - static createCanvas(element) { - var canvas = document.createElement("canvas"); - element.appendChild(canvas); - return canvas; - } - static initGL(element, elementId) { - console.log("inside initGL"); - var view = Canvas.init(true, element, elementId); - if (!view || !view.glInfo) - return null; - return view.glInfo; - } - static init(useGL, element, elementId) { - var htmlCanvas = element; - if (!htmlCanvas) { - console.error(`No canvas element was provided.`); - return null; - } - if (!Canvas.elements) - Canvas.elements = /* @__PURE__ */ new Map(); - Canvas.elements.set(elementId, element); - const view = new Canvas(useGL, element); - htmlCanvas.SKHtmlCanvas = view; - return view; - } - static Foo(canvas) { - const ctx = canvas.getContext("2d"); - ctx.fillStyle = "#FF0000"; - ctx.fillRect(0, 0, 150, 75); - } - static createWebGLContext(htmlCanvas) { - const contextAttributes = { - alpha: 1, - depth: 1, - stencil: 8, - antialias: 0, - premultipliedAlpha: 1, - preserveDrawingBuffer: 0, - preferLowPowerToHighPerformance: 0, - failIfMajorPerformanceCaveat: 0, - majorVersion: 2, - minorVersion: 0, - enableExtensionsByDefault: 1, - explicitSwapControl: 0, - renderViaOffscreenBackBuffer: 1 - }; - var GL = globalThis.AvaloniaGL; - let ctx = 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; - } -}; - -// modules/avalonia/runtime.ts -var AvaloniaRuntime = class { - constructor(dotnetAssembly, api) { - this.dotnetAssembly = dotnetAssembly; - api.setModuleImports("avalonia.ts", { - Canvas - }); - } -}; - -// modules/avalonia.ts -function createAvaloniaRuntime(api) { - return __async(this, null, function* () { - const dotnetAssembly = yield api.getAssemblyExports("Avalonia.Web.dll"); - return new AvaloniaRuntime(dotnetAssembly, api); - }); -} -export { - createAvaloniaRuntime -}; -//# sourceMappingURL=avalonia.js.map diff --git a/src/Web/Avalonia.Web/wwwroot/storage.js b/src/Web/Avalonia.Web/wwwroot/storage.js deleted file mode 100644 index a2042aaa11..0000000000 --- a/src/Web/Avalonia.Web/wwwroot/storage.js +++ /dev/null @@ -1,10 +0,0 @@ -// modules/storage.ts -var StorageProvider = class { - static isFileApiSupported() { - return globalThis.showOpenFilePicker !== void 0; - } -}; -export { - StorageProvider -}; -//# sourceMappingURL=storage.js.map From 03db5ac8986544c1b070fbbee3971baf52caa38c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:02:26 -0400 Subject: [PATCH 020/103] Ignore wwwroot of web project --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 84faae1806..61a3b53de1 100644 --- a/.gitignore +++ b/.gitignore @@ -215,3 +215,4 @@ src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js node_modules src/Web/Avalonia.Web.Blazor/webapp/package-lock.json src/Web/Avalonia.Web.Blazor/wwwroot +src/Web/Avalonia.Web/wwwroot From 722c05acc73e46604306c96c4efba5495b4c718d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:16:57 -0400 Subject: [PATCH 021/103] Add initial stream interop --- src/Web/Avalonia.Web/Interop/StreamHelper.cs | 55 +++++++ .../Storage/BlobReadableStream.cs | 100 +++++++++++++ .../Avalonia.Web/Storage/WriteableStream.cs | 136 ++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 src/Web/Avalonia.Web/Interop/StreamHelper.cs create mode 100644 src/Web/Avalonia.Web/Storage/BlobReadableStream.cs create mode 100644 src/Web/Avalonia.Web/Storage/WriteableStream.cs diff --git a/src/Web/Avalonia.Web/Interop/StreamHelper.cs b/src/Web/Avalonia.Web/Interop/StreamHelper.cs new file mode 100644 index 0000000000..c77d87fbe4 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/StreamHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Storage; + +/// +/// Set of FileSystemWritableFileStream and Blob methods. +/// +internal static partial class StreamHelper +{ + [JSImport("StreamHelper.seek", "avalonia.ts")] + public static partial void Seek(JSObject stream, [JSMarshalAs] long position); + + [JSImport("StreamHelper.truncate", "avalonia.ts")] + public static partial void Truncate(JSObject stream, [JSMarshalAs] long position); + + [JSImport("StreamHelper.write", "avalonia.ts")] + public static partial void Write(JSObject stream, [JSMarshalAs] Span data); + + [JSImport("StreamHelper.write", "avalonia.ts")] + public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); + + [JSImport("StreamHelper.close", "avalonia.ts")] + public static partial void Close(JSObject stream); + + [JSImport("StreamHelper.close", "avalonia.ts")] + public static partial Task CloseAsync(JSObject stream); + + [JSImport("StreamHelper.size", "avalonia.ts")] + [return: JSMarshalAs] + public static partial long Size(JSObject stream); + + [JSImport("StreamHelper.byteLength", "avalonia.ts")] + [return: JSMarshalAs] + public static partial long ByteLength(JSObject stream); + + [JSImport("StreamHelper.sliceToArray", "avalonia.ts")] + [return: JSMarshalAs] + public static partial Span Slice(JSObject stream, [JSMarshalAs] long offset, int count); + + public static async Task> SliceAsync(JSObject stream, long offset, int count) + { + using var buffer = await SliceToBufferAsync(stream, offset, count); + return BufferToArray(buffer); + } + + [JSImport("StreamHelper.slice", "avalonia.ts")] + [return: JSMarshalAs>] + private static partial Task SliceToBufferAsync(JSObject stream, [JSMarshalAs] long offset, int count); + + [JSImport("StreamHelper.toArray", "avalonia.ts")] + [return: JSMarshalAs] + private static partial ArraySegment BufferToArray(JSObject stream); +} diff --git a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs new file mode 100644 index 0000000000..6afc5e3054 --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Runtime.InteropServices.JavaScript; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Web.Storage; + +internal class BlobReadableStream : Stream +{ + private JSObject? _jSReference; + private long _position; + private readonly long _length; + + public BlobReadableStream(JSObject jsStreamReference) + { + _jSReference = jsStreamReference; + _position = 0; + _length = StreamHelper.ByteLength(JSReference); + } + + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => _length; + + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override void Flush() { } + + public override long Seek(long offset, SeekOrigin origin) + { + return _position = origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => offset + }; + } + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) + { + var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); + var bytesRead = StreamHelper.Slice(JSReference, _position, numBytesToRead); + if (bytesRead.Length != numBytesToRead) + { + throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); + } + + _position += bytesRead.Length; + bytesRead.CopyTo(buffer); + + return bytesRead.Length; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); + var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead); + if (bytesRead.Count != numBytesToRead) + { + throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); + } + + _position += bytesRead.Count; + bytesRead.AsMemory().CopyTo(buffer); + + return bytesRead.Count; + } + + protected override void Dispose(bool disposing) + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + jsReference.Dispose(); + } + } +} diff --git a/src/Web/Avalonia.Web/Storage/WriteableStream.cs b/src/Web/Avalonia.Web/Storage/WriteableStream.cs new file mode 100644 index 0000000000..4dce5a4c6c --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/WriteableStream.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Runtime.InteropServices.JavaScript; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Web.Storage; + +// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream +internal sealed class JSWriteableStream : Stream +{ + private JSObject? _jSReference; + + // Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. + private long _length, _position; + + internal JSWriteableStream(JSObject jSReference, long initialLength) + { + _jSReference = jSReference; + _length = initialLength; + } + + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); + + public override bool CanRead => false; + + public override bool CanSeek => true; + + public override bool CanWrite => true; + + public override long Length => _length; + + public override long Position + { + get => _position; + set => Seek(_position, SeekOrigin.Begin); + } + + public override void Flush() + { + // no-op + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + var position = origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => offset + }; + StreamHelper.Seek(JSReference, position); + return position; + } + + public override void SetLength(long value) + { + _length = value; + + // See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate + // If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size + if (_position > _length) + { + _position = _length; + } + + StreamHelper.Truncate(JSReference, value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + StreamHelper.Write(JSReference, buffer.AsSpan(offset, count)); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (offset != 0 || count != buffer.Length) + { + // TODO, we need to pass prepared buffer to the JS + // Can't use ArrayPool as it can return bigger array than requested + // Can't use Span/Memory, as it's not supported by JS interop yet. + // Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?) + buffer = buffer.AsMemory(offset, count).ToArray(); + } + return WriteAsyncInternal(buffer, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken)); + } + + private Task WriteAsyncInternal(byte[] buffer, CancellationToken _) + { + _position += buffer.Length; + + return StreamHelper.WriteAsync(JSReference, buffer); + } + + protected override void Dispose(bool disposing) + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + try + { + StreamHelper.Close(JSReference); + } + finally + { + jsReference.Dispose(); + } + } + } + + public override async ValueTask DisposeAsync() + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + try + { + await StreamHelper.CloseAsync(JSReference); + } + finally + { + jsReference.Dispose(); + } + } + } +} From 77f06aa5299ce17f9eeb6ece6b56a898bf280517 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 09:18:56 +0100 Subject: [PATCH 022/103] remove test function. --- src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index ad39d58a8c..9698e72f82 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -89,13 +89,6 @@ export class Canvas { } } - - static Foo(canvas: HTMLCanvasElement) { - const ctx = canvas.getContext("2d")!; - ctx.fillStyle = "#FF0000"; - ctx.fillRect(0, 0, 150, 75); - } - static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { const contextAttributes = { alpha: 1, From e1a4609233a81c8739111fa398851214e50032fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 10:15:12 +0100 Subject: [PATCH 023/103] render avalonia content. --- samples/ControlCatalog/MainView.xaml | 4 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 17 ++- src/Web/Avalonia.Web/AvaloniaView.cs | 61 +++------- .../webapp/modules/avalonia/canvas.ts | 111 +++++++++++++++--- 4 files changed, 129 insertions(+), 64 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 15cdb93896..55dec57b70 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,7 +4,7 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - + + diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 2580f52666..13b53f6cc4 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -31,11 +31,11 @@ public partial class AvaloniaRuntime } - public static GLInfo InitialiseGL (JSObject canvas) + public static GLInfo InitialiseGL (JSObject canvas, Action renderFrameCallback) { InterceptGLObject(); - var info = InitGL(canvas, "testCanvas"); + var info = InitGL(canvas, "testCanvas", renderFrameCallback); var glInfo = new GLInfo( @@ -48,14 +48,21 @@ public partial class AvaloniaRuntime return glInfo; } + [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] + public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); + [JSImport("Canvas.createCanvas", "avalonia.ts")] public static partial JSObject CreateCanvas(JSObject container); - [JSImport("Canvas.Foo", "avalonia.ts")] - public static partial void Foo(JSObject canvas); + [JSImport("Canvas.setCanvasSize", "avalonia.ts")] + public static partial void SetCanvasSize(JSObject canvas, int height, int width); [JSImport("Canvas.initGL", "avalonia.ts")] - private static partial JSObject InitGL(JSObject canvas, string canvasId); + private static partial JSObject InitGL( + JSObject canvas, + string canvasId, + [JSMarshalAs] + Action renderFrameCallback); [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 index 54652bc7e2..2ee4655e40 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -1,12 +1,6 @@ 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; @@ -14,7 +8,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Web.Blazor; using Avalonia.Web.Interop; - using SkiaSharp; using static Avalonia.Web.AvaloniaRuntime; @@ -55,23 +48,17 @@ namespace Avalonia.Web public AvaloniaView() { - Console.WriteLine("In AvaloniaView"); var div = GetElementById("out"); - Console.WriteLine("got div"); - var canvas = AvaloniaRuntime.CreateCanvas(div); + var canvas = CreateCanvas(div); canvas.SetProperty("id", "mycanvas"); _topLevelImpl = new RazorViewTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); - Console.WriteLine("created toplevel"); - _topLevel.Prepare(); - Console.WriteLine("Prepped"); - _topLevel.Renderer.Start(); InputHelper.SubscribeKeyboardEvents( @@ -87,7 +74,7 @@ namespace Avalonia.Web if (_useGL) { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas); + _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas, OnRenderFrame); Console.WriteLine("jsglinfo created - init gl"); } else @@ -120,22 +107,26 @@ namespace Avalonia.Web // 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); + AvaloniaRuntime.SetCanvasSize(canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - // _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); - // _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); + _topLevelImpl.SetClientSize(_canvasSize, _dpi); - //_sizeWatcher.Start(); - }); + RequestAnimationFrame(canvas, true); } - - internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + private void OnRenderFrame() { - //_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + 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(); } public Control? Content @@ -158,22 +149,6 @@ namespace Avalonia.Web //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 diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 9698e72f82..a6e3c5acb4 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -8,16 +8,16 @@ type SKGLViewInfo = { depth: number; } -type SKHtmlCanvasElement = { - SKHtmlCanvas: Canvas | undefined +type CanvasElement = { + Canvas: Canvas | undefined } & HTMLCanvasElement export class Canvas { static elements: Map; - //htmlCanvas: HTMLCanvasElement; + htmlCanvas: HTMLCanvasElement; glInfo?: SKGLViewInfo; - //renderFrameCallback: DotNet.DotNetObject; + renderFrameCallback: () => void; renderLoopEnabled: boolean = false; renderLoopRequest: number = 0; newWidth?: number; @@ -31,17 +31,17 @@ export class Canvas { return canvas; } - public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { + public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { console.log("inside initGL"); - var view = Canvas.init(true, element, elementId); + var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) return null; return view.glInfo; } - static init(useGL: boolean, element: HTMLCanvasElement, elementId: string): Canvas | null { - var htmlCanvas = element as SKHtmlCanvasElement; + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { + var htmlCanvas = element as CanvasElement; if (!htmlCanvas) { console.error(`No canvas element was provided.`); return null; @@ -51,17 +51,16 @@ export class Canvas { Canvas.elements = new Map(); Canvas.elements.set(elementId, element); - const view = new Canvas(useGL, element); + const view = new Canvas(useGL, element, renderFrameCallback); - htmlCanvas.SKHtmlCanvas = view; + htmlCanvas.Canvas = view; return view; } - - public constructor(useGL: boolean, element: HTMLCanvasElement) { - //this.htmlCanvas = element; - //this.renderFrameCallback = callback; + public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) { + this.htmlCanvas = element; + this.renderFrameCallback = renderFrameCallback; if (useGL) { const ctx = Canvas.createWebGLContext(element); @@ -79,6 +78,7 @@ export class Canvas { // read values const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { context: ctx, fboId: fbo ? fbo.id : 0, @@ -89,6 +89,88 @@ export class Canvas { } } + 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 requestAnimationFrame(renderLoop?: boolean) { + // optionally update the render loop + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) + this.setEnableRenderLoop(renderLoop); + + // skip because we have a render loop + if (this.renderLoopRequest !== 0) + return; + + // add the draw to the next frame + this.renderLoopRequest = window.requestAnimationFrame(() => { + if (this.glInfo) { + var GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + + if (this.htmlCanvas.width != this.newWidth) { + this.htmlCanvas.width = this.newWidth || 0; + } + + if (this.htmlCanvas.height != this.newHeight) { + this.htmlCanvas.height = this.newHeight || 0; + } + + this.renderFrameCallback(); + this.renderLoopRequest = 0; + + // we may want to draw the next frame + if (this.renderLoopEnabled) + this.requestAnimationFrame(); + }); + } + + public setCanvasSize(width: number, height: number) { + this.newWidth = width; + this.newHeight = height; + + if (this.htmlCanvas.width != this.newWidth) { + this.htmlCanvas.width = this.newWidth; + } + + if (this.htmlCanvas.height != this.newHeight) { + this.htmlCanvas.height = this.newHeight; + } + + if (this.glInfo) { + var GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + } + + public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) + return; + + htmlCanvas.Canvas.setCanvasSize(width, height); + } + + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) + return; + + htmlCanvas.Canvas.requestAnimationFrame(renderLoop); + } + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { const contextAttributes = { alpha: 1, @@ -109,6 +191,7 @@ export class Canvas { var GL = (globalThis as any).AvaloniaGL; let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); + if (!ctx && contextAttributes.majorVersion > 1) { console.warn('Falling back to WebGL 1.0'); contextAttributes.majorVersion = 1; From 8d6050bf97fbe47044379ca4a7fea1ed96ab66e7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:34:52 +0100 Subject: [PATCH 024/103] implement dpi and size tracking. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web.Sample/css/style.css | 10 ++ src/Web/Avalonia.Web.Sample/index.html | 28 +++-- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 12 ++ src/Web/Avalonia.Web/AvaloniaView.cs | 50 ++++---- src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 15 +-- .../webapp/modules/avalonia/canvas.ts | 110 ++++++++++++++++++ .../webapp/modules/avalonia/runtime.ts | 6 +- 8 files changed, 188 insertions(+), 44 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/css/style.css diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 1d03118993..6446bc2618 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Web/Avalonia.Web.Sample/css/style.css b/src/Web/Avalonia.Web.Sample/css/style.css new file mode 100644 index 0000000000..4c8c72c29d --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/css/style.css @@ -0,0 +1,10 @@ +.avalonia-canvas { + opacity: 1; + background-color: red; + position: fixed; + width: 100vw; + height: 100vh; + top: 0px; + left: 0px; + z-index: 500; +} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 518abd4a15..c67096b3d2 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -4,17 +4,29 @@ - Avalonia.Web.Sample - - - - - + Avalonia.Web.Sample + + + + + -
- + +
+ diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 13b53f6cc4..c7c0f79cab 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -64,6 +64,18 @@ public partial class AvaloniaRuntime [JSMarshalAs] Action renderFrameCallback); + [JSImport("SizeWatcher.observe", "avalonia.ts")] + public static partial JSObject ObserveSize( + JSObject canvas, + string canvasId, + [JSMarshalAs>] + Action onSizeChanged); + + [JSImport("DpiWatcher.start", "avalonia.ts")] + public static partial double ObserveDpi( + [JSMarshalAs>] + Action onDpiChanged); + [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 index 2ee4655e40..de93614162 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -36,8 +36,9 @@ namespace Avalonia.Web private ElementReference _inputElement; private ElementReference _containerElement; private ElementReference _nativeControlsContainer;*/ + private JSObject _canvas; private double _dpi = 1; - private SKSize _canvasSize = new(100, 100); + private Size _canvasSize = new(100.0, 100.0); private GRContext? _context; private GRGlInterface? _glInterface; @@ -50,8 +51,8 @@ namespace Avalonia.Web { var div = GetElementById("out"); - var canvas = CreateCanvas(div); - canvas.SetProperty("id", "mycanvas"); + _canvas = CreateCanvas(div); + _canvas.SetProperty("id", "mycanvas"); _topLevelImpl = new RazorViewTopLevelImpl(this); @@ -66,26 +67,17 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); - //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); + var skiaOptions = AvaloniaLocator.Current.GetService(); + _dpi = ObserveDpi(OnDpiChanged); - var skiaOptions = AvaloniaLocator.Current.GetService(); - _useGL = skiaOptions?.CustomGpuFactory != null; + Console.WriteLine($"Started observing dpi: {_dpi}"); - if (_useGL) - { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas, OnRenderFrame); - Console.WriteLine("jsglinfo created - init gl"); - } - else - { - throw new NotImplementedException(); - //var rasterInitialized = _interop.InitRaster(); - //Console.WriteLine("raster initialized: {0}", rasterInitialized); - } + _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) { + _jsGlInfo = AvaloniaRuntime.InitialiseGL(_canvas, OnRenderFrame); // create the SkiaSharp context if (_context == null) { @@ -93,25 +85,29 @@ namespace Avalonia.Web _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) }; + _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; } else { + //var rasterInitialized = _interop.InitRaster(); + //Console.WriteLine("raster initialized: {0}", rasterInitialized); + //_topLevelImpl.SetSurface(ColorType, - // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - AvaloniaRuntime.SetCanvasSize(canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + AvaloniaRuntime.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); - RequestAnimationFrame(canvas, true); + ObserveSize(_canvas, "mycanvas", OnSizeChanged); + + RequestAnimationFrame(_canvas, true); } private void OnRenderFrame() { @@ -164,13 +160,13 @@ namespace Avalonia.Web } } - private void OnDpiChanged(double newDpi) + private void OnDpiChanged(double oldDpi, double newDpi) { if (Math.Abs(_dpi - newDpi) > 0.0001) { _dpi = newDpi; - //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -178,13 +174,15 @@ namespace Avalonia.Web } } - private void OnSizeChanged(SKSize newSize) + private void OnSizeChanged(int height, int width) { + var newSize = new Size(height, width); + if (_canvasSize != newSize) { _canvasSize = newSize; - //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs index 2bcc36e687..3417dd18e6 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -21,7 +21,6 @@ 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; @@ -44,15 +43,13 @@ namespace Avalonia.Web.Blazor - public void SetClientSize(SKSize size, double dpi) + public void SetClientSize(Size newSize, double dpi) { - var newSize = new Size(size.Width, size.Height); - if (Math.Abs(RenderScaling - dpi) > 0.0001) { - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) { - _currentSurface.Scaling = dpi; + surface.Scaling = dpi; } ScalingChanged?.Invoke(dpi); @@ -62,9 +59,9 @@ namespace Avalonia.Web.Blazor { _clientSize = newSize; - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) { - _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); + surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } Resized?.Invoke(newSize, PlatformResizeReason.User); @@ -192,7 +189,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => _currentSurface?.Scaling ?? 1; + public double RenderScaling => (Surfaces.FirstOrDefault() as BlazorSkiaSurface)?.Scaling ?? 1; public IEnumerable Surfaces { get; set; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index a6e3c5acb4..423689cde9 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -27,6 +27,7 @@ export class Canvas { var canvas = document.createElement("canvas"); element.appendChild(canvas); + canvas.classList.add('avalonia-canvas'); return canvas; } @@ -202,3 +203,112 @@ export class Canvas { return ctx; } } + +type SizeWatcherElement = { + SizeWatcher: SizeWatcherInstance; +} & HTMLElement + +type SizeWatcherInstance = { + callback: (width: number, height: number) => void; +} + +export class SizeWatcher { + static observer: ResizeObserver; + static elements: Map; + + public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void) { + if (!element || !callback) + return; + + //console.info(`Adding size watcher observation with callback ${callback._id}...`); + + SizeWatcher.init(); + + const watcherElement = element as SizeWatcherElement; + watcherElement.SizeWatcher = { + callback: callback + }; + + SizeWatcher.elements.set(elementId, element); + SizeWatcher.observer.observe(element); + + SizeWatcher.invoke(element); + } + + public static unobserve(elementId: string) { + if (!elementId || !SizeWatcher.observer) + return; + + //console.info('Removing size watcher observation...'); + + const element = SizeWatcher.elements.get(elementId)!; + + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } + + static init() { + if (SizeWatcher.observer) + return; + + //console.info('Starting size watcher...'); + + SizeWatcher.elements = new Map(); + 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(element.clientWidth, element.clientHeight); + } +} + +export class DpiWatcher { + static lastDpi: number; + static timerId: number; + static callback: (old: number, newdpi: number) => void; + + public static getDpi() { + return window.devicePixelRatio; + } + + public static start(callback: (old: number, newdpi: number) => void) : 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(lastDpi, currentDpi); + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index 2f154aeb3f..d54583b15e 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -1,5 +1,7 @@ import { RuntimeAPI } from "../../types/dotnet"; +import { SizeWatcher } from "./canvas"; +import { DpiWatcher } from "./canvas"; import { Canvas } from "./canvas"; import { InputHelper } from "./input"; @@ -10,7 +12,9 @@ export class AvaloniaRuntime { ) { api.setModuleImports("avalonia.ts", { Canvas, - InputHelper + InputHelper, + SizeWatcher, + DpiWatcher }); } } From 19ea413e4e15cce34789e1faa4be6e3a34251287 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:36:16 +0100 Subject: [PATCH 025/103] Revert "temporarilty trim down control catalog." This reverts commit 2a67b7950b79667e1bbe53ab6988b4de5158da25. # Conflicts: # samples/ControlCatalog/MainView.xaml --- .../ControlCatalog.NetCore.csproj | 2 +- samples/ControlCatalog/MainView.xaml | 217 +++++++++++++++++- samples/ControlCatalog/MainView.xaml.cs | 99 +++++++- src/Avalonia.Base/Properties/AssemblyInfo.cs | 1 + 4 files changed, 314 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 0667644643..32f199ea8b 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ WinExe - net6.0 + net7.0 true true diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 55dec57b70..7f5a191519 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,7 +4,218 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None + BorderOnly + Full + + + + + FluentLight + FluentDark + SimpleLight + SimpleDark + + + + + None + Transparent + Blur + AcrylicBlur + Mica + + + + + LeftToRight + RightToLeft + + + + + + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 52dbd25c8c..7133ddaa6a 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -18,7 +18,104 @@ namespace ControlCatalog { AvaloniaXamlLoader.Load(this); - Console.WriteLine("Main view loaded"); + var sideBar = this.Get("Sidebar"); + + if (AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo().IsDesktop == true) + { + var tabItems = (sideBar.Items as IList); + tabItems?.Add(new TabItem() + { + Header = "Screens", + Content = new ScreenPage() + }); + + } + + var themes = this.Get("Themes"); + themes.SelectionChanged += (sender, e) => + { + if (themes.SelectedItem is CatalogTheme theme) + { + var themeStyle = Application.Current!.Styles[0]; + if (theme == CatalogTheme.FluentLight) + { + if (App.Fluent.Mode != FluentThemeMode.Light) + { + App.Fluent.Mode = FluentThemeMode.Light; + } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; + } + else if (theme == CatalogTheme.FluentDark) + { + + if (App.Fluent.Mode != FluentThemeMode.Dark) + { + App.Fluent.Mode = FluentThemeMode.Dark; + } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; + } + else if (theme == CatalogTheme.SimpleLight) + { + App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; + Application.Current.Styles[0] = App.SimpleLight; + Application.Current.Styles[1] = App.ColorPickerSimple; + Application.Current.Styles[2] = App.DataGridSimple; + } + else if (theme == CatalogTheme.SimpleDark) + { + App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; + Application.Current.Styles[0] = App.SimpleDark; + Application.Current.Styles[1] = App.ColorPickerSimple; + Application.Current.Styles[2] = App.DataGridSimple; + } + } + }; + + var flowDirections = this.Get("FlowDirection"); + flowDirections.SelectionChanged += (sender, e) => + { + if (flowDirections.SelectedItem is FlowDirection flowDirection) + { + this.FlowDirection = flowDirection; + } + }; + + var decorations = this.Get("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + if (VisualRoot is Window window + && decorations.SelectedItem is SystemDecorations systemDecorations) + { + window.SystemDecorations = systemDecorations; + } + }; + + var transparencyLevels = this.Get("TransparencyLevels"); + IDisposable? backgroundSetter = null, paneBackgroundSetter = null; + transparencyLevels.SelectionChanged += (sender, e) => + { + backgroundSetter?.Dispose(); + paneBackgroundSetter?.Dispose(); + if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected + && selected != WindowTransparencyLevel.None) + { + var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5); + backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + } + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Get("Decorations"); + if (VisualRoot is Window window) + decorations.SelectedIndex = (int)window.SystemDecorations; } } } diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index c8368e6d7a..d5e01087ef 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -30,4 +30,5 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] From 35ca12e9b1156563b15dca9484c917ca07ba8213 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:42:17 +0100 Subject: [PATCH 026/103] add missing stubs. --- src/Web/Avalonia.Web/ClipboardImpl.cs | 35 +++++++++ src/Web/Avalonia.Web/Cursor.cs | 95 +++++++++++++++++++++++ src/Web/Avalonia.Web/WindowingPlatform.cs | 4 +- 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/Web/Avalonia.Web/ClipboardImpl.cs create mode 100644 src/Web/Avalonia.Web/Cursor.cs diff --git a/src/Web/Avalonia.Web/ClipboardImpl.cs b/src/Web/Avalonia.Web/ClipboardImpl.cs new file mode 100644 index 0000000000..c8b2d74c83 --- /dev/null +++ b/src/Web/Avalonia.Web/ClipboardImpl.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Platform; + +namespace Avalonia.Web.Blazor +{ + internal class ClipboardImpl : IClipboard + { + public async Task GetTextAsync() + { + throw new NotImplementedException(); + //return await AvaloniaLocator.Current.GetRequiredService(). + // InvokeAsync("navigator.clipboard.readText"); + } + + public async Task SetTextAsync(string text) + { + throw new NotImplementedException(); + //await AvaloniaLocator.Current.GetRequiredService(). + // InvokeAsync("navigator.clipboard.writeText",text); + } + + public async Task ClearAsync() => await SetTextAsync(""); + + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult(new()); + } +} diff --git a/src/Web/Avalonia.Web/Cursor.cs b/src/Web/Avalonia.Web/Cursor.cs new file mode 100644 index 0000000000..843932bfde --- /dev/null +++ b/src/Web/Avalonia.Web/Cursor.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using Avalonia.Input; +using Avalonia.Platform; + +namespace Avalonia.Web.Blazor +{ + public class CssCursor : ICursorImpl + { + public const string Default = "default"; + public string? Value { get; set; } + + public CssCursor(StandardCursorType type) + { + Value = ToKeyword(type); + } + + /// + /// Create a cursor from base64 image + /// + public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) + { + Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from url to *.cur file. + /// + public CssCursor(string url, StandardCursorType fallback) + { + Value = $"url('{url}'), {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from png/svg and hotspot position + /// + public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback) + { + Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}"; + } + + private static string ToKeyword(StandardCursorType type) => type switch + { + StandardCursorType.Hand => "pointer", + StandardCursorType.Cross => "crosshair", + StandardCursorType.Help => "help", + StandardCursorType.Ibeam => "text", + StandardCursorType.No => "not-allowed", + StandardCursorType.None => "none", + StandardCursorType.Wait => "progress", + StandardCursorType.AppStarting => "wait", + + StandardCursorType.DragMove => "move", + StandardCursorType.DragCopy => "copy", + StandardCursorType.DragLink => "alias", + + StandardCursorType.UpArrow => "default",/*not found matching one*/ + StandardCursorType.SizeWestEast => "ew-resize", + StandardCursorType.SizeNorthSouth => "ns-resize", + StandardCursorType.SizeAll => "move", + + StandardCursorType.TopSide => "n-resize", + StandardCursorType.BottomSide => "s-resize", + StandardCursorType.LeftSide => "w-resize", + StandardCursorType.RightSide => "e-resize", + StandardCursorType.TopLeftCorner => "nw-resize", + StandardCursorType.TopRightCorner => "ne-resize", + StandardCursorType.BottomLeftCorner => "sw-resize", + StandardCursorType.BottomRightCorner => "se-resize", + + _ => Default, + }; + + public void Dispose() {} + } + + internal class CssCursorFactory : ICursorFactory + { + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var imageStream = new MemoryStream(); + cursor.Save(imageStream); + + //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. + var base64String = Convert.ToBase64String(imageStream.ToArray()); + return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); + } + + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) + { + return new CssCursor(cursorType); + } + } +} + diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index 4d52f54690..f9191926ad 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -38,8 +38,8 @@ namespace Avalonia.Web.Blazor var instance = new BlazorWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - //.Bind().ToSingleton() - //.Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(instance) From 31090e4989391377f44f04bf86e85e40d8073656 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 13:30:59 +0100 Subject: [PATCH 027/103] implement mouse events. --- src/Web/Avalonia.Web/AvaloniaView.cs | 80 +++++++++++++++++++ src/Web/Avalonia.Web/Interop/InputHelper.cs | 14 ++++ .../webapp/modules/avalonia/input.ts | 40 ++++++++++ 3 files changed, 134 insertions(+) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index de93614162..15c31ea0fd 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; +using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; @@ -67,6 +68,85 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); + InputHelper.SubscribePointerEvents(_canvas, args => + { + var type = args.GetPropertyAsString("pointertype"); + + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY") + // Twist = args.Twist - read from JS code directly when + }; + + _topLevelImpl.RawPointerEvent(Input.Raw.RawPointerEventType.Move, type!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + + return false; + + }, args => { + + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchBegin, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonDown, + 1 => RawPointerEventType.MiddleButtonDown, + 2 => RawPointerEventType.RightButtonDown, + 3 => RawPointerEventType.XButton1Down, + 4 => RawPointerEventType.XButton2Down, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY") + // Twist = args.Twist - read from JS code directly when + }; + + _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + return false; + }, args => { + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchEnd, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonUp, + 1 => RawPointerEventType.MiddleButtonUp, + 2 => RawPointerEventType.RightButtonUp, + 3 => RawPointerEventType.XButton1Up, + 4 => RawPointerEventType.XButton2Up, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY") + // Twist = args.Twist - read from JS code directly when + }; + + _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + return false; + }); + + var skiaOptions = AvaloniaLocator.Current.GetService(); _dpi = ObserveDpi(OnDpiChanged); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 6856a021f2..43cc0b84a2 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -19,6 +19,20 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyUp); + + + + [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] + public static partial void SubscribePointerEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func pointerMove, + [JSMarshalAs>] + Func pointerDown, + [JSMarshalAs>] + Func pointerUp); + + [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] public static partial void SubscribeInputEvents( JSObject htmlElement, diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index a6734c8ce0..c27955ce11 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -45,6 +45,46 @@ export class InputHelper { element.removeEventListener("keyup", keyUpHandler); }; } + + public static subscribePointerEvents( + element: HTMLInputElement, + pointerMoveCallback: (args: PointerEvent) => boolean, + pointerDownCallback: (args: PointerEvent) => boolean, + pointerUpCallback: (args: PointerEvent) => boolean, + ) { + const pointerMoveHandler = (args: PointerEvent) => { + + if (pointerMoveCallback(args)) { + args.preventDefault(); + } + }; + + const pointerDownHandler = (args: PointerEvent) => { + + if (pointerDownCallback(args)) { + args.preventDefault(); + } + }; + + const pointerUpHandler = (args: PointerEvent) => { + + if (pointerUpCallback(args)) { + args.preventDefault(); + } + }; + + element.addEventListener("pointermove", pointerMoveHandler); + element.addEventListener("pointerdown", pointerDownHandler); + element.addEventListener("pointerup", pointerUpHandler); + + + + return () => { + element.removeEventListener("pointerover", pointerMoveHandler); + element.removeEventListener("pointerdown", pointerDownHandler); + element.removeEventListener("pointerup", pointerUpHandler); + }; + } public static subscribeInputEvents( element: HTMLInputElement, From 8351932bf6635b8075c8a9be69de8f3da6d439c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sat, 24 Sep 2022 14:39:44 +0200 Subject: [PATCH 028/103] Set control adorner from xaml --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/AdornerLayerPage.xaml | 30 ++++++++ .../Pages/AdornerLayerPage.xaml.cs | 19 +++++ .../Primitives/AdornerLayer.cs | 72 +++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 samples/ControlCatalog/Pages/AdornerLayerPage.xaml create mode 100644 samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7f5a191519..ec198c6bba 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -19,6 +19,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml new file mode 100644 index 0000000000..853bae4695 --- /dev/null +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -0,0 +1,30 @@ + + + + + diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs new file mode 100644 index 0000000000..2a9e5bf9ba --- /dev/null +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class AdornerLayerPage : UserControl + { + public AdornerLayerPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 57fb7226e8..51975ccd1a 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -27,6 +27,12 @@ namespace Avalonia.Controls.Primitives public static readonly AttachedProperty IsClipEnabledProperty = AvaloniaProperty.RegisterAttached("IsClipEnabled", true); + /// + /// Allows for getting and setting of the adorner for control. + /// + public static readonly AttachedProperty AdornerProperty = + AvaloniaProperty.RegisterAttached("Adorner"); + private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); @@ -65,6 +71,72 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(IsClipEnabledProperty, isClipEnabled); } + public static Control? GetAdorner(Visual visual) + { + return visual.GetValue(AdornerProperty); + } + + public static void SetAdorner(Visual visual, Control? adorner) + { + visual.SetValue(AdornerProperty, adorner); + + SetVisualAdorner(visual, adorner); + } + + private static void SetVisualAdorner(Visual visual, Control? adorner) + { + var layer = default(AdornerLayer); + + visual.AttachedToVisualTree += (_, _) => + { + layer = AddVisualAdorner(visual, adorner); + }; + + visual.DetachedFromVisualTree += (_, _) => + { + RemoveVisualAdorner(visual, adorner, layer); + }; + } + + private static AdornerLayer? AddVisualAdorner(Visual visual, Control? adorner) + { + if (adorner is null) + { + return null; + } + + var layer = AdornerLayer.GetAdornerLayer(visual); + if (layer == null || layer.Children.Contains(adorner)) + { + return layer; + } + + AdornerLayer.SetAdornedElement(adorner, visual); + AdornerLayer.SetIsClipEnabled(adorner, false); + + ((ISetLogicalParent) adorner).SetParent(visual); + layer.Children.Add(adorner); + + return layer; + } + + private static void RemoveVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer) + { + if (adorner is null) + { + return; + } + + // var layer = AdornerLayer.GetAdornerLayer(visual); + if (layer is null || !layer.Children.Contains(adorner)) + { + return; + } + + layer.Children.Remove(adorner); + ((ISetLogicalParent) adorner).SetParent(null); + } + protected override Size MeasureOverride(Size availableSize) { foreach (var child in Children) From db35037dfb0ce9fe46b311b97635186ea7bc3601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sat, 24 Sep 2022 14:48:23 +0200 Subject: [PATCH 029/103] Add LayoutTransformControl --- .../Pages/AdornerLayerPage.xaml | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 853bae4695..8bf948c58d 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -6,25 +6,49 @@ d:DesignHeight="800" d:DesignWidth="400" mc:Ignorable="d"> - - - + + + + Rotation + + + + + + + + + + + + + + + + + + + + From c19797988cf0535e8c210bc868c5a15e6c839e2e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 19:11:59 +0100 Subject: [PATCH 030/103] allow any div to be turned into a toplevel. --- .../Avalonia.Web.Sample/Avalonia.Web.Sample.csproj | 9 ++++++++- src/Web/Avalonia.Web.Sample/Program.cs | 2 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 2 +- src/Web/Avalonia.Web/AvaloniaView.cs | 7 ++++--- src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs | 13 +++---------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 6446bc2618..68fa9c0003 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -7,10 +7,17 @@ true true true - -sVERBOSE + -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0 --js-library="C:\Users\User\repos\Avalonia\src\Web\Avalonia.Web.Sample\interop.js" + + true + true + -O3 + -O3 + + diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 2a807b8cdf..f101b2e13a 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -18,7 +18,7 @@ internal partial class Program private static void Main(string[] args) { - BuildAvaloniaApp().UseBrowserWasm(); + BuildAvaloniaApp().SetupBrowserApp("out1"); } public static AppBuilder BuildAvaloniaApp() diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index c7c0f79cab..a927b2f2ce 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -35,7 +35,7 @@ public partial class AvaloniaRuntime { InterceptGLObject(); - var info = InitGL(canvas, "testCanvas", renderFrameCallback); + var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); var glInfo = new GLInfo( diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 15c31ea0fd..1aa5a2ef45 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -47,13 +47,14 @@ namespace Avalonia.Web private bool _useGL; private bool _inputElementFocused; + private static int _canvasCount; - public AvaloniaView() + public AvaloniaView(string divId) { - var div = GetElementById("out"); + var div = GetElementById(divId); _canvas = CreateCanvas(div); - _canvas.SetProperty("id", "mycanvas"); + _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); _topLevelImpl = new RazorViewTopLevelImpl(this); diff --git a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs index 04c1117510..011413da1d 100644 --- a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs @@ -19,13 +19,10 @@ namespace Avalonia.Web.Blazor public static partial class WebAppBuilder { - - - public static T UseBrowserWasm( - this T builder) + public static T SetupBrowserApp( + this T builder, string mainDivId) where T : AppBuilderBase, new() { - Console.WriteLine("In UseBrowserWasm"); var lifetime = new BlazorSingleViewLifetime(); return builder @@ -34,13 +31,9 @@ namespace Avalonia.Web.Blazor .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }) .AfterSetup(b => { - var view = new AvaloniaView(); - lifetime.View = view; - Console.WriteLine("After setup"); - + lifetime.View = new AvaloniaView(mainDivId); }) .SetupWithLifetime(lifetime); - } } } From c1a3974fb09ce2379291520ad818a60de85a95f8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 19:23:06 +0100 Subject: [PATCH 031/103] correctly style canvas to fill users div. --- src/Web/Avalonia.Web.Sample/Program.cs | 2 +- src/Web/Avalonia.Web.Sample/index.html | 17 ++++++++--------- src/Web/Avalonia.Web/AvaloniaView.cs | 5 +++++ .../webapp/modules/avalonia/canvas.ts | 2 ++ 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index f101b2e13a..496fac2a7c 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -18,7 +18,7 @@ internal partial class Program private static void Main(string[] args) { - BuildAvaloniaApp().SetupBrowserApp("out1"); + BuildAvaloniaApp().SetupBrowserApp("out"); } public static AppBuilder BuildAvaloniaApp() diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index c67096b3d2..7a3c93eb34 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -14,15 +14,14 @@
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 1aa5a2ef45..14f4337f17 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -53,6 +53,11 @@ namespace Avalonia.Web { var div = GetElementById(divId); + if(div == null) + { + throw new Exception($"div with id: {divId}, was not found in the html document."); + } + _canvas = CreateCanvas(div); _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 423689cde9..43b50294d7 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -25,6 +25,8 @@ export class Canvas { public static createCanvas(element: HTMLDivElement): HTMLCanvasElement { var canvas = document.createElement("canvas"); + canvas.style.setProperty("width", "100%"); + canvas.style.setProperty("height", "100%"); element.appendChild(canvas); canvas.classList.add('avalonia-canvas'); From 42e0cdce372dcb88bdae1bd560c9bba16165b23e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 19:26:25 +0100 Subject: [PATCH 032/103] remove logging. --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 1 - src/Web/Avalonia.Web/AvaloniaView.cs | 4 ---- src/Web/Avalonia.Web/WindowingPlatform.cs | 4 +--- src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts | 1 - 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 87245a201a..333304895a 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -287,7 +287,6 @@ namespace Avalonia.Web.Blazor // create the SkiaSharp context if (_context == null) { - Console.WriteLine("create glcontext"); _glInterface = GRGlInterface.Create(); _context = GRContext.CreateGl(_glInterface); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 14f4337f17..c502edf203 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -157,8 +157,6 @@ namespace Avalonia.Web _dpi = ObserveDpi(OnDpiChanged); - Console.WriteLine($"Started observing dpi: {_dpi}"); - _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) @@ -167,13 +165,11 @@ namespace Avalonia.Web // 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((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index f9191926ad..4dc200f58c 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -29,12 +29,10 @@ namespace Avalonia.Web.Blazor } public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("123 BlazorWindowingPlatform not registered."); + throw new InvalidOperationException("BlazorWindowingPlatform not registered."); public static void Register() { - Console.WriteLine("Registering windowing"); - var instance = new BlazorWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 43b50294d7..fb31eac7b1 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -35,7 +35,6 @@ export class Canvas { } public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { - console.log("inside initGL"); var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) return null; From 50cc71996c5641baf015ad0b63974e84ff964642 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 25 Sep 2022 02:02:55 -0400 Subject: [PATCH 033/103] Reorganize interop, remove "blazor" terminology and add some missing features --- .../Avalonia.Web.Sample.csproj | 4 +- src/Web/Avalonia.Web.Sample/Program.cs | 24 --- src/Web/Avalonia.Web.Sample/css/style.css | 10 -- src/Web/Avalonia.Web.Sample/index.html | 15 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 83 ---------- src/Web/Avalonia.Web/AvaloniaView.cs | 143 ++++++++++++------ .../BlazorSkiaGpuRenderSession.cs | 37 ----- .../Avalonia.Web/BlazorSkiaGpuRenderTarget.cs | 39 ----- ...fetime.cs => BrowserSingleViewLifetime.cs} | 10 +- .../{BlazorSkiaGpu.cs => BrowserSkiaGpu.cs} | 8 +- .../BrowserSkiaGpuRenderSession.cs | 36 +++++ .../BrowserSkiaGpuRenderTarget.cs | 39 +++++ ...Surface.cs => BrowserSkiaRasterSurface.cs} | 6 +- ...orSkiaSurface.cs => BrowserSkiaSurface.cs} | 8 +- ...TopLevelImpl.cs => BrowserTopLevelImpl.cs} | 23 ++- src/Web/Avalonia.Web/ClipboardImpl.cs | 18 +-- src/Web/Avalonia.Web/Cursor.cs | 4 +- ...rSkiaSurface.cs => IBrowserSkiaSurface.cs} | 4 +- src/Web/Avalonia.Web/Interop/CanvasHelper.cs | 42 +++++ src/Web/Avalonia.Web/Interop/DomHelper.cs | 25 +++ src/Web/Avalonia.Web/Interop/InputHelper.cs | 21 +-- .../Avalonia.Web/ManualTriggerRenderTimer.cs | 2 +- src/Web/Avalonia.Web/WinStubs.cs | 6 +- src/Web/Avalonia.Web/WindowingPlatform.cs | 13 +- .../Avalonia.Web/webapp/modules/avalonia.ts | 19 ++- .../webapp/modules/avalonia/canvas.ts | 11 -- .../webapp/modules/avalonia/dom.ts | 51 +++++++ .../webapp/modules/avalonia/input.ts | 20 ++- .../webapp/modules/avalonia/runtime.ts | 20 --- 29 files changed, 373 insertions(+), 368 deletions(-) delete mode 100644 src/Web/Avalonia.Web.Sample/css/style.css delete mode 100644 src/Web/Avalonia.Web/AvaloniaRuntime.cs delete mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs delete mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs rename src/Web/Avalonia.Web/{BlazorSingleViewLifetime.cs => BrowserSingleViewLifetime.cs} (76%) rename src/Web/Avalonia.Web/{BlazorSkiaGpu.cs => BrowserSkiaGpu.cs} (67%) create mode 100644 src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs create mode 100644 src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs rename src/Web/Avalonia.Web/{BlazorSkiaRasterSurface.cs => BrowserSkiaRasterSurface.cs} (93%) rename src/Web/Avalonia.Web/{BlazorSkiaSurface.cs => BrowserSkiaSurface.cs} (65%) rename src/Web/Avalonia.Web/{RazorViewTopLevelImpl.cs => BrowserTopLevelImpl.cs} (88%) rename src/Web/Avalonia.Web/{IBlazorSkiaSurface.cs => IBrowserSkiaSurface.cs} (59%) create mode 100644 src/Web/Avalonia.Web/Interop/CanvasHelper.cs create mode 100644 src/Web/Avalonia.Web/Interop/DomHelper.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts delete mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 68fa9c0003..ca4d29aa21 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -14,6 +14,9 @@ true true + true + true + true -O3 -O3 @@ -26,7 +29,6 @@ - diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 496fac2a7c..233f95852f 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,21 +1,9 @@ -using System; -using System.Reflection.Metadata; -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 { - - [JSImport("globalThis.document.getElementById")] - internal static partial JSObject GetElementById(string id); - private static void Main(string[] args) { BuildAvaloniaApp().SetupBrowserApp("out"); @@ -24,15 +12,3 @@ internal partial class Program public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } - -public partial class MyClass -{ - [JSExport] - internal static async Task TestDynamicModule() - { - await JSHost.ImportAsync("storage.ts", "./storage.js"); - var fileApiSupported = AvaloniaRuntime.IsFileApiSupported(); - - Console.WriteLine("DynamicModule result: " + fileApiSupported); - } -} diff --git a/src/Web/Avalonia.Web.Sample/css/style.css b/src/Web/Avalonia.Web.Sample/css/style.css deleted file mode 100644 index 4c8c72c29d..0000000000 --- a/src/Web/Avalonia.Web.Sample/css/style.css +++ /dev/null @@ -1,10 +0,0 @@ -.avalonia-canvas { - opacity: 1; - background-color: red; - position: fixed; - width: 100vw; - height: 100vh; - top: 0px; - left: 0px; - z-index: 500; -} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 7a3c93eb34..8f6d402651 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -12,19 +12,8 @@ - - -
+ +
diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs deleted file mode 100644 index a927b2f2ce..0000000000 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ /dev/null @@ -1,83 +0,0 @@ -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)] - static extern JSObject InterceptGLObject(); - - [JSExport] - internal static void StartAvaloniaView(JSObject canvas) - { - - // setup, get gl context... - - } - - public static GLInfo InitialiseGL (JSObject canvas, Action renderFrameCallback) - { - InterceptGLObject(); - - var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); - - - var glInfo = new GLInfo( - info.GetPropertyAsInt32("context"), - (uint)info.GetPropertyAsInt32("fboId"), - info.GetPropertyAsInt32("stencil"), - info.GetPropertyAsInt32("sample"), - info.GetPropertyAsInt32("depth")); - - return glInfo; - } - - [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] - public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); - - [JSImport("Canvas.createCanvas", "avalonia.ts")] - public static partial JSObject CreateCanvas(JSObject container); - - [JSImport("Canvas.setCanvasSize", "avalonia.ts")] - public static partial void SetCanvasSize(JSObject canvas, int height, int width); - - [JSImport("Canvas.initGL", "avalonia.ts")] - private static partial JSObject InitGL( - JSObject canvas, - string canvasId, - [JSMarshalAs] - Action renderFrameCallback); - - [JSImport("SizeWatcher.observe", "avalonia.ts")] - public static partial JSObject ObserveSize( - JSObject canvas, - string canvasId, - [JSMarshalAs>] - Action onSizeChanged); - - [JSImport("DpiWatcher.start", "avalonia.ts")] - public static partial double ObserveDpi( - [JSMarshalAs>] - Action onDpiChanged); - - [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 index c502edf203..7df8cb84e1 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -3,41 +3,27 @@ using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; +using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; -using Avalonia.Web.Blazor; using Avalonia.Web.Interop; 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 readonly BrowserTopLevelImpl _topLevelImpl; private EmbeddableControlRoot _topLevel; - // Interop - /*private SKHtmlCanvasInterop? _interop = null; - private SizeWatcherInterop? _sizeWatcher = null; - private DpiWatcherInterop? _dpiWatcher = null;*/ + private readonly JSObject _containerElement; + private readonly JSObject _canvas; + private readonly JSObject _nativeControlsContainer; + private readonly JSObject _inputElement; + 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 JSObject _canvas; private double _dpi = 1; private Size _canvasSize = new(100.0, 100.0); @@ -51,30 +37,48 @@ namespace Avalonia.Web public AvaloniaView(string divId) { - var div = GetElementById(divId); + var host = DomHelper.GetElementById(divId); + if (host == null) + { + throw new Exception($"Element with id {divId} was not found in the html document."); + } - if(div == null) + var hostContent = DomHelper.CreateAvaloniaHost(host); + if (hostContent == null) { - throw new Exception($"div with id: {divId}, was not found in the html document."); + throw new InvalidOperationException("Avalonia WASM host wasn't initialized."); } - _canvas = CreateCanvas(div); + _containerElement = hostContent.GetPropertyAsJSObject("host") + ?? throw new InvalidOperationException("Host cannot be null"); + _canvas = hostContent.GetPropertyAsJSObject("canvas") + ?? throw new InvalidOperationException("Canvas cannot be null"); + _nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost") + ?? throw new InvalidOperationException("NativeHost cannot be null"); + _inputElement = hostContent.GetPropertyAsJSObject("inputElement") + ?? throw new InvalidOperationException("InputElement cannot be null"); + _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); - _topLevelImpl = new RazorViewTopLevelImpl(this); + _topLevelImpl = new BrowserTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); + _topLevelImpl.SetCssCursor = (cursor) => + { + InputHelper.SetCursor(_containerElement, cursor); // macOS + InputHelper.SetCursor(_canvas, cursor); // windows + }; _topLevel.Prepare(); _topLevel.Renderer.Start(); InputHelper.SubscribeKeyboardEvents( - div, - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); + _containerElement, + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier)); - InputHelper.SubscribePointerEvents(_canvas, args => + InputHelper.SubscribePointerEvents(_containerElement, args => { var type = args.GetPropertyAsString("pointertype"); @@ -83,11 +87,11 @@ namespace Avalonia.Web Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY") - // Twist = args.Twist - read from JS code directly when + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") }; - _topLevelImpl.RawPointerEvent(Input.Raw.RawPointerEventType.Move, type!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); return false; @@ -115,14 +119,14 @@ namespace Avalonia.Web Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY") - // Twist = args.Twist - read from JS code directly when + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") }; - _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); return false; }, args => { - var pointerType = args.GetPropertyAsString("pointerType"); + var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; var type = pointerType switch { @@ -144,24 +148,29 @@ namespace Avalonia.Web Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY") - // Twist = args.Twist - read from JS code directly when + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") }; - _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + return false; + }, args => + { + _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); return false; }); var skiaOptions = AvaloniaLocator.Current.GetService(); - _dpi = ObserveDpi(OnDpiChanged); + _dpi = DomHelper.ObserveDpi(OnDpiChanged); _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(_canvas, OnRenderFrame); + _jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame); // create the SkiaSharp context if (_context == null) { @@ -172,7 +181,7 @@ namespace Avalonia.Web _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); } - _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; + _topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; } else { @@ -183,14 +192,50 @@ namespace Avalonia.Web // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - AvaloniaRuntime.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); - ObserveSize(_canvas, "mycanvas", OnSizeChanged); + DomHelper.ObserveSize(_canvas, "mycanvas", OnSizeChanged); + + CanvasHelper.RequestAnimationFrame(_canvas, true); + } + + private static RawInputModifiers GetModifiers(JSObject e) + { + var modifiers = RawInputModifiers.None; + + if (e.GetPropertyAsBoolean("ctrlKey")) + modifiers |= RawInputModifiers.Control; + if (e.GetPropertyAsBoolean("altKey")) + modifiers |= RawInputModifiers.Alt; + if (e.GetPropertyAsBoolean("shiftKey")) + modifiers |= RawInputModifiers.Shift; + if (e.GetPropertyAsBoolean("metaKey")) + modifiers |= RawInputModifiers.Meta; + + var buttons = e.GetPropertyAsInt32("buttons"); + if ((buttons & 1L) == 1) + modifiers |= RawInputModifiers.LeftMouseButton; + + if ((buttons & 2L) == 2) + modifiers |= e.GetPropertyAsString("type") == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton; + + if ((buttons & 4L) == 4) + modifiers |= RawInputModifiers.MiddleMouseButton; - RequestAnimationFrame(_canvas, true); + if ((buttons & 8L) == 8) + modifiers |= RawInputModifiers.XButton1MouseButton; + + if ((buttons & 16L) == 16) + modifiers |= RawInputModifiers.XButton2MouseButton; + + if ((buttons & 32L) == 32) + modifiers |= RawInputModifiers.PenEraser; + + return modifiers; } + private void OnRenderFrame() { if (_useGL && (_jsGlInfo == null)) @@ -213,8 +258,6 @@ namespace Avalonia.Web set => _topLevel.Content = value; } - public bool KeyPreventDefault { get; set; } - internal INativeControlHostImpl GetNativeControlHostImpl() { throw new NotImplementedException(); @@ -248,7 +291,7 @@ namespace Avalonia.Web { _dpi = newDpi; - SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -264,7 +307,7 @@ namespace Avalonia.Web { _canvasSize = newSize; - SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs deleted file mode 100644 index 0c53825131..0000000000 --- a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index fa6a39f210..0000000000 --- a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs +++ /dev/null @@ -1,39 +0,0 @@ -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/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs similarity index 76% rename from src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs rename to src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index 011413da1d..e50c11acd6 100644 --- a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -4,9 +4,9 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; @@ -23,12 +23,12 @@ namespace Avalonia.Web.Blazor this T builder, string mainDivId) where T : AppBuilderBase, new() { - var lifetime = new BlazorSingleViewLifetime(); + var lifetime = new BrowserSingleViewLifetime(); return builder - .UseWindowingSubsystem(BlazorWindowingPlatform.Register) + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }) + .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }) .AfterSetup(b => { lifetime.View = new AvaloniaView(mainDivId); diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web/BrowserSkiaGpu.cs similarity index 67% rename from src/Web/Avalonia.Web/BlazorSkiaGpu.cs rename to src/Web/Avalonia.Web/BrowserSkiaGpu.cs index 7191a00a66..20765b5449 100644 --- a/src/Web/Avalonia.Web/BlazorSkiaGpu.cs +++ b/src/Web/Avalonia.Web/BrowserSkiaGpu.cs @@ -1,17 +1,17 @@ using System.Collections.Generic; using Avalonia.Skia; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class BlazorSkiaGpu : ISkiaGpu + public class BrowserSkiaGpu : ISkiaGpu { public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { foreach (var surface in surfaces) { - if (surface is BlazorSkiaSurface blazorSkiaSurface) + if (surface is BrowserSkiaSurface browserSkiaSurface) { - return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); + return new BrowserSkiaGpuRenderTarget(browserSkiaSurface); } } diff --git a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs new file mode 100644 index 0000000000..022ed20022 --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs @@ -0,0 +1,36 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web +{ + internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession + { + private readonly SKSurface _surface; + + public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget) + { + _surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin, browserSkiaSurface.ColorType); + + GrContext = browserSkiaSurface.Context; + + ScaleFactor = browserSkiaSurface.Scaling; + + SurfaceOrigin = browserSkiaSurface.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/BrowserSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..644ebf19c8 --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs @@ -0,0 +1,39 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web +{ + internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget + { + private readonly GRBackendRenderTarget _renderTarget; + private readonly BrowserSkiaSurface _browserSkiaSurface; + private readonly PixelSize _size; + + public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface) + { + _size = browserSkiaSurface.Size; + + var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); + { + _browserSkiaSurface = browserSkiaSurface; + _renderTarget = new GRBackendRenderTarget( + (int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), + (int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), + browserSkiaSurface.GlInfo.Samples, + browserSkiaSurface.GlInfo.Stencils, glFbInfo); + } + } + + public void Dispose() + { + _renderTarget.Dispose(); + } + + public ISkiaGpuRenderSession BeginRenderingSession() + { + return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget); + } + + public bool IsCorrupted => _browserSkiaSurface.Size != _size; + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs similarity index 93% rename from src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs rename to src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs index a1d58cdda0..0946e72d1d 100644 --- a/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs +++ b/src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs @@ -5,9 +5,9 @@ using Avalonia.Platform; using Avalonia.Skia; using SkiaSharp; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable { public SKColorType ColorType { get; set; } @@ -19,7 +19,7 @@ namespace Avalonia.Web.Blazor private readonly Action _blitCallback; private readonly Action _onDisposeAction; - public BlazorSkiaRasterSurface( + public BrowserSkiaRasterSurface( SKColorType colorType, PixelSize size, double scaling, Action blitCallback) { ColorType = colorType; diff --git a/src/Web/Avalonia.Web/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web/BrowserSkiaSurface.cs similarity index 65% rename from src/Web/Avalonia.Web/BlazorSkiaSurface.cs rename to src/Web/Avalonia.Web/BrowserSkiaSurface.cs index 30dcddc66a..6970cb1f22 100644 --- a/src/Web/Avalonia.Web/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web/BrowserSkiaSurface.cs @@ -1,11 +1,11 @@ +using Avalonia.Web.Interop; using SkiaSharp; -using static Avalonia.Web.AvaloniaRuntime; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal class BlazorSkiaSurface : IBlazorSkiaSurface + internal class BrowserSkiaSurface : IBrowserSkiaSurface { - public BlazorSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) { Context = context; GlInfo = glInfo; diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs similarity index 88% rename from src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs rename to src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index 3417dd18e6..f8ac2a17c0 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -11,14 +11,12 @@ 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 +namespace Avalonia.Web { - internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; private IInputRoot? _inputRoot; @@ -26,9 +24,9 @@ namespace Avalonia.Web.Blazor private readonly AvaloniaView _avaloniaView; private readonly TouchDevice _touchDevice; private readonly PenDevice _penDevice; - //private string _currentCursor = CssCursor.Default; + private string _currentCursor = CssCursor.Default; - public RazorViewTopLevelImpl(AvaloniaView avaloniaView) + public BrowserTopLevelImpl(AvaloniaView avaloniaView) { Surfaces = Enumerable.Empty(); _avaloniaView = avaloniaView; @@ -47,7 +45,7 @@ namespace Avalonia.Web.Blazor { if (Math.Abs(RenderScaling - dpi) > 0.0001) { - if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) + if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { surface.Scaling = dpi; } @@ -59,7 +57,7 @@ namespace Avalonia.Web.Blazor { _clientSize = newSize; - if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) + if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } @@ -107,7 +105,6 @@ namespace Avalonia.Web.Blazor public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) { - Console.WriteLine($"{type} {code} {key} {modifiers}"); if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) { if (_inputRoot is { }) @@ -169,12 +166,12 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl? cursor) { - /* var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; + var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; if (_currentCursor != val) { SetCssCursor?.Invoke(val); _currentCursor = val; - }*/ + } } public IPopupImpl? CreatePopup() @@ -189,7 +186,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => (Surfaces.FirstOrDefault() as BlazorSkiaSurface)?.Scaling ?? 1; + public double RenderScaling => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1; public IEnumerable Surfaces { get; set; } @@ -203,7 +200,7 @@ namespace Avalonia.Web.Blazor public Action? LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); - public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; + public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; public WindowTransparencyLevel TransparencyLevel { get; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } diff --git a/src/Web/Avalonia.Web/ClipboardImpl.cs b/src/Web/Avalonia.Web/ClipboardImpl.cs index c8b2d74c83..793099f55a 100644 --- a/src/Web/Avalonia.Web/ClipboardImpl.cs +++ b/src/Web/Avalonia.Web/ClipboardImpl.cs @@ -1,27 +1,21 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Web.Interop; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { internal class ClipboardImpl : IClipboard { - public async Task GetTextAsync() + public Task GetTextAsync() { - throw new NotImplementedException(); - //return await AvaloniaLocator.Current.GetRequiredService(). - // InvokeAsync("navigator.clipboard.readText"); + return InputHelper.ReadClipboardTextAsync(); } - public async Task SetTextAsync(string text) + public Task SetTextAsync(string text) { - throw new NotImplementedException(); - //await AvaloniaLocator.Current.GetRequiredService(). - // InvokeAsync("navigator.clipboard.writeText",text); + return InputHelper.WriteClipboardTextAsync(text); } public async Task ClearAsync() => await SetTextAsync(""); diff --git a/src/Web/Avalonia.Web/Cursor.cs b/src/Web/Avalonia.Web/Cursor.cs index 843932bfde..5db0bdeda2 100644 --- a/src/Web/Avalonia.Web/Cursor.cs +++ b/src/Web/Avalonia.Web/Cursor.cs @@ -3,7 +3,7 @@ using System.IO; using Avalonia.Input; using Avalonia.Platform; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { public class CssCursor : ICursorImpl { @@ -80,7 +80,7 @@ namespace Avalonia.Web.Blazor { using var imageStream = new MemoryStream(); cursor.Save(imageStream); - + //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. var base64String = Convert.ToBase64String(imageStream.ToArray()); return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); diff --git a/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web/IBrowserSkiaSurface.cs similarity index 59% rename from src/Web/Avalonia.Web/IBlazorSkiaSurface.cs rename to src/Web/Avalonia.Web/IBrowserSkiaSurface.cs index 5463893e27..4b492f6b17 100644 --- a/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web/IBrowserSkiaSurface.cs @@ -1,6 +1,6 @@ -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal interface IBlazorSkiaSurface + internal interface IBrowserSkiaSurface { public PixelSize Size { get; set; } diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs new file mode 100644 index 0000000000..1e93d11c34 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + +internal static partial class CanvasHelper +{ + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject InterceptGLObject(); + + public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback) + { + InterceptGLObject(); + + var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); + + var glInfo = new GLInfo( + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("depth")); + + return glInfo; + } + + [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] + public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); + + [JSImport("Canvas.setCanvasSize", "avalonia.ts")] + public static partial void SetCanvasSize(JSObject canvas, int height, int width); + + [JSImport("Canvas.initGL", "avalonia.ts")] + private static partial JSObject InitGL( + JSObject canvas, + string canvasId, + [JSMarshalAs] Action renderFrameCallback); +} diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs new file mode 100644 index 0000000000..1167d21a6b --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class DomHelper +{ + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject GetElementById(string id); + + [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] + public static partial JSObject CreateAvaloniaHost(JSObject element); + + [JSImport("SizeWatcher.observe", "avalonia.ts")] + public static partial JSObject ObserveSize( + JSObject canvas, + string canvasId, + [JSMarshalAs>] + Action onSizeChanged); + + [JSImport("DpiWatcher.start", "avalonia.ts")] + public static partial double ObserveDpi( + [JSMarshalAs>] + Action onDpiChanged); +} diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 43cc0b84a2..7fb2ba35f4 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -1,16 +1,11 @@ using System; using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; namespace Avalonia.Web.Interop; internal static partial class InputHelper { - //[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] - //public static partial void SubscribePointerEvents( - // JSObject htmlElement, - // [JSMarshalAs>] - // Func wheel); - [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] public static partial void SubscribeKeyboardEvents( JSObject htmlElement, @@ -19,9 +14,6 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyUp); - - - [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] public static partial void SubscribePointerEvents( JSObject htmlElement, @@ -30,7 +22,9 @@ internal static partial class InputHelper [JSMarshalAs>] Func pointerDown, [JSMarshalAs>] - Func pointerUp); + Func pointerUp, + [JSMarshalAs>] + Func wheel); [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] @@ -57,4 +51,11 @@ internal static partial class InputHelper [JSImport("InputHelper.show", "avalonia.ts")] public static partial void ShowElement(JSObject htmlElement); + + + [JSImport("navigator.clipboard.readText")] + public static partial Task ReadClipboardTextAsync(); + + [JSImport("navigator.clipboard.writeText")] + public static partial Task WriteClipboardTextAsync(string text); } diff --git a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs index 505b978a91..774848e19f 100644 --- a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics; using Avalonia.Rendering; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { public class ManualTriggerRenderTimer : IRenderTimer { diff --git a/src/Web/Avalonia.Web/WinStubs.cs b/src/Web/Avalonia.Web/WinStubs.cs index e41f6ca29c..b0961115fe 100644 --- a/src/Web/Avalonia.Web/WinStubs.cs +++ b/src/Web/Avalonia.Web/WinStubs.cs @@ -1,14 +1,10 @@ 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 +namespace Avalonia.Web { internal class IconLoaderStub : IPlatformIconLoader { diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index 4dc200f58c..3f14680241 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -1,17 +1,14 @@ 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 +namespace Avalonia.Web { - public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface { private bool _signaled; private static KeyboardDevice? s_keyboard; @@ -29,11 +26,11 @@ namespace Avalonia.Web.Blazor } public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("BlazorWindowingPlatform not registered."); + throw new InvalidOperationException("BrowserWindowingPlatform not registered."); public static void Register() { - var instance = new BlazorWindowingPlatform(); + var instance = new BrowserWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() @@ -94,7 +91,7 @@ namespace Avalonia.Web.Blazor { get { - return true; // Blazor is single threaded. + return true; // Browser is single threaded. } } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index bf9ef48772..9e5cec838f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -1,7 +1,16 @@ import { RuntimeAPI } from "../types/dotnet"; -import { AvaloniaRuntime } from "./avalonia/runtime"; +import { SizeWatcher } from "./avalonia/canvas"; +import { DpiWatcher } from "./avalonia/canvas"; +import { Canvas } from "./avalonia/canvas"; +import { InputHelper } from "./avalonia/input"; +import { AvaloniaDOM } from "./avalonia/dom"; -export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { - const dotnetAssembly = await api.getAssemblyExports("Avalonia.Web.dll"); - return new AvaloniaRuntime(dotnetAssembly, api); -} \ No newline at end of file +export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { + api.setModuleImports("avalonia.ts", { + Canvas, + InputHelper, + SizeWatcher, + DpiWatcher, + AvaloniaDOM + }); +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index fb31eac7b1..51f851c9c7 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -23,17 +23,6 @@ export class Canvas { newWidth?: number; newHeight?: number; - public static createCanvas(element: HTMLDivElement): HTMLCanvasElement { - var canvas = document.createElement("canvas"); - canvas.style.setProperty("width", "100%"); - canvas.style.setProperty("height", "100%"); - - element.appendChild(canvas); - canvas.classList.add('avalonia-canvas'); - - return canvas; - } - public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts new file mode 100644 index 0000000000..fcb66f80be --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -0,0 +1,51 @@ +export class AvaloniaDOM { + static createAvaloniaHost(host: HTMLElement) { + // Root element + host.classList.add("avalonia-container"); + host.tabIndex = 0; + host.style.touchAction = "none"; + // host.style.position = "relative"; + host.style.position = "fixed"; + host.style.width = "100vw"; + host.style.height = "100vh"; + host.oncontextmenu = function () { return false; }; + + // Rendering target canvas + const canvas = document.createElement("canvas"); + canvas.classList.add('avalonia-canvas'); + canvas.style.backgroundColor = "#ccc"; + canvas.style.width = "100%"; + canvas.style.height = "100%"; + //canvas.style.position = "absolute"; + host.appendChild(canvas); + + // Native controls host + const nativeHost = document.createElement("div"); + nativeHost.classList.add('avalonia-native-host'); + nativeHost.style.width = "100%"; + nativeHost.style.height = "100%"; + nativeHost.style.position = "absolute"; + host.appendChild(nativeHost); + + // IME + const inputElement = document.createElement("input"); + inputElement.autocapitalize = "none"; + inputElement.type = "text"; + inputElement.classList.add('avalonia-input-element'); + inputElement.style.opacity = "0"; + inputElement.style.width = "100%"; + inputElement.style.height = "100%"; + inputElement.style.position = "absolute"; + inputElement.onpaste = function () { return false; }; + inputElement.oncopy = function () { return false; }; + inputElement.oncut = function () { return false; }; + host.appendChild(inputElement); + + return { + host, + canvas, + nativeHost, + inputElement + }; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index c27955ce11..75bdecd71c 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -24,17 +24,15 @@ export class InputHelper { keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, ) { - const keyDownHandler = (args: Event) => { - const keyArgs = args; - if (keyDownCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + const keyDownHandler = (args: KeyboardEvent) => { + if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); } }; element.addEventListener("keydown", keyDownHandler); - const keyUpHandler = (args: Event) => { - const keyArgs = args; - if (keyUpCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + const keyUpHandler = (args: KeyboardEvent) => { + if (keyUpCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); } }; @@ -51,6 +49,7 @@ export class InputHelper { pointerMoveCallback: (args: PointerEvent) => boolean, pointerDownCallback: (args: PointerEvent) => boolean, pointerUpCallback: (args: PointerEvent) => boolean, + wheelCallback: (args: WheelEvent) => boolean, ) { const pointerMoveHandler = (args: PointerEvent) => { @@ -73,9 +72,17 @@ export class InputHelper { } }; + const wheelHandler = (args: WheelEvent) => { + + if (wheelCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("pointermove", pointerMoveHandler); element.addEventListener("pointerdown", pointerDownHandler); element.addEventListener("pointerup", pointerUpHandler); + element.addEventListener("wheel", wheelHandler); @@ -83,6 +90,7 @@ export class InputHelper { element.removeEventListener("pointerover", pointerMoveHandler); element.removeEventListener("pointerdown", pointerDownHandler); element.removeEventListener("pointerup", pointerUpHandler); + element.removeEventListener("wheel", wheelHandler); }; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts deleted file mode 100644 index d54583b15e..0000000000 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { RuntimeAPI } from "../../types/dotnet"; - -import { SizeWatcher } from "./canvas"; -import { DpiWatcher } from "./canvas"; -import { Canvas } from "./canvas"; -import { InputHelper } from "./input"; - -export class AvaloniaRuntime { - constructor( - private dotnetAssembly: any, - api: RuntimeAPI - ) { - api.setModuleImports("avalonia.ts", { - Canvas, - InputHelper, - SizeWatcher, - DpiWatcher - }); - } -} From 710f015b68077856f8154de7cbd3e36e757ea016 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 11:21:41 +0100 Subject: [PATCH 034/103] refactor subscription of pointer events to share code, and preventdefault when handled. --- src/Web/Avalonia.Web/AvaloniaView.cs | 166 ++++++++++---------- src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 16 +- 2 files changed, 94 insertions(+), 88 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 7df8cb84e1..80ef577368 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -78,88 +78,7 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier)); - InputHelper.SubscribePointerEvents(_containerElement, args => - { - var type = args.GetPropertyAsString("pointertype"); - - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); - - return false; - - }, args => { - - var pointerType = args.GetPropertyAsString("pointerType"); - - var type = pointerType switch - { - "touch" => RawPointerEventType.TouchBegin, - _ => args.GetPropertyAsInt32("button") switch - { - 0 => RawPointerEventType.LeftButtonDown, - 1 => RawPointerEventType.MiddleButtonDown, - 2 => RawPointerEventType.RightButtonDown, - 3 => RawPointerEventType.XButton1Down, - 4 => RawPointerEventType.XButton2Down, - // 5 => Pen eraser button, - _ => RawPointerEventType.Move - } - }; - - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); - return false; - }, args => { - var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; - - var type = pointerType switch - { - "touch" => RawPointerEventType.TouchEnd, - _ => args.GetPropertyAsInt32("button") switch - { - 0 => RawPointerEventType.LeftButtonUp, - 1 => RawPointerEventType.MiddleButtonUp, - 2 => RawPointerEventType.RightButtonUp, - 3 => RawPointerEventType.XButton1Up, - 4 => RawPointerEventType.XButton2Up, - // 5 => Pen eraser button, - _ => RawPointerEventType.Move - } - }; - - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); - return false; - }, args => - { - _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); - return false; - }); + InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); var skiaOptions = AvaloniaLocator.Current.GetService(); @@ -196,11 +115,88 @@ namespace Avalonia.Web _topLevelImpl.SetClientSize(_canvasSize, _dpi); - DomHelper.ObserveSize(_canvas, "mycanvas", OnSizeChanged); + DomHelper.ObserveSize(_canvas, _canvas.GetPropertyAsString("id")!, OnSizeChanged); CanvasHelper.RequestAnimationFrame(_canvas, true); } + private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args) + { + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") + }; + + return point; + } + + private bool OnPointerMove(JSObject args) + { + var type = args.GetPropertyAsString("pointertype"); + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnPointerDown(JSObject args) + { + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchBegin, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonDown, + 1 => RawPointerEventType.MiddleButtonDown, + 2 => RawPointerEventType.RightButtonDown, + 3 => RawPointerEventType.XButton1Down, + 4 => RawPointerEventType.XButton2Down, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnPointerUp(JSObject args) + { + var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchEnd, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonUp, + 1 => RawPointerEventType.MiddleButtonUp, + 2 => RawPointerEventType.RightButtonUp, + 3 => RawPointerEventType.XButton1Up, + 4 => RawPointerEventType.XButton2Up, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnWheel(JSObject args) + { + return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); + } + private static RawInputModifiers GetModifiers(JSObject e) { var modifiers = RawInputModifiers.None; @@ -291,7 +287,7 @@ namespace Avalonia.Web { _dpi = newDpi; - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -307,7 +303,7 @@ namespace Avalonia.Web { _canvasSize = newSize; - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index f8ac2a17c0..f9812c4ae8 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -66,7 +66,7 @@ namespace Avalonia.Web } } - public void RawPointerEvent( + public bool RawPointerEvent( RawPointerEventType eventType, string pointerType, RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) { @@ -82,7 +82,11 @@ namespace Avalonia.Web }; input.Invoke(args); + + return args.Handled; } + + return false; } private IPointerDevice GetPointerDevice(string pointerType) @@ -95,12 +99,18 @@ namespace Avalonia.Web }; } - public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) + public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) { if (_inputRoot is { }) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); + var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers); + + Input?.Invoke(args); + + return args.Handled; } + + return false; } public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) From cd0cbedf10859cb0a3727f350399b60c914cb473 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 11:29:32 +0100 Subject: [PATCH 035/103] set canvas height and width properties to resize backing buffer. --- src/Web/Avalonia.Web/AvaloniaView.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 80ef577368..08d4505bd6 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -277,7 +277,7 @@ namespace Avalonia.Web if (_topLevel.Renderer is CompositingRenderer dr) { - //dr.CompositionTarget.ImmediateUIThreadRender(); + dr.CompositionTarget.ImmediateUIThreadRender(); } } @@ -287,7 +287,7 @@ namespace Avalonia.Web { _dpi = newDpi; - //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -303,7 +303,7 @@ namespace Avalonia.Web { _canvasSize = newSize; - //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); From 82aebeeb34846ddf1f3df578b1b208b0992a2de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sun, 25 Sep 2022 13:09:04 +0200 Subject: [PATCH 036/103] Handler Adorner property changes --- .../Primitives/AdornerLayer.cs | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 51975ccd1a..d557424fbb 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -36,9 +36,13 @@ namespace Avalonia.Controls.Primitives private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); + private static readonly AttachedProperty s_savedAdornerLayerProperty = + AvaloniaProperty.RegisterAttached("SavedAdornerLayer"); + static AdornerLayer() { AdornedElementProperty.Changed.Subscribe(AdornedElementChanged); + AdornerProperty.Changed.Subscribe(AdornerChanged); } public AdornerLayer() @@ -79,36 +83,79 @@ namespace Avalonia.Controls.Primitives public static void SetAdorner(Visual visual, Control? adorner) { visual.SetValue(AdornerProperty, adorner); - - SetVisualAdorner(visual, adorner); } - private static void SetVisualAdorner(Visual visual, Control? adorner) + private static void AdornerChanged(AvaloniaPropertyChangedEventArgs e) { - var layer = default(AdornerLayer); - - visual.AttachedToVisualTree += (_, _) => + if (e.Sender is Visual visual) { - layer = AddVisualAdorner(visual, adorner); - }; + var oldAdorner = e.OldValue.GetValueOrDefault(); + var newAdorner = e.NewValue.GetValueOrDefault(); + + if (Equals(oldAdorner, newAdorner)) + { + return; + } + + if (oldAdorner is { }) + { + visual.AttachedToVisualTree -= VisualOnAttachedToVisualTree; + visual.DetachedFromVisualTree -= VisualOnDetachedFromVisualTree; + Detach(visual, oldAdorner); + } - visual.DetachedFromVisualTree += (_, _) => + if (newAdorner is { }) + { + visual.AttachedToVisualTree += VisualOnAttachedToVisualTree; + visual.DetachedFromVisualTree += VisualOnDetachedFromVisualTree; + Attach(visual, newAdorner); + } + } + } + + private static void VisualOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is Visual visual) { - RemoveVisualAdorner(visual, adorner, layer); - }; + var adorner = GetAdorner(visual); + if (adorner is { }) + { + Attach(visual, adorner); + } + } } - private static AdornerLayer? AddVisualAdorner(Visual visual, Control? adorner) + private static void VisualOnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { - if (adorner is null) + if (sender is Visual visual) { - return null; + var adorner = GetAdorner(visual); + if (adorner is { }) + { + Detach(visual, adorner); + } } + } + private static void Attach(Visual visual, Control adorner) + { var layer = AdornerLayer.GetAdornerLayer(visual); - if (layer == null || layer.Children.Contains(adorner)) + AddVisualAdorner(visual, adorner, layer); + visual.SetValue(s_savedAdornerLayerProperty, layer); + } + + private static void Detach(Visual visual, Control adorner) + { + var layer = visual.GetValue(s_savedAdornerLayerProperty); + RemoveVisualAdorner(visual, adorner, layer); + visual.ClearValue(s_savedAdornerLayerProperty); + } + + private static void AddVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer) + { + if (adorner is null || layer == null || layer.Children.Contains(adorner)) { - return layer; + return; } AdornerLayer.SetAdornedElement(adorner, visual); @@ -116,19 +163,11 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent) adorner).SetParent(visual); layer.Children.Add(adorner); - - return layer; } private static void RemoveVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer) { - if (adorner is null) - { - return; - } - - // var layer = AdornerLayer.GetAdornerLayer(visual); - if (layer is null || !layer.Children.Contains(adorner)) + if (adorner is null || layer is null || !layer.Children.Contains(adorner)) { return; } From 272775a3d908e7740f9938985b62d253a4faf89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sun, 25 Sep 2022 13:09:29 +0200 Subject: [PATCH 037/103] Format xaml --- .../Pages/AdornerLayerPage.xaml | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 8bf948c58d..7ddbbbfcf1 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -27,25 +27,25 @@ - + From 4b31690b12c55368181c1726d2975386936f29d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Sun, 25 Sep 2022 13:30:16 +0200 Subject: [PATCH 038/103] Add add / remove adorner buttons for testing --- .../Pages/AdornerLayerPage.xaml | 14 +++++++- .../Pages/AdornerLayerPage.xaml.cs | 33 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml index 7ddbbbfcf1..598844d695 100644 --- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml +++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml @@ -13,6 +13,17 @@ + +