194 changed files with 19846 additions and 1485 deletions
@ -0,0 +1,89 @@ |
|||||
|
#include "common.h" |
||||
|
|
||||
|
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop) |
||||
|
{ |
||||
|
int effects = 0; |
||||
|
if((nsop & NSDragOperationCopy) != 0) |
||||
|
effects |= (int)AvnDragDropEffects::Copy; |
||||
|
if((nsop & NSDragOperationMove) != 0) |
||||
|
effects |= (int)AvnDragDropEffects::Move; |
||||
|
if((nsop & NSDragOperationLink) != 0) |
||||
|
effects |= (int)AvnDragDropEffects::Link; |
||||
|
return (AvnDragDropEffects)effects; |
||||
|
}; |
||||
|
|
||||
|
extern NSString* GetAvnCustomDataType() |
||||
|
{ |
||||
|
char buffer[256]; |
||||
|
sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid()); |
||||
|
return [NSString stringWithUTF8String:buffer]; |
||||
|
} |
||||
|
|
||||
|
@interface AvnDndSource : NSObject<NSDraggingSource> |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation AvnDndSource |
||||
|
{ |
||||
|
NSDragOperation _operation; |
||||
|
ComPtr<IAvnDndResultCallback> _cb; |
||||
|
void* _sourceHandle; |
||||
|
}; |
||||
|
|
||||
|
- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context |
||||
|
{ |
||||
|
return NSDragOperationCopy; |
||||
|
} |
||||
|
|
||||
|
- (AvnDndSource*) initWithOperation: (NSDragOperation)operation |
||||
|
andCallback: (IAvnDndResultCallback*) cb |
||||
|
andSourceHandle: (void*) handle |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
_operation = operation; |
||||
|
_cb = cb; |
||||
|
_sourceHandle = handle; |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)draggingSession:(NSDraggingSession *)session |
||||
|
endedAtPoint:(NSPoint)screenPoint |
||||
|
operation:(NSDragOperation)operation |
||||
|
{ |
||||
|
if(_cb != nil) |
||||
|
{ |
||||
|
auto cb = _cb; |
||||
|
_cb = nil; |
||||
|
cb->OnDragAndDropComplete(ConvertDragDropEffects(operation)); |
||||
|
} |
||||
|
if(_sourceHandle != nil) |
||||
|
{ |
||||
|
FreeAvnGCHandle(_sourceHandle); |
||||
|
_sourceHandle = nil; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void*) gcHandle |
||||
|
{ |
||||
|
return _sourceHandle; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle) |
||||
|
{ |
||||
|
return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle]; |
||||
|
}; |
||||
|
|
||||
|
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info) |
||||
|
{ |
||||
|
id obj = [info draggingSource]; |
||||
|
if(obj == nil) |
||||
|
return nil; |
||||
|
if([obj isKindOfClass: [AvnDndSource class]]) |
||||
|
{ |
||||
|
auto src = (AvnDndSource*)obj; |
||||
|
return [src gcHandle]; |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
@ -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.
@ -0,0 +1,115 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.ObjectModel; |
||||
|
using System.Linq; |
||||
|
using System.Reactive; |
||||
|
using Avalonia.Controls; |
||||
|
using ReactiveUI; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels |
||||
|
{ |
||||
|
public class TreeViewPageViewModel : ReactiveObject |
||||
|
{ |
||||
|
private readonly Node _root; |
||||
|
private SelectionMode _selectionMode; |
||||
|
|
||||
|
public TreeViewPageViewModel() |
||||
|
{ |
||||
|
_root = new Node(); |
||||
|
|
||||
|
Items = _root.Children; |
||||
|
Selection = new SelectionModel(); |
||||
|
Selection.SelectionChanged += SelectionChanged; |
||||
|
|
||||
|
AddItemCommand = ReactiveCommand.Create(AddItem); |
||||
|
RemoveItemCommand = ReactiveCommand.Create(RemoveItem); |
||||
|
} |
||||
|
|
||||
|
public ObservableCollection<Node> Items { get; } |
||||
|
public SelectionModel Selection { get; } |
||||
|
public ReactiveCommand<Unit, Unit> AddItemCommand { get; } |
||||
|
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; } |
||||
|
|
||||
|
public SelectionMode SelectionMode |
||||
|
{ |
||||
|
get => _selectionMode; |
||||
|
set |
||||
|
{ |
||||
|
Selection.ClearSelection(); |
||||
|
this.RaiseAndSetIfChanged(ref _selectionMode, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void AddItem() |
||||
|
{ |
||||
|
var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root; |
||||
|
parentItem.AddItem(); |
||||
|
} |
||||
|
|
||||
|
private void RemoveItem() |
||||
|
{ |
||||
|
while (Selection.SelectedItems.Count > 0) |
||||
|
{ |
||||
|
Node lastItem = (Node)Selection.SelectedItems[0]; |
||||
|
RecursiveRemove(Items, lastItem); |
||||
|
Selection.DeselectAt(Selection.SelectedIndices[0]); |
||||
|
} |
||||
|
|
||||
|
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem) |
||||
|
{ |
||||
|
if (items.Remove(selectedItem)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
foreach (Node item in items) |
||||
|
{ |
||||
|
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) |
||||
|
{ |
||||
|
var selected = string.Join(",", e.SelectedIndices); |
||||
|
var deselected = string.Join(",", e.DeselectedIndices); |
||||
|
System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'"); |
||||
|
} |
||||
|
|
||||
|
public class Node |
||||
|
{ |
||||
|
private ObservableCollection<Node> _children; |
||||
|
private int _childIndex = 10; |
||||
|
|
||||
|
public Node() |
||||
|
{ |
||||
|
Header = "Item"; |
||||
|
} |
||||
|
|
||||
|
public Node(Node parent, int index) |
||||
|
{ |
||||
|
Parent = parent; |
||||
|
Header = parent.Header + ' ' + index; |
||||
|
} |
||||
|
|
||||
|
public Node Parent { get; } |
||||
|
public string Header { get; } |
||||
|
public bool AreChildrenInitialized => _children != null; |
||||
|
public ObservableCollection<Node> Children => _children ??= CreateChildren(); |
||||
|
public void AddItem() => Children.Add(new Node(this, _childIndex++)); |
||||
|
public void RemoveItem(Node child) => Children.Remove(child); |
||||
|
public override string ToString() => Header; |
||||
|
|
||||
|
private ObservableCollection<Node> CreateChildren() |
||||
|
{ |
||||
|
return new ObservableCollection<Node>( |
||||
|
Enumerable.Range(0, 10).Select(i => new Node(this, i))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
using System; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace RenderDemo.Controls |
||||
|
{ |
||||
|
public class LineBoundsDemoControl : Control |
||||
|
{ |
||||
|
static LineBoundsDemoControl() |
||||
|
{ |
||||
|
AffectsRender<LineBoundsDemoControl>(AngleProperty); |
||||
|
} |
||||
|
|
||||
|
public LineBoundsDemoControl() |
||||
|
{ |
||||
|
var timer = new DispatcherTimer(); |
||||
|
timer.Interval = TimeSpan.FromSeconds(1 / 60.0); |
||||
|
timer.Tick += (sender, e) => Angle += Math.PI / 360; |
||||
|
timer.Start(); |
||||
|
} |
||||
|
|
||||
|
public static readonly StyledProperty<double> AngleProperty = |
||||
|
AvaloniaProperty.Register<LineBoundsDemoControl, double>(nameof(Angle)); |
||||
|
|
||||
|
public double Angle |
||||
|
{ |
||||
|
get => GetValue(AngleProperty); |
||||
|
set => SetValue(AngleProperty, value); |
||||
|
} |
||||
|
|
||||
|
public override void Render(DrawingContext drawingContext) |
||||
|
{ |
||||
|
var lineLength = Math.Sqrt((100 * 100) + (100 * 100)); |
||||
|
|
||||
|
var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength); |
||||
|
var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength); |
||||
|
|
||||
|
|
||||
|
var p1 = new Point(200, 200); |
||||
|
var p2 = new Point(p1.X + diffX, p1.Y + diffY); |
||||
|
|
||||
|
var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square); |
||||
|
var boundPen = new Pen(Brushes.Black); |
||||
|
|
||||
|
drawingContext.DrawLine(pen, p1, p2); |
||||
|
|
||||
|
drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
|
xmlns:controls="clr-namespace:RenderDemo.Controls" |
||||
|
x:Class="RenderDemo.Pages.LineBoundsPage"> |
||||
|
<controls:LineBoundsDemoControl /> |
||||
|
</UserControl> |
||||
@ -0,0 +1,19 @@ |
|||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace RenderDemo.Pages |
||||
|
{ |
||||
|
public class LineBoundsPage : UserControl |
||||
|
{ |
||||
|
public LineBoundsPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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\netcoreapp3.1\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\netcoreapp3.1\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\netcoreapp3.1\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\netcoreapp3.1\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.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\netcoreapp3.1\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\netcoreapp3.1\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\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$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\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$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.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,76 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Platform; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Native.Interop; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Native |
||||
|
{ |
||||
|
class AvaloniaNativeDragSource : IPlatformDragSource |
||||
|
{ |
||||
|
private readonly IAvaloniaNativeFactory _factory; |
||||
|
|
||||
|
public AvaloniaNativeDragSource(IAvaloniaNativeFactory factory) |
||||
|
{ |
||||
|
_factory = factory; |
||||
|
} |
||||
|
|
||||
|
TopLevel FindRoot(IInteractive interactive) |
||||
|
{ |
||||
|
while (interactive != null && !(interactive is IVisual)) |
||||
|
interactive = interactive.InteractiveParent; |
||||
|
if (interactive == null) |
||||
|
return null; |
||||
|
var visual = (IVisual)interactive; |
||||
|
return visual.VisualRoot as TopLevel; |
||||
|
} |
||||
|
|
||||
|
class DndCallback : CallbackBase, IAvnDndResultCallback |
||||
|
{ |
||||
|
private TaskCompletionSource<DragDropEffects> _tcs; |
||||
|
|
||||
|
public DndCallback(TaskCompletionSource<DragDropEffects> tcs) |
||||
|
{ |
||||
|
_tcs = tcs; |
||||
|
} |
||||
|
public void OnDragAndDropComplete(AvnDragDropEffects effect) |
||||
|
{ |
||||
|
_tcs?.TrySetResult((DragDropEffects)effect); |
||||
|
_tcs = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) |
||||
|
{ |
||||
|
// Sanity check
|
||||
|
var tl = FindRoot(triggerEvent.Source); |
||||
|
var view = tl?.PlatformImpl as WindowBaseImpl; |
||||
|
if (view == null) |
||||
|
throw new ArgumentException(); |
||||
|
|
||||
|
triggerEvent.Pointer.Capture(null); |
||||
|
|
||||
|
var tcs = new TaskCompletionSource<DragDropEffects>(); |
||||
|
|
||||
|
var clipboardImpl = _factory.CreateDndClipboard(); |
||||
|
using (var clipboard = new ClipboardImpl(clipboardImpl)) |
||||
|
using (var cb = new DndCallback(tcs)) |
||||
|
{ |
||||
|
if (data.Contains(DataFormats.Text)) |
||||
|
// API is synchronous, so it's OK
|
||||
|
clipboard.SetTextAsync(data.GetText()).Wait(); |
||||
|
|
||||
|
view.BeginDraggingSession((AvnDragDropEffects)allowedEffects, |
||||
|
triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb, |
||||
|
GCHandle.ToIntPtr(GCHandle.Alloc(data))); |
||||
|
} |
||||
|
|
||||
|
return tcs.Task; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace Avalonia.Native.Interop |
||||
|
{ |
||||
|
unsafe partial class IAvnString |
||||
|
{ |
||||
|
private string _managed; |
||||
|
|
||||
|
public string String |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_managed == null) |
||||
|
{ |
||||
|
var ptr = Pointer(); |
||||
|
if (ptr == null) |
||||
|
return null; |
||||
|
_managed = System.Text.Encoding.UTF8.GetString((byte*)ptr.ToPointer(), Length()); |
||||
|
} |
||||
|
|
||||
|
return _managed; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override string ToString() => String; |
||||
|
} |
||||
|
|
||||
|
partial class IAvnStringArray |
||||
|
{ |
||||
|
public string[] ToStringArray() |
||||
|
{ |
||||
|
var arr = new string[Count]; |
||||
|
for(uint c = 0; c<arr.Length;c++) |
||||
|
using (var s = Get(c)) |
||||
|
arr[c] = s.String; |
||||
|
return arr; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue