Reorganize project structure

This commit is contained in:
Stone_Red
2026-06-19 00:56:46 +02:00
parent 6f58eefb66
commit 95d88f1a8d
84 changed files with 267 additions and 186 deletions
@@ -0,0 +1,69 @@
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using System.Security.Cryptography;
using System.Text;
namespace RemSox.Shared.Cryptography;
public static class AesGcmCrypto
{
private const int NonceSize = 12;
private const int TagSize = 128;
public static byte[] Encrypt(byte[] plainData, byte[] key)
{
byte[] nonce = new byte[NonceSize];
RandomNumberGenerator.Fill(nonce);
GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
cipher.Init(true, new AeadParameters(new KeyParameter(key), TagSize, nonce));
byte[] output = new byte[cipher.GetOutputSize(plainData.Length)];
int len = cipher.ProcessBytes(plainData, 0, plainData.Length, output, 0);
_ = cipher.DoFinal(output, len);
byte[] result = new byte[nonce.Length + output.Length];
Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length);
Buffer.BlockCopy(output, 0, result, nonce.Length, output.Length);
return result;
}
public static string Encrypt(string text, byte[] key)
{
byte[] data = Encoding.UTF8.GetBytes(text);
return Convert.ToBase64String(Encrypt(data, key));
}
public static byte[] Decrypt(byte[] encryptedData, byte[] key)
{
byte[] nonce = new byte[NonceSize];
Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize);
int cipherLength = encryptedData.Length - NonceSize;
byte[] cipherBytes = new byte[cipherLength];
Buffer.BlockCopy(encryptedData, NonceSize, cipherBytes, 0, cipherLength);
GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
cipher.Init(false, new AeadParameters(new KeyParameter(key), TagSize, nonce));
byte[] plain = new byte[cipher.GetOutputSize(cipherBytes.Length)];
int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plain, 0);
_ = cipher.DoFinal(plain, len);
return plain;
}
public static string Decrypt(string cipherText, byte[] key)
{
byte[] data = Convert.FromBase64String(cipherText);
byte[] plain = Decrypt(data, key);
return Encoding.UTF8.GetString(plain);
}
}
@@ -0,0 +1,26 @@
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
namespace RemSox.Shared.Cryptography;
public static class Pkcs5S2PasswordKeyDeriver
{
private const int KeySize = 32; // 256-bit key
private const int Iterations = 150_000; // PBKDF2 cost factor
public static byte[] DeriveKey(string password, byte[] salt)
{
Pkcs5S2ParametersGenerator generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
generator.Init(
PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()),
salt,
Iterations
);
return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize * 8))
.GetKey();
}
}
@@ -0,0 +1,16 @@
using RemSox.Shared.Cryptography;
namespace RemSox.Shared.Networking;
public class AesPacketCrypto(byte[] key) : IPacketCrypto
{
public byte[] Decrypt(byte[] data)
{
return AesGcmCrypto.Decrypt(data, key);
}
public byte[] Encrypt(byte[] data)
{
return AesGcmCrypto.Encrypt(data, key);
}
}
@@ -0,0 +1,8 @@
namespace RemSox.Shared.Networking;
public interface IPacketCrypto
{
byte[] Encrypt(byte[] data);
byte[] Decrypt(byte[] data);
}
+19
View File
@@ -0,0 +1,19 @@
using System.Net.Sockets;
namespace RemSox.Shared.Networking;
/// <summary>
/// Simple wrapper around TcpClient to hold connection-scoped resources safely.
/// </summary>
public class TcpConnection(TcpClient client) : IDisposable
{
public TcpClient Client { get; } = client;
public NetworkStream Stream { get; } = client.GetStream();
public object SendLock { get; } = new();
public void Dispose()
{
Stream.Dispose();
Client.Dispose();
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace RemSox.Shared.Networking;
public class TcpMessage
{
public string Type { get; set; } = string.Empty;
public string RequestId { get; set; } = string.Empty;
public byte[] Payload { get; set; } = [];
}
+237
View File
@@ -0,0 +1,237 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Text;
namespace RemSox.Shared.Networking;
public abstract class TcpRpcBase(IPacketCrypto? crypto = null)
{
protected readonly IPacketCrypto? crypto = crypto;
protected readonly Dictionary<string, Func<TcpConnection, TcpMessage, Task>> handlers = [];
protected readonly ConcurrentDictionary<string, TaskCompletionSource<byte[]>> pendingRequests = new();
// Security Guard: Prevent OOM/DoS via oversized length headers (Default: 32MB)
protected const int MaxMessageSize = 32 * 1024 * 1024;
public void ListenTo(string type, Func<TcpConnection, byte[], Task> handler)
{
handlers[type] = async (conn, msg) => await handler(conn, msg.Payload);
}
public void ListenTo(string type, Func<byte[], Task> handler)
{
handlers[type] = async (conn, msg) => await handler(msg.Payload);
}
public void RespondTo(string type, Func<byte[], Task<byte[]>> handler)
{
handlers[type] = async (conn, msg) =>
{
byte[] res = await handler(msg.Payload);
SendRaw(conn, new TcpMessage
{
Type = type,
RequestId = msg.RequestId,
Payload = res
});
};
}
protected void SendRaw(TcpConnection conn, TcpMessage msg)
{
byte[] rawData = SerializeMessage(msg);
if (crypto is not null)
{
rawData = crypto.Encrypt(rawData);
}
byte[] packet = new byte[4 + rawData.Length];
BinaryPrimitives.WriteInt32LittleEndian(packet, rawData.Length);
Array.Copy(rawData, 0, packet, 4, rawData.Length);
lock (conn.SendLock)
{
conn.Stream.Write(packet, 0, packet.Length);
}
}
private static void ReadExact(NetworkStream stream, byte[] buffer, int offset, int count)
{
int totalRead = 0;
while (totalRead < count)
{
int read = stream.Read(buffer, offset + totalRead, count - totalRead);
if (read == 0)
{
Thread.Sleep(10);
continue;
}
totalRead += read;
}
}
protected void HandleConnection(TcpConnection conn, CancellationToken token = default)
{
NetworkStream stream = conn.Stream;
byte[] lengthBytes = new byte[4];
try
{
while (!token.IsCancellationRequested && conn.Client.Connected)
{
Console.WriteLine("[TcpRpc] Waiting for incoming packet...");
ReadExact(stream, lengthBytes, 0, 4);
int length = BinaryPrimitives.ReadInt32LittleEndian(lengthBytes);
if (length is <= 0 or > MaxMessageSize)
{
Console.WriteLine($"[TcpRpc] Invalid packet length: {length}");
Thread.Sleep(10);
continue;
}
Console.WriteLine($"[TcpRpc] Incoming packet length: {length} bytes");
byte[] buffer = new byte[length];
ReadExact(stream, buffer, 0, length);
Console.WriteLine($"[TcpRpc] Incoming packet payload: {BitConverter.ToString(buffer)}");
if (crypto is not null)
{
buffer = crypto.Decrypt(buffer);
}
Console.WriteLine($"[TcpRpc] Decrypted packet payload: {BitConverter.ToString(buffer)}");
TcpMessage? msg = DeserializeMessage(buffer);
if (msg is null)
{
Console.WriteLine("[TcpRpc] Failed to deserialize incoming message.");
continue;
}
Console.WriteLine($"[TcpRpc] Incoming message type: {msg.Type}, requestId: {msg.RequestId}, payload length: {msg.Payload.Length} bytes");
if (pendingRequests.TryGetValue(msg.RequestId, out TaskCompletionSource<byte[]>? tcs))
{
_ = tcs.TrySetResult(msg.Payload);
_ = pendingRequests.TryRemove(msg.RequestId, out _);
continue;
}
if (handlers.TryGetValue(msg.Type, out Func<TcpConnection, TcpMessage, Task>? handler))
{
_ = Task.Run(async () =>
{
try
{
await handler(conn, msg);
}
catch
{
}
}, token);
}
}
}
catch
{
}
finally
{
OnConnectionClosed(conn);
conn.Dispose();
}
}
private static byte[] SerializeMessage(TcpMessage msg)
{
byte[] typeBytes = Encoding.UTF8.GetBytes(msg.Type);
byte[] requestIdBytes = Encoding.UTF8.GetBytes(msg.RequestId);
byte[] data = new byte[4 + typeBytes.Length + 4 + requestIdBytes.Length + 4 + msg.Payload.Length];
int offset = 0;
WriteInt32(data, ref offset, typeBytes.Length);
typeBytes.CopyTo(data, offset);
offset += typeBytes.Length;
WriteInt32(data, ref offset, requestIdBytes.Length);
requestIdBytes.CopyTo(data, offset);
offset += requestIdBytes.Length;
WriteInt32(data, ref offset, msg.Payload.Length);
msg.Payload.CopyTo(data, offset);
return data;
}
private static TcpMessage? DeserializeMessage(byte[] data)
{
int offset = 0;
if (offset + 4 > data.Length)
{
return null;
}
int typeLen = ReadInt32(data, ref offset);
if (offset + typeLen > data.Length)
{
return null;
}
string type = Encoding.UTF8.GetString(data, offset, typeLen);
offset += typeLen;
if (offset + 4 > data.Length)
{
return null;
}
int requestIdLen = ReadInt32(data, ref offset);
if (offset + requestIdLen > data.Length)
{
return null;
}
string requestId = Encoding.UTF8.GetString(data, offset, requestIdLen);
offset += requestIdLen;
if (offset + 4 > data.Length)
{
return null;
}
int payloadLen = ReadInt32(data, ref offset);
if (offset + payloadLen > data.Length)
{
return null;
}
byte[] payload = new byte[payloadLen];
Array.Copy(data, offset, payload, 0, payloadLen);
return new TcpMessage { Type = type, RequestId = requestId, Payload = payload };
}
private static void WriteInt32(byte[] data, ref int offset, int value)
{
data[offset++] = (byte)(value & 0xFF);
data[offset++] = (byte)((value >> 8) & 0xFF);
data[offset++] = (byte)((value >> 16) & 0xFF);
data[offset++] = (byte)((value >> 24) & 0xFF);
}
private static int ReadInt32(byte[] data, ref int offset)
{
int val = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
offset += 4;
return val;
}
protected abstract void OnConnectionClosed(TcpConnection conn);
}
+85
View File
@@ -0,0 +1,85 @@
using System.Net;
using System.Net.Sockets;
namespace RemSox.Shared.Networking;
public class TcpRpcClient(IPacketCrypto? crypto = null) : TcpRpcBase(crypto)
{
private TcpConnection? connection;
public void Connect(string host, int port)
{
TcpClient client = new();
client.Connect(host, port);
connection = new TcpConnection(client);
_ = Task.Run(() => HandleConnection(connection));
}
public async Task<byte[]> RequestAsync(string type, byte[] request, TimeSpan timeout = default)
{
if (connection is null)
{
throw new InvalidOperationException("Client not connected.");
}
if (timeout == default)
{
timeout = TimeSpan.FromSeconds(30);
}
string requestId = Guid.NewGuid().ToString();
TaskCompletionSource<byte[]> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
pendingRequests[requestId] = tcs;
try
{
SendRaw(connection, new TcpMessage
{
Type = type,
RequestId = requestId,
Payload = request
});
using CancellationTokenSource timeoutCts = new(timeout);
using (timeoutCts.Token.Register(() => tcs.TrySetCanceled()))
{
return await tcs.Task;
}
}
finally
{
_ = pendingRequests.TryRemove(requestId, out _);
}
}
public void Send(string type, byte[] data)
{
if (connection is null)
{
throw new InvalidOperationException("Client not connected.");
}
SendRaw(connection, new TcpMessage
{
Type = type,
RequestId = Guid.NewGuid().ToString(),
Payload = data
});
}
protected override void OnConnectionClosed(TcpConnection conn)
{
if (connection == conn)
{
connection = null;
}
foreach (TaskCompletionSource<byte[]> req in pendingRequests.Values)
{
_ = req.TrySetException(new SocketException((int)SocketError.ConnectionReset));
}
pendingRequests.Clear();
}
}
+82
View File
@@ -0,0 +1,82 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
namespace RemSox.Shared.Networking;
public class TcpRpcServer(IPacketCrypto? crypto = null) : TcpRpcBase(crypto)
{
private readonly ConcurrentDictionary<TcpConnection, byte> connections = new();
private readonly ConcurrentDictionary<string, byte> activeEndpoints = new();
private TcpListener? listener;
private CancellationTokenSource? cts;
public void StartAsync(int port, CancellationToken token = default)
{
cts = CancellationTokenSource.CreateLinkedTokenSource(token);
listener = new TcpListener(IPAddress.Any, port);
listener.Start();
try
{
while (!cts.IsCancellationRequested)
{
TcpClient client = listener.AcceptTcpClient();
string ep = client.Client.RemoteEndPoint?.ToString() ?? "";
if (!activeEndpoints.TryAdd(ep, 0))
{
client.Close();
Thread.Sleep(10);
continue;
}
Console.WriteLine($"Client connected: {ep}");
TcpConnection conn = new(client);
_ = connections.TryAdd(conn, 0);
_ = Task.Run(() => HandleConnection(conn, cts.Token));
}
}
catch (OperationCanceledException)
{
}
}
public void Stop()
{
cts?.Cancel();
listener?.Stop();
foreach (TcpConnection c in connections.Keys)
{
c.Dispose();
}
connections.Clear();
}
public void SendRawToAll(string type, byte[] payload)
{
foreach (TcpConnection conn in connections.Keys)
{
SendRaw(conn, new TcpMessage
{
Type = type,
RequestId = Guid.NewGuid().ToString(),
Payload = payload
});
}
}
protected override void OnConnectionClosed(TcpConnection conn)
{
_ = connections.TryRemove(conn, out _);
string ep = conn.Client.Client.RemoteEndPoint?.ToString() ?? "";
_ = activeEndpoints.TryRemove(ep, out _);
Console.WriteLine($"Client disconnected: {ep}");
}
}
+11
View File
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
</ItemGroup>
</Project>
@@ -0,0 +1,7 @@
namespace RemSox.Shared.UI.GUI.Rendering;
public interface IRenderSource
{
void Render(IEnumerable<RenderCommand> commands);
void Composite();
}
@@ -0,0 +1,263 @@
using System.Drawing;
using System.Text;
namespace RemSox.Shared.UI.GUI.Rendering;
public class RenderCommand
{
public required int WindowId { get; set; }
public required int ElementId { get; set; }
public required RenderCommandType Type { get; set; }
public Point Position { get; set; }
public Dictionary<string, object?> Properties { get; set; } = [];
public byte[] ToBytes()
{
using MemoryStream ms = new();
Write(ms);
return ms.ToArray();
}
public void Write(Stream s)
{
s.WriteByte((byte)Type);
WriteVarint(s, WindowId);
WriteVarint(s, ElementId);
switch (Type)
{
case RenderCommandType.CreateWindow:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
Size size = GetProp("Size", new Size(160, 120));
WriteUInt16(s, size.Width);
WriteUInt16(s, size.Height);
WriteVarint(s, GetProp("ZIndex", 0));
break;
case RenderCommandType.DestroyWindow:
break;
case RenderCommandType.MoveWindow:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
break;
case RenderCommandType.DrawFilledRect:
case RenderCommandType.DrawRectBorder:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
Size rs = GetProp("Size", new Size(10, 10));
WriteUInt16(s, rs.Width);
WriteUInt16(s, rs.Height);
WriteColor(s, GetProp("Color", Color.White));
break;
case RenderCommandType.DrawFilledCircle:
case RenderCommandType.DrawCircle:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
WriteUInt16(s, GetProp("Radius", 10));
WriteColor(s, GetProp("Color", Color.White));
break;
case RenderCommandType.DrawPoint:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
WriteColor(s, GetProp("Color", Color.White));
break;
case RenderCommandType.DrawText:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
WriteColor(s, GetProp("Color", Color.White));
s.WriteByte((byte)GetProp("FontSize", 12));
string text = GetProp("Content", string.Empty);
byte[] utf8 = Encoding.UTF8.GetBytes(text);
WriteVarint(s, utf8.Length);
s.Write(utf8, 0, utf8.Length);
WriteVarint(s, GetProp("MaxWidth", int.MaxValue));
break;
case RenderCommandType.DrawLine:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
Point end = GetProp("EndPosition", Position);
WriteInt16(s, end.X);
WriteInt16(s, end.Y);
WriteColor(s, GetProp("Color", Color.White));
break;
case RenderCommandType.RemovePrimitives:
break;
case RenderCommandType.SetCursor:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
break;
case RenderCommandType.ScreenInfo:
WriteUInt16(s, GetProp("Width", 0));
WriteUInt16(s, GetProp("Height", 0));
break;
}
}
public static RenderCommand FromBytes(byte[] data)
{
int offset = 0;
RenderCommandType type = (RenderCommandType)data[offset++];
int windowId = ReadVarint(data, ref offset);
int elementId = ReadVarint(data, ref offset);
Point pos = default;
Dictionary<string, object?> props = [];
switch (type)
{
case RenderCommandType.CreateWindow:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["Size"] = new Size(ReadUInt16(data, ref offset), ReadUInt16(data, ref offset));
props["ZIndex"] = ReadVarint(data, ref offset);
break;
case RenderCommandType.DestroyWindow:
break;
case RenderCommandType.MoveWindow:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
break;
case RenderCommandType.DrawFilledRect:
case RenderCommandType.DrawRectBorder:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["Size"] = new Size(ReadUInt16(data, ref offset), ReadUInt16(data, ref offset));
props["Color"] = ReadColor(data, ref offset);
break;
case RenderCommandType.DrawFilledCircle:
case RenderCommandType.DrawCircle:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["Radius"] = ReadUInt16(data, ref offset);
props["Color"] = ReadColor(data, ref offset);
break;
case RenderCommandType.DrawPoint:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["Color"] = ReadColor(data, ref offset);
break;
case RenderCommandType.DrawText:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["Color"] = ReadColor(data, ref offset);
props["FontSize"] = data[offset++];
int textLen = ReadVarint(data, ref offset);
props["Content"] = Encoding.UTF8.GetString(data, offset, textLen);
offset += textLen;
props["MaxWidth"] = ReadVarint(data, ref offset);
break;
case RenderCommandType.DrawLine:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["EndPosition"] = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
props["Color"] = ReadColor(data, ref offset);
break;
case RenderCommandType.RemovePrimitives:
break;
case RenderCommandType.SetCursor:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
break;
case RenderCommandType.ScreenInfo:
props["Width"] = ReadUInt16(data, ref offset);
props["Height"] = ReadUInt16(data, ref offset);
break;
}
return new RenderCommand
{
Type = type,
WindowId = windowId,
ElementId = elementId,
Position = pos,
Properties = props,
};
}
// --- Serialization helpers ---
private T GetProp<T>(string key, T fallback)
{
return Properties.TryGetValue(key, out object? raw) && raw is T val ? val : fallback;
}
private static void WriteVarint(Stream s, int value)
{
uint v = (uint)value;
while (v >= 0x80)
{
s.WriteByte((byte)(v | 0x80));
v >>= 7;
}
s.WriteByte((byte)v);
}
private static void WriteInt16(Stream s, int value)
{
s.WriteByte((byte)(value & 0xFF));
s.WriteByte((byte)((value >> 8) & 0xFF));
}
private static void WriteUInt16(Stream s, int value)
{
s.WriteByte((byte)(value & 0xFF));
s.WriteByte((byte)((value >> 8) & 0xFF));
}
private static void WriteColor(Stream s, Color c)
{
s.WriteByte(c.R);
s.WriteByte(c.G);
s.WriteByte(c.B);
}
private static int ReadVarint(byte[] data, ref int offset)
{
uint result = 0;
int shift = 0;
while (true)
{
byte b = data[offset++];
result |= (uint)(b & 0x7F) << shift;
if ((b & 0x80) == 0)
{
return (int)result;
}
shift += 7;
}
}
private static int ReadInt16(byte[] data, ref int offset)
{
int lo = data[offset++];
int hi = data[offset++];
return (short)(lo | (hi << 8));
}
private static int ReadUInt16(byte[] data, ref int offset)
{
int lo = data[offset++];
int hi = data[offset++];
return lo | (hi << 8);
}
private static Color ReadColor(byte[] data, ref int offset)
{
byte r = data[offset++];
byte g = data[offset++];
byte b = data[offset++];
return Color.FromArgb(r, g, b);
}
}
@@ -0,0 +1,35 @@
namespace RemSox.Shared.UI.GUI.Rendering;
/// <summary>
/// <para>Render command opcodes, grouped by category:</para>
/// <para>
/// 0x010x0F System/setup<br/>
/// 0x100x1F Window lifecycle<br/>
/// 0x200x2F Primitives lifecycle<br/>
/// 0x300x3F Primitives draw<br/>
/// 0x40+ Future expansion
/// </para>
/// </summary>
public enum RenderCommandType : byte
{
// System / setup (0x010x0F)
ScreenInfo = 0x01,
SetCursor = 0x02,
// Window lifecycle (0x100x1F)
CreateWindow = 0x10,
DestroyWindow = 0x11,
MoveWindow = 0x12,
// Primitives lifecycle (0x200x2F)
RemovePrimitives = 0x20,
// Primitives draw (0x300x3F)
DrawFilledRect = 0x30,
DrawRectBorder = 0x31,
DrawFilledCircle = 0x32,
DrawCircle = 0x33,
DrawText = 0x34,
DrawLine = 0x35,
DrawPoint = 0x36,
}
+39
View File
@@ -0,0 +1,39 @@
namespace RemSox.Shared.UI;
public record MouseEvent(MouseEventType Type, int X = 0, int Y = 0, MouseButton Button = MouseButton.None, int Delta = 0)
{
public static MouseEvent Move(int x, int y)
{
return new(MouseEventType.Move, x, y);
}
public static MouseEvent ButtonDown(int x, int y, MouseButton button)
{
return new(MouseEventType.ButtonDown, x, y, button);
}
public static MouseEvent ButtonUp(int x, int y, MouseButton button)
{
return new(MouseEventType.ButtonUp, x, y, button);
}
public static MouseEvent Wheel(int x, int y, int delta)
{
return new(MouseEventType.Wheel, x, y, Delta: delta);
}
}
public enum MouseEventType
{
Move,
ButtonDown,
ButtonUp,
Wheel
}
public enum MouseButton
{
Left,
Right,
Middle,
None
}