committed by
GitHub
111 changed files with 17689 additions and 517 deletions
@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="1.68.2" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" /> |
|||
<PackageReference Include="SkiaSharp" Version="1.68.2.1" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2.1" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.OpenGlPage" |
|||
xmlns:pages="clr-namespace:ControlCatalog.Pages"> |
|||
<Grid> |
|||
<pages:OpenGlPageControl x:Name="GL"/> |
|||
<StackPanel> |
|||
<TextBlock Text="{Binding #GL.Info}"/> |
|||
</StackPanel> |
|||
<Grid ColumnDefinitions="*,Auto" Margin="20"> |
|||
<StackPanel Grid.Column="1" MinWidth="300"> |
|||
<TextBlock>Yaw</TextBlock> |
|||
<Slider Value="{Binding Yaw, Mode=TwoWay, ElementName=GL}" Maximum="10"/> |
|||
<TextBlock>Pitch</TextBlock> |
|||
<Slider Value="{Binding Pitch, Mode=TwoWay, ElementName=GL}" Maximum="10"/> |
|||
<TextBlock>Roll</TextBlock> |
|||
<Slider Value="{Binding Roll, Mode=TwoWay, ElementName=GL}" Maximum="10"/> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<TextBlock FontWeight="Bold" Foreground="#C000C0">D</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#00C090">I</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#90C000">S</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#C09000">C</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#00C090">O</TextBlock> |
|||
</StackPanel> |
|||
<Slider Value="{Binding Disco, Mode=TwoWay, ElementName=GL}" Maximum="1"/> |
|||
</StackPanel> |
|||
</Grid> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -0,0 +1,401 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.OpenGL; |
|||
using Avalonia.Platform.Interop; |
|||
using Avalonia.Threading; |
|||
using static Avalonia.OpenGL.GlConsts; |
|||
// ReSharper disable StringLiteralTypo
|
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class OpenGlPage : UserControl |
|||
{ |
|||
|
|||
} |
|||
|
|||
public class OpenGlPageControl : OpenGlControlBase |
|||
{ |
|||
private float _yaw; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> YawProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); |
|||
|
|||
public float Yaw |
|||
{ |
|||
get => _yaw; |
|||
set => SetAndRaise(YawProperty, ref _yaw, value); |
|||
} |
|||
|
|||
private float _pitch; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> PitchProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); |
|||
|
|||
public float Pitch |
|||
{ |
|||
get => _pitch; |
|||
set => SetAndRaise(PitchProperty, ref _pitch, value); |
|||
} |
|||
|
|||
|
|||
private float _roll; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> RollProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Roll", o => o.Roll, (o, v) => o.Roll = v); |
|||
|
|||
public float Roll |
|||
{ |
|||
get => _roll; |
|||
set => SetAndRaise(RollProperty, ref _roll, value); |
|||
} |
|||
|
|||
|
|||
private float _disco; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> DiscoProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Disco", o => o.Disco, (o, v) => o.Disco = v); |
|||
|
|||
public float Disco |
|||
{ |
|||
get => _disco; |
|||
set => SetAndRaise(DiscoProperty, ref _disco, value); |
|||
} |
|||
|
|||
private string _info; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, string> InfoProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, string>("Info", o => o.Info, (o, v) => o.Info = v); |
|||
|
|||
public string Info |
|||
{ |
|||
get => _info; |
|||
private set => SetAndRaise(InfoProperty, ref _info, value); |
|||
} |
|||
|
|||
static OpenGlPageControl() |
|||
{ |
|||
AffectsRender<OpenGlPageControl>(YawProperty, PitchProperty, RollProperty, DiscoProperty); |
|||
} |
|||
|
|||
private int _vertexShader; |
|||
private int _fragmentShader; |
|||
private int _shaderProgram; |
|||
private int _vertexBufferObject; |
|||
private int _indexBufferObject; |
|||
private int _vertexArrayObject; |
|||
private GlExtrasInterface _glExt; |
|||
|
|||
private string GetShader(bool fragment, string shader) |
|||
{ |
|||
var version = (GlVersion.Type == GlProfileType.OpenGL ? |
|||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 : |
|||
100); |
|||
var data = "#version " + version + "\n"; |
|||
if (GlVersion.Type == GlProfileType.OpenGLES) |
|||
data += "precision mediump float;\n"; |
|||
if (version >= 150) |
|||
{ |
|||
shader = shader.Replace("attribute", "in"); |
|||
if (fragment) |
|||
shader = shader |
|||
.Replace("varying", "in") |
|||
.Replace("//DECLAREGLFRAG", "out vec4 outFragColor;") |
|||
.Replace("gl_FragColor", "outFragColor"); |
|||
else |
|||
shader = shader.Replace("varying", "out"); |
|||
} |
|||
|
|||
data += shader; |
|||
|
|||
return data; |
|||
} |
|||
|
|||
|
|||
private string VertexShaderSource => GetShader(false, @"
|
|||
attribute vec3 aPos; |
|||
attribute vec3 aNormal; |
|||
uniform mat4 uModel; |
|||
uniform mat4 uProjection; |
|||
uniform mat4 uView; |
|||
|
|||
varying vec3 FragPos; |
|||
varying vec3 VecPos; |
|||
varying vec3 Normal; |
|||
uniform float uTime; |
|||
uniform float uDisco; |
|||
void main() |
|||
{ |
|||
float discoScale = sin(uTime * 10.0) / 10.0; |
|||
float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0; |
|||
|
|||
float scale = 1.0 + uDisco * discoScale; |
|||
|
|||
vec3 scaledPos = aPos; |
|||
scaledPos.x = scaledPos.x * distortionX; |
|||
|
|||
scaledPos *= scale; |
|||
gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0); |
|||
FragPos = vec3(uModel * vec4(aPos, 1.0)); |
|||
VecPos = aPos; |
|||
Normal = normalize(vec3(uModel * vec4(aNormal, 1.0))); |
|||
} |
|||
");
|
|||
|
|||
private string FragmentShaderSource => GetShader(true, @"
|
|||
varying vec3 FragPos; |
|||
varying vec3 VecPos; |
|||
varying vec3 Normal; |
|||
uniform float uMaxY; |
|||
uniform float uMinY; |
|||
uniform float uTime; |
|||
uniform float uDisco; |
|||
//DECLAREGLFRAG
|
|||
|
|||
void main() |
|||
{ |
|||
float y = (VecPos.y - uMinY) / (uMaxY - uMinY); |
|||
float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0); |
|||
float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0); |
|||
|
|||
vec3 discoColor = vec3( |
|||
0.5 + abs(0.5 - y) * cos(uTime * 10.0), |
|||
0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), |
|||
0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); |
|||
|
|||
vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); |
|||
objectColor = objectColor * (1.0 - uDisco) + discoColor * uDisco; |
|||
|
|||
float ambientStrength = 0.3; |
|||
vec3 lightColor = vec3(1.0, 1.0, 1.0); |
|||
vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 2.0); |
|||
vec3 ambient = ambientStrength * lightColor; |
|||
|
|||
|
|||
vec3 norm = normalize(Normal); |
|||
vec3 lightDir = normalize(lightPos - FragPos); |
|||
|
|||
float diff = max(dot(norm, lightDir), 0.0); |
|||
vec3 diffuse = diff * lightColor; |
|||
|
|||
vec3 result = (ambient + diffuse) * objectColor; |
|||
gl_FragColor = vec4(result, 1.0); |
|||
|
|||
} |
|||
");
|
|||
|
|||
[StructLayout(LayoutKind.Sequential, Pack = 4)] |
|||
private struct Vertex |
|||
{ |
|||
public Vector3 Position; |
|||
public Vector3 Normal; |
|||
} |
|||
|
|||
private readonly Vertex[] _points; |
|||
private readonly ushort[] _indices; |
|||
private readonly float _minY; |
|||
private readonly float _maxY; |
|||
|
|||
|
|||
public OpenGlPageControl() |
|||
{ |
|||
var name = typeof(OpenGlPage).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin")); |
|||
using (var sr = new BinaryReader(typeof(OpenGlPage).Assembly.GetManifestResourceStream(name))) |
|||
{ |
|||
var buf = new byte[sr.ReadInt32()]; |
|||
sr.Read(buf, 0, buf.Length); |
|||
var points = new float[buf.Length / 4]; |
|||
Buffer.BlockCopy(buf, 0, points, 0, buf.Length); |
|||
buf = new byte[sr.ReadInt32()]; |
|||
sr.Read(buf, 0, buf.Length); |
|||
_indices = new ushort[buf.Length / 2]; |
|||
Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length); |
|||
_points = new Vertex[points.Length / 3]; |
|||
for (var primitive = 0; primitive < points.Length / 3; primitive++) |
|||
{ |
|||
var srci = primitive * 3; |
|||
_points[primitive] = new Vertex |
|||
{ |
|||
Position = new Vector3(points[srci], points[srci + 1], points[srci + 2]) |
|||
}; |
|||
} |
|||
|
|||
for (int i = 0; i < _indices.Length; i += 3) |
|||
{ |
|||
Vector3 a = _points[_indices[i]].Position; |
|||
Vector3 b = _points[_indices[i + 1]].Position; |
|||
Vector3 c = _points[_indices[i + 2]].Position; |
|||
var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b)); |
|||
|
|||
_points[_indices[i]].Normal += normal; |
|||
_points[_indices[i + 1]].Normal += normal; |
|||
_points[_indices[i + 2]].Normal += normal; |
|||
} |
|||
|
|||
for (int i = 0; i < _points.Length; i++) |
|||
{ |
|||
_points[i].Normal = Vector3.Normalize(_points[i].Normal); |
|||
_maxY = Math.Max(_maxY, _points[i].Position.Y); |
|||
_minY = Math.Min(_minY, _points[i].Position.Y); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
private void CheckError(GlInterface gl) |
|||
{ |
|||
int err; |
|||
while ((err = gl.GetError()) != GL_NO_ERROR) |
|||
Console.WriteLine(err); |
|||
} |
|||
|
|||
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) |
|||
{ |
|||
CheckError(GL); |
|||
_glExt = new GlExtrasInterface(GL); |
|||
|
|||
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}"; |
|||
|
|||
// Load the source of the vertex shader and compile it.
|
|||
_vertexShader = GL.CreateShader(GL_VERTEX_SHADER); |
|||
Console.WriteLine(GL.CompileShaderAndGetError(_vertexShader, VertexShaderSource)); |
|||
|
|||
// Load the source of the fragment shader and compile it.
|
|||
_fragmentShader = GL.CreateShader(GL_FRAGMENT_SHADER); |
|||
Console.WriteLine(GL.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource)); |
|||
|
|||
// Create the shader program, attach the vertex and fragment shaders and link the program.
|
|||
_shaderProgram = GL.CreateProgram(); |
|||
GL.AttachShader(_shaderProgram, _vertexShader); |
|||
GL.AttachShader(_shaderProgram, _fragmentShader); |
|||
const int positionLocation = 0; |
|||
const int normalLocation = 1; |
|||
GL.BindAttribLocationString(_shaderProgram, positionLocation, "aPos"); |
|||
GL.BindAttribLocationString(_shaderProgram, normalLocation, "aNormal"); |
|||
Console.WriteLine(GL.LinkProgramAndGetError(_shaderProgram)); |
|||
CheckError(GL); |
|||
|
|||
// Create the vertex buffer object (VBO) for the vertex data.
|
|||
_vertexBufferObject = GL.GenBuffer(); |
|||
// Bind the VBO and copy the vertex data into it.
|
|||
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); |
|||
CheckError(GL); |
|||
var vertexSize = Marshal.SizeOf<Vertex>(); |
|||
fixed (void* pdata = _points) |
|||
GL.BufferData(GL_ARRAY_BUFFER, new IntPtr(_points.Length * vertexSize), |
|||
new IntPtr(pdata), GL_STATIC_DRAW); |
|||
|
|||
_indexBufferObject = GL.GenBuffer(); |
|||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); |
|||
CheckError(GL); |
|||
fixed (void* pdata = _indices) |
|||
GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata), |
|||
GL_STATIC_DRAW); |
|||
CheckError(GL); |
|||
_vertexArrayObject = _glExt.GenVertexArray(); |
|||
_glExt.BindVertexArray(_vertexArrayObject); |
|||
CheckError(GL); |
|||
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT, |
|||
0, vertexSize, IntPtr.Zero); |
|||
GL.VertexAttribPointer(normalLocation, 3, GL_FLOAT, |
|||
0, vertexSize, new IntPtr(12)); |
|||
GL.EnableVertexAttribArray(positionLocation); |
|||
GL.EnableVertexAttribArray(normalLocation); |
|||
CheckError(GL); |
|||
|
|||
} |
|||
|
|||
protected override void OnOpenGlDeinit(GlInterface GL, int fb) |
|||
{ |
|||
// Unbind everything
|
|||
GL.BindBuffer(GL_ARRAY_BUFFER, 0); |
|||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); |
|||
_glExt.BindVertexArray(0); |
|||
GL.UseProgram(0); |
|||
|
|||
// Delete all resources.
|
|||
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject }); |
|||
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject }); |
|||
GL.DeleteProgram(_shaderProgram); |
|||
GL.DeleteShader(_fragmentShader); |
|||
GL.DeleteShader(_vertexShader); |
|||
} |
|||
|
|||
static Stopwatch St = Stopwatch.StartNew(); |
|||
protected override unsafe void OnOpenGlRender(GlInterface gl, int fb) |
|||
{ |
|||
gl.ClearColor(0, 0, 0, 0); |
|||
gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
|||
gl.Enable(GL_DEPTH_TEST); |
|||
gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); |
|||
var GL = gl; |
|||
|
|||
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); |
|||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); |
|||
_glExt.BindVertexArray(_vertexArrayObject); |
|||
GL.UseProgram(_shaderProgram); |
|||
CheckError(GL); |
|||
var projection = |
|||
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)(Bounds.Width / Bounds.Height), |
|||
0.01f, 1000); |
|||
|
|||
|
|||
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); |
|||
var model = Matrix4x4.CreateFromYawPitchRoll(_yaw, _pitch, _roll); |
|||
var modelLoc = GL.GetUniformLocationString(_shaderProgram, "uModel"); |
|||
var viewLoc = GL.GetUniformLocationString(_shaderProgram, "uView"); |
|||
var projectionLoc = GL.GetUniformLocationString(_shaderProgram, "uProjection"); |
|||
var maxYLoc = GL.GetUniformLocationString(_shaderProgram, "uMaxY"); |
|||
var minYLoc = GL.GetUniformLocationString(_shaderProgram, "uMinY"); |
|||
var timeLoc = GL.GetUniformLocationString(_shaderProgram, "uTime"); |
|||
var discoLoc = GL.GetUniformLocationString(_shaderProgram, "uDisco"); |
|||
GL.UniformMatrix4fv(modelLoc, 1, false, &model); |
|||
GL.UniformMatrix4fv(viewLoc, 1, false, &view); |
|||
GL.UniformMatrix4fv(projectionLoc, 1, false, &projection); |
|||
GL.Uniform1f(maxYLoc, _maxY); |
|||
GL.Uniform1f(minYLoc, _minY); |
|||
GL.Uniform1f(timeLoc, (float)St.Elapsed.TotalSeconds); |
|||
GL.Uniform1f(discoLoc, _disco); |
|||
CheckError(GL); |
|||
GL.DrawElements(GL_TRIANGLES, _indices.Length, GL_UNSIGNED_SHORT, IntPtr.Zero); |
|||
|
|||
CheckError(GL); |
|||
if (_disco > 0.01) |
|||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); |
|||
} |
|||
|
|||
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo> |
|||
{ |
|||
public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo) |
|||
{ |
|||
} |
|||
|
|||
public delegate void GlDeleteVertexArrays(int count, int[] buffers); |
|||
[GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)] |
|||
[GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")] |
|||
public GlDeleteVertexArrays DeleteVertexArrays { get; } |
|||
|
|||
public delegate void GlBindVertexArray(int array); |
|||
[GlMinVersionEntryPoint("glBindVertexArray", 3,0)] |
|||
[GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")] |
|||
public GlBindVertexArray BindVertexArray { get; } |
|||
public delegate void GlGenVertexArrays(int n, int[] rv); |
|||
|
|||
[GlMinVersionEntryPoint("glGenVertexArrays",3,0)] |
|||
[GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")] |
|||
public GlGenVertexArrays GenVertexArrays { get; } |
|||
|
|||
public int GenVertexArray() |
|||
{ |
|||
var rv = new int[1]; |
|||
GenVertexArrays(1, rv); |
|||
return rv[0]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -1,5 +1,5 @@ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
public class DisposableLock |
|||
{ |
|||
private readonly object _lock = new object(); |
|||
|
|||
/// <summary>
|
|||
/// Tries to take a lock
|
|||
/// </summary>
|
|||
/// <returns>IDisposable if succeeded to obtain the lock</returns>
|
|||
public IDisposable TryLock() |
|||
{ |
|||
if (Monitor.TryEnter(_lock)) |
|||
return new UnlockDisposable(_lock); |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Enters a waiting lock
|
|||
/// </summary>
|
|||
public IDisposable Lock() |
|||
{ |
|||
Monitor.Enter(_lock); |
|||
return new UnlockDisposable(_lock); |
|||
} |
|||
|
|||
private sealed class UnlockDisposable : IDisposable |
|||
{ |
|||
private object _lock; |
|||
|
|||
public UnlockDisposable(object @lock) |
|||
{ |
|||
_lock = @lock; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
object @lock = Interlocked.Exchange(ref _lock, null); |
|||
|
|||
if (@lock != null) |
|||
{ |
|||
Monitor.Exit(@lock); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Remote.Protocol; |
|||
using Avalonia.Remote.Protocol.Designer; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.DesignerSupport.Remote |
|||
{ |
|||
class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod |
|||
{ |
|||
private string _path; |
|||
private string _lastContents; |
|||
private bool _disposed; |
|||
|
|||
public FileWatcherTransport(Uri file) |
|||
{ |
|||
_path = file.LocalPath; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_disposed = true; |
|||
} |
|||
|
|||
void Dump(object o, string pad) |
|||
{ |
|||
foreach (var p in o.GetType().GetProperties()) |
|||
{ |
|||
Console.Write($"{pad}{p.Name}: "); |
|||
var v = p.GetValue(o); |
|||
if (v == null || v.GetType().IsPrimitive || v is string || v is Guid) |
|||
Console.WriteLine(v); |
|||
else |
|||
{ |
|||
Console.WriteLine(); |
|||
Dump(v, pad + " "); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
public Task Send(object data) |
|||
{ |
|||
Console.WriteLine(data.GetType().Name); |
|||
Dump(data, " "); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage; |
|||
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage |
|||
{ |
|||
add |
|||
{ |
|||
_onMessage+=value; |
|||
} |
|||
remove { _onMessage -= value; } |
|||
} |
|||
|
|||
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException; |
|||
public void Start() |
|||
{ |
|||
UpdaterThread(); |
|||
} |
|||
|
|||
// I couldn't get FileSystemWatcher working on Linux, so I came up with this abomination
|
|||
async void UpdaterThread() |
|||
{ |
|||
while (!_disposed) |
|||
{ |
|||
var data = File.ReadAllText(_path); |
|||
if (data != _lastContents) |
|||
{ |
|||
Console.WriteLine("Triggering XAML update"); |
|||
_lastContents = data; |
|||
_onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data }); |
|||
} |
|||
|
|||
await Task.Delay(100); |
|||
} |
|||
} |
|||
|
|||
public string PreviewerMethod => RemoteDesignerEntryPoint.Methods.Html; |
|||
} |
|||
|
|||
interface ITransportWithEnforcedMethod |
|||
{ |
|||
string PreviewerMethod { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,266 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.IO.Compression; |
|||
using System.Linq; |
|||
using System.Net; |
|||
using System.Text; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Remote.Protocol; |
|||
using Avalonia.Remote.Protocol.Viewport; |
|||
|
|||
namespace Avalonia.DesignerSupport.Remote.HtmlTransport |
|||
{ |
|||
public class HtmlWebSocketTransport : IAvaloniaRemoteTransportConnection |
|||
{ |
|||
private readonly IAvaloniaRemoteTransportConnection _signalTransport; |
|||
private readonly SimpleWebSocketHttpServer _simpleServer; |
|||
private readonly Dictionary<string, byte[]> _resources; |
|||
private SimpleWebSocket _pendingSocket; |
|||
private bool _disposed; |
|||
private object _lock = new object(); |
|||
private AutoResetEvent _wakeup = new AutoResetEvent(false); |
|||
private FrameMessage _lastFrameMessage = null; |
|||
private FrameMessage _lastSentFrameMessage = null; |
|||
private RequestViewportResizeMessage _lastViewportRequest; |
|||
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage; |
|||
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException; |
|||
|
|||
private static readonly Dictionary<string, string> Mime = new Dictionary<string, string> |
|||
{ |
|||
["html"] = "text/html", ["htm"] = "text/html", ["js"] = "text/javascript", ["css"] = "text/css" |
|||
}; |
|||
|
|||
private static readonly byte[] NotFound = Encoding.UTF8.GetBytes("404 - Not Found"); |
|||
|
|||
|
|||
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri) |
|||
{ |
|||
if (listenUri.Scheme != "http") |
|||
throw new ArgumentException("listenUri"); |
|||
|
|||
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build."; |
|||
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames() |
|||
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary( |
|||
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3), |
|||
r => |
|||
{ |
|||
|
|||
using (var s = |
|||
new GZipStream(typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceStream(r), |
|||
CompressionMode.Decompress)) |
|||
{ |
|||
var ms = new MemoryStream(); |
|||
s.CopyTo(ms); |
|||
return ms.ToArray(); |
|||
} |
|||
}); |
|||
|
|||
_signalTransport = signalTransport; |
|||
var address = IPAddress.Parse(listenUri.Host); |
|||
|
|||
_simpleServer = new SimpleWebSocketHttpServer(address, listenUri.Port); |
|||
_simpleServer.Listen(); |
|||
Task.Run(AcceptWorker); |
|||
Task.Run(SocketWorker); |
|||
_signalTransport.Send(new HtmlTransportStartedMessage { Uri = "http://" + address + ":" + listenUri.Port + "/" }); |
|||
} |
|||
|
|||
async void AcceptWorker() |
|||
{ |
|||
while (true) |
|||
{ |
|||
|
|||
using (var req = await _simpleServer.AcceptAsync()) |
|||
{ |
|||
|
|||
if (!req.IsWebsocketRequest) |
|||
{ |
|||
|
|||
var key = req.Path == "/" ? "index.html" : req.Path.TrimStart('/').Replace('/', '.'); |
|||
if (_resources.TryGetValue(key, out var data)) |
|||
{ |
|||
var ext = Path.GetExtension(key).Substring(1); |
|||
string mime = null; |
|||
if (ext == null || !Mime.TryGetValue(ext, out mime)) |
|||
mime = "application/octet-stream"; |
|||
await req.RespondAsync(200, data, mime); |
|||
} |
|||
else |
|||
{ |
|||
await req.RespondAsync(404, NotFound, "text/plain"); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var socket = await req.AcceptWebSocket(); |
|||
SocketReceiveWorker(socket); |
|||
lock (_lock) |
|||
{ |
|||
_pendingSocket?.Dispose(); |
|||
_pendingSocket = socket; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
async void SocketReceiveWorker(SimpleWebSocket socket) |
|||
{ |
|||
try |
|||
{ |
|||
while (true) |
|||
{ |
|||
var msg = await socket.ReceiveMessage().ConfigureAwait(false); |
|||
if(msg == null) |
|||
return; |
|||
if (msg.IsText) |
|||
{ |
|||
var s = Encoding.UTF8.GetString(msg.Data); |
|||
var parts = s.Split(':'); |
|||
if (parts[0] == "frame-received") |
|||
_onMessage?.Invoke(this, new FrameReceivedMessage { SequenceId = long.Parse(parts[1]) }); |
|||
} |
|||
} |
|||
} |
|||
catch(Exception e) |
|||
{ |
|||
Console.Error.WriteLine(e.ToString()); |
|||
} |
|||
} |
|||
|
|||
async void SocketWorker() |
|||
{ |
|||
try |
|||
{ |
|||
SimpleWebSocket socket = null; |
|||
while (true) |
|||
{ |
|||
if (_disposed) |
|||
{ |
|||
socket?.Dispose(); |
|||
return; |
|||
} |
|||
|
|||
FrameMessage sendNow = null; |
|||
lock (_lock) |
|||
{ |
|||
if (_pendingSocket != null) |
|||
{ |
|||
socket?.Dispose(); |
|||
socket = _pendingSocket; |
|||
_pendingSocket = null; |
|||
_lastSentFrameMessage = null; |
|||
} |
|||
|
|||
if (_lastFrameMessage != _lastSentFrameMessage) |
|||
_lastSentFrameMessage = sendNow = _lastFrameMessage; |
|||
} |
|||
|
|||
if (sendNow != null && socket != null) |
|||
{ |
|||
await socket.SendMessage( |
|||
$"frame:{sendNow.SequenceId}:{sendNow.Width}:{sendNow.Height}:{sendNow.Stride}:{sendNow.DpiX}:{sendNow.DpiY}"); |
|||
await socket.SendMessage(false, sendNow.Data); |
|||
} |
|||
|
|||
_wakeup.WaitOne(TimeSpan.FromSeconds(1)); |
|||
} |
|||
} |
|||
catch(Exception e) |
|||
{ |
|||
Console.Error.WriteLine(e.ToString()); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_pendingSocket?.Dispose(); |
|||
_simpleServer.Dispose(); |
|||
} |
|||
|
|||
|
|||
public Task Send(object data) |
|||
{ |
|||
if (data is FrameMessage frame) |
|||
{ |
|||
_lastFrameMessage = frame; |
|||
_wakeup.Set(); |
|||
return Task.CompletedTask; |
|||
} |
|||
if (data is RequestViewportResizeMessage req) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
return _signalTransport.Send(data); |
|||
} |
|||
|
|||
public void Start() |
|||
{ |
|||
_onMessage?.Invoke(this, new Avalonia.Remote.Protocol.Viewport.ClientSupportedPixelFormatsMessage |
|||
{ |
|||
Formats = new []{PixelFormat.Rgba8888} |
|||
}); |
|||
_signalTransport.Start(); |
|||
} |
|||
|
|||
#region Forward
|
|||
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage |
|||
{ |
|||
add |
|||
{ |
|||
bool subscribeToInner; |
|||
lock (_lock) |
|||
{ |
|||
subscribeToInner = _onMessage == null; |
|||
_onMessage += value; |
|||
} |
|||
|
|||
if (subscribeToInner) |
|||
_signalTransport.OnMessage += OnSignalTransportMessage; |
|||
} |
|||
remove |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
_onMessage -= value; |
|||
if (_onMessage == null) |
|||
_signalTransport.OnMessage -= OnSignalTransportMessage; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void OnSignalTransportMessage(IAvaloniaRemoteTransportConnection signal, object message) => _onMessage?.Invoke(this, message); |
|||
|
|||
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException |
|||
{ |
|||
add |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
var subscribeToInner = _onException == null; |
|||
_onException += value; |
|||
if (subscribeToInner) |
|||
_signalTransport.OnException += OnSignalTransportException; |
|||
} |
|||
} |
|||
remove |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
_onException -= value; |
|||
if(_onException==null) |
|||
_signalTransport.OnException -= OnSignalTransportException; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
private void OnSignalTransportException(IAvaloniaRemoteTransportConnection arg1, Exception ex) |
|||
{ |
|||
_onException?.Invoke(this, ex); |
|||
} |
|||
#endregion
|
|||
} |
|||
} |
|||
@ -0,0 +1,472 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Net; |
|||
using System.Net.Sockets; |
|||
using System.Runtime.InteropServices; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.DesignerSupport.Remote.HtmlTransport |
|||
{ |
|||
public class SimpleWebSocketHttpServer : IDisposable |
|||
{ |
|||
private readonly IPAddress _address; |
|||
private readonly int _port; |
|||
private TcpListener _listener; |
|||
|
|||
public async Task<SimpleWebSocketHttpRequest> AcceptAsync() |
|||
{ |
|||
while (true) |
|||
{ |
|||
var res = await AcceptAsyncImpl(); |
|||
if (res != null) |
|||
return res; |
|||
} |
|||
} |
|||
async Task<SimpleWebSocketHttpRequest> AcceptAsyncImpl() |
|||
{ |
|||
if (_listener == null) |
|||
throw new InvalidOperationException("Currently not listening"); |
|||
var socket = await _listener.AcceptSocketAsync(); |
|||
var stream = new NetworkStream(socket); |
|||
bool error = true; |
|||
async Task<string> ReadLineAsync() |
|||
{ |
|||
var readBuffer = new byte[1]; |
|||
var lineBuffer = new byte[1024]; |
|||
for (var c = 0; c < 1024; c++) |
|||
{ |
|||
if (await stream.ReadAsync(readBuffer, 0, 1) == 0) |
|||
throw new EndOfStreamException(); |
|||
if (readBuffer[0] == 10) |
|||
{ |
|||
if (c == 0) |
|||
return ""; |
|||
if (lineBuffer[c - 1] == 13) |
|||
c--; |
|||
if (c == 0) |
|||
return ""; |
|||
|
|||
return Encoding.UTF8.GetString(lineBuffer, 0, c); |
|||
} |
|||
lineBuffer[c] = readBuffer[0]; |
|||
} |
|||
|
|||
throw new InvalidDataException("Header is too large"); |
|||
} |
|||
|
|||
var headers = new Dictionary<string, string>(); |
|||
string line = null; |
|||
try |
|||
{ |
|||
|
|||
line = await ReadLineAsync(); |
|||
var sp = line.Split(' '); |
|||
if (sp.Length != 3 || !sp[2].StartsWith("HTTP") || sp[0] != "GET") |
|||
return null; |
|||
var path = sp[1]; |
|||
|
|||
while (true) |
|||
{ |
|||
line = await ReadLineAsync(); |
|||
if (line == "") |
|||
break; |
|||
sp = line.Split(new[] {':'}, 2); |
|||
headers[sp[0]] = sp[1].TrimStart(); |
|||
} |
|||
|
|||
error = false; |
|||
|
|||
return new SimpleWebSocketHttpRequest(stream, path, headers); |
|||
} |
|||
catch |
|||
{ |
|||
error = true; |
|||
return null; |
|||
} |
|||
finally |
|||
{ |
|||
if (error) |
|||
stream.Dispose(); |
|||
} |
|||
|
|||
} |
|||
|
|||
public void Listen() |
|||
{ |
|||
var listener = new TcpListener(_address, _port); |
|||
listener.Start(); |
|||
_listener = listener; |
|||
} |
|||
|
|||
public SimpleWebSocketHttpServer(IPAddress address, int port) |
|||
{ |
|||
_address = address; |
|||
_port = port; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_listener?.Stop(); |
|||
_listener = null; |
|||
} |
|||
} |
|||
|
|||
|
|||
public class SimpleWebSocketHttpRequest : IDisposable |
|||
{ |
|||
public Dictionary<string, string> Headers { get; } |
|||
public string Path { get; } |
|||
private NetworkStream _stream; |
|||
public bool IsWebsocketRequest { get; } |
|||
public IReadOnlyList<string> WebSocketProtocols { get; } |
|||
private string _websocketKey; |
|||
|
|||
public SimpleWebSocketHttpRequest(NetworkStream stream, string path, Dictionary<string, string> headers) |
|||
{ |
|||
Path = path; |
|||
Headers = headers; |
|||
|
|||
_stream = stream; |
|||
if (headers.TryGetValue("Connection", out var h) |
|||
&& h.Contains("Upgrade") |
|||
&& headers.TryGetValue("Upgrade", out h) && |
|||
h == "websocket" |
|||
&& headers.TryGetValue("Sec-WebSocket-Key", out _websocketKey)) |
|||
{ |
|||
IsWebsocketRequest = true; |
|||
if (headers.TryGetValue("Sec-WebSocket-Protocol", out h)) |
|||
WebSocketProtocols = h.Split(',').Select(x => x.Trim()).ToArray(); |
|||
else WebSocketProtocols = new string[0]; |
|||
} |
|||
} |
|||
|
|||
public async Task RespondAsync(int code, byte[] data, string contentType) |
|||
{ |
|||
var headers = Encoding.UTF8.GetBytes($"HTTP/1.1 {code} {(HttpStatusCode)code}\r\nConnection: close\r\nContent-Type: {contentType}\r\nContent-Length: {data.Length}\r\n\r\n"); |
|||
await _stream.WriteAsync(headers, 0, headers.Length); |
|||
await _stream.WriteAsync(data, 0, data.Length); |
|||
_stream.Dispose(); |
|||
_stream = null; |
|||
|
|||
} |
|||
|
|||
|
|||
public async Task<SimpleWebSocket> AcceptWebSocket(string protocol = null) |
|||
{ |
|||
|
|||
var handshakeSource = _websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; |
|||
string handshake; |
|||
using (var sha1 = SHA1.Create()) |
|||
handshake = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(handshakeSource))); |
|||
var headers = |
|||
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " |
|||
+ handshake + "\r\n"; |
|||
if (protocol != null) |
|||
headers += protocol + "\r\n"; |
|||
headers += "\r\n"; |
|||
var bheaders = Encoding.UTF8.GetBytes(headers); |
|||
await _stream.WriteAsync(bheaders, 0, bheaders.Length); |
|||
|
|||
var s = _stream; |
|||
_stream = null; |
|||
return new SimpleWebSocket(s); |
|||
} |
|||
|
|||
public void Dispose() => _stream?.Dispose(); |
|||
} |
|||
|
|||
|
|||
|
|||
public class SimpleWebSocket : IDisposable |
|||
{ |
|||
class AsyncLock |
|||
{ |
|||
private object _syncRoot = new object(); |
|||
private Queue<TaskCompletionSource<IDisposable>> _queue = new Queue<TaskCompletionSource<IDisposable>>(); |
|||
private bool _locked; |
|||
public Task<IDisposable> LockAsync() |
|||
{ |
|||
lock (_syncRoot) |
|||
{ |
|||
if (!_locked) |
|||
{ |
|||
_locked = true; |
|||
return Task.FromResult<IDisposable>(new Lock(this)); |
|||
} |
|||
else |
|||
{ |
|||
var tcs = new TaskCompletionSource<IDisposable>(); |
|||
_queue.Enqueue(tcs); |
|||
return tcs.Task; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void Unlock() |
|||
{ |
|||
lock (_syncRoot) |
|||
{ |
|||
if (_queue.Count != 0) |
|||
_queue.Dequeue().SetResult(new Lock(this)); |
|||
else |
|||
_locked = false; |
|||
} |
|||
} |
|||
|
|||
class Lock : IDisposable |
|||
{ |
|||
private AsyncLock _parent; |
|||
private object _syncRoot = new object(); |
|||
|
|||
public Lock(AsyncLock parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
lock (_syncRoot) |
|||
{ |
|||
if (_parent == null) |
|||
return; |
|||
var p = _parent; |
|||
_parent = null; |
|||
p.Unlock(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Stream _stream; |
|||
private AsyncLock _sendLock = new AsyncLock(); |
|||
private AsyncLock _recvLock = new AsyncLock(); |
|||
private const int WebsocketInitialHeaderLength = 2; |
|||
private const int WebsocketLen16Length = 4; |
|||
private const int WebsocketLen64Length = 10; |
|||
|
|||
private const int WebsocketLen16Code = 126; |
|||
private const int WebsocketLen64Code = 127; |
|||
|
|||
[StructLayout(LayoutKind.Explicit)] |
|||
struct WebSocketHeader |
|||
{ |
|||
[FieldOffset(0)] public byte Mask; |
|||
[FieldOffset(1)] public byte Length8; |
|||
[FieldOffset(2)] public ushort Length16; |
|||
[FieldOffset(2)] public ulong Length64; |
|||
} |
|||
|
|||
readonly byte[] _sendHeaderBuffer = new byte[10]; |
|||
readonly MemoryStream _receiveFrameStream = new MemoryStream(); |
|||
readonly MemoryStream _receiveMessageStream = new MemoryStream(); |
|||
private FrameType _currentMessageFrameType; |
|||
|
|||
enum FrameType |
|||
{ |
|||
Continue = 0x0, |
|||
Text = 0x1, |
|||
Binary = 0x2, |
|||
Close = 0x8, |
|||
Ping = 0x9, |
|||
Pong = 0xA |
|||
} |
|||
|
|||
internal SimpleWebSocket(Stream stream) |
|||
{ |
|||
_stream = stream; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_stream?.Dispose(); |
|||
_stream = null; |
|||
} |
|||
|
|||
public Task SendMessage(string text) |
|||
{ |
|||
var data = Encoding.UTF8.GetBytes(text); |
|||
return SendMessage(true, data); |
|||
} |
|||
public Task SendMessage(bool isText, byte[] data) => SendMessage(isText, data, 0, data.Length); |
|||
|
|||
|
|||
public Task SendMessage(bool isText, byte[] data, int offset, int length) |
|||
=> SendFrame(isText ? FrameType.Text : FrameType.Binary, data, offset, length); |
|||
|
|||
async Task SendFrame(FrameType type, byte[] data, int offset, int length) |
|||
{ |
|||
using (var l = await _sendLock.LockAsync()) |
|||
{ |
|||
var header = new WebSocketHeader(); |
|||
|
|||
int headerLength; |
|||
if (data.Length <= 125) |
|||
{ |
|||
headerLength = WebsocketInitialHeaderLength; |
|||
header.Length8 = (byte) length; |
|||
} |
|||
else if (length <= 0xffff) |
|||
{ |
|||
headerLength = WebsocketLen16Length; |
|||
header.Length8 = WebsocketLen16Code; |
|||
header.Length16 = (ushort) IPAddress.HostToNetworkOrder((short) (ushort) length); |
|||
|
|||
} |
|||
else |
|||
{ |
|||
headerLength = WebsocketLen64Length; |
|||
header.Length8 = WebsocketLen64Code; |
|||
header.Length64 = (ulong) IPAddress.HostToNetworkOrder((long) length); |
|||
} |
|||
|
|||
var endOfMessage = true; |
|||
header.Mask = (byte) (((endOfMessage ? 1u : 0u) << 7) | ((byte) (type) & 0xf)); |
|||
unsafe |
|||
{ |
|||
Marshal.Copy(new IntPtr(&header), _sendHeaderBuffer, 0, headerLength); |
|||
} |
|||
|
|||
await _stream.WriteAsync(_sendHeaderBuffer, 0, headerLength); |
|||
await _stream.WriteAsync(data, offset, length); |
|||
} |
|||
} |
|||
|
|||
struct Frame |
|||
{ |
|||
public byte[] Data; |
|||
public bool EndOfMessage; |
|||
public FrameType FrameType; |
|||
} |
|||
|
|||
byte[] _recvHeaderBuffer = new byte[8]; |
|||
byte[] _maskBuffer = new byte[4]; |
|||
async Task<Frame> ReadFrame() |
|||
{ |
|||
_receiveFrameStream.Position = 0; |
|||
_receiveFrameStream.SetLength(0); |
|||
await ReadExact(_stream, _recvHeaderBuffer, 0, 2); |
|||
var masked = (_recvHeaderBuffer[1] & 0x80) != 0; |
|||
var len0 = (_recvHeaderBuffer[1] & 0x7F); |
|||
var endOfMessage = (_recvHeaderBuffer[0] & 0x80) != 0; |
|||
var frameType = (FrameType) (_recvHeaderBuffer[0] & 0xf); |
|||
int length; |
|||
if (len0 <= 125) |
|||
length = len0; |
|||
else if (len0 == WebsocketLen16Code) |
|||
{ |
|||
await ReadExact(_stream, _recvHeaderBuffer, 0, 2); |
|||
length = (ushort) IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_recvHeaderBuffer, 0)); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
await ReadExact(_stream, _recvHeaderBuffer, 0, 8); |
|||
length = (int) (ulong) IPAddress.NetworkToHostOrder((long) BitConverter.ToUInt64(_recvHeaderBuffer, 0)); |
|||
} |
|||
|
|||
if (masked) |
|||
await ReadExact(_stream, _maskBuffer, 0, 4); |
|||
await ReadExact(_stream, _receiveFrameStream, length); |
|||
var data = _receiveFrameStream.ToArray(); |
|||
if(masked) |
|||
for (var c = 0; c < data.Length; c++) |
|||
data[c] = (byte) (data[c] ^ _maskBuffer[c % 4]); |
|||
|
|||
return new Frame |
|||
{ |
|||
Data = data, |
|||
EndOfMessage = endOfMessage, |
|||
FrameType = frameType |
|||
}; |
|||
} |
|||
|
|||
|
|||
public async Task<SimpleWebSocketMessage> ReceiveMessage() |
|||
{ |
|||
using (await _recvLock.LockAsync()) |
|||
{ |
|||
while (true) |
|||
{ |
|||
var frame = await ReadFrame(); |
|||
|
|||
if (frame.FrameType == FrameType.Close) |
|||
return null; |
|||
if (frame.FrameType == FrameType.Ping) |
|||
await SendFrame(FrameType.Pong, frame.Data, 0, frame.Data.Length); |
|||
if (frame.FrameType == FrameType.Text || frame.FrameType == FrameType.Binary) |
|||
{ |
|||
var isText = frame.FrameType == FrameType.Text; |
|||
if (_receiveMessageStream.Length == 0 && frame.EndOfMessage) |
|||
return new SimpleWebSocketMessage |
|||
{ |
|||
IsText = isText, |
|||
Data = frame.Data |
|||
}; |
|||
|
|||
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length); |
|||
_currentMessageFrameType = frame.FrameType; |
|||
} |
|||
if (frame.FrameType == FrameType.Continue) |
|||
{ |
|||
frame.FrameType = _currentMessageFrameType; |
|||
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length); |
|||
if (frame.EndOfMessage) |
|||
{ |
|||
var isText = frame.FrameType == FrameType.Text; |
|||
var data = _receiveMessageStream.ToArray(); |
|||
_receiveMessageStream.Position = 0; |
|||
_receiveMessageStream.SetLength(0); |
|||
return new SimpleWebSocketMessage |
|||
{ |
|||
IsText = isText, |
|||
Data = data |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
byte[] _readExactBuffer = new byte[4096]; |
|||
async Task ReadExact(Stream from, MemoryStream to, int length) |
|||
{ |
|||
while (length>0) |
|||
{ |
|||
var toRead = Math.Min(length, _readExactBuffer.Length); |
|||
var read = await from.ReadAsync(_readExactBuffer, 0, toRead); |
|||
to.Write(_readExactBuffer, 0, read); |
|||
if (read <= 0) |
|||
throw new EndOfStreamException(); |
|||
length -= read; |
|||
} |
|||
} |
|||
|
|||
async Task ReadExact(Stream from, byte[] to, int offset, int length) |
|||
{ |
|||
while (length > 0) |
|||
{ |
|||
var read = await from.ReadAsync(to, offset, length); |
|||
if (read <= 0) |
|||
throw new EndOfStreamException(); |
|||
length -= read; |
|||
offset += read; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class SimpleWebSocketMessage |
|||
{ |
|||
public bool IsText { get; set; } |
|||
public byte[] Data { get; set; } |
|||
|
|||
public string AsString() |
|||
{ |
|||
return Encoding.UTF8.GetString(Data); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
build |
|||
node_modules |
|||
File diff suppressed because it is too large
@ -0,0 +1,41 @@ |
|||
{ |
|||
"name": "simple", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"webpack-ver": "cross-env NODE_ENV=production webpack --version", |
|||
"dist": "cross-env NODE_ENV=production webpack --display-error-details", |
|||
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details" |
|||
}, |
|||
"author": "", |
|||
"license": "ISC", |
|||
"devDependencies": { |
|||
"awesome-typescript-loader": "^5.0.0", |
|||
"clean-webpack-plugin": "^0.1.19", |
|||
"compression-webpack-plugin": "^2.0.0", |
|||
"copy-webpack-plugin": "^4.6.0", |
|||
"cross-env": "^5.1.6", |
|||
"css-loader": "^1.0.0", |
|||
"file-loader": "^1.1.11", |
|||
"html-webpack-plugin": "^3.2.0", |
|||
"mini-css-extract-plugin": "^0.4.1", |
|||
"source-map-loader": "^0.2.3", |
|||
"style-loader": "^0.21.0", |
|||
"to-string-loader": "^1.1.5", |
|||
"tsconfig-paths-webpack-plugin": "^3.2.0", |
|||
"typescript": "^2.9.2", |
|||
"url-loader": "^1.0.1", |
|||
"webpack": "~4.16.3", |
|||
"webpack-cli": "~2.1.3", |
|||
"webpack-livereload-plugin": "~2.1.1" |
|||
}, |
|||
"dependencies": { |
|||
"@types/react": "^16.3.14", |
|||
"@types/react-dom": "^16.0.5", |
|||
"mobx": "4.3.0", |
|||
"mobx-react": "^5.1.2", |
|||
"react": "^16.3.2", |
|||
"react-dom": "^16.3.2" |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
import {PreviewerFrame, PreviewerServerConnection} from "src/PreviewerServerConnection"; |
|||
import * as React from "react"; |
|||
|
|||
interface PreviewerPresenterProps { |
|||
conn: PreviewerServerConnection; |
|||
} |
|||
|
|||
export class PreviewerPresenter extends React.Component<PreviewerPresenterProps> { |
|||
private canvasRef: React.RefObject<HTMLCanvasElement>; |
|||
|
|||
constructor(props: PreviewerPresenterProps) { |
|||
super(props); |
|||
this.state = {width: 1, height: 1}; |
|||
this.canvasRef = React.createRef() |
|||
this.componentDidUpdate({ |
|||
conn: null! |
|||
}, this.state); |
|||
} |
|||
|
|||
componentDidMount(): void { |
|||
this.updateCanvas(this.canvasRef.current, this.props.conn.currentFrame); |
|||
} |
|||
|
|||
componentDidUpdate(prevProps: Readonly<PreviewerPresenterProps>, prevState: Readonly<{}>, snapshot?: any): void { |
|||
if(prevProps.conn != this.props.conn) |
|||
{ |
|||
if(prevProps.conn) |
|||
prevProps.conn.removeFrameListener(this.frameHandler); |
|||
if(this.props.conn) |
|||
this.props.conn.addFrameListener(this.frameHandler); |
|||
} |
|||
} |
|||
|
|||
private frameHandler = (frame: PreviewerFrame)=>{ |
|||
this.updateCanvas(this.canvasRef.current, frame); |
|||
}; |
|||
|
|||
|
|||
updateCanvas(canvas: HTMLCanvasElement | null, frame: PreviewerFrame | null) { |
|||
if (!canvas) |
|||
return; |
|||
if (frame == null){ |
|||
canvas.width = canvas.height = 1; |
|||
canvas.getContext('2d')!.clearRect(0,0,1,1); |
|||
} |
|||
else { |
|||
canvas.width = frame.data.width; |
|||
canvas.height = frame.data.height; |
|||
const ctx = canvas.getContext('2d')!; |
|||
ctx.putImageData(frame.data, 0,0); |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
return <canvas ref={this.canvasRef}/> |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
export interface PreviewerFrame { |
|||
data: ImageData; |
|||
dpiX: number; |
|||
dpiY: number; |
|||
} |
|||
|
|||
export class PreviewerServerConnection { |
|||
private nextFrame = { |
|||
width: 0, |
|||
height: 0, |
|||
stride: 0, |
|||
dpiX: 0, |
|||
dpiY: 0, |
|||
sequenceId: "0" |
|||
}; |
|||
|
|||
public currentFrame: PreviewerFrame | null; |
|||
private handlers = new Set<(frame: PreviewerFrame | null) => void>(); |
|||
private conn: WebSocket; |
|||
|
|||
public addFrameListener(listener: (frame: PreviewerFrame | null) => void) { |
|||
this.handlers.add(listener); |
|||
if (this.currentFrame) |
|||
listener(this.currentFrame); |
|||
} |
|||
|
|||
public removeFrameListener(listener: (frame: PreviewerFrame | null) => void) { |
|||
this.handlers.delete(listener); |
|||
} |
|||
|
|||
constructor(uri: string) { |
|||
this.currentFrame = null; |
|||
var conn = this.conn = new WebSocket(uri); |
|||
conn.binaryType = 'arraybuffer'; |
|||
|
|||
const onMessage = this.onMessage; |
|||
conn.onmessage = msg => onMessage(msg); |
|||
|
|||
const onClose = () => this.setFrame(null); |
|||
conn.onclose = () => onClose(); |
|||
conn.onerror = (err: Event) => { |
|||
onClose(); |
|||
console.log("Connection error: " + err); |
|||
} |
|||
} |
|||
|
|||
private onMessage = (msg: MessageEvent) => { |
|||
if (typeof msg.data == 'string' || msg.data instanceof String) { |
|||
const parts = msg.data.split(':'); |
|||
if (parts[0] == 'frame') { |
|||
this.nextFrame = { |
|||
sequenceId: parts[1], |
|||
width: parseInt(parts[2]), |
|||
height: parseInt(parts[3]), |
|||
stride: parseInt(parts[4]), |
|||
dpiX: parseInt(parts[5]), |
|||
dpiY: parseInt(parts[6]) |
|||
} |
|||
} |
|||
} else if (msg.data instanceof ArrayBuffer) { |
|||
const arr = new Uint8ClampedArray(msg.data, 0); |
|||
const imageData = new ImageData(arr, this.nextFrame.width, this.nextFrame.height); |
|||
this.conn.send('frame-received:' + this.nextFrame.sequenceId); |
|||
this.setFrame({ |
|||
data: imageData, |
|||
dpiX: this.nextFrame.dpiX, |
|||
dpiY: this.nextFrame.dpiY |
|||
}); |
|||
|
|||
|
|||
} |
|||
}; |
|||
|
|||
private setFrame(frame: PreviewerFrame | null) { |
|||
this.currentFrame = frame; |
|||
this.handlers.forEach(h => h(this.currentFrame)); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>Avalonia XAML previewer web edition</title> |
|||
</head> |
|||
|
|||
<body> |
|||
<div id="app"> |
|||
<center>Loading...</center> |
|||
</div> |
|||
<noscript>Javascript is required</noscript> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,15 @@ |
|||
import * as React from "react"; |
|||
import {PreviewerPresenter} from './FramePresenter' |
|||
import {PreviewerServerConnection} from "src/PreviewerServerConnection"; |
|||
import * as ReactDOM from "react-dom"; |
|||
|
|||
const loc = window.location; |
|||
const conn = new PreviewerServerConnection((loc.protocol === "https:" ? "wss" : "ws") + "://" + loc.host + "/ws"); |
|||
|
|||
const App = function(){ |
|||
return <div style={{width: '100%'}}> |
|||
<PreviewerPresenter conn={conn}/> |
|||
</div> |
|||
}; |
|||
|
|||
ReactDOM.render(<App/>, document.getElementById("app")); |
|||
@ -0,0 +1,35 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"outDir": "build/dist", |
|||
"module": "esnext", |
|||
"target": "es5", |
|||
"lib": ["es6", "dom"], |
|||
"sourceMap": true, |
|||
"allowJs": true, |
|||
"jsx": "react", |
|||
"moduleResolution": "node", |
|||
"forceConsistentCasingInFileNames": true, |
|||
"noImplicitReturns": true, |
|||
"noImplicitThis": true, |
|||
"noImplicitAny": true, |
|||
"strictNullChecks": true, |
|||
"suppressImplicitAnyIndexErrors": true, |
|||
"noUnusedLocals": false, |
|||
"baseUrl": ".", |
|||
"experimentalDecorators": true, |
|||
"paths": { |
|||
"*": ["./node_modules/@types/*", "./node_modules/*"], |
|||
"src/*": ["./src/*"] |
|||
} |
|||
}, |
|||
"include": ["./src/**/*"], |
|||
"exclude": [ |
|||
"node_modules", |
|||
"build", |
|||
"scripts", |
|||
"acceptance-tests", |
|||
"webpack", |
|||
"jest", |
|||
"src/setupTests.ts" |
|||
] |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
const webpack = require('webpack'); |
|||
const path = require('path'); |
|||
const LiveReloadPlugin = require('webpack-livereload-plugin'); |
|||
const HtmlWebpackPlugin = require('html-webpack-plugin'); |
|||
const CompressionPlugin = require('compression-webpack-plugin'); |
|||
const CleanWebpackPlugin = require('clean-webpack-plugin'); |
|||
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); |
|||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); |
|||
const CopyWebpackPlugin = require('copy-webpack-plugin'); |
|||
const prod = process.env.NODE_ENV == 'production'; |
|||
|
|||
class Printer { |
|||
apply(compiler) { |
|||
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString())); |
|||
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString())); |
|||
} |
|||
} |
|||
|
|||
const config = { |
|||
entry: { |
|||
bundle: './src/index.tsx' |
|||
}, |
|||
output: { |
|||
path: path.resolve(__dirname, 'build'), |
|||
publicPath: '/', |
|||
filename: '[name].[chunkhash].js' |
|||
}, |
|||
performance: { hints: false }, |
|||
mode: prod ? "production" : "development", |
|||
module: { |
|||
rules: [ |
|||
{ |
|||
enforce: "pre", |
|||
test: /\.js$/, |
|||
loader: "source-map-loader", |
|||
exclude: [ |
|||
path.resolve(__dirname, 'node_modules/mobx-state-router') |
|||
] |
|||
}, |
|||
{ |
|||
"oneOf": [ |
|||
{ |
|||
test: /\.(ts|tsx)$/, |
|||
exclude: /node_modules/, |
|||
use: 'awesome-typescript-loader' |
|||
}, |
|||
{ |
|||
test: /\.css$/, |
|||
use: [ |
|||
MiniCssExtractPlugin.loader, |
|||
'css-loader' |
|||
] |
|||
}, |
|||
{ |
|||
test: /\.(jpg|png)$/, |
|||
use: { |
|||
loader: "url-loader", |
|||
options: { |
|||
limit: 25000, |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, |
|||
use: [{ |
|||
loader: 'file-loader', |
|||
options: { |
|||
name: '[name].[ext]', |
|||
outputPath: 'fonts/', // where the fonts will go
|
|||
} |
|||
}] |
|||
}, |
|||
{ |
|||
loader: require.resolve('file-loader'), |
|||
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/], |
|||
options: { |
|||
name: 'assets/[name].[hash:8].[ext]', |
|||
}, |
|||
}] |
|||
}, |
|||
|
|||
] |
|||
}, |
|||
devtool: "source-map", |
|||
resolve: { |
|||
modules: [path.resolve(__dirname, 'node_modules')], |
|||
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })], |
|||
extensions: ['.ts', '.tsx', '.js', '.json'], |
|||
alias: { |
|||
'src': path.resolve(__dirname, 'src') |
|||
} |
|||
}, |
|||
plugins: |
|||
[ |
|||
new Printer(), |
|||
new CleanWebpackPlugin([path.resolve(__dirname, 'build')]), |
|||
new MiniCssExtractPlugin({ |
|||
filename: "[name].[chunkhash]h" + |
|||
".css", |
|||
chunkFilename: "[id].[chunkhash].css" |
|||
}), |
|||
new LiveReloadPlugin({appendScriptTag: !prod}), |
|||
new HtmlWebpackPlugin({ |
|||
template: path.resolve(__dirname, './src/index.html'), |
|||
filename: 'index.html' //relative to root of the application
|
|||
}), |
|||
new CopyWebpackPlugin([ |
|||
// relative path from src
|
|||
//{ from: './src/favicon.ico' },
|
|||
//{ from: './src/assets' }
|
|||
]), |
|||
new CompressionPlugin({ |
|||
test: /(\?.*)?$/i |
|||
}) |
|||
] |
|||
}; |
|||
module.exports = config; |
|||
@ -0,0 +1,72 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform.Interop; |
|||
|
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public class GlBasicInfoInterface : GlBasicInfoInterface<object> |
|||
{ |
|||
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress) : base(getProcAddress, null) |
|||
{ |
|||
} |
|||
|
|||
public GlBasicInfoInterface(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : base(nativeGetProcAddress, null) |
|||
{ |
|||
} |
|||
|
|||
public delegate void GlGetIntegerv(int name, out int rv); |
|||
public delegate IntPtr GlGetString(int v); |
|||
public delegate IntPtr GlGetStringi(int v, int v1); |
|||
} |
|||
|
|||
public class GlBasicInfoInterface<TContextInfo> : GlInterfaceBase<TContextInfo> |
|||
{ |
|||
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress, TContextInfo context) : base(getProcAddress, context) |
|||
{ |
|||
} |
|||
|
|||
public GlBasicInfoInterface(Func<Utf8Buffer, IntPtr> nativeGetProcAddress, TContextInfo context) : base(nativeGetProcAddress, context) |
|||
{ |
|||
} |
|||
|
|||
[GlEntryPoint("glGetIntegerv")] |
|||
public GlBasicInfoInterface.GlGetIntegerv GetIntegerv { get; } |
|||
|
|||
|
|||
[GlEntryPoint("glGetString")] |
|||
public GlBasicInfoInterface.GlGetString GetStringNative { get; } |
|||
|
|||
[GlEntryPoint("glGetStringi")] |
|||
public GlBasicInfoInterface.GlGetStringi GetStringiNative { get; } |
|||
|
|||
public string GetString(int v) |
|||
{ |
|||
var ptr = GetStringNative(v); |
|||
if (ptr != IntPtr.Zero) |
|||
return Marshal.PtrToStringAnsi(ptr); |
|||
return null; |
|||
} |
|||
|
|||
public string GetString(int v, int index) |
|||
{ |
|||
var ptr = GetStringiNative(v, index); |
|||
if (ptr != IntPtr.Zero) |
|||
return Marshal.PtrToStringAnsi(ptr); |
|||
return null; |
|||
} |
|||
|
|||
public List<string> GetExtensions() |
|||
{ |
|||
var sp = GetString(GlConsts.GL_EXTENSIONS); |
|||
if (sp != null) |
|||
return sp.Split(' ').ToList(); |
|||
GetIntegerv(GlConsts.GL_NUM_EXTENSIONS, out int count); |
|||
var rv = new List<string>(count); |
|||
for (var c = 0; c < count; c++) |
|||
rv.Add(GetString(GlConsts.GL_EXTENSIONS, c)); |
|||
return rv; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,8 +0,0 @@ |
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public enum GlDisplayType |
|||
{ |
|||
OpenGL2, |
|||
OpenGLES2 |
|||
} |
|||
} |
|||
@ -1,53 +1,79 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform.Interop; |
|||
|
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public class GlInterfaceBase |
|||
public class GlInterfaceBase : GlInterfaceBase<object> |
|||
{ |
|||
|
|||
private readonly Func<string, bool, IntPtr> _getProcAddress; |
|||
public GlInterfaceBase(Func<string, bool, IntPtr> getProcAddress) |
|||
public GlInterfaceBase(Func<string, IntPtr> getProcAddress) : base(getProcAddress, null) |
|||
{ |
|||
} |
|||
|
|||
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : base(nativeGetProcAddress, null) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
public class GlInterfaceBase<TContext> |
|||
{ |
|||
private readonly Func<string, IntPtr> _getProcAddress; |
|||
public GlInterfaceBase(Func<string, IntPtr> getProcAddress, TContext context) |
|||
{ |
|||
_getProcAddress = getProcAddress; |
|||
foreach (var prop in this.GetType().GetProperties()) |
|||
{ |
|||
var a = prop.GetCustomAttribute<GlEntryPointAttribute>(); |
|||
if (a != null) |
|||
var attrs = prop.GetCustomAttributes() |
|||
.Where(a => |
|||
a is IGlEntryPointAttribute || a is IGlEntryPointAttribute<TContext>) |
|||
.ToList(); |
|||
if(attrs.Count == 0) |
|||
continue; |
|||
|
|||
var isOptional = prop.GetCustomAttribute<GlOptionalEntryPoint>() != null; |
|||
|
|||
var fieldName = $"<{prop.Name}>k__BackingField"; |
|||
var field = prop.DeclaringType.GetField(fieldName, |
|||
BindingFlags.Instance | BindingFlags.NonPublic); |
|||
if (field == null) |
|||
throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}"); |
|||
|
|||
|
|||
IntPtr proc = IntPtr.Zero; |
|||
foreach (var attr in attrs) |
|||
{ |
|||
var fieldName = $"<{prop.Name}>k__BackingField"; |
|||
var field = prop.DeclaringType.GetField(fieldName, |
|||
BindingFlags.Instance | BindingFlags.NonPublic); |
|||
if (field == null) |
|||
throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}"); |
|||
var proc = getProcAddress(a.EntryPoint, a.Optional); |
|||
if (attr is IGlEntryPointAttribute<TContext> typed) |
|||
proc = typed.GetProcAddress(context, getProcAddress); |
|||
else if (attr is IGlEntryPointAttribute untyped) |
|||
proc = untyped.GetProcAddress(getProcAddress); |
|||
if (proc != IntPtr.Zero) |
|||
field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType)); |
|||
break; |
|||
} |
|||
|
|||
if (proc != IntPtr.Zero) |
|||
field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType)); |
|||
else if (!isOptional) |
|||
throw new OpenGlException("Unable to find a suitable GL function for " + prop.Name); |
|||
} |
|||
} |
|||
|
|||
protected static Func<string, bool, IntPtr> ConvertNative(Func<Utf8Buffer, IntPtr> func) => |
|||
(proc, optional) => |
|||
protected static Func<string, IntPtr> ConvertNative(Func<Utf8Buffer, IntPtr> func) => |
|||
(proc) => |
|||
{ |
|||
using (var u = new Utf8Buffer(proc)) |
|||
{ |
|||
var rv = func(u); |
|||
if (rv == IntPtr.Zero && !optional) |
|||
throw new OpenGlException("Missing function " + proc); |
|||
return rv; |
|||
} |
|||
}; |
|||
|
|||
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : this(ConvertNative(nativeGetProcAddress)) |
|||
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress, TContext context) : this(ConvertNative(nativeGetProcAddress), context) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IntPtr GetProcAddress(string proc) => _getProcAddress(proc, true); |
|||
public IntPtr GetProcAddress(string proc, bool optional) => _getProcAddress(proc, optional); |
|||
|
|||
public IntPtr GetProcAddress(string proc) => _getProcAddress(proc); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,22 @@ |
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public enum GlProfileType |
|||
{ |
|||
OpenGL, |
|||
OpenGLES |
|||
} |
|||
|
|||
public struct GlVersion |
|||
{ |
|||
public GlProfileType Type { get; } |
|||
public int Major { get; } |
|||
public int Minor { get; } |
|||
|
|||
public GlVersion(GlProfileType type, int major, int minor) |
|||
{ |
|||
Type = type; |
|||
Major = major; |
|||
Minor = minor; |
|||
} |
|||
} |
|||
} |
|||
@ -1,8 +1,13 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public interface IGlContext |
|||
public interface IGlContext : IDisposable |
|||
{ |
|||
IGlDisplay Display { get; } |
|||
void MakeCurrent(); |
|||
GlVersion Version { get; } |
|||
GlInterface GlInterface { get; } |
|||
int SampleCount { get; } |
|||
int StencilSize { get; } |
|||
IDisposable MakeCurrent(); |
|||
} |
|||
} |
|||
|
|||
@ -1,11 +0,0 @@ |
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public interface IGlDisplay |
|||
{ |
|||
GlDisplayType Type { get; } |
|||
GlInterface GlInterface { get; } |
|||
void ClearContext(); |
|||
int SampleCount { get; } |
|||
int StencilSize { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Avalonia.OpenGL.Imaging; |
|||
|
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public interface IOpenGlAwarePlatformRenderInterface |
|||
{ |
|||
IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.OpenGL.Imaging |
|||
{ |
|||
public interface IOpenGlTextureBitmapImpl : IBitmapImpl |
|||
{ |
|||
IDisposable Lock(); |
|||
void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling); |
|||
void SetDirty(); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.OpenGL.Imaging |
|||
{ |
|||
public class OpenGlTextureBitmap : Bitmap, IAffectsRender |
|||
{ |
|||
private IOpenGlTextureBitmapImpl _impl; |
|||
static IOpenGlTextureBitmapImpl CreateOrThrow() |
|||
{ |
|||
if (!(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>() is IOpenGlAwarePlatformRenderInterface |
|||
glAware)) |
|||
throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); |
|||
return glAware.CreateOpenGlTextureBitmap(); |
|||
} |
|||
|
|||
public OpenGlTextureBitmap() |
|||
: base(CreateOrThrow()) |
|||
{ |
|||
_impl = (IOpenGlTextureBitmapImpl)PlatformImpl.Item; |
|||
} |
|||
|
|||
public IDisposable Lock() => _impl.Lock(); |
|||
|
|||
public void SetTexture(int textureId, int internalFormat, PixelSize size, double dpiScaling) |
|||
{ |
|||
_impl.SetBackBuffer(textureId, internalFormat, size, dpiScaling); |
|||
SetIsDirty(); |
|||
} |
|||
|
|||
public void SetIsDirty() |
|||
{ |
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
CallInvalidated(); |
|||
else |
|||
Dispatcher.UIThread.Post(CallInvalidated); |
|||
} |
|||
|
|||
private void CallInvalidated() => Invalidated?.Invoke(this, EventArgs.Empty); |
|||
|
|||
public event EventHandler Invalidated; |
|||
} |
|||
} |
|||
@ -0,0 +1,215 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Media; |
|||
using Avalonia.OpenGL.Imaging; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.VisualTree; |
|||
using static Avalonia.OpenGL.GlConsts; |
|||
|
|||
namespace Avalonia.OpenGL |
|||
{ |
|||
public abstract class OpenGlControlBase : Control |
|||
{ |
|||
private IGlContext _context; |
|||
private int _fb, _texture, _renderBuffer; |
|||
private OpenGlTextureBitmap _bitmap; |
|||
private PixelSize _oldSize; |
|||
private bool _glFailed; |
|||
protected GlVersion GlVersion { get; private set; } |
|||
public sealed override void Render(DrawingContext context) |
|||
{ |
|||
if(!EnsureInitialized()) |
|||
return; |
|||
|
|||
using (_context.MakeCurrent()) |
|||
{ |
|||
using (_bitmap.Lock()) |
|||
{ |
|||
var gl = _context.GlInterface; |
|||
gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); |
|||
if (_oldSize != GetPixelSize()) |
|||
ResizeTexture(gl); |
|||
|
|||
OnOpenGlRender(gl, _fb); |
|||
gl.Flush(); |
|||
} |
|||
} |
|||
|
|||
context.DrawImage(_bitmap, new Rect(_bitmap.Size), Bounds); |
|||
base.Render(context); |
|||
} |
|||
|
|||
void DoCleanup(bool callUserDeinit) |
|||
{ |
|||
if (_context != null) |
|||
{ |
|||
using (_context.MakeCurrent()) |
|||
{ |
|||
var gl = _context.GlInterface; |
|||
gl.BindTexture(GL_TEXTURE_2D, 0); |
|||
gl.BindFramebuffer(GL_FRAMEBUFFER, 0); |
|||
gl.DeleteFramebuffers(1, new[] { _fb }); |
|||
using (_bitmap.Lock()) |
|||
_bitmap.SetTexture(0, 0, new PixelSize(1, 1), 1); |
|||
gl.DeleteTextures(1, new[] { _texture }); |
|||
gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); |
|||
_bitmap.Dispose(); |
|||
|
|||
try |
|||
{ |
|||
if (callUserDeinit) |
|||
OnOpenGlDeinit(_context.GlInterface, _fb); |
|||
} |
|||
finally |
|||
{ |
|||
_context.Dispose(); |
|||
_context = null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
DoCleanup(true); |
|||
base.OnDetachedFromVisualTree(e); |
|||
} |
|||
|
|||
bool EnsureInitialized() |
|||
{ |
|||
if (_context != null) |
|||
return true; |
|||
|
|||
if (_glFailed) |
|||
return false; |
|||
|
|||
var feature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>(); |
|||
if (feature == null) |
|||
return false; |
|||
try |
|||
{ |
|||
_context = feature.CreateContext(); |
|||
|
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase", |
|||
"Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); |
|||
_glFailed = true; |
|||
return false; |
|||
} |
|||
|
|||
GlVersion = _context.Version; |
|||
try |
|||
{ |
|||
_bitmap = new OpenGlTextureBitmap(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_context.Dispose(); |
|||
_context = null; |
|||
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase", |
|||
"Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e); |
|||
_glFailed = true; |
|||
return false; |
|||
} |
|||
|
|||
using (_context.MakeCurrent()) |
|||
{ |
|||
try |
|||
{ |
|||
_oldSize = GetPixelSize(); |
|||
var gl = _context.GlInterface; |
|||
var oneArr = new int[1]; |
|||
gl.GenFramebuffers(1, oneArr); |
|||
_fb = oneArr[0]; |
|||
gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); |
|||
|
|||
gl.GenTextures(1, oneArr); |
|||
_texture = oneArr[0]; |
|||
|
|||
ResizeTexture(gl); |
|||
|
|||
gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); |
|||
|
|||
var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); |
|||
if (status != GL_FRAMEBUFFER_COMPLETE) |
|||
{ |
|||
int code; |
|||
while ((code = gl.GetError()) != 0) |
|||
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase", |
|||
"Unable to initialize OpenGL FBO: {code}", code); |
|||
|
|||
_glFailed = true; |
|||
return false; |
|||
} |
|||
} |
|||
catch(Exception e) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase", |
|||
"Unable to initialize OpenGL FBO: {exception}", e); |
|||
_glFailed = true; |
|||
} |
|||
|
|||
if (!_glFailed) |
|||
OnOpenGlInit(_context.GlInterface, _fb); |
|||
} |
|||
|
|||
if (_glFailed) |
|||
{ |
|||
DoCleanup(false); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void ResizeTexture(GlInterface gl) |
|||
{ |
|||
var size = GetPixelSize(); |
|||
|
|||
gl.GetIntegerv( GL_TEXTURE_BINDING_2D, out var oldTexture); |
|||
gl.BindTexture(GL_TEXTURE_2D, _texture); |
|||
gl.TexImage2D(GL_TEXTURE_2D, 0, |
|||
GlVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8, |
|||
size.Width, size.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); |
|||
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
|||
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
|||
gl.BindTexture(GL_TEXTURE_2D, oldTexture); |
|||
|
|||
gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); |
|||
gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); |
|||
var oneArr = new int[1]; |
|||
gl.GenRenderbuffers(1, oneArr); |
|||
_renderBuffer = oneArr[0]; |
|||
gl.BindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); |
|||
gl.RenderbufferStorage(GL_RENDERBUFFER, |
|||
GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, |
|||
size.Width, size.Height); |
|||
gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderBuffer); |
|||
gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); |
|||
using (_bitmap.Lock()) |
|||
_bitmap.SetTexture(_texture, GL_RGBA8, size, 1); |
|||
} |
|||
|
|||
PixelSize GetPixelSize() |
|||
{ |
|||
var scaling = VisualRoot.RenderScaling; |
|||
return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), |
|||
Math.Max(1, (int)(Bounds.Height * scaling))); |
|||
} |
|||
|
|||
|
|||
protected virtual void OnOpenGlInit(GlInterface gl, int fb) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected virtual void OnOpenGlDeinit(GlInterface gl, int fb) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected abstract void OnOpenGlRender(GlInterface gl, int fb); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Avalonia.Remote.Protocol |
|||
{ |
|||
[AvaloniaRemoteMessageGuid("53778004-78fa-4381-8ec3-176a6f2328b6")] |
|||
public class HtmlTransportStartedMessage |
|||
{ |
|||
public string Uri { get; set; } |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Animation.Animators |
|||
{ |
|||
public class BoxShadowAnimator : Animator<BoxShadow> |
|||
{ |
|||
static ColorAnimator s_colorAnimator = new ColorAnimator(); |
|||
static DoubleAnimator s_doubleAnimator = new DoubleAnimator(); |
|||
static BoolAnimator s_boolAnimator = new BoolAnimator(); |
|||
public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue) |
|||
{ |
|||
return new BoxShadow |
|||
{ |
|||
OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX), |
|||
OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY), |
|||
Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur), |
|||
Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread), |
|||
Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color), |
|||
IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset) |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Animation.Animators |
|||
{ |
|||
public class BoxShadowsAnimator : Animator<BoxShadows> |
|||
{ |
|||
private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator(); |
|||
public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue) |
|||
{ |
|||
int cnt = progress >= 1d ? newValue.Count : oldValue.Count; |
|||
if (cnt == 0) |
|||
return new BoxShadows(); |
|||
|
|||
BoxShadow first; |
|||
if (oldValue.Count > 0 && newValue.Count > 0) |
|||
first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]); |
|||
else if (oldValue.Count > 0) |
|||
first = oldValue[0]; |
|||
else |
|||
first = newValue[0]; |
|||
|
|||
if (cnt == 1) |
|||
return new BoxShadows(first); |
|||
|
|||
var rest = new BoxShadow[cnt - 1]; |
|||
for (var c = 0; c < rest.Length; c++) |
|||
{ |
|||
var idx = c + 1; |
|||
if (oldValue.Count > idx && newValue.Count > idx) |
|||
rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]); |
|||
else if (oldValue.Count > idx) |
|||
rest[c] = oldValue[idx]; |
|||
else |
|||
rest[c] = newValue[idx]; |
|||
} |
|||
|
|||
return new BoxShadows(first, rest); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Animation.Animators; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
public struct BoxShadow |
|||
{ |
|||
public double OffsetX { get; set; } |
|||
public double OffsetY { get; set; } |
|||
public double Blur { get; set; } |
|||
public double Spread { get; set; } |
|||
public Color Color { get; set; } |
|||
public bool IsInset { get; set; } |
|||
|
|||
static BoxShadow() |
|||
{ |
|||
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop => |
|||
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); |
|||
} |
|||
|
|||
public bool Equals(in BoxShadow other) |
|||
{ |
|||
return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color); |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return obj is BoxShadow other && Equals(other); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
var hashCode = OffsetX.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ OffsetY.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ Blur.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ Spread.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ Color.GetHashCode(); |
|||
return hashCode; |
|||
} |
|||
} |
|||
|
|||
public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; |
|||
|
|||
private readonly static char[] s_Separator = new char[] { ' ', '\t' }; |
|||
|
|||
struct ArrayReader |
|||
{ |
|||
private int _index; |
|||
private string[] _arr; |
|||
|
|||
public ArrayReader(string[] arr) |
|||
{ |
|||
_arr = arr; |
|||
_index = 0; |
|||
} |
|||
|
|||
public bool TryReadString(out string s) |
|||
{ |
|||
s = null; |
|||
if (_index >= _arr.Length) |
|||
return false; |
|||
s = _arr[_index]; |
|||
_index++; |
|||
return true; |
|||
} |
|||
|
|||
public string ReadString() |
|||
{ |
|||
if(!TryReadString(out var rv)) |
|||
throw new FormatException(); |
|||
return rv; |
|||
} |
|||
} |
|||
public static unsafe BoxShadow Parse(string s) |
|||
{ |
|||
if(s == null) |
|||
throw new ArgumentNullException(); |
|||
if (s.Length == 0) |
|||
throw new FormatException(); |
|||
|
|||
var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries); |
|||
if (p.Length == 1 && p[0] == "none") |
|||
return default; |
|||
|
|||
if (p.Length < 3 || p.Length > 6) |
|||
throw new FormatException(); |
|||
|
|||
bool inset = false; |
|||
|
|||
var tokenizer = new ArrayReader(p); |
|||
|
|||
string firstToken = tokenizer.ReadString(); |
|||
if (firstToken == "inset") |
|||
{ |
|||
inset = true; |
|||
firstToken = tokenizer.ReadString(); |
|||
} |
|||
|
|||
var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture); |
|||
var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture); |
|||
double blur = 0; |
|||
double spread = 0; |
|||
|
|||
|
|||
tokenizer.TryReadString(out var token3); |
|||
tokenizer.TryReadString(out var token4); |
|||
tokenizer.TryReadString(out var token5); |
|||
|
|||
if (token4 != null) |
|||
blur = double.Parse(token3, CultureInfo.InvariantCulture); |
|||
if (token5 != null) |
|||
spread = double.Parse(token4, CultureInfo.InvariantCulture); |
|||
|
|||
var color = Color.Parse(token5 ?? token4 ?? token3); |
|||
return new BoxShadow |
|||
{ |
|||
IsInset = inset, |
|||
OffsetX = offsetX, |
|||
OffsetY = offsetY, |
|||
Blur = blur, |
|||
Spread = spread, |
|||
Color = color |
|||
}; |
|||
} |
|||
|
|||
public Rect TransformBounds(in Rect rect) |
|||
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); |
|||
} |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using Avalonia.Animation.Animators; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
public struct BoxShadows |
|||
{ |
|||
private readonly BoxShadow _first; |
|||
private readonly BoxShadow[] _list; |
|||
public int Count { get; } |
|||
|
|||
static BoxShadows() |
|||
{ |
|||
Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop => |
|||
typeof(BoxShadows).IsAssignableFrom(prop.PropertyType)); |
|||
} |
|||
|
|||
public BoxShadows(BoxShadow shadow) |
|||
{ |
|||
_first = shadow; |
|||
_list = null; |
|||
Count = 1; |
|||
} |
|||
|
|||
public BoxShadows(BoxShadow first, BoxShadow[] rest) |
|||
{ |
|||
_first = first; |
|||
_list = rest; |
|||
Count = 1 + (rest?.Length ?? 0); |
|||
} |
|||
|
|||
public BoxShadow this[int c] |
|||
{ |
|||
get |
|||
{ |
|||
if (c< 0 || c >= Count) |
|||
throw new IndexOutOfRangeException(); |
|||
if (c == 0) |
|||
return _first; |
|||
return _list[c - 1]; |
|||
} |
|||
} |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public struct BoxShadowsEnumerator |
|||
{ |
|||
private int _index; |
|||
private BoxShadows _shadows; |
|||
|
|||
public BoxShadowsEnumerator(BoxShadows shadows) |
|||
{ |
|||
_shadows = shadows; |
|||
_index = -1; |
|||
} |
|||
|
|||
public BoxShadow Current => _shadows[_index]; |
|||
|
|||
public bool MoveNext() |
|||
{ |
|||
_index++; |
|||
return _index < _shadows.Count; |
|||
} |
|||
} |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this); |
|||
|
|||
private static readonly char[] s_Separators = new[] { ',' }; |
|||
public static BoxShadows Parse(string s) |
|||
{ |
|||
var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries); |
|||
if (sp.Length == 0 |
|||
|| (sp.Length == 1 && |
|||
(string.IsNullOrWhiteSpace(sp[0]) |
|||
|| sp[0] == "none"))) |
|||
return new BoxShadows(); |
|||
|
|||
var first = BoxShadow.Parse(sp[0]); |
|||
if (sp.Length == 1) |
|||
return new BoxShadows(first); |
|||
|
|||
var rest = new BoxShadow[sp.Length - 1]; |
|||
for (var c = 0; c < rest.Length; c++) |
|||
rest[c] = BoxShadow.Parse(sp[c + 1]); |
|||
return new BoxShadows(first, rest); |
|||
} |
|||
|
|||
public Rect TransformBounds(in Rect rect) |
|||
{ |
|||
var final = rect; |
|||
foreach (var shadow in this) |
|||
final = final.Union(shadow.TransformBounds(rect)); |
|||
return final; |
|||
} |
|||
|
|||
public bool HasInsetShadows |
|||
{ |
|||
get |
|||
{ |
|||
foreach(var boxShadow in this) |
|||
if (!boxShadow.IsEmpty && boxShadow.IsInset) |
|||
return true; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow); |
|||
|
|||
public bool Equals(BoxShadows other) |
|||
{ |
|||
if (other.Count != Count) |
|||
return false; |
|||
for(var c=0; c<Count ; c++) |
|||
if (!this[c].Equals(other[c])) |
|||
return false; |
|||
return true; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return obj is BoxShadows other && Equals(other); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
int hashCode = 0; |
|||
foreach (var s in this) |
|||
hashCode = (hashCode * 397) ^ s.GetHashCode(); |
|||
return hashCode; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,50 +1,11 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class ManagedDeferredRendererLock : IDeferredRendererLock |
|||
public class ManagedDeferredRendererLock : DisposableLock, IDeferredRendererLock |
|||
{ |
|||
private readonly object _lock = new object(); |
|||
|
|||
/// <summary>
|
|||
/// Tries to lock the target surface or window
|
|||
/// </summary>
|
|||
/// <returns>IDisposable if succeeded to obtain the lock</returns>
|
|||
public IDisposable TryLock() |
|||
{ |
|||
if (Monitor.TryEnter(_lock)) |
|||
return new UnlockDisposable(_lock); |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Enters a waiting lock, only use from platform code, not from the renderer
|
|||
/// </summary>
|
|||
public IDisposable Lock() |
|||
{ |
|||
Monitor.Enter(_lock); |
|||
return new UnlockDisposable(_lock); |
|||
} |
|||
|
|||
private sealed class UnlockDisposable : IDisposable |
|||
{ |
|||
private object _lock; |
|||
|
|||
public UnlockDisposable(object @lock) |
|||
{ |
|||
_lock = @lock; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
object @lock = Interlocked.Exchange(ref _lock, null); |
|||
|
|||
if (@lock != null) |
|||
{ |
|||
Monitor.Exit(@lock); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,136 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
public struct RoundedRect |
|||
{ |
|||
public bool Equals(RoundedRect other) |
|||
{ |
|||
return Rect.Equals(other.Rect) && RadiiTopLeft.Equals(other.RadiiTopLeft) && RadiiTopRight.Equals(other.RadiiTopRight) && RadiiBottomLeft.Equals(other.RadiiBottomLeft) && RadiiBottomRight.Equals(other.RadiiBottomRight); |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return obj is RoundedRect other && Equals(other); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
var hashCode = Rect.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ RadiiTopLeft.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ RadiiTopRight.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ RadiiBottomLeft.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ RadiiBottomRight.GetHashCode(); |
|||
return hashCode; |
|||
} |
|||
} |
|||
|
|||
public Rect Rect { get; } |
|||
public Vector RadiiTopLeft { get; } |
|||
public Vector RadiiTopRight { get; } |
|||
public Vector RadiiBottomLeft { get; } |
|||
public Vector RadiiBottomRight { get; } |
|||
|
|||
public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft) |
|||
{ |
|||
Rect = rect; |
|||
RadiiTopLeft = radiiTopLeft; |
|||
RadiiTopRight = radiiTopRight; |
|||
RadiiBottomRight = radiiBottomRight; |
|||
RadiiBottomLeft = radiiBottomLeft; |
|||
} |
|||
|
|||
public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight, |
|||
double radiusBottomLeft) |
|||
: this(rect, |
|||
new Vector(radiusTopLeft, radiusTopLeft), |
|||
new Vector(radiusTopRight, radiusTopRight), |
|||
new Vector(radiusBottomRight, radiusBottomRight), |
|||
new Vector(radiusBottomLeft, radiusBottomLeft) |
|||
) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY)) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public RoundedRect(Rect rect, double radius) : this(rect, radius, radius) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public RoundedRect(Rect rect) : this(rect, 0) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public static implicit operator RoundedRect(Rect r) => new RoundedRect(r); |
|||
|
|||
public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default || |
|||
RadiiBottomLeft != default; |
|||
|
|||
public bool IsUniform => |
|||
RadiiTopLeft.Equals(RadiiTopRight) && |
|||
RadiiTopLeft.Equals(RadiiBottomRight) && |
|||
RadiiTopLeft.Equals(RadiiBottomLeft); |
|||
|
|||
public RoundedRect Inflate(double dx, double dy) |
|||
{ |
|||
return Deflate(-dx, -dy); |
|||
} |
|||
|
|||
public unsafe RoundedRect Deflate(double dx, double dy) |
|||
{ |
|||
if (!IsRounded) |
|||
return new RoundedRect(Rect.Deflate(new Thickness(dx, dy))); |
|||
|
|||
// Ported from SKRRect
|
|||
var left = Rect.X + dx; |
|||
var top = Rect.Y + dy; |
|||
var right = left + Rect.Width - dx * 2; |
|||
var bottom = top + Rect.Height - dy * 2; |
|||
var radii = stackalloc Vector[4]; |
|||
radii[0] = RadiiTopLeft; |
|||
radii[1] = RadiiTopRight; |
|||
radii[2] = RadiiBottomRight; |
|||
radii[3] = RadiiBottomLeft; |
|||
|
|||
bool degenerate = false; |
|||
if (right <= left) { |
|||
degenerate = true; |
|||
left = right = (left + right)*0.5; |
|||
} |
|||
if (bottom <= top) { |
|||
degenerate = true; |
|||
top = bottom = (top + bottom) * 0.5; |
|||
} |
|||
if (degenerate) |
|||
{ |
|||
return new RoundedRect(new Rect(left, top, right - left, bottom - top)); |
|||
} |
|||
|
|||
for (var c = 0; c < 4; c++) |
|||
{ |
|||
var rx = Math.Max(0, radii[c].X - dx); |
|||
var ry = Math.Max(0, radii[c].Y - dy); |
|||
if (rx == 0 || ry == 0) |
|||
radii[c] = default; |
|||
else |
|||
radii[c] = new Vector(rx, ry); |
|||
} |
|||
|
|||
return new RoundedRect(new Rect(left, top, right - left, bottom - top), |
|||
radii[0], radii[1], radii[2], radii[3]); |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +1,19 @@ |
|||
using System; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Custom Skia render target.
|
|||
/// </summary>
|
|||
public interface ICustomSkiaRenderTarget : IDisposable |
|||
public interface ISkiaGpuRenderTarget : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Start rendering to this render target.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
ICustomSkiaRenderSession BeginRendering(); |
|||
ISkiaGpuRenderSession BeginRenderingSession(); |
|||
|
|||
bool IsCorrupted { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.OpenGL; |
|||
using Avalonia.OpenGL.Imaging; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
class GlSkiaGpu : IOpenGlAwareSkiaGpu |
|||
{ |
|||
private GRContext _grContext; |
|||
|
|||
public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes) |
|||
{ |
|||
var context = gl.MainContext; |
|||
using (context.MakeCurrent()) |
|||
{ |
|||
using (var iface = context.Version.Type == GlProfileType.OpenGL ? |
|||
GRGlInterface.AssembleGlInterface((_, proc) => context.GlInterface.GetProcAddress(proc)) : |
|||
GRGlInterface.AssembleGlesInterface((_, proc) => context.GlInterface.GetProcAddress(proc))) |
|||
{ |
|||
_grContext = GRContext.Create(GRBackend.OpenGL, iface); |
|||
if (maxResourceBytes.HasValue) |
|||
{ |
|||
_grContext.GetResourceCacheLimits(out var maxResources, out _); |
|||
_grContext.SetResourceCacheLimits(maxResources, maxResourceBytes.Value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces) |
|||
{ |
|||
foreach (var surface in surfaces) |
|||
{ |
|||
if (surface is IGlPlatformSurface glSurface) |
|||
{ |
|||
return new GlRenderTarget(_grContext, glSurface); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() => new OpenGlTextureBitmapImpl(); |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
using System; |
|||
using System.IO; |
|||
using Avalonia.OpenGL; |
|||
using Avalonia.OpenGL.Imaging; |
|||
using Avalonia.Skia.Helpers; |
|||
using Avalonia.Utilities; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
class OpenGlTextureBitmapImpl : IOpenGlTextureBitmapImpl, IDrawableBitmapImpl |
|||
{ |
|||
private DisposableLock _lock = new DisposableLock(); |
|||
private int _textureId; |
|||
private int _internalFormat; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
using (Lock()) |
|||
{ |
|||
_textureId = 0; |
|||
PixelSize = new PixelSize(1, 1); |
|||
Version++; |
|||
} |
|||
} |
|||
|
|||
public Vector Dpi { get; private set; } = new Vector(96, 96); |
|||
public PixelSize PixelSize { get; private set; } = new PixelSize(1, 1); |
|||
public int Version { get; private set; } = 0; |
|||
|
|||
public void Save(string fileName) => throw new System.NotSupportedException(); |
|||
public void Save(Stream stream) => throw new System.NotSupportedException(); |
|||
|
|||
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) |
|||
{ |
|||
// For now silently ignore
|
|||
if (context.GrContext == null) |
|||
return; |
|||
|
|||
using (Lock()) |
|||
{ |
|||
if (_textureId == 0) |
|||
return; |
|||
using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, |
|||
new GRGlTextureInfo( |
|||
GlConsts.GL_TEXTURE_2D, (uint)_textureId, |
|||
(uint)_internalFormat))) |
|||
using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, |
|||
SKColorType.Rgba8888)) |
|||
{ |
|||
// Again, silently ignore, if something went wrong it's not our fault
|
|||
if (surface == null) |
|||
return; |
|||
|
|||
using (var snapshot = surface.Snapshot()) |
|||
context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IDisposable Lock() => _lock.Lock(); |
|||
|
|||
public void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling) |
|||
{ |
|||
using (_lock.Lock()) |
|||
{ |
|||
_textureId = textureId; |
|||
_internalFormat = internalFormat; |
|||
PixelSize = pixelSize; |
|||
Dpi = new Vector(96 * dpiScaling, 96 * dpiScaling); |
|||
Version++; |
|||
} |
|||
} |
|||
|
|||
public void SetDirty() |
|||
{ |
|||
using (_lock.Lock()) |
|||
Version++; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue