48 Commits

Author SHA1 Message Date
7c3141822a Add image brushes as a construct, as well as move some resource types to Common assembly. 2026-01-05 22:34:34 +03:00
e30e50e860 Unfinished work I want to document in the Git history. 2026-01-05 22:26:34 +03:00
4f67e0fb75 Create basic controls without much regards to layout rules. 2025-11-21 23:04:37 +03:00
5cba1ab7db Give label a proper implementation. 2025-11-19 20:12:52 +03:00
9ca309bd52 Remove all old project files. 2025-11-17 22:00:58 +03:00
043060db66 Rework the library architecture. 2025-11-17 21:59:07 +03:00
66c5eecc26 Move StbImage to its own assembly. 2025-11-14 22:31:54 +03:00
9ee6c1180d Add new functions to extension system. 2025-11-14 22:28:21 +03:00
c4fe4841fe Work on creating a simple message box dialog. 2025-11-13 21:35:01 +03:00
2932b3b85e Implement new immediate mode. 2025-11-12 21:24:25 +03:00
6e8888df48 Add CreatePhysicalWindow(). 2025-11-09 12:00:21 +03:00
edc85c3f24 Create a new context system for dashboard. 2025-08-05 23:50:29 +03:00
50eda46b13 Fix dependency versions. 2025-07-31 20:59:44 +03:00
c538dbd56b Implement type dictionaries, with hierarchical sets and unsets. 2025-07-25 23:51:14 +03:00
1dcf167022 Implement ReFuel style type atoms and type collections. 2025-07-24 23:16:24 +03:00
2690c5bec0 Commit WIP changes. 2025-04-29 23:20:54 +03:00
1c3c730e82 Some additional work on dashboard architecture. 2025-02-11 23:29:26 +03:00
2c957a0c1a Create some high level classes. 2025-02-02 21:58:49 +03:00
49257574f4 Add a few demo controls. 2025-01-25 15:39:18 +03:00
1cd005f827 Create the first immediate mode UI base. 2025-01-25 12:34:37 +03:00
ef860f39bf Patch in BlurgText upstream as well. 2025-01-23 22:00:58 +03:00
b1aadae0f2 Add initial version of text rendering. 2025-01-22 22:42:43 +03:00
afa6ed4f4c Add new draw call recorder class. 2025-01-20 23:14:30 +03:00
57620f5d5c Fix missing variable command parameter data in DrawQueue. 2025-01-20 23:14:15 +03:00
78f06a2359 Fix null reference exception in TextCommand. 2025-01-20 23:13:56 +03:00
87ab64f727 Add IDpiAwareGLContext. 2025-01-19 16:48:15 +03:00
95cc1648b2 Implement basic commands. 2025-01-18 23:30:56 +03:00
5014d218e2 Work on resource classes and generic interfaces. 2025-01-18 20:43:42 +03:00
8ce1329dfc Make all commands be parallel to the XY plane because i can't be bothered to fix them all. 2025-01-17 21:34:15 +03:00
cde0fe2901 Added new mathematical types and enums related to drawing. 2025-01-17 21:32:52 +03:00
1a52f17990 Add missing properties. 2025-01-15 20:42:09 +03:00
b841dbe6c2 Begin implementing the GL engine. 2025-01-14 20:34:47 +03:00
bb5b87417f Add clone to gradients. 2024-12-25 22:58:23 +03:00
165801800e Add basic text support. 2024-12-24 23:20:43 +03:00
cf7cf9a77a Fix root namespaces. 2024-12-24 22:30:19 +03:00
3dff7438b3 Add gradient brushes. 2024-12-24 22:10:31 +03:00
6ad11812e2 Add hash function to Gradient. 2024-12-24 22:10:24 +03:00
f8dea00021 Move basic types to Dashboard.Common. 2024-12-24 22:00:16 +03:00
7485ec5eaa Add Gradient. 2024-12-24 21:58:51 +03:00
6acaf0a7d5 Rename SolidBrush.cs to Brush.cs 2024-12-24 21:58:43 +03:00
b5fda1ce3e Create ImageProperties.cs. 2024-12-22 23:27:48 +03:00
1067d3e188 Implement command iterator. 2024-12-14 20:44:27 +03:00
1b6bf09f95 Move types to Dashboard.Common. 2024-12-14 19:57:23 +03:00
509bad221f Add new commands. 2024-12-14 19:46:56 +03:00
9ab2edffee Change the IDrawCommand interface. 2024-12-14 19:46:47 +03:00
1a1e761326 Add custom hashcode to SolidBrush. 2024-12-14 19:43:50 +03:00
34720970ff Make bounds a Box3d. 2024-12-14 16:27:40 +03:00
c706e16db1 Create Box3d structure. 2024-12-14 16:27:13 +03:00
123 changed files with 10203 additions and 111 deletions

View File

@@ -0,0 +1,235 @@
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using BlurgText;
using Dashboard.Drawing;
using Dashboard.OpenGL;
using OpenTK.Graphics.OpenGL;
using OPENGL = OpenTK.Graphics.OpenGL;
namespace Dashboard.BlurgText.OpenGL
{
public class BlurgGLExtension : BlurgDcExtension
{
private readonly List<int> _textures = new List<int>();
private int _program = 0;
private int _transformsLocation = -1;
private int _atlasLocation = -1;
private int _borderWidthLocation = -1;
private int _borderColorLocation = -1;
private int _fillColorLocation = -1;
private int _vertexArray = 0;
public override Blurg Blurg { get; }
public bool SystemFontsEnabled { get; set; }
public bool IsDisposed { get; private set; } = false;
public override string DriverName => "BlurgText";
public override string DriverVendor => "Dashboard and BlurgText";
public override Version DriverVersion => new Version(1, 0);
private new GLDeviceContext Context => (GLDeviceContext)base.Context;
public BlurgGLExtension()
{
Blurg = new Blurg(AllocateTexture, UpdateTexture);
SystemFontsEnabled = Blurg.EnableSystemFonts();
}
~BlurgGLExtension()
{
Dispose(false);
}
private void UseProgram()
{
if (_program != 0)
{
GL.UseProgram(_program);
return;
}
Assembly self = typeof(BlurgGLExtension).Assembly;
using Stream vsource = self.GetManifestResourceStream("Dashboard.BlurgText.OpenGL.text.vert")!;
using Stream fsource = self.GetManifestResourceStream("Dashboard.BlurgText.OpenGL.text.frag")!;
int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
_program = ShaderUtil.LinkProgram(vs, fs, [
"a_v3Position",
"a_v2TexCoords",
]);
GL.DeleteShader(vs);
GL.DeleteShader(fs);
_transformsLocation = GL.GetUniformLocation(_program, "m4Transforms");
_atlasLocation = GL.GetUniformLocation(_program, "txAtlas");
_borderWidthLocation = GL.GetUniformLocation(_program, "fBorderWidth");
_borderColorLocation = GL.GetUniformLocation(_program, "v4BorderColor");
_fillColorLocation = GL.GetUniformLocation(_program, "v4FillColor");
GL.UseProgram(_program);
GL.Uniform1i(_atlasLocation, 0);
}
private void UpdateTexture(IntPtr texture, IntPtr buffer, int x, int y, int width, int height)
{
GL.BindTexture(TextureTarget.Texture2d, (int)texture);
GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, buffer);
// GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Red, PixelType.Byte, buffer);
}
private IntPtr AllocateTexture(int width, int height)
{
int texture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2d, texture);
GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.Rgba, width, height, 0, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
// GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.R8, width, height, 0, OPENGL.PixelFormat.Red, PixelType.Byte, IntPtr.Zero);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.One);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.One);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.One);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.Red);
_textures.Add(texture);
return texture;
}
protected virtual void Dispose(bool disposing)
{
if (IsDisposed)
return;
IsDisposed = true;
if (disposing)
{
GC.SuppressFinalize(this);
foreach (int texture in _textures)
{
Context.Collector.DeleteTexture(texture);
}
}
}
public override void DrawBlurgResult(BlurgResult result, Vector3 position)
{
if (result.Count == 0)
return;
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
List<DrawCall> drawCalls = new List<DrawCall>();
List<Vertex> vertices = new List<Vertex>();
List<ushort> indices = new List<ushort>();
int offset = 0;
int count = 0;
DrawCall prototype = default;
for (int i = 0; i < result.Count; i++)
{
BlurgRect rect = result[i];
int texture = (int)rect.UserData;
Vector4 fillColor = new Vector4(rect.Color.R / 255f, rect.Color.G / 255f, rect.Color.B / 255f,
rect.Color.A / 255f);
if (i == 0)
{
prototype = new DrawCall(0, 0, texture, fillColor);
}
else if (prototype.Texture != texture || prototype.FillColor != fillColor)
{
drawCalls.Add(prototype with { Count = count, Offset = offset });
prototype = new DrawCall(0, 0, texture, fillColor);
offset += count;
count = 0;
}
vertices.Add(new Vertex(rect.X, rect.Y, 0, rect.U0, rect.V0));
vertices.Add(new Vertex(rect.X + rect.Width, rect.Y, 0, rect.U1, rect.V0));
vertices.Add(new Vertex(rect.X + rect.Width, rect.Y + rect.Height, 0, rect.U1, rect.V1));
vertices.Add(new Vertex(rect.X, rect.Y + rect.Height, 0, rect.U0, rect.V1));
indices.Add((ushort)(vertices.Count - 4));
indices.Add((ushort)(vertices.Count - 3));
indices.Add((ushort)(vertices.Count - 2));
indices.Add((ushort)(vertices.Count - 4));
indices.Add((ushort)(vertices.Count - 2));
indices.Add((ushort)(vertices.Count - 1));
count += 6;
}
drawCalls.Add(prototype with { Count = count, Offset = offset });
if (_vertexArray == 0)
{
_vertexArray = GL.GenVertexArray();
}
GL.BindVertexArray(_vertexArray);
Span<int> buffers = stackalloc int[2];
GL.GenBuffers(2, buffers);
GL.BindBuffer(BufferTarget.ArrayBuffer, buffers[0]);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Count * Vertex.Size, (ReadOnlySpan<Vertex>)CollectionsMarshal.AsSpan(vertices), BufferUsage.StaticRead);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, buffers[1]);
GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Count * sizeof(ushort), (ReadOnlySpan<ushort>)CollectionsMarshal.AsSpan(indices), BufferUsage.StaticRead);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, Vertex.Size, Vertex.PositionOffset);
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex.Size, Vertex.TexCoordOffset);
GL.EnableVertexAttribArray(0); GL.EnableVertexAttribArray(1);
UseProgram();
GL.UniformMatrix4f(_transformsLocation, 1, true, in view);
GL.ActiveTexture(TextureUnit.Texture0);
foreach (DrawCall call in drawCalls)
{
GL.BindTexture(TextureTarget.Texture2d, call.Texture);
GL.Uniform4f(_fillColorLocation, call.FillColor.X, call.FillColor.Y, call.FillColor.Z,
call.FillColor.W);
GL.DrawElements(PrimitiveType.Triangles, call.Count, DrawElementsType.UnsignedShort, call.Offset);
}
GL.DeleteBuffers(2, buffers);
}
public override void Dispose()
{
Dispose(true);
}
[StructLayout(LayoutKind.Explicit, Size = Size)]
private struct Vertex(Vector3 position, Vector2 texCoord)
{
[FieldOffset(PositionOffset)]
public Vector3 Position = position;
[FieldOffset(TexCoordOffset)]
public Vector2 TexCoord = texCoord;
public Vertex(float x, float y, float z, float u, float v)
: this(new Vector3(x, y, z), new Vector2(u, v))
{
}
public const int Size = 8 * sizeof(float);
public const int PositionOffset = 0 * sizeof(float);
public const int TexCoordOffset = 4 * sizeof(float);
}
private struct DrawCall(int offset, int count, int texture, Vector4 fillColor)
{
public int Offset = offset;
public int Count = count;
public int Texture = texture;
public Vector4 FillColor = fillColor;
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Dashboard.BlurgText/Dashboard.BlurgText.csproj"/>
<ProjectReference Include="..\Dashboard.OpenGL\Dashboard.OpenGL.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="text.frag" />
<EmbeddedResource Include="text.vert" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
#version 140
in vec3 v_v3Position;
in vec2 v_v2TexCoords;
out vec4 f_Color;
uniform sampler2D txAtlas;
uniform float fBorderWidth;
uniform vec4 v4BorderColor;
uniform vec4 v4FillColor;
void main() {
// For now just honor the fill color
vec4 color = texture(txAtlas, v_v2TexCoords) * v4FillColor;
if (color.a <= 0.01)
discard;
f_Color = color;
}

View File

@@ -0,0 +1,18 @@
#version 140
in vec3 a_v3Position;
in vec2 a_v2TexCoords;
out vec3 v_v3Position;
out vec2 v_v2TexCoords;
uniform mat4 m4Transforms;
void main(void)
{
vec4 position = vec4(a_v3Position, 1) * m4Transforms;
gl_Position = position;
v_v3Position = position.xyz/position.w;
v_v2TexCoords = a_v2TexCoords;
}

View File

@@ -0,0 +1,21 @@
using BlurgText;
using Dashboard.Drawing;
namespace Dashboard.BlurgText
{
public class BlurgFontProxy(Blurg owner, BlurgFont font) : IFont
{
public Blurg Owner { get; } = owner;
public BlurgFont Font { get; } = font;
public string Family => Font.FamilyName;
public FontWeight Weight => (FontWeight)Font.Weight.Value;
public FontSlant Slant => Font.Italic ? FontSlant.Italic : FontSlant.Normal;
public FontStretch Stretch => FontStretch.Normal;
public string? Path { get; init; }
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,210 @@
using System.Diagnostics;
using System.Numerics;
using BlurgText;
using Dashboard.Drawing;
using Dashboard.Pal;
namespace Dashboard.BlurgText
{
public interface IBlurgDcExtensionFactory
{
public BlurgDcExtension CreateExtension(BlurgTextExtension appExtension, DeviceContext dc);
}
public class BlurgTextExtension(IBlurgDcExtensionFactory dcExtensionFactory) : IFontLoader
{
private readonly Blurg _blurg = new Blurg(GlobalTextureAllocation, GlobalTextureUpdate);
public Application Context { get; private set; } = null!;
public string DriverName { get; } = "BlurgText";
public string DriverVendor { get; } = "Dashbord and BlurgText";
public Version DriverVersion { get; } = new Version(1, 0);
public IBlurgDcExtensionFactory DcExtensionFactory { get; } = dcExtensionFactory;
IContextBase IContextExtensionBase.Context => Context;
public bool IsDisposed { get; private set; } = false;
public void Require(Application context)
{
Context = context;
context.DeviceContextCreated += OnDeviceContextCreated;
_blurg.EnableSystemFonts();
}
void IContextExtensionBase.Require(IContextBase context) => Require((Application)context);
private void RequireDeviceContextExtension(DeviceContext dc)
{
dc.ExtensionPreload<BlurgDcExtension>(() => DcExtensionFactory.CreateExtension(this, dc));
}
private void OnDeviceContextCreated(object? sender, DeviceContext dc)
{
RequireDeviceContextExtension(dc);
}
public void Dispose()
{
if (IsDisposed)
return;
IsDisposed = true;
_blurg.Dispose();
Context.DeviceContextCreated -= OnDeviceContextCreated;
}
private static void GlobalTextureUpdate(IntPtr userdata, IntPtr buffer, int x, int y, int width, int height)
{
// Report the user error.
Debug.WriteLine("Attempt to create or update a texture from the global BlurgText.", "Dashboard/BlurgEngine");
}
private static IntPtr GlobalTextureAllocation(int width, int height)
{
Debug.WriteLine("Attempt to create or update a texture from the global BlurgText.", "Dashboard/BlurgEngine");
return IntPtr.Zero;
}
public IFont Load(FontInfo info)
{
BlurgFont? font = _blurg.QueryFont(
info.Family,
info.Weight switch
{
FontWeight._100 => global::BlurgText.FontWeight.Thin,
FontWeight._200 => global::BlurgText.FontWeight.ExtraLight,
FontWeight._300 => global::BlurgText.FontWeight.Light,
FontWeight._500 => global::BlurgText.FontWeight.Medium,
FontWeight._600 => global::BlurgText.FontWeight.SemiBold,
FontWeight._700 => global::BlurgText.FontWeight.Bold,
FontWeight._800 => global::BlurgText.FontWeight.ExtraBold,
FontWeight._900 => global::BlurgText.FontWeight.Black,
_ => global::BlurgText.FontWeight.Regular,
},
info.Slant switch
{
FontSlant.Oblique or FontSlant.Italic => true,
_ => false,
});
if (font == null)
throw new Exception("Font not found.");
return new BlurgFontProxy(_blurg, font);
}
public IFont Load(string path)
{
BlurgFont? font = _blurg.AddFontFile(path);
if (font == null)
throw new Exception("Font not found.");
return new BlurgFontProxy(_blurg, font);
}
public IFont Load(Stream stream)
{
string path;
Stream dest;
for (int i = 0;; i++)
{
path = Path.GetTempFileName();
try
{
dest = File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
}
catch (IOException ex)
{
if (i < 3)
continue;
else
throw new Exception("Could not open a temporary file for writing the font.", ex);
}
break;
}
stream.CopyTo(dest);
dest.Dispose();
return Load(path);
}
}
public abstract class BlurgDcExtension : IDeviceContextExtension, ITextRenderer
{
public abstract Blurg Blurg { get; }
public abstract string DriverName { get; }
public abstract string DriverVendor { get; }
public abstract Version DriverVersion { get; }
public Application Application => Context.Application;
public DeviceContext Context { get; private set; } = null!;
public abstract void DrawBlurgResult(BlurgResult result, Vector3 position);
public void DrawBlurgFormattedText(BlurgFormattedText text, Vector3 position, float width = 0f)
{
BlurgResult? result = Blurg.BuildFormattedText(text, maxWidth: width);
if (result == null)
throw new Exception("Could not build formatted text result.");
DrawBlurgResult(result, position);
}
protected BlurgFontProxy InternFont(IFont font)
{
BlurgTextExtension appExtension = Application.ExtensionRequire<BlurgTextExtension>();
if (font is FontInfo fontInfo)
{
return (BlurgFontProxy)appExtension.Load(fontInfo);
}
else if (font is BlurgFontProxy blurgFont)
{
if (blurgFont.Owner != Blurg)
{
if (blurgFont.Path == null)
return (BlurgFontProxy)appExtension.Load(new FontInfo(blurgFont.Family, blurgFont.Weight,
blurgFont.Slant,
blurgFont.Stretch));
else
return (BlurgFontProxy)appExtension.Load(blurgFont.Path);
}
else
{
return blurgFont;
}
}
else
{
throw new Exception("Unsupported font resource.");
}
}
public abstract void Dispose();
IContextBase IContextExtensionBase.Context => Context;
public void Require(DeviceContext context)
{
Context = context;
}
void IContextExtensionBase.Require(IContextBase context) => Require((DeviceContext)context);
public Box2d MeasureText(IFont font, float size, string text)
{
BlurgFontProxy proxy = InternFont(font);
Vector2 sz = Blurg.MeasureString(proxy.Font, size * Context.ExtensionRequire<IDeviceContextBase>().Scale, text);
return new Box2d(0, 0, sz.X, sz.Y);
}
public void DrawText(Vector2 position, Vector4 color, float size, IFont font, string text)
{
BlurgFontProxy proxy = InternFont(font);
BlurgResult? result = Blurg.BuildString(proxy.Font, size * Context.ExtensionRequire<IDeviceContextBase>().Scale,
new BlurgColor((byte)(color.X * 255), (byte)(color.Y * 255), (byte)(color.Z * 255), (byte)(color.W * 255)), text);
DrawBlurgResult(result, new Vector3(position, 0));
}
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
namespace Dashboard
{
[Flags]
public enum Anchor
{
Auto = 0,
Right = (1 << 0),
Left = (1 << 1),
HCenter = Left | Right,
Top = (1 << 2),
Bottom = (1 << 3),
VCenter = Top | Bottom,
Middle = HCenter | VCenter,
}
}

65
Dashboard.Common/Box2d.cs Normal file
View File

@@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Numerics;
namespace Dashboard
{
public readonly record struct Box2d(Vector2 Min, Vector2 Max)
{
public float Left => Min.X;
public float Right => Max.X;
public float Top => Min.Y;
public float Bottom => Max.Y;
public Vector2 Size => Max - Min;
public Vector2 Center => (Min + Max) * 0.5f;
public Box2d(RectangleF rectangle)
: this(new Vector2(rectangle.Left, rectangle.Top), new Vector2(rectangle.Right, rectangle.Bottom))
{
}
public Box2d(float x0, float y0, float x1, float y1)
: this(new Vector2(x0, y0), new Vector2(x1, y1))
{
}
public static Box2d FromPositionAndSize(Vector2 position, Vector2 size, Origin anchor = Origin.Center)
{
Vector2 half = size * 0.5f;
switch (anchor)
{
case Origin.Center:
return new Box2d(position - half, position + half);
case Origin.TopLeft:
return new Box2d(position, position + size);
default:
throw new NotImplementedException();
}
}
public static Box2d Union(Box2d left, Box2d right)
{
Vector2 min = Vector2.Min(left.Min, right.Min);
Vector2 max = Vector2.Max(left.Max, right.Max);
return new Box2d(min, max);
}
public static Box2d Intersect(Box2d left, Box2d right)
{
Vector2 min = Vector2.Max(left.Min, right.Min);
Vector2 max = Vector2.Min(left.Max, right.Max);
return new Box2d(min, max);
}
public static explicit operator RectangleF(Box2d box2d)
{
return new RectangleF((PointF)box2d.Center, (SizeF)box2d.Size);
}
public static explicit operator Box2d(RectangleF rectangle)
{
return new Box2d(rectangle);
}
}
}

46
Dashboard.Common/Box3d.cs Normal file
View File

@@ -0,0 +1,46 @@
using System.Drawing;
using System.Numerics;
namespace Dashboard
{
public readonly record struct Box3d(Vector3 Min, Vector3 Max)
{
public float Left => Min.X;
public float Right => Max.X;
public float Top => Min.Y;
public float Bottom => Max.Y;
public float Far => Min.Z;
public float Near => Max.Z;
public Vector3 Size => Max - Min;
public Vector3 Center => Min + Size * 0.5f;
public static Box3d Union(Box3d left, Box3d right)
{
Vector3 min = Vector3.Min(left.Min, right.Min);
Vector3 max = Vector3.Max(left.Max, right.Max);
return new Box3d(min, max);
}
public static Box3d Union(Box3d box, Box2d bounds, float depth)
{
Vector3 min = Vector3.Min(box.Min, new Vector3(bounds.Left, bounds.Top, depth));
Vector3 max = Vector3.Max(box.Max, new Vector3(bounds.Right, bounds.Bottom, depth));
return new Box3d(min, max);
}
public static Box3d Intersect(Box3d left, Box3d right)
{
Vector3 min = Vector3.Max(left.Min, right.Min);
Vector3 max = Vector3.Min(left.Max, right.Max);
return new Box3d(min, max);
}
public static Box3d Intersect(Box3d box, Box2d bounds, float depth)
{
Vector3 min = Vector3.Max(box.Min, new Vector3(bounds.Min, depth));
Vector3 max = Vector3.Min(box.Max, new Vector3(bounds.Max, depth));
return new Box3d(min, max);
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace Dashboard.Collections
{
/// <summary>
/// Helper class for better type access performance.
/// </summary>
public record TypeAtom
{
public Type Type { get; }
public int Id { get; }
public ImmutableHashSet<TypeAtom> Ancestors { get; }
// Makes it so TypeAtom doesn't get pissed at me
private protected TypeAtom()
{
throw new NotSupportedException();
}
private TypeAtom(Type type)
{
Type = type;
HashSet<TypeAtom> ancestors = new HashSet<TypeAtom>();
FindAncestors(Type, ancestors);
ancestors.Add(this);
Ancestors = ancestors.ToImmutableHashSet();
lock (s_lockObject)
{
Id = s_counter++;
s_atoms.Add(Id, this);
s_types.Add(Type, this);
}
}
private static readonly object s_lockObject = new object();
private static readonly Dictionary<int, TypeAtom> s_atoms = new Dictionary<int, TypeAtom>();
private static readonly Dictionary<Type, TypeAtom> s_types = new Dictionary<Type, TypeAtom>();
private static int s_counter = 0;
public static TypeAtom? Get(int id) => s_atoms.GetValueOrDefault(id);
public static TypeAtom Get(Type type)
{
if (s_types.TryGetValue(type, out TypeAtom? id))
return id;
// Type is not registered, try to acquire lock.
lock (s_lockObject)
{
// Maybe somebody else registered this type whilst acquiring the lock.
if (s_types.TryGetValue(type, out id))
return id;
// Register the type if applicable and leave.
return new TypeAtom(type);
}
}
private static void FindAncestors(Type type, HashSet<TypeAtom> destination)
{
// Traverse the object tree for all possible aliases.
if (type.BaseType != null)
{
foreach (TypeAtom ancestor in Get(type.BaseType).Ancestors)
{
destination.Add(ancestor);
}
}
foreach (Type trait in type.GetInterfaces())
{
TypeAtom atom = Get(trait);
destination.Add(atom);
}
}
}
/// <summary>
/// Helper class for better type access performance.
/// </summary>
public sealed record TypeAtom<T> : TypeAtom
{
public static TypeAtom Atom { get; } = Get(typeof(T));
public new static int Id => Atom.Id;
public new static Type Type => Atom.Type;
public new static ImmutableHashSet<TypeAtom> Ancestors => Atom.Ancestors;
private TypeAtom() { }
}
}

View File

@@ -0,0 +1,157 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
namespace Dashboard.Collections
{
public class TypeDictionary<T>(bool hierarchical = false) : IEnumerable<T>
{
private readonly Dictionary<int, T> _objects = new Dictionary<int, T>();
public bool Contains<T2>() where T2 : T => _objects.ContainsKey(TypeAtom<T2>.Id);
public bool Add<T2>(T2 value) where T2 : T
{
TypeAtom atom = TypeAtom<T2>.Atom;
if (!_objects.TryAdd(atom.Id, value))
return false;
if (!hierarchical)
return true;
foreach (TypeAtom ancestor in atom.Ancestors)
{
_objects[ancestor.Id] = value;
}
return true;
}
public T2 Get<T2>() where T2 : T => TryGet(out T2? value) ? value : throw new KeyNotFoundException();
public void Set<T2>(T2 value) where T2 : T
{
TypeAtom atom = TypeAtom<T2>.Atom;
_objects[atom.Id] = value;
if (!hierarchical)
return;
foreach (TypeAtom ancestor in atom.Ancestors)
{
_objects[ancestor.Id] = value;
}
}
public bool Remove<T2>() where T2 : T => Remove<T>(out _);
public bool Remove<T2>([NotNullWhen(true)] out T2? value) where T2 : T
{
TypeAtom atom = TypeAtom<T2>.Atom;
if (!_objects.Remove(atom.Id, out T? subValue))
{
value = default;
return false;
}
value = (T2?)subValue;
if (hierarchical)
{
foreach (TypeAtom ancestor in atom.Ancestors)
{
_objects.Remove(ancestor.Id);
}
}
return value != null;
}
public bool TryGet<T2>([NotNullWhen(true)] out T2? value) where T2 : T
{
if (!_objects.TryGetValue(TypeAtom<T2>.Id, out T? subValue))
{
value = default;
return false;
}
value = (T2?)subValue;
return value != null;
}
public void Clear() => _objects.Clear();
public IEnumerator<T> GetEnumerator() => _objects.Values.Distinct().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class TypeDictionary<TKey, TValue>(bool hierarchical = false) : IEnumerable<KeyValuePair<TypeAtom, TValue>>
{
private readonly Dictionary<int, TValue> _objects = new Dictionary<int, TValue>();
public bool Contains<TKey2>() where TKey2 : TKey => _objects.ContainsKey(TypeAtom<TKey2>.Id);
public bool Add<TKey2>(TValue value) where TKey2 : TKey
{
TypeAtom atom = TypeAtom<TKey2>.Atom;
if (!_objects.TryAdd(atom.Id, value))
return false;
if (!hierarchical)
return true;
foreach (TypeAtom ancestor in atom.Ancestors)
{
_objects[ancestor.Id] = value;
}
return true;
}
public TValue Get<TKey2>() where TKey2 : TKey => (TValue)_objects[TypeAtom<TKey2>.Id]!;
public void Set<TKey2>(TValue value) where TKey2 : TKey
{
TypeAtom atom = TypeAtom<TKey2>.Atom;
_objects[atom.Id] = value;
if (!hierarchical)
return;
foreach (TypeAtom ancestor in atom.Ancestors)
{
_objects[ancestor.Id] = value;
}
}
public bool Remove<TKey2>() where TKey2 : TKey => Remove<TKey2>(out _);
public bool Remove<TKey2>([MaybeNullWhen(false)] out TValue? value) where TKey2 : TKey
{
TypeAtom atom = TypeAtom<TKey2>.Atom;
if (!_objects.Remove(atom.Id, out value))
{
return false;
}
if (hierarchical)
{
foreach (TypeAtom ancestor in atom.Ancestors)
{
_objects.Remove(ancestor.Id);
}
}
return true;
}
public bool TryGet<TKey2>([NotNullWhen(true)] out TValue? value) where TKey2 : TKey => _objects.TryGetValue(TypeAtom<TKey2>.Id, out value);
public void Clear() => _objects.Clear();
public IEnumerator<KeyValuePair<TypeAtom, TValue>> GetEnumerator() => _objects.Select(x => new KeyValuePair<TypeAtom, TValue>(TypeAtom.Get(x.Key)!, x.Value)).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections;
namespace Dashboard.Collections
{
public class TypeHashSet(bool hierarchical = false) : IEnumerable<TypeAtom>
{
private readonly HashSet<int> _set = new HashSet<int>();
public bool Contains<T>() => _set.Contains(TypeAtom<T>.Id);
public bool Set<T>()
{
if (!_set.Add(TypeAtom<T>.Id))
return false;
if (hierarchical)
foreach (TypeAtom ancestor in TypeAtom<T>.Ancestors)
{
_set.Add(ancestor.Id);
}
return true;
}
public bool Reset<T>()
{
if (!_set.Remove(TypeAtom<T>.Id))
return false;
if (hierarchical)
foreach (TypeAtom ancestor in TypeAtom<T>.Ancestors)
{
_set.Remove(ancestor.Id);
}
return true;
}
public void Clear() => _set.Clear();
public IEnumerator<TypeAtom> GetEnumerator() => _set.Select(x => TypeAtom.Get(x)!).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Dashboard</RootNamespace>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,25 @@
using System.Drawing;
using System.Numerics;
namespace Dashboard.Drawing
{
public abstract class Brush
{
}
public class SolidColorBrush(Color color) : Brush
{
public Color Color { get; } = color;
}
public class ImageBrush(Image image) : Brush
{
public Box2d TextureCoordinates { get; set; } = new Box2d(0, 0, 1, 1);
}
public class NinePatchImageBrush(Image image) : Brush
{
public Box2d CenterCoordinates { get; set; } = new Box2d(0, 0, 1, 1);
public Vector4 Extents { get; set; } = Vector4.Zero;
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.IO;
using System.Net.Mime;
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public class Font(IFont iFont) : IFont
{
public IFont Base => iFont;
public string Family => iFont.Family;
public FontWeight Weight => iFont.Weight;
public FontSlant Slant => iFont.Slant;
public FontStretch Stretch => iFont.Stretch;
public void Dispose()
{
iFont.Dispose();
}
public static Font Create(Stream stream)
{
IFont iFont = Application.Current.ExtensionRequire<IFontLoader>().Load(stream);
return new Font(iFont);
}
public static Font Create(FontInfo info)
{
IFont iFont = Application.Current.ExtensionRequire<IFontLoader>().Load(info);
return new Font(iFont);
}
public static Font Create(string path)
{
IFont iFont = Application.Current.ExtensionRequire<IFontLoader>().Load(path);
return new Font(iFont);
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Drawing;
using System.Numerics;
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public interface IDeviceContextBase : IDeviceContextExtension
{
Box2d ClipRegion { get; }
Box2d ScissorRegion { get; }
Matrix4x4 Transforms { get; }
float Scale { get; }
float ScaleOverride { get; set; }
void ResetClip();
void PushClip(Box2d clipRegion);
void PopClip();
void ResetScissor();
void PushScissor(Box2d scissorRegion);
void PopScissor();
void ResetTransforms();
void PushTransforms(in Matrix4x4 matrix);
void PopTransforms();
void ClearColor(Color color);
void ClearDepth();
int IncrementZ();
int DecrementZ();
}
}

View File

@@ -0,0 +1,27 @@
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public interface IFont : IDisposable
{
string Family { get; }
FontWeight Weight { get; }
FontSlant Slant { get; }
FontStretch Stretch { get; }
}
public record struct FontInfo(string Family, FontWeight Weight = FontWeight.Normal,
FontSlant Slant = FontSlant.Normal, FontStretch Stretch = FontStretch.Normal) : IFont
{
public void Dispose()
{
}
}
public interface IFontLoader : IApplicationExtension
{
IFont Load(FontInfo info);
IFont Load(string path);
IFont Load(Stream stream);
}
}

View File

@@ -0,0 +1,61 @@
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public record ImageData(
TextureType Type,
PixelFormat Format,
int Width,
int Height,
byte[] Bitmap)
{
public int Depth { get; init; } = 1;
public int Levels { get; init; } = 1;
public bool Premultiplied { get; init; } = false;
public int Alignment { get; init; } = 4;
public long GetLevelOffset(int level)
{
ArgumentOutOfRangeException.ThrowIfLessThan(level, 0, nameof(level));
ArgumentOutOfRangeException.ThrowIfGreaterThan(level, Levels, nameof(level));
ArgumentOutOfRangeException.ThrowIfGreaterThan(level, Math.ILogB(Math.Max(Width, Height)));
long offset = 0;
long row = Width * Format switch
{
PixelFormat.R8I => 1,
PixelFormat.R16F => 2,
PixelFormat.Rg8I => 2,
PixelFormat.Rg16F => 4,
PixelFormat.Rgb8I => 3,
PixelFormat.Rgb16F => 6,
PixelFormat.Rgba8I => 4,
PixelFormat.Rgba16F => 8,
};
row += Alignment - (row % Alignment);
long plane = row * Height;
long volume = plane * Depth;
for (int i = 0; i < level; i++)
{
if (Depth == 1)
{
offset += plane / (1 << i) / (1 << i);
}
else
{
offset += volume / (1 << i) / (1 << i) / (1 << i);
}
}
return offset;
}
}
public interface IImageLoader : IApplicationExtension
{
public ImageData LoadImageData(Stream stream);
}
}

View File

@@ -0,0 +1,17 @@
using System.Drawing;
using System.Numerics;
using Dashboard.Layout;
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public record struct RectangleDrawInfo(Vector2 Position, ComputedBox Box, Brush Fill, Brush? Border = null);
public interface IImmediateMode : IDeviceContextExtension
{
void Line(Vector2 a, Vector2 b, float width, float depth, Vector4 color);
void Rectangle(Box2d rectangle, float depth, Vector4 color);
void Rectangle(in RectangleDrawInfo rectangle);
void Image(Box2d rectangle, Box2d uv, float depth, ITexture texture);
}
}

View File

@@ -0,0 +1,12 @@
using System.Numerics;
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public interface ITextRenderer : IDeviceContextExtension
{
Box2d MeasureText(IFont font, float size, string text);
void DrawText(Vector2 position, Vector4 color, float size, IFont font, string text);
}
}

View File

@@ -0,0 +1,84 @@
using System.Drawing;
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public interface ITextureExtension : IDeviceContextExtension
{
ITexture CreateTexture(TextureType type);
}
public enum TextureType
{
Texture1D,
Texture2D,
Texture2DArray,
Texture2DCube,
Texture3D,
}
public enum TextureFilter
{
Nearest,
Linear,
NearestMipmapNearest,
LinearMipmapNearest,
NearestMipmapLinear,
LinearMipmapLinear,
Anisotropic,
}
public enum TextureRepeat
{
Repeat,
MirroredRepeat,
ClampToEdge,
ClampToBorder,
MirrorClampToEdge,
}
public enum CubeMapFace
{
PositiveX,
PositiveY,
PositiveZ,
NegativeX,
NegativeY,
NegativeZ,
}
public interface ITexture : IDisposable
{
public TextureType Type { get; }
public PixelFormat Format { get; }
public int Width { get; }
public int Height { get; }
public int Depth { get; }
public int Levels { get; }
public bool Premultiplied { get; set; }
public ColorSwizzle Swizzle { get; set; }
public TextureFilter MinifyFilter { get; set; }
public TextureFilter MagnifyFilter { get; set; }
public Color BorderColor { get; set; }
public TextureRepeat RepeatS { get; set; }
public TextureRepeat RepeatT { get; set; }
public TextureRepeat RepeatR { get; set; }
public int Anisotropy { get; set; }
void SetStorage(PixelFormat format, int width, int height, int depth, int levels);
void Read<T>(Span<T> buffer, int level = 0, int align = 0) where T : unmanaged;
void Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level = 0, int align = 4) where T : unmanaged;
void Premultiply();
void Unmultiply();
void GenerateMipmaps();
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using Dashboard.Pal;
namespace Dashboard.Drawing
{
public class Image(ImageData data) : IDisposable
{
protected readonly ConditionalWeakTable<DeviceContext, ITexture> Textures =
new ConditionalWeakTable<DeviceContext, ITexture>();
public virtual TextureType Type => data.Type;
public PixelFormat Format { get; } = data.Format;
public int Width { get; } = data.Width;
public int Height { get; } = data.Height;
public int Depth { get; } = data.Depth;
public int Levels { get; } = data.Levels;
public bool Premultiplied { get; } = data.Premultiplied;
public bool IsDisposed { get; private set; } = false;
~Image()
{
InvokeDispose(false);
}
public virtual ITexture InternTexture(DeviceContext dc)
{
if (Textures.TryGetValue(dc, out ITexture? texture))
return texture;
ITextureExtension ext = dc.ExtensionRequire<ITextureExtension>();
texture = ext.CreateTexture(Type);
texture.SetStorage(Format, Width, Height, Depth, Levels);
for (int i = 0; i < Levels; i++)
{
texture.Write<byte>(Format, data.Bitmap.AsSpan()[(int)data.GetLevelOffset(i)..], level: i, align: data.Alignment);
}
texture.Premultiplied = Premultiplied;
texture.GenerateMipmaps();
Textures.Add(dc, texture);
return texture;
}
private void InvokeDispose(bool disposing)
{
if (IsDisposed)
return;
IsDisposed = true;
Dispose(disposing);
}
protected virtual void Dispose(bool disposing)
{
foreach ((DeviceContext dc, ITexture texture) in Textures)
{
texture.Dispose();
}
}
public void Dispose() => InvokeDispose(true);
public static Image Load(Stream stream)
{
IImageLoader imageLoader = Application.Current.ExtensionRequire<IImageLoader>();
return new Image(imageLoader.LoadImageData(stream));
}
}
}

View File

@@ -0,0 +1,59 @@
namespace Dashboard.Events
{
[Flags]
public enum ModifierKeys
{
None = 0,
LeftBitPos = 8,
RightBitPos = 16,
Shift = (1 << 0),
Control = (1 << 1),
Alt = (1 << 2),
Meta = (1 << 3),
NumLock = (1 << 4),
CapsLock = (1 << 5),
ScrollLock = (1 << 6),
LeftShift = (Shift << LeftBitPos),
LeftControl = (Control << LeftBitPos),
LeftAlt = (Alt << LeftBitPos),
LeftMeta = (Meta << LeftBitPos),
RightShift = (Shift << RightBitPos),
RightControl = (Control << RightBitPos),
RightAlt = (Alt << RightBitPos),
RightMeta = (Meta << RightBitPos),
}
public enum KeyCode
{
// TODO:
}
public enum ScanCode
{
// TODO:
}
public class KeyboardButtonEventArgs(KeyCode keyCode, ScanCode scanCode, ModifierKeys modifierKeys, bool up)
: UiEventArgs(up ? UiEventType.KeyUp : UiEventType.KeyDown)
{
public KeyCode KeyCode { get; } = keyCode;
public ScanCode ScanCode { get; } = scanCode;
public ModifierKeys ModifierKeys { get; } = modifierKeys;
}
public class TextInputEventArgs(string text) : UiEventArgs(UiEventType.TextEdit)
{
public string Text { get; } = text;
}
public class TextEditEventArgs(string candidate, int cursor, int length) : UiEventArgs(UiEventType.TextEdit)
{
public string Candidate { get; } = candidate;
public int Cursor { get; } = cursor;
public int Length { get; } = length;
}
}

View File

@@ -0,0 +1,43 @@
using System.Drawing;
using System.Numerics;
namespace Dashboard.Events
{
[Flags]
public enum MouseButtons
{
M1 = 1 << 0,
M2 = 1 << 1,
M3 = 1 << 2,
M4 = 1 << 3,
M5 = 1 << 4,
M6 = 1 << 5,
M7 = 1 << 6,
M8 = 1 << 7,
Left = M1,
Right = M2,
Middle = M3,
}
public sealed class MouseMoveEventArgs(Vector2 clientPosition, Vector2 delta) : UiEventArgs(UiEventType.MouseMove)
{
public Vector2 ClientPosition { get; } = clientPosition;
public Vector2 Delta { get; } = delta;
}
public sealed class MouseButtonEventArgs(Vector2 clientPosition, MouseButtons buttons, ModifierKeys modifierKeys, bool up)
: UiEventArgs(up ? UiEventType.MouseButtonUp : UiEventType.MouseButtonDown)
{
public ModifierKeys ModifierKeys { get; } = modifierKeys;
public Vector2 ClientPosition { get; } = clientPosition;
public MouseButtons Buttons { get; } = buttons;
}
public sealed class MouseScrollEventArgs(Vector2 clientPosition, Vector2 scrollDelta)
: UiEventArgs(UiEventType.MouseScroll)
{
public Vector2 ClientPosition { get; } = clientPosition;
public Vector2 ScrollDelta { get; } = scrollDelta;
}
}

View File

@@ -0,0 +1,15 @@
namespace Dashboard.Events
{
public class TickEventArgs : UiEventArgs
{
/// <summary>
/// Animation delta time in seconds.
/// </summary>
public float Delta { get; }
public TickEventArgs(float delta) : base(UiEventType.AnimationTick)
{
Delta = delta;
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Numerics;
using Dashboard.Pal;
namespace Dashboard.Events
{
public enum UiEventType
{
None,
AnimationTick, // Generic timer event.
Paint, // Generic paint event.
// Text input related events.
KeyDown, // Keyboard key down.
KeyUp, // Keyboard key up.
TextInput, // Non-IME text event.
TextEdit, // IME text event.
TextCandidates, // IME text candidate list.
TextLanguage, // Keyboard language changed event.
// Mouse & touch related events
MouseButtonDown, // Mouse button down.
MouseButtonUp, // Mouse button up.
MouseMove, // Mouse moved.
MouseScroll, // Mouse scrolled.
// Reserved event names
StylusEnter, // The stylus has entered the hover region.
StylusLeave, // The stylus has left the hover region.
StylusMove, // The stylus has moved.
StylusDown, // The stylus is touching.
StylusUp, // The stylus is no longer touching.
StylusButtonUp, // Stylus button up.
StylusButtonDown, // Stylus button down.
StylusAxes, // Extra stylus axes data.
// Window & Control Events
ControlInvalidateVisual, // Force rendering the control again.
ControlStateChanged, // Control state changed.
ControlMoved, // Control moved.
ControlResized, // Control resized.
ControlEnter, // The pointing device entered the control.
ControlLeave, // The pointing device left the control.
ControlFocusGet, // The control acquired focus.
ControlFocusLost, // The control lost focus.
WindowClose, // The window closed.
UserRangeStart = 1 << 12,
}
public class UiEventArgs : EventArgs
{
public UiEventType Type { get; }
public UiEventArgs(UiEventType type)
{
Type = type;
}
public static readonly UiEventArgs None = new UiEventArgs(UiEventType.None);
}
public class PaintEventArgs(DeviceContext dc) : UiEventArgs(UiEventType.Paint)
{
public DeviceContext DeviceContext { get; } = dc;
}
public class ControlMovedEventArgs : UiEventArgs
{
public Vector2 OldPosition { get; }
public Vector2 NewPosition { get; }
public ControlMovedEventArgs(Vector2 oldPosition, Vector2 newPosition) : base(UiEventType.ControlMoved)
{
OldPosition = oldPosition;
NewPosition = newPosition;
}
}
public class ResizeEventArgs() : UiEventArgs(UiEventType.ControlResized)
{
}
}

View File

@@ -0,0 +1,7 @@
namespace Dashboard.Events
{
public class WindowCloseEvent() : UiEventArgs(UiEventType.WindowClose)
{
public bool Cancel { get; set; } = false;
}
}

View File

@@ -0,0 +1,41 @@
namespace Dashboard
{
public enum FontWeight
{
_100 = 100,
_200 = 200,
_300 = 300,
_400 = 400,
_500 = 500,
_600 = 600,
_700 = 700,
_800 = 800,
_900 = 900,
Thin = _100,
Normal = _400,
Bold = _600,
Heavy = _900,
}
public enum FontSlant
{
Normal,
Italic,
Oblique,
}
public enum FontStretch
{
UltraCondensed = 500,
ExtraCondensed = 625,
Condensed = 750,
SemiCondensed = 875,
Normal = 1000,
SemiExpanded = 1125,
Expanded = 1250,
ExtraExpanded = 1500,
UltraExpanded = 2000,
}
}

View File

@@ -0,0 +1,227 @@
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Numerics;
namespace Dashboard
{
/// <summary>
/// Enumeration of the kinds of gradients available.
/// </summary>
public enum GradientType
{
/// <summary>
/// A gradient which transitions over a set axis.
/// </summary>
Axial,
/// <summary>
/// A gradient which transitions along elliptical curves.
/// </summary>
Radial,
}
/// <summary>
/// A single gradient stop.
/// </summary>
/// <param name="Position">The position of the gradient stop. Must be [0,1].</param>
/// <param name="Color">The color value for the stop.</param>
public record struct GradientStop(float Position, Color Color);
/// <summary>
/// Represents a linear gradient.
/// </summary>
public struct Gradient : ICollection<GradientStop>, ICloneable, IEquatable<Gradient>
{
private readonly List<GradientStop> _stops = new List<GradientStop>();
/// <summary>
/// Gradient type.
/// </summary>
public GradientType Type { get; set; } = GradientType.Axial;
/// <summary>
/// First gradient control point.
/// </summary>
public Vector2 C0 { get; set; } = Vector2.Zero;
/// <summary>
/// Second gradient control point.
/// </summary>
public Vector2 C1 { get; set; } = Vector2.One;
/// <summary>
/// Number of stops in a gradient.
/// </summary>
public int Count => _stops.Count;
public bool IsReadOnly => false;
/// <summary>
/// Get a gradient control point.
/// </summary>
/// <param name="index">The index to get the control point for.</param>
public GradientStop this[int index]
{
get => _stops[index];
set
{
RemoveAt(index);
Add(value);
}
}
public Gradient()
{
}
public Gradient(Color a, Color b)
{
Add(new GradientStop(0, a));
Add(new GradientStop(1, b));
}
public Gradient(IEnumerable<GradientStop> stops)
{
_stops.AddRange(stops);
if (_stops.Any(x => x.Position < 0 || x.Position > 1))
throw new Exception("Gradient stop positions must be in the range [0, 1].");
_stops.Sort((a, b) => a.Position.CompareTo(b.Position));
}
public Color GetColor(float position)
{
if (Count == 0)
return Color.Black;
else if (Count == 1)
return _stops[0].Color;
int pivot = _stops.FindIndex(x => x.Position < position);
GradientStop left, right;
if (pivot == -1)
{
left = right = _stops[^1];
}
else if (pivot == 0)
{
left = right = _stops[0];
}
else
{
left = _stops[pivot-1];
right = _stops[pivot];
}
float weight = (position - left.Position) / (right.Position - left.Position);
Vector4 lcolor = new Vector4(left.Color.R, left.Color.G, left.Color.B, left.Color.A) * (1-weight);
Vector4 rcolor = new Vector4(right.Color.R, right.Color.G, right.Color.B, right.Color.A) * weight;
Vector4 color = lcolor + rcolor;
return Color.FromArgb((byte)color.W, (byte)color.X, (byte)color.Y, (byte)color.Z);
}
public Gradient Clone()
{
Gradient gradient = new Gradient()
{
Type = Type,
C0 = C0,
C1 = C1,
};
foreach (GradientStop stop in _stops)
{
gradient.Add(stop);
}
return gradient;
}
object ICloneable.Clone()
{
return Clone();
}
public IEnumerator<GradientStop> GetEnumerator()
{
return _stops.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_stops).GetEnumerator();
}
public void Add(GradientStop item)
{
if (item.Position < 0 || item.Position > 1)
throw new Exception("Gradient stop positions must be in the range [0, 1].");
int index = _stops.FindIndex(x => x.Position > item.Position);
if (index == -1)
index = _stops.Count;
_stops.Insert(index, item);
}
public void Clear()
{
_stops.Clear();
}
public bool Contains(GradientStop item)
{
return _stops.Contains(item);
}
public void CopyTo(GradientStop[] array, int arrayIndex)
{
_stops.CopyTo(array, arrayIndex);
}
public bool Remove(GradientStop item)
{
return _stops.Remove(item);
}
public void RemoveAt(int index)
{
_stops.RemoveAt(index);
}
public override int GetHashCode()
{
HashCode code = new HashCode();
code.Add(Count);
foreach (GradientStop item in this)
code.Add(item.GetHashCode());
return code.ToHashCode();
}
public bool Equals(Gradient other)
{
return
Type == other.Type &&
C0 == other.C0 &&
C1 == other.C1 &&
_stops.Equals(other._stops);
}
public override bool Equals(object? obj)
{
return obj is Gradient other && Equals(other);
}
public static bool operator ==(Gradient left, Gradient right)
{
return left.Equals(right);
}
public static bool operator !=(Gradient left, Gradient right)
{
return !left.Equals(right);
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections;
using System.Collections.Generic;
namespace Dashboard
{
public class HashList<T> : IReadOnlyList<T>
where T : notnull
{
private readonly List<T> _list = new List<T>();
private readonly Dictionary<T, int> _map = new Dictionary<T, int>();
public T this[int index] => _list[index];
public int Count => _list.Count;
public int Intern(T value)
{
if (_map.TryGetValue(value, out int index))
return index;
index = Count;
_list.Add(value);
_map.Add(value, index);
return index;
}
public void Clear()
{
_list.Clear();
_map.Clear();
}
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
}
}

View File

@@ -0,0 +1,211 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Dashboard
{
/// <summary>
/// Pixel format for images.
/// </summary>
[Flags]
public enum PixelFormat
{
None = 0,
R8I = R | I8,
Rg8I = Rg | I8,
Rgb8I = Rgb | I8,
Rgba8I = Rgba | I8,
R16F = R | F16,
Rg16F = Rg | F16,
Rgb16F = Rgb | F16,
Rgba16F = Rgba | F16,
// Channels
R = 0x01,
Rg = 0x02,
Rgb = 0x03,
Rgba = 0x04,
A = 0x05,
ColorMask = 0x0F,
I8 = 0x10,
F16 = 0x20,
TypeMask = 0xF0,
}
/// <summary>
/// Color channels for images.
/// </summary>
public enum ColorChannel
{
/// <summary>
/// The zero channel. Used for swizzle masks.
/// </summary>
Zero = 0,
/// <summary>
/// The one channel. Used for swizzle masks.
/// </summary>
One = 1,
/// <summary>
/// An invalid swizzle mask.
/// </summary>
Unknown = 2,
/// <summary>
/// The red channel.
/// </summary>
Red = 4,
/// <summary>
/// The green channel.
/// </summary>
Green = 5,
/// <summary>
/// The blue channel.
/// </summary>
Blue = 6,
/// <summary>
/// The alpha channel.
/// </summary>
Alpha = 7,
}
/// <summary>
/// Defines a image swizzle mask.
/// </summary>
public struct ColorSwizzle : IEquatable<ColorSwizzle>
{
public short Mask;
private const int MASK = 7;
private const int RBIT = 0;
private const int GBIT = 3;
private const int BBIT = 6;
private const int ABIT = 9;
/// <summary>
/// Swizzle the red channel.
/// </summary>
public ColorChannel R
{
get => (ColorChannel)((Mask >> RBIT) & MASK);
set => Mask = (short)(((int)value << RBIT) | (Mask & ~(MASK << RBIT)));
}
/// <summary>
/// Swizzle the green channel.
/// </summary>
public ColorChannel G
{
get => (ColorChannel)((Mask >> GBIT) & MASK);
set => Mask = (short)(((int)value << GBIT) | (Mask & ~(MASK << GBIT)));
}
/// <summary>
/// Swizzle the blue channel.
/// </summary>
public ColorChannel B
{
get => (ColorChannel)((Mask >> BBIT) & MASK);
set => Mask = (short)(((int)value << BBIT) | (Mask & ~(MASK << BBIT)));
}
/// <summary>
/// Swizzle the alpha channel.
/// </summary>
public ColorChannel A
{
get => (ColorChannel)((Mask >> ABIT) & MASK);
set => Mask = (short)(((int)value << ABIT) | (Mask & ~(MASK << ABIT)));
}
public ColorSwizzle(short mask)
{
Mask = mask;
}
public ColorSwizzle(ColorChannel r, ColorChannel g, ColorChannel b, ColorChannel a)
{
Mask = (short)(((int)r << RBIT) | ((int)g << GBIT) | ((int)b << BBIT) | ((int)a << ABIT));
}
public override string ToString()
{
return $"{GetChannelChar(R)}{GetChannelChar(G)}{GetChannelChar(B)}{GetChannelChar(A)}";
char GetChannelChar(ColorChannel channel) => channel switch
{
ColorChannel.Zero => '0',
ColorChannel.Red => 'R',
ColorChannel.Green => 'G',
ColorChannel.Blue => 'B',
ColorChannel.Alpha => 'A',
ColorChannel.One => '1',
_ => '?',
};
}
public override int GetHashCode()
{
return Mask.GetHashCode();
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is ColorSwizzle other && Equals(other);
}
public bool Equals(ColorSwizzle other)
{
return Mask == other.Mask;
}
public static readonly ColorSwizzle Default = Parse("RGBA");
public static readonly ColorSwizzle White = Parse("1111");
public static readonly ColorSwizzle Black = Parse("0001");
public static readonly ColorSwizzle Transparent = Parse("0000");
public static readonly ColorSwizzle RedToGrayscale = Parse("RRR1");
public static readonly ColorSwizzle RedToWhiteAlpha = Parse("111A");
public static bool TryParse(ReadOnlySpan<char> str, out ColorSwizzle value)
{
if (str.Length < 4)
{
value = default;
return false;
}
ColorChannel r = GetChannelFromChar(str[0]);
ColorChannel g = GetChannelFromChar(str[1]);
ColorChannel b = GetChannelFromChar(str[2]);
ColorChannel a = GetChannelFromChar(str[3]);
value = new ColorSwizzle(r, g, b, a);
return true;
ColorChannel GetChannelFromChar(char chr) => chr switch
{
'0' => ColorChannel.Zero,
'R' => ColorChannel.Red,
'G' => ColorChannel.Green,
'B' => ColorChannel.Blue,
'A' => ColorChannel.Alpha,
'1' => ColorChannel.One,
_ => ColorChannel.Unknown,
};
}
public static ColorSwizzle Parse(ReadOnlySpan<char> str) =>
TryParse(str, out ColorSwizzle value) ? value : throw new FormatException(nameof(str));
public static bool operator ==(ColorSwizzle left, ColorSwizzle right) =>
left.Mask == right.Mask;
public static bool operator !=(ColorSwizzle left, ColorSwizzle right) =>
left.Mask != right.Mask;
}
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.Numerics;
namespace Dashboard.Layout
{
public interface ILayoutItem : INotifyPropertyChanged
{
public LayoutInfo Layout { get; }
public Vector2 CalculateIntrinsicSize();
public Vector2 CalculateSize(Vector2 limits);
}
public interface ILayoutContainer : ILayoutItem, IEnumerable<ILayoutItem>
{
public ContainerLayoutInfo ContainerLayout { get; }
}
}

View File

@@ -0,0 +1,435 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Dashboard.Layout
{
public readonly record struct ComputedBox(Vector4 Margin, Vector4 Padding, Vector4 Border, Vector2 Size)
{
public float MarginLeft => Margin.X;
public float MarginTop => Margin.Y;
public float MarginRight => Margin.Z;
public float MarginBottom => Margin.W;
public float PaddingLeft => Padding.X;
public float PaddingTop => Padding.Y;
public float PaddingRight => Padding.Z;
public float PaddingBottom => Padding.W;
public float BorderLeft => Border.X;
public float BorderTop => Border.Y;
public float BorderRight => Border.Z;
public float BorderBottom => Border.W;
public float Width => Size.X;
public float Height => Size.Y;
public Vector2 BoundingSize => new Vector2(
MarginLeft + BorderLeft + Width + BorderRight + MarginRight,
MarginTop + BorderTop + Height + BorderBottom + MarginBottom);
public Vector2 ContentSize => new Vector2(
Width - PaddingLeft - PaddingRight,
Height - PaddingTop - PaddingBottom);
public Vector4 ContentExtents => new Vector4(
PaddingLeft,
PaddingTop,
PaddingLeft + PaddingRight + Width,
PaddingTop + PaddingBottom + Height);
public Vector4 CornerRadii { get; init; } = Vector4.Zero;
}
public class LayoutBox : INotifyPropertyChanged
{
private Vector4 _margin = Vector4.Zero;
private Vector4 _padding = Vector4.Zero;
private Vector4 _border = Vector4.Zero;
private Vector2 _size = Vector2.Zero;
private Vector2 _minimumSize = -Vector2.One;
private Vector2 _maximumSize = -Vector2.One;
private Vector4 _cornerRadii = Vector4.Zero;
private LayoutUnit _marginLeftUnit = LayoutUnit.Pixel;
private LayoutUnit _marginTopUnit = LayoutUnit.Pixel;
private LayoutUnit _marginRightUnit = LayoutUnit.Pixel;
private LayoutUnit _marginBottomUnit = LayoutUnit.Pixel;
private LayoutUnit _paddingLeftUnit = LayoutUnit.Pixel;
private LayoutUnit _paddingTopUnit = LayoutUnit.Pixel;
private LayoutUnit _paddingRightUnit = LayoutUnit.Pixel;
private LayoutUnit _paddingBottomUnit = LayoutUnit.Pixel;
private LayoutUnit _borderLeftUnit = LayoutUnit.Pixel;
private LayoutUnit _borderTopUnit = LayoutUnit.Pixel;
private LayoutUnit _borderRightUnit = LayoutUnit.Pixel;
private LayoutUnit _borderBottomUnit = LayoutUnit.Pixel;
private LayoutUnit _widthUnit = LayoutUnit.Pixel;
private LayoutUnit _heightUnit = LayoutUnit.Pixel;
private LayoutUnit _minimumWidthUnit = LayoutUnit.Pixel;
private LayoutUnit _minimumHeightUnit = LayoutUnit.Pixel;
private LayoutUnit _maximumWidthUnit = LayoutUnit.Pixel;
private LayoutUnit _maximumHeightUnit = LayoutUnit.Pixel;
private LayoutUnit _cornerRadiusTopLeftUnit = LayoutUnit.Pixel;
private LayoutUnit _cornerRadiusBottomLeftUnit = LayoutUnit.Pixel;
private LayoutUnit _cornerRadiusBottomRightUnit = LayoutUnit.Pixel;
private LayoutUnit _cornerRadiusTopRightUnit = LayoutUnit.Pixel;
private ComputedBox _computedBox = new ComputedBox();
public bool UpdateRequired { get; private set; } = true;
public Vector4 Margin
{
get => _margin;
set => SetField(ref _margin, value);
}
public Vector4 Padding
{
get => _padding;
set => SetField(ref _padding, value);
}
public Vector4 Border
{
get => _border;
set => SetField(ref _border, value);
}
public Vector2 Size
{
get => _size;
set => SetField(ref _size, value);
}
public Vector2 MinimumSize
{
get => _minimumSize;
set => SetField(ref _minimumSize, value);
}
public Vector2 MaximumSize
{
get => _maximumSize;
set => SetField(ref _maximumSize, value);
}
public Vector4 CornerRadii
{
get => _cornerRadii;
set => SetField(ref _cornerRadii, value);
}
public float MarginLeft
{
get => Margin.X;
set => Margin = Margin with { X = value };
}
public float MarginTop
{
get => Margin.Y;
set => Margin = Margin with { Y = value };
}
public float MarginRight
{
get => Margin.Z;
set => Margin = Margin with { Z = value };
}
public float MarginBottom
{
get => Margin.W;
set => Margin = Margin with { W = value };
}
public float PaddingLeft
{
get => Padding.X;
set => Padding = Padding with { X = value };
}
public float PaddingTop
{
get => Padding.Y;
set => Padding = Padding with { Y = value };
}
public float PaddingRight
{
get => Padding.Z;
set => Padding = Padding with { Z = value };
}
public float PaddingBottom
{
get => Padding.W;
set => Padding = Padding with { W = value };
}
public float BorderLeft
{
get => Border.X;
set => Border = Border with { X = value };
}
public float BorderTop
{
get => Border.Y;
set => Border = Border with { Y = value };
}
public float BorderRight
{
get => Border.Z;
set => Border = Border with { Z = value };
}
public float BorderBottom
{
get => Border.W;
set => Border = Border with { W = value };
}
public float CornerRadiusTopLeft
{
get => CornerRadii.X;
set => CornerRadii = CornerRadii with { X = value };
}
public float CornerRadiusBottomLeft
{
get => CornerRadii.Y;
set => CornerRadii = CornerRadii with { Y = value };
}
public float CornerRadiusBottomRight
{
get => CornerRadii.Z;
set => CornerRadii = CornerRadii with { Z = value };
}
public float CornerRadiusTopRight
{
get => CornerRadii.W;
set => CornerRadii = CornerRadii with { W = value };
}
public LayoutUnit MarginLeftUnit
{
get => _marginLeftUnit;
set => SetField(ref _marginLeftUnit, value);
}
public LayoutUnit MarginTopUnit
{
get => _marginTopUnit;
set => SetField(ref _marginTopUnit, value);
}
public LayoutUnit MarginRightUnit
{
get => _marginRightUnit;
set => SetField(ref _marginRightUnit, value);
}
public LayoutUnit MarginBottomUnit
{
get => _marginBottomUnit;
set => SetField(ref _marginBottomUnit, value);
}
public LayoutUnit PaddingLeftUnit
{
get => _paddingLeftUnit;
set => SetField(ref _paddingLeftUnit, value);
}
public LayoutUnit PaddingTopUnit
{
get => _paddingTopUnit;
set => SetField(ref _paddingTopUnit, value);
}
public LayoutUnit PaddingRightUnit
{
get => _paddingRightUnit;
set => SetField(ref _paddingRightUnit, value);
}
public LayoutUnit PaddingBottomUnit
{
get => _paddingBottomUnit;
set => SetField(ref _paddingBottomUnit, value);
}
public LayoutUnit BorderLeftUnit
{
get => _borderLeftUnit;
set => SetField(ref _borderLeftUnit, value);
}
public LayoutUnit BorderTopUnit
{
get => _borderTopUnit;
set => SetField(ref _borderTopUnit, value);
}
public LayoutUnit BorderRightUnit
{
get => _borderRightUnit;
set => SetField(ref _borderRightUnit, value);
}
public LayoutUnit BorderBottomUnit
{
get => _borderBottomUnit;
set => SetField(ref _borderBottomUnit, value);
}
public LayoutUnit WidthUnit
{
get => _widthUnit;
set => SetField(ref _widthUnit, value);
}
public LayoutUnit HeightUnit
{
get => _heightUnit;
set => SetField(ref _heightUnit, value);
}
public LayoutUnit MinimumWidthUnit
{
get => _minimumWidthUnit;
set => SetField(ref _minimumWidthUnit, value);
}
public LayoutUnit MinimumHeightUnit
{
get => _minimumHeightUnit;
set => SetField(ref _minimumHeightUnit, value);
}
public LayoutUnit MaximumWidthUnit
{
get => _maximumWidthUnit;
set => SetField(ref _maximumWidthUnit, value);
}
public LayoutUnit MaximumHeightUnit
{
get => _maximumHeightUnit;
set => SetField(ref _maximumHeightUnit, value);
}
public LayoutUnit CornerRadiusTopLeftUnit
{
get => _cornerRadiusTopLeftUnit;
set => SetField(ref _cornerRadiusTopLeftUnit, value);
}
public LayoutUnit CornerRadiusBottomLeftUnit
{
get => _cornerRadiusBottomLeftUnit;
set => SetField(ref _cornerRadiusBottomLeftUnit, value);
}
public LayoutUnit CornerRadiusBottomRightUnit
{
get => _cornerRadiusBottomRightUnit;
set => SetField(ref _cornerRadiusBottomRightUnit, value);
}
public LayoutUnit CornerRadiusTopRightUnit
{
get => _cornerRadiusTopRightUnit;
set => SetField(ref _cornerRadiusTopRightUnit, value);
}
public ComputedBox ComputedBox
{
get => _computedBox;
private set => SetField(ref _computedBox, value, false);
}
public event PropertyChangedEventHandler? PropertyChanged;
public ComputedBox ComputeLayout(Vector2 intrinsic, Vector2 dpi, Vector2 area, Vector2 star)
{
// TODO: take intrinsic into account.
Vector4 margin = Compute(_margin, dpi, area, star, _marginLeftUnit, _marginTopUnit, _marginRightUnit, _marginBottomUnit);
Vector4 padding = Compute(_padding, dpi, area, star, _paddingLeftUnit, _paddingTopUnit, _paddingRightUnit, _paddingBottomUnit);
Vector4 border = Compute(_border, dpi, area, star, _borderLeftUnit, _borderTopUnit, _borderRightUnit, _borderBottomUnit);
Vector2 size = Compute(_size, dpi, area, star, _widthUnit, _heightUnit);
Vector2 minimumSize = Compute(_minimumSize, dpi, area, star, _minimumWidthUnit, _minimumHeightUnit);
Vector2 maximumSize = Compute(_maximumSize, dpi, area, star, _maximumWidthUnit, _maximumHeightUnit);
Vector4 cornerRadii = Compute(_cornerRadii, dpi, area, star, _cornerRadiusTopLeftUnit,
_cornerRadiusBottomLeftUnit, _cornerRadiusBottomRightUnit, _cornerRadiusTopRightUnit);
size = Vector2.Clamp(size, minimumSize, maximumSize);
ComputedBox = new ComputedBox(margin, padding, border, size)
{
CornerRadii = cornerRadii,
};
UpdateRequired = false;
return ComputedBox;
}
private static float Compute(float value, float dpi, float length, float star, LayoutUnit unit)
{
const float dpiToMm = 0f;
const float dpiToPt = 0f;
return unit switch
{
LayoutUnit.Pixel => value,
LayoutUnit.Millimeter => value * dpi * dpiToMm,
LayoutUnit.Percent => value * length,
LayoutUnit.Point => value * dpi * dpiToPt,
LayoutUnit.Star => value * star,
_ => throw new ArgumentException(nameof(unit)),
};
}
private static Vector2 Compute(Vector2 value, Vector2 dpi, Vector2 size, Vector2 star, LayoutUnit xUnit, LayoutUnit yUnit)
{
return new Vector2(
Compute(value.X, dpi.X, size.X, star.X, xUnit),
Compute(value.Y, dpi.Y, size.Y, star.Y, yUnit));
}
private static Vector4 Compute(Vector4 value, Vector2 dpi, Vector2 size, Vector2 star, LayoutUnit xUnit, LayoutUnit yUnit, LayoutUnit zUnit, LayoutUnit wUnit)
{
return new Vector4(
Compute(value.X, dpi.X, size.X, star.X, xUnit),
Compute(value.Y, dpi.Y, size.Y, star.Y, yUnit),
Compute(value.Z, dpi.X, size.X, star.X, zUnit),
Compute(value.W, dpi.Y, size.Y, star.Y, wUnit));
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool SetField<T>(ref T field, T value, bool updateRequired = true, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
UpdateRequired |= updateRequired;
OnPropertyChanged(propertyName);
return true;
}
}
}

View File

@@ -0,0 +1,171 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Dashboard.Layout
{
public enum DisplayMode
{
None,
Inline,
Block,
}
public enum ContainerMode
{
Basic,
Flex,
Grid,
}
public enum FlowDirection
{
Row,
Column,
RowReverse,
ColumnReverse,
}
public enum PositionMode
{
Absolute,
Relative,
}
public enum OverflowMode
{
Hidden,
Overflow,
ScrollHorizontal,
ScrollVertical,
ScrollBoth,
}
public record struct TrackInfo(float Width, LayoutUnit Unit)
{
public static readonly TrackInfo Default = new TrackInfo(0, LayoutUnit.Auto);
}
public class ContainerLayoutInfo : INotifyPropertyChanged
{
private ContainerMode _containerMode;
private FlowDirection _flowDirection = FlowDirection.Row;
public ObservableCollection<TrackInfo> Rows { get; } = new ObservableCollection<TrackInfo>() { TrackInfo.Default };
public ObservableCollection<TrackInfo> Columns { get; } =
new ObservableCollection<TrackInfo>() { TrackInfo.Default };
public ContainerMode ContainerMode
{
get => _containerMode;
set => SetField(ref _containerMode, value);
}
public FlowDirection FlowDirection
{
get => _flowDirection;
set => SetField(ref _flowDirection, value);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
public class LayoutInfo : INotifyPropertyChanged
{
private DisplayMode _displayMode = DisplayMode.Inline;
private PositionMode _positionMode = PositionMode.Relative;
private OverflowMode _overflowMode = OverflowMode.Overflow;
private int _row = 0;
private int _column = 0;
/// <summary>
/// Changes the control display.
/// </summary>
public DisplayMode DisplayMode
{
get => _displayMode;
set => SetField(ref _displayMode, value);
}
/// <summary>
/// Changes how the control is positioned.
/// </summary>
public PositionMode PositionMode
{
get => _positionMode;
set => SetField(ref _positionMode, value);
}
/// <summary>
/// Changes how overflows are handled.
/// </summary>
public OverflowMode OverflowMode
{
get => _overflowMode;
set => SetField(ref _overflowMode, value);
}
public LayoutBox Box { get; } = new LayoutBox();
/// <summary>
/// The row of the control in a grid container.
/// </summary>
public int Row
{
get => _row;
set => SetField(ref _row, value);
}
/// <summary>
/// The column of the control in a grid container.
/// </summary>
public int Column
{
get => _column;
set => SetField(ref _column, value);
}
public event PropertyChangedEventHandler? PropertyChanged;
public LayoutInfo()
{
Box.PropertyChanged += BoxOnPropertyChanged;
// Rows.CollectionChanged += RowsChanged;
// Columns.CollectionChanged += ColumnsChanged;
}
private void BoxOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(Box));
}
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}

View File

@@ -0,0 +1,364 @@
using System.Numerics;
namespace Dashboard.Layout
{
public record struct LayoutItemSolution(ILayoutItem Item, ComputedBox Solution);
public class LayoutSolution
{
public ILayoutContainer Container { get; }
public IReadOnlyList<LayoutItemSolution> Items { get; }
private LayoutSolution(ILayoutContainer container, IEnumerable<LayoutItemSolution> itemSolutions)
{
Container = container;
Items = itemSolutions.ToList().AsReadOnly();
}
public static LayoutSolution CalculateLayout<T1>(T1 container, Vector2 limits, int iterations = 3, float absTol = 0.001f, float relTol = 0.01f)
where T1 : ILayoutContainer
{
switch (container.ContainerLayout.ContainerMode)
{
default:
case ContainerMode.Basic:
return SolveForBasicLayout(container, limits, iterations, absTol, relTol);
case ContainerMode.Flex:
return SolveForFlex(container, limits, iterations, absTol, relTol);
case ContainerMode.Grid:
return SolveForGrid(container, limits, iterations, absTol, relTol);
}
}
private static LayoutSolution SolveForGrid<T1>(T1 container, Vector2 limits, int iterations, float absTol, float relTol) where T1 : ILayoutContainer
{
throw new NotImplementedException();
}
private static LayoutSolution SolveForFlex<T1>(T1 container, Vector2 limits,int iterations, float absTol, float relTol) where T1 : ILayoutContainer
{
throw new NotImplementedException();
}
private static LayoutSolution SolveForBasicLayout<T1>(T1 container, Vector2 limits, int iterations, float absTol, float relTol) where T1 : ILayoutContainer
{
int count = container.Count();
LayoutItemSolution[] items = new LayoutItemSolution[count];
int i = 0;
foreach (ILayoutItem item in container)
{
items[i++] = new LayoutItemSolution(item, default);
}
bool limitX = limits.X > 0;
bool limitY = limits.Y > 0;
while (iterations-- > 0)
{
Vector2 pen = Vector2.Zero;
i = 0;
foreach (ILayoutItem item in container)
{
Vector2 size = item.CalculateIntrinsicSize();
}
}
return new LayoutSolution(container, items);
}
private record TrackSolution(TrackInfo Track)
{
public bool Auto => Track.Unit == LayoutUnit.Auto;
public bool Absolute => Track.Unit.IsAbsolute();
public float Requested { get; private set; }
public float Value { get; private set; }
public float Result { get; set; } = 0.0f;
public bool IsFrozen { get; set; } = false;
public void CalculateRequested(float dpi, float rel, float star)
{
Requested = new Metric(Track.Unit, Track.Width).Compute(dpi, rel, star);
}
public void Freeze()
{
if (IsFrozen)
return;
IsFrozen = true;
Result = Value;
}
}
private delegate float GetItemLength<in T1>(float dpi, float rel, float star, T1 item)
where T1 : ILayoutItem;
private TrackSolution[] SolveForGridTracks<T1, T2, T3>(
float limit,
T1 tracks,
T2 container,
int iterations,
float absTol,
float relTol,
Func<int, T3> getItemTrack,
GetItemLength<T3> getItemLength)
where T1 : IList<TrackInfo>
where T2 : ILayoutContainer
where T3 : ILayoutItem
{
int itemCount = container.Count();
bool auto = limit < 0;
TrackSolution[] solution = new TrackSolution[tracks.Count];
foreach (TrackSolution track in solution)
{
if (track.Absolute) {
// FIXME: pass DPI here.
track.CalculateRequested(96f, limit, 0);
track.Freeze();
}
}
while (iterations-- > 0)
{
}
for (int i = 0; i < tracks.Count; i++)
{
solution[i].Freeze();
}
return solution;
}
// private static void GetIntrinsicGridSizes<T1, T2>(Span<float> cols, Span<float> rows, T1 parent, T2 items)
// where T1 : ILayoutItem
// where T2 : IEnumerable<ILayoutItem>
// {
// CopyToSpan(rows, parent.Layout.Rows);
// CopyToSpan(cols, parent.Layout.Columns);
//
// foreach (ILayoutItem item in items)
// {
// int col = Math.Clamp(item.Layout.Column, 0, cols.Length - 1);
// int row = Math.Clamp(item.Layout.Row, 0, rows.Length - 1);
//
// bool autoCols = parent.Layout.Columns[col] < 0;
// bool autoRows = parent.Layout.Rows[row] < 0;
//
// if (!autoRows && !autoCols)
// continue;
//
// Vector2 size = item.CalculateIntrinsicSize();
// cols[col] = autoCols ? Math.Max(size.X, cols[col]) : cols[col];
// rows[row] = autoRows ? Math.Max(size.Y, rows[row]) : rows[row];
// }
// }
//
// public static Vector2 CalculateIntrinsicSize<T1, T2>(T1 parent, T2 items)
// where T1 : ILayoutItem
// where T2 : IEnumerable<ILayoutItem>
// {
// // Copy layout details.
// Span<float> cols = stackalloc float[parent.Layout.Columns.Count];
// Span<float> rows = stackalloc float[parent.Layout.Rows.Count];
//
// GetIntrinsicGridSizes(cols, rows, parent, items);
//
// float width = parent.Layout.Margin.X + parent.Layout.Margin.Z + parent.Layout.Padding.X + parent.Layout.Padding.Z + SumSpan<float>(cols);
// float height = parent.Layout.Margin.Y + parent.Layout.Margin.W + parent.Layout.Padding.Y + parent.Layout.Padding.W + SumSpan<float>(rows);
//
// return new Vector2(width, height);
// }
//
// public static GridResult Layout<T1, T2>(T1 parent, T2 items, Vector2 limits, int iterations = 3, float abstol = 0.0001f, float reltol = 0.01f)
// where T1 : ILayoutItem
// where T2 : IEnumerable<ILayoutItem>
// {
// Vector4 contentSpace = parent.Layout.Margin + parent.Layout.Padding;
// Vector2 contentLimits = new Vector2(
// limits.X > 0 ? limits.X - contentSpace.X - contentSpace.Z : -1,
// limits.Y > 0 ? limits.Y - contentSpace.Y - contentSpace.W : -1);
//
// // Get rows and columns for now.
// Span<Track> cols = stackalloc Track[parent.Layout.Columns.Count];
// Span<Track> rows = stackalloc Track[parent.Layout.Rows.Count];
//
// for (int i = 0; i < cols.Length; i++)
// {
// cols[i] = new Track(parent.Layout.Columns[i]);
//
// if (!cols[i].Auto)
// {
// cols[i].Freeze();
// }
// }
//
// for (int i = 0; i < rows.Length; i++)
// {
// rows[i] = new Track(parent.Layout.Rows[i]);
//
// if (!rows[i].Auto)
// {
// rows[i].Freeze();
// }
// }
//
// int freeRows = 0;
// int freeCols = 0;
// while (iterations-- > 0 && ((freeRows = CountFree(rows)) > 0 || (freeCols = CountFree(cols)) > 0))
// {
// // Calculate the remaining size.
// Vector2 remaining = contentLimits;
//
// for (int i = 0; contentLimits.X > 0 && i < cols.Length; i++)
// {
// if (cols[i].IsFrozen)
// remaining.X -= cols[i].Value;
// }
//
// for (int i = 0; contentLimits.Y > 0 && i < rows.Length; i++)
// {
// if (rows[i].IsFrozen)
// remaining.Y -= rows[i].Value;
// }
//
// Vector2 childLimits = remaining / new Vector2(Math.Max(freeCols, 1), Math.Max(freeRows, 1));
//
//
// // Calculate the size of each free track.
// foreach (ILayoutItem child in items)
// {
// int c = Math.Clamp(child.Layout.Column, 0, cols.Length - 1);
// int r = Math.Clamp(child.Layout.Row, 0, rows.Length - 1);
//
// bool autoRow = rows[r].Auto;
// bool autoCol = cols[c].Auto;
//
// if (!autoRow && !autoCol)
// continue;
//
// Vector2 childSize = child.CalculateSize(childLimits);
//
// if (autoCol)
// cols[c].Value = Math.Max(childLimits.X, childSize.X);
//
// if (autoRow)
// rows[r].Value = Math.Max(childLimits.Y, childSize.Y);
// }
//
// // Calculate for errors and decide to freeze them.
//
// for (int i = 0; limits.X > 0 && i < cols.Length; i++)
// {
// if (WithinTolerance(cols[i].Value, childLimits.X, abstol, reltol))
// {
// cols[i].Freeze();
// }
// }
//
// for (int i = 0; limits.Y > 0 && i < rows.Length; i++)
// {
// if (WithinTolerance(rows[i].Value, childLimits.Y, abstol, reltol))
// {
// rows[i].Freeze();
// }
// }
// }
//
// Vector2 size = new Vector2(
// parent.Layout.Margin.X + parent.Layout.Margin.Z + parent.Layout.Padding.X + parent.Layout.Padding.Z,
// parent.Layout.Margin.Y + parent.Layout.Margin.W + parent.Layout.Padding.Y + parent.Layout.Padding.W);
//
// foreach (ref Track col in cols)
// {
// col.Freeze();
// size.X += col.Result;
// }
//
// foreach (ref Track row in rows)
// {
// row.Freeze();
// size.Y += row.Result;
// }
//
// if (limits.X > 0) size.X = Math.Max(size.X, limits.X);
// if (limits.Y > 0) size.Y = Math.Max(size.Y, limits.Y);
//
// // Temporary solution
// return new GridResult(size, cols.ToArray().Select(x => x.Result).ToArray(), rows.ToArray().Select(x => x.Result).ToArray());
//
// static int CountFree(Span<Track> tracks)
// {
// int i = 0;
// foreach (Track track in tracks)
// {
// if (!track.IsFrozen)
// i++;
// }
//
// return i;
// }
// }
//
// private static void CopyToSpan<T1, T2>(Span<T1> span, T2 items)
// where T2 : IEnumerable<T1>
// {
// using IEnumerator<T1> iterator = items.GetEnumerator();
// for (int i = 0; i < span.Length; i++)
// {
// if (!iterator.MoveNext())
// break;
//
// span[i] = iterator.Current;
// }
// }
//
// private static T1 SumSpan<T1>(ReadOnlySpan<T1> span)
// where T1 : struct, INumber<T1>
// {
// T1 value = default;
//
// foreach (T1 item in span)
// {
// value += item;
// }
//
// return value;
// }
//
// private static bool WithinTolerance<T>(T value, T limit, T abstol, T reltol)
// where T : INumber<T>
// {
// T tol = T.Max(abstol, value * reltol);
// return T.Abs(value - limit) < tol;
// }
//
// public record GridResult(Vector2 Size, float[] Columns, float[] Rows)
// {
//
// }
//
// private record struct Track(float Request)
// {
// public bool Auto => Request < 0;
//
// public bool IsFrozen { get; private set; } = false;
// public float Value { get; set; } = Request;
//
// public float Result { get; private set; }
//
// public void Freeze()
// {
// Result = Value;
// IsFrozen = true;
// }
// }
}
}

View File

@@ -0,0 +1,30 @@
namespace Dashboard
{
public enum LayoutUnit : short
{
/// <summary>
/// Does not specify a unit.
/// </summary>
Auto,
/// <summary>
/// The default unit. A size of a single picture element.
/// </summary>
Pixel = 1,
/// <summary>
/// 1/72th of an inch traditional in graphics design.
/// </summary>
Point = 2,
/// <summary>
/// The universal length unit for small distances.
/// </summary>
Millimeter = 3,
/// <summary>
/// An inverse proportional unit with respect to the container size.
/// </summary>
Star = 4,
/// <summary>
/// A directly proportional unit with respect to the container size.
/// </summary>
Percent = 5,
}
}

View File

@@ -0,0 +1,23 @@
namespace Dashboard
{
public enum BorderKind
{
Inset = -1,
Center = 0,
Outset = 1,
}
public enum CapType
{
None,
Circular,
Rectangular,
}
public enum CuspType
{
None,
Circular,
Rectangular,
}
}

128
Dashboard.Common/Measure.cs Normal file
View File

@@ -0,0 +1,128 @@
using System.Numerics;
namespace Dashboard
{
public record struct LayoutUnits(LayoutUnit All)
{
public LayoutUnit XUnit
{
get => (LayoutUnit)(((int)All & 0xF) >> 0);
set => All = (LayoutUnit)(((int)All & ~(0xF << 0)) | ((int)value << 0));
}
public LayoutUnit YUnit
{
get => (LayoutUnit)(((int)All & 0xF) >> 4);
set => All = (LayoutUnit)(((int)All & ~(0xF << 4)) | ((int)value << 4));
}
public LayoutUnit ZUnit
{
get => (LayoutUnit)(((int)All & 0xF) >> 8);
set => All = (LayoutUnit)(((int)All & ~(0xF << 8)) | ((int)value << 8));
}
public LayoutUnit WUnit
{
get => (LayoutUnit)(((int)All & 0xF) >> 12);
set => All = (LayoutUnit)(((int)All & ~(0xF << 12)) | ((int)value << 12));
}
}
public record struct Metric(LayoutUnit Units, float Value)
{
public float Compute(float dpi, float rel, float star)
{
switch (Units)
{
case LayoutUnit.Auto:
return -1;
case LayoutUnit.Millimeter:
float mm2Px = dpi / 25.4f;
return Value * mm2Px;
case LayoutUnit.Pixel:
return Value;
case LayoutUnit.Point:
float pt2Px = 72 / dpi;
return Value * pt2Px;
case LayoutUnit.Percent:
return rel * Value;
case LayoutUnit.Star:
return star * Value;
default:
throw new Exception("Unrecognized unit.");
}
}
}
public record struct Metric2D(LayoutUnits Units, Vector2 Value)
{
public float X
{
get => Value.X;
set => Value = new Vector2(value, Value.Y);
}
public LayoutUnit XUnits
{
get => Units.XUnit;
set => Units = Units with { XUnit = value };
}
public float Y
{
get => Value.Y;
set => Value = new Vector2(Value.X, value);
}
public LayoutUnit YUnits
{
get => Units.YUnit;
set => Units = Units with { YUnit = value };
}
}
public record struct BoxMetric(LayoutUnit Units, Box2d Value);
public record struct AdvancedMetric(LayoutUnit Unit, float Value)
{
public AdvancedMetric Convert(LayoutUnit target, float dpi, float rel, int stars)
{
if (Unit == target)
return this;
float pixels = Unit switch {
LayoutUnit.Pixel => Value,
LayoutUnit.Point => Value * (72f / dpi),
LayoutUnit.Millimeter => Value * (28.3464566929f / dpi),
LayoutUnit.Star => Value * rel / stars,
LayoutUnit.Percent => Value * rel / 100,
_ => throw new Exception(),
};
float value = target switch {
LayoutUnit.Pixel => pixels,
LayoutUnit.Point => Value * (dpi / 72f),
// MeasurementUnit.Millimeter =>
};
return new AdvancedMetric(target, value);
}
public override string ToString()
{
return $"{Value} {Unit.ToShortString()}";
}
public static bool TryParse(ReadOnlySpan<char> str, out AdvancedMetric metric)
{
metric = default;
return false;
}
public static AdvancedMetric Parse(ReadOnlySpan<char> str) =>
TryParse(str, out AdvancedMetric metric)
? metric
: throw new Exception($"Could not parse the value '{str}'.");
}
}

View File

@@ -0,0 +1,30 @@
namespace Dashboard
{
public static class MeasurementExtensions
{
public static bool IsRelative(this LayoutUnit unit) => unit switch {
LayoutUnit.Star or LayoutUnit.Percent => true,
_ => false,
};
public static bool IsAbsolute(this LayoutUnit unit) => !IsRelative(unit);
public static string ToShortString(this LayoutUnit unit) => unit switch {
LayoutUnit.Pixel => "px",
LayoutUnit.Point => "pt",
LayoutUnit.Millimeter => "mm",
LayoutUnit.Star => "*",
LayoutUnit.Percent => "%",
_ => throw new Exception("Unknown unit."),
};
public static bool WithinTolerance(this float value, float reference, float absTol, float relTol)
=> value.CompareTolerance(reference, absTol, relTol) == 0;
public static int CompareTolerance(this float value, float reference, float absTol, float relTol)
{
float tolerance = Math.Max(absTol, Math.Abs(reference) * relTol);
float difference = value - reference;
return difference < -tolerance ? -1 : difference > tolerance ? 1 : 0;
}
}
}

View File

@@ -0,0 +1,17 @@
namespace Dashboard
{
public enum Origin
{
Center = 0,
Left = (1 << 0),
Top = (1 << 1),
Right = (1 << 2),
Bottom = (1 << 3),
TopLeft = Top | Left,
BottomLeft = Bottom | Left,
BottomRight = Bottom | Right,
TopRight = Top | Right,
}
}

View File

@@ -0,0 +1,202 @@
using Dashboard.Collections;
using Dashboard.Windowing;
using BindingFlags = System.Reflection.BindingFlags;
namespace Dashboard.Pal
{
public abstract class Application : IContextBase<Application, IApplicationExtension>
{
public abstract string DriverName { get; }
public abstract string DriverVendor { get; }
public abstract Version DriverVersion { get; }
public virtual string AppTitle { get; set; } = "Dashboard Application";
public bool IsInitialized { get; private set; } = false;
public bool IsDisposed { get; private set; } = false;
public IContextDebugger? Debugger { get; set; }
protected CancellationToken? CancellationToken { get; private set; }
protected bool Quit { get; set; } = false;
private readonly TypeDictionary<IApplicationExtension> _extensions =
new TypeDictionary<IApplicationExtension>(true);
private readonly TypeDictionary<IApplicationExtension, Func<IApplicationExtension>> _preloadedExtensions =
new TypeDictionary<IApplicationExtension, Func<IApplicationExtension>>(true);
public event EventHandler<DeviceContext>? DeviceContextCreated;
public Application()
{
Current = this;
}
~Application()
{
InvokeDispose(false);
}
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
InitializeInternal();
}
protected virtual void InitializeInternal()
{
}
protected internal virtual void OnDeviceContextCreated(DeviceContext dc)
{
DeviceContextCreated?.Invoke(this, dc);
}
public virtual void RunEvents(bool wait)
{
if (!IsInitialized)
Initialize();
}
public void Run() => Run(true, System.Threading.CancellationToken.None);
public void Run(bool wait) => Run(wait, System.Threading.CancellationToken.None);
public void Run(bool waitForEvents, CancellationToken token)
{
CancellationToken = token;
CancellationToken.Value.Register(() => Quit = true);
InitializeInternal();
while (!Quit && !token.IsCancellationRequested)
{
RunEvents(waitForEvents);
}
}
#region Window API
/// <summary>
/// Creates a window. It could be a virtual window, or a physical window.
/// </summary>
/// <returns>A window.</returns>
public abstract IWindow CreateWindow();
/// <summary>
/// Always creates a physical window.
/// </summary>
/// <returns>A physical window.</returns>
public abstract IPhysicalWindow CreatePhysicalWindow();
/// <summary>
/// Create a physical window with a window manager.
/// </summary>
/// <returns>A physical window with the given window manager.</returns>
public IPhysicalWindow CreatePhysicalWindow(IWindowManager wm)
{
IPhysicalWindow window = CreatePhysicalWindow();
window.WindowManager = wm;
return window;
}
public IWindow CreateDialogWindow(IWindow? parent = null)
{
if (parent is IVirtualWindow virtualWindow)
{
IWindow? window = virtualWindow.WindowManager?.CreateWindow();
if (window != null)
return window;
}
return CreatePhysicalWindow();
}
#endregion
public bool IsExtensionAvailable<T>() where T : IApplicationExtension
{
return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>();
}
public bool ExtensionPreload<T>(Func<IApplicationExtension> loader) where T : IApplicationExtension
{
return _preloadedExtensions.Add<T>(loader);
}
public bool ExtensionPreload<T>() where T : IApplicationExtension, new()
{
return _preloadedExtensions.Add<T>(() => new T());
}
public T ExtensionRequire<T>() where T : IApplicationExtension
{
T? extension = default;
if (_extensions.TryGet(out extension))
return extension;
lock (_extensions)
{
if (_extensions.TryGet(out extension))
return extension;
if (_preloadedExtensions.Remove<T>(out Func<IApplicationExtension>? loader))
{
extension = (T)loader!();
}
else
{
extension = Activator.CreateInstance<T>();
}
_extensions.Add(extension);
extension.Require(this);
}
return extension;
}
public bool ExtensionLoad<T>(T instance) where T : IApplicationExtension
{
if (_extensions.Contains(instance))
return false;
_extensions.Add(instance);
instance.Require(this);
return true;
}
protected virtual void Dispose(bool isDisposing)
{
if (!isDisposing) return;
Quit = true;
foreach (IApplicationExtension extension in _extensions)
{
extension.Dispose();
}
GC.SuppressFinalize(this);
}
private void InvokeDispose(bool isDisposing)
{
if (IsDisposed) return;
IsDisposed = true;
Dispose(isDisposing);
}
public void Dispose() => InvokeDispose(true);
[ThreadStatic] private static Application _current;
public static Application Current
{
get => _current ?? throw new InvalidOperationException("There is currently no current application.");
set => _current = value;
}
}
}

View File

@@ -0,0 +1,149 @@
using Dashboard.Collections;
using Dashboard.Windowing;
using BindingFlags = System.Reflection.BindingFlags;
namespace Dashboard.Pal
{
public abstract class DeviceContext : IContextBase<DeviceContext, IDeviceContextExtension>
{
private readonly TypeDictionary<IDeviceContextExtension> _extensions =
new TypeDictionary<IDeviceContextExtension>(true);
private readonly TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>> _preloadedExtensions =
new TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>>(true);
private readonly Dictionary<string, object> _attributes = new Dictionary<string, object>();
public Application Application { get; }
public IWindow? Window { get; }
public abstract string DriverName { get; }
public abstract string DriverVendor { get; }
public abstract Version DriverVersion { get; }
public bool IsDisposed { get; private set; }
/// <summary>
/// Optional debugging object for your pleasure.
/// </summary>
public IContextDebugger? Debugger { get; set; }
protected DeviceContext(Application app, IWindow? window)
{
Application = app;
Window = window;
app.OnDeviceContextCreated(this);
}
~DeviceContext()
{
Dispose(false);
}
public virtual void Begin() { }
// public abstract void Paint(object renderbuffer);
public virtual void End() { }
public bool IsExtensionAvailable<T>() where T : IDeviceContextExtension
{
return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>();
}
public bool ExtensionPreload<T>(Func<IDeviceContextExtension> loader) where T : IDeviceContextExtension
{
return _preloadedExtensions.Add<T>(loader);
}
public bool ExtensionPreload<T>() where T : IDeviceContextExtension, new()
{
return _preloadedExtensions.Add<T>(() => new T());
}
public T ExtensionRequire<T>() where T : IDeviceContextExtension
{
T? extension = default;
if (_extensions.TryGet(out extension))
return extension;
lock (_extensions)
{
if (_extensions.TryGet(out extension))
return extension;
if (_preloadedExtensions.Remove<T>(out Func<IDeviceContextExtension>? loader))
{
extension = (T)loader!();
}
else
{
extension = Activator.CreateInstance<T>();
}
_extensions.Add(extension);
extension.Require(this);
}
return extension;
}
public bool ExtensionLoad<T>(T instance) where T : IDeviceContextExtension
{
if (_extensions.Contains(instance))
return false;
_extensions.Add(instance);
return true;
}
public void SetAttribute(string name, object? v)
{
if (v != null)
_attributes[name] = v;
else
_attributes.Remove(name);
}
public void SetAttribute<T>(string name, T v) => SetAttribute(name, (object?)v);
public object? GetAttribute(string name)
{
return _attributes.GetValueOrDefault(name);
}
public T? GetAttribute<T>(string name)
{
object? o = GetAttribute(name);
if (o != null)
return (T?)o;
return default;
}
/// <summary>
/// Implement your dispose in this function.
/// </summary>
/// <param name="isDisposing">True if disposing, false otherwise.</param>
protected virtual void Dispose(bool isDisposing)
{
if (!isDisposing) return;
foreach (IDeviceContextExtension extension in _extensions)
{
extension.Dispose();
}
GC.SuppressFinalize(this);
}
private void InvokeDispose(bool isDisposing)
{
if (IsDisposed) return;
IsDisposed = true;
Dispose(isDisposing);
}
public void Dispose() => InvokeDispose(true);
}
}

View File

@@ -0,0 +1,7 @@
namespace Dashboard.Pal
{
public interface IApplicationExtension : IContextExtensionBase<Application>
{
}
}

View File

@@ -0,0 +1,122 @@
namespace Dashboard.Pal
{
/// <summary>
/// Information about this context interface.
/// </summary>
public interface IContextInterfaceInfo
{
/// <summary>
/// Name of this driver.
/// </summary>
string DriverName { get; }
/// <summary>
/// The vendor for this driver.
/// </summary>
string DriverVendor { get; }
/// <summary>
/// The version of this driver.
/// </summary>
Version DriverVersion { get; }
}
/// <summary>
/// The base context interface.
/// </summary>
public interface IContextBase : IContextInterfaceInfo, IDisposable
{
/// <summary>
/// The debugger for this context.
/// </summary>
IContextDebugger? Debugger { get; set; }
}
/// <summary>
/// The base context interface.
/// </summary>
/// <typeparam name="TContext">The context type.</typeparam>
/// <typeparam name="TExtension">The extension type, if used.</typeparam>
public interface IContextBase<TContext, in TExtension> : IContextBase
where TContext : IContextBase<TContext, TExtension>
where TExtension : IContextExtensionBase<TContext>
{
/// <summary>
/// Is such an extension available?
/// </summary>
/// <typeparam name="T">The extension to check.</typeparam>
/// <returns>True if the extension is available.</returns>
bool IsExtensionAvailable<T>() where T : TExtension;
/// <summary>
/// Preload extensions, to be lazy loaded when required.
/// </summary>
/// <typeparam name="T">The extension to preload.</typeparam>
/// <returns>
/// True if the extension was added to the preload set. Otherwise, already loaded or another extension
/// exists which provides this.
/// </returns>
bool ExtensionPreload<T>() where T : TExtension, new();
/// <summary>
/// Preload extensions, to be lazy loaded when required.
/// </summary>
/// <param name="loader">The loader delegate.</param>
/// <typeparam name="T">The extension to preload.</typeparam>
/// <returns>
/// True if the extension was added to the preload set. Otherwise, already loaded or another extension
/// exists which provides this.
/// </returns>
bool ExtensionPreload<T>(Func<TExtension> loader) where T : TExtension;
/// <summary>
/// Require an extension.
/// </summary>
/// <typeparam name="T">The extension to require.</typeparam>
/// <returns>The extension instance.</returns>
T ExtensionRequire<T>() where T : TExtension;
/// <summary>
/// Load an extension.
/// </summary>
/// <param name="instance">The extension instance.</param>
/// <typeparam name="T">The extension to require.</typeparam>
/// <returns>True if the extension was loaded, false if there was already one.</returns>
bool ExtensionLoad<T>(T instance) where T : TExtension;
}
/// <summary>
/// Base interface for all context extensions.
/// </summary>
public interface IContextExtensionBase : IContextInterfaceInfo, IDisposable
{
/// <summary>
/// The context that loaded this extension.
/// </summary>
IContextBase Context { get; }
/// <summary>
/// Require this extension.
/// </summary>
/// <param name="context">The context that required this extension.</param>
void Require(IContextBase context);
}
/// <summary>
/// Base interface for all context extensions.
/// </summary>
public interface IContextExtensionBase<TContext> : IContextExtensionBase
where TContext : IContextBase
{
/// <summary>
/// The context that loaded this extension.
/// </summary>
new TContext Context { get; }
/// <summary>
/// Require this extension.
/// </summary>
/// <param name="context">The context that required this extension.</param>
void Require(TContext context);
}
}

View File

@@ -0,0 +1,10 @@
namespace Dashboard.Pal
{
public interface IContextDebugger : IDisposable
{
void LogDebug(string message);
void LogInfo(string message);
void LogWarning(string message);
void LogError(string message);
}
}

View File

@@ -0,0 +1,6 @@
namespace Dashboard.Pal
{
public interface IDeviceContextExtension : IContextExtensionBase<DeviceContext>
{
}
}

View File

@@ -0,0 +1,43 @@
namespace Dashboard.Windowing
{
/// <summary>
/// Interface for a class that composites multiple windows together.
/// </summary>
public interface ICompositor : IDisposable
{
void Composite(IPhysicalWindow window, IEnumerable<IVirtualWindow> windows);
}
/// <summary>
/// Interface for classes that implement a window manager.
/// </summary>
public interface IWindowManager : IEnumerable<IVirtualWindow>, IEventListener
{
/// <summary>
/// The physical window that this window manager is associated with.
/// </summary>
IPhysicalWindow PhysicalWindow { get; }
/// <summary>
/// The compositor that will composite all virtual windows.
/// </summary>
ICompositor Compositor { get; set; }
/// <summary>
/// The window that is currently focused.
/// </summary>
IVirtualWindow? FocusedWindow { get; }
/// <summary>
/// Create a virtual window.
/// </summary>
/// <returns>Virtual window handle.</returns>
IVirtualWindow CreateWindow();
/// <summary>
/// Focus a virtual window, if it is owned by this window manager.
/// </summary>
/// <param name="window">The window to focus.</param>
void Focus(IVirtualWindow window);
}
}

View File

@@ -0,0 +1,21 @@
using System.Drawing;
using System.Numerics;
namespace Dashboard.Windowing
{
/// <summary>
/// Generic interface for the rendering system present in a <see cref="IPhysicalWindow"/>
/// </summary>
public interface IDeviceContext
{
/// <summary>
/// The swap group for this device context.
/// </summary>
ISwapGroup SwapGroup { get; }
/// <summary>
/// The size of the window framebuffer in pixels.
/// </summary>
Vector2 FramebufferSize { get; }
}
}

View File

@@ -0,0 +1,14 @@
namespace Dashboard.Windowing
{
public interface IEventListener
{
event EventHandler? EventRaised;
/// <summary>
/// Send an event to this windowing object.
/// </summary>
/// <param name="sender">The object which generated the event.</param>
/// <param name="args">The event arguments sent.</param>
void SendEvent(object? sender, EventArgs args);
}
}

View File

@@ -0,0 +1,8 @@
namespace Dashboard.Windowing
{
public interface IForm : IEventListener, IDisposable
{
public IWindow Window { get; }
public string Title { get; }
}
}

View File

@@ -0,0 +1,12 @@
namespace Dashboard.Windowing
{
public interface IPaintable
{
event EventHandler Painting;
/// <summary>
/// Paint this paintable object.
/// </summary>
void Paint();
}
}

View File

@@ -0,0 +1,18 @@
namespace Dashboard.Windowing
{
/// <summary>
/// Interface that is used to swap the buffers of windows
/// </summary>
public interface ISwapGroup
{
/// <summary>
/// The swap interval for this swap group.
/// </summary>
public int SwapInterval { get; set; }
/// <summary>
/// Swap buffers.
/// </summary>
void Swap();
}
}

View File

@@ -0,0 +1,93 @@
using System.Drawing;
using Dashboard.Pal;
namespace Dashboard.Windowing
{
/// <summary>
/// Base class of all Dashboard windows.
/// </summary>
public interface IWindow : IDisposable
{
/// <summary>
/// The application for this window.
/// </summary>
Application Application { get; }
/// <summary>
/// Name of the window.
/// </summary>
string Title { get; set; }
/// <summary>
/// The size of the window that includes the window extents.
/// </summary>
SizeF OuterSize { get; set; }
/// <summary>
/// The size of the window that excludes the window extents.
/// </summary>
SizeF ClientSize { get; set; }
IForm? Form { get; set; }
public event EventHandler? EventRaised;
/// <summary>
/// Subscribe to events from this window.
/// </summary>
/// <param name="listener">The event listener instance.</param>
/// <returns>An unsubscription token.</returns>
public void SubcribeEvent(IEventListener listener);
/// <summary>
/// Unsubscribe from events in from this window.
/// </summary>
/// <param name="listener">The event listener to unsubscribe.</param>
public void UnsubscribeEvent(IEventListener listener);
}
/// <summary>
/// Base class for all Dashboard windows that are DPI-aware.
/// </summary>
public interface IDpiAwareWindow : IWindow
{
/// <summary>
/// DPI of the window.
/// </summary>
float Dpi { get; }
/// <summary>
/// Scale of the window.
/// </summary>
float Scale { get; }
}
/// <summary>
/// An object that represents a window in a virtual space, usually another window or a rendering system.
/// </summary>
public interface IVirtualWindow : IWindow, IEventListener
{
IWindowManager? WindowManager { get; set; }
}
/// <summary>
/// An object that represents a native operating system window.
/// </summary>
public interface IPhysicalWindow : IWindow
{
/// <summary>
/// The device context for this window.
/// </summary>
DeviceContext DeviceContext { get; }
/// <summary>
/// True if the window is double buffered.
/// </summary>
public bool DoubleBuffered { get; }
/// <summary>
/// The window manager for this physical window.
/// </summary>
public IWindowManager? WindowManager { get; set; }
}
}

View File

@@ -0,0 +1,48 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing.OpenGL
{
public enum SimpleDrawCommand : int
{
Point = 1,
Line = 2,
Rect = 3,
/// <summary>
/// Make sure your custom commands have values higher than this if you plan on using the default command
/// buffer.
/// </summary>
CustomCommandStart = 4096
}
[StructLayout(LayoutKind.Explicit, Size = 64)]
public struct CommandInfo
{
[FieldOffset(0)]
public SimpleDrawCommand Type;
[FieldOffset(4)]
public int Flags;
[FieldOffset(8)]
public float Arg0;
[FieldOffset(12)]
public float Arg1;
[FieldOffset(16)]
public int FgGradientIndex;
[FieldOffset(20)]
public int FgGradientCount;
[FieldOffset(24)]
public int BgGradientIndex;
[FieldOffset(28)]
public int BgGradientCount;
[FieldOffset(32)]
public Vector4 FgColor;
[FieldOffset(48)]
public Vector4 BgColor;
}
}

View File

@@ -0,0 +1,184 @@
using System.Drawing;
using Dashboard.Drawing.OpenGL.Executors;
using Dashboard.OpenGL;
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL
{
public interface ICommandExecutor
{
IEnumerable<string> Extensions { get; }
IContextExecutor Executor { get; }
void SetContextExecutor(IContextExecutor executor);
void BeginFrame();
void BeginDraw();
void EndDraw();
void EndFrame();
void ProcessCommand(ICommandFrame frame);
}
public interface IContextExecutor : IInitializer, IGLDisposable
{
GLEngine Engine { get; }
IGLContext Context { get; }
ContextResourcePool ResourcePool { get; }
TransformStack TransformStack { get; }
}
public class ContextExecutor : IContextExecutor
{
public GLEngine Engine { get; }
public IGLContext Context { get; }
public ContextResourcePool ResourcePool { get; }
public TransformStack TransformStack { get; } = new TransformStack();
protected bool IsDisposed { get; private set; } = false;
public bool IsInitialized { get; private set; } = false;
private readonly List<ICommandExecutor> _executorsList = new List<ICommandExecutor>();
private readonly Dictionary<string, ICommandExecutor> _executorsMap = new Dictionary<string, ICommandExecutor>();
public ContextExecutor(GLEngine engine, IGLContext context)
{
Engine = engine;
Context = context;
ResourcePool = Engine.ResourcePoolManager.Get(context);
ResourcePool.IncrementReference();
AddExecutor(new BaseCommandExecutor());
AddExecutor(new TextCommandExecutor());
}
~ContextExecutor()
{
DisposeInvoker(true, false);
}
public void AddExecutor(ICommandExecutor executor, bool overwrite = false)
{
if (IsInitialized)
throw new Exception("This context executor is already initialized. Cannot add new command executors.");
IInitializer? initializer = executor as IInitializer;
if (initializer?.IsInitialized == true)
throw new InvalidOperationException("This command executor has already been initialized, cannot add here.");
if (!overwrite)
{
foreach (string extension in executor.Extensions)
{
if (_executorsMap.ContainsKey(extension))
throw new InvalidOperationException("An executor already handles this extension.");
}
}
foreach (string extension in executor.Extensions)
{
_executorsMap[extension] = executor;
}
_executorsList.Add(executor);
executor.SetContextExecutor(this);
}
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
foreach (ICommandExecutor executor in _executorsList)
{
if (executor is IInitializer initializer)
initializer.Initialize();
}
}
public virtual void BeginFrame()
{
foreach (ICommandExecutor executor in _executorsList)
executor.BeginFrame();
}
protected virtual void BeginDraw()
{
foreach (ICommandExecutor executor in _executorsList)
executor.BeginDraw();
}
protected virtual void EndDraw()
{
foreach (ICommandExecutor executor in _executorsList)
executor.EndDraw();
}
public virtual void EndFrame()
{
ResourcePool.Collector.Dispose();
TransformStack.Clear();
foreach (ICommandExecutor executor in _executorsList)
executor.EndFrame();
}
public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize));
public virtual void Draw(DrawQueue drawQueue, RectangleF bounds, float scale = 1.0f)
{
BeginDraw();
if (scale != 1.0f)
TransformStack.Push(Matrix4.CreateScale(scale, scale, 1));
foreach (ICommandFrame frame in drawQueue)
{
if (_executorsMap.TryGetValue(frame.Command.Extension.Name, out ICommandExecutor? executor))
executor.ProcessCommand(frame);
}
EndDraw();
}
private void DisposeInvoker(bool safeExit, bool disposing)
{
if (!IsDisposed)
return;
IsDisposed = true;
if (disposing)
GC.SuppressFinalize(this);
Dispose(safeExit, disposing);
}
protected virtual void Dispose(bool safeExit, bool disposing)
{
if (disposing)
{
foreach (ICommandExecutor executor in _executorsList)
{
if (executor is IGLDisposable glDisposable)
glDisposable.Dispose(safeExit);
else if (executor is IDisposable disposable)
disposable.Dispose();
}
if (ResourcePool.DecrementReference())
Dispose();
}
}
public void Dispose() => DisposeInvoker(true, true);
public void Dispose(bool safeExit) => DisposeInvoker(safeExit, true);
}
}

View File

@@ -0,0 +1,133 @@
using Dashboard.OpenGL;
namespace Dashboard.Drawing.OpenGL
{
public class ContextResourcePoolManager
{
private readonly Dictionary<IGLContext, ContextResourcePool> _unique = new Dictionary<IGLContext, ContextResourcePool>();
private readonly Dictionary<int, ContextResourcePool> _shared = new Dictionary<int, ContextResourcePool>();
public ContextResourcePool Get(IGLContext context)
{
if (context.ContextGroup == -1)
{
if (!_unique.TryGetValue(context, out ContextResourcePool? pool))
{
pool = new ContextResourcePool(this, context);
_unique.Add(context, pool);
}
return pool;
}
else
{
if (!_shared.TryGetValue(context.ContextGroup, out ContextResourcePool? pool))
{
pool = new ContextResourcePool(this, context.ContextGroup);
_shared.Add(context.ContextGroup, pool);
}
return pool;
}
}
internal void Disposed(ContextResourcePool pool)
{
// TODO:
}
}
public class ContextResourcePool : IGLDisposable, IArc
{
private int _references = 0;
private bool _isDisposed = false;
private readonly Dictionary<int, IResourceManager> _managers = new Dictionary<int, IResourceManager>();
public ContextResourcePoolManager Manager { get; }
public IGLContext? Context { get; private set; } = null;
public int ContextGroup { get; private set; } = -1;
public int References => _references;
public ContextCollector Collector { get; } = new ContextCollector();
internal ContextResourcePool(ContextResourcePoolManager manager, int contextGroup)
{
Manager = manager;
ContextGroup = contextGroup;
}
internal ContextResourcePool(ContextResourcePoolManager manager, IGLContext context)
{
Manager = manager;
Context = context;
}
public T GetResourceManager<T>(bool init = true) where T : IResourceManager, new()
{
int index = ManagerAtom<T>.Atom;
if (!_managers.TryGetValue(index, out IResourceManager? resourceClass))
{
_managers[index] = resourceClass = new T();
}
if (init && resourceClass is IInitializer initializer)
{
initializer.Initialize();
}
return (T)resourceClass;
}
~ContextResourcePool()
{
Dispose(true, false);
}
public void Dispose() => Dispose(true, false);
public void Dispose(bool safeExit) => Dispose(safeExit, true);
private void Dispose(bool safeExit, bool disposing)
{
if (_isDisposed)
return;
_isDisposed = true;
Manager.Disposed(this);
if (disposing)
{
foreach ((int _, IResourceManager manager) in _managers)
{
if (manager is IGLDisposable glDisposable)
glDisposable.Dispose(safeExit);
else if (manager is IDisposable disposable)
disposable.Dispose();
}
GC.SuppressFinalize(this);
}
}
public void IncrementReference()
{
Interlocked.Increment(ref _references);
}
public bool DecrementReference()
{
return Interlocked.Decrement(ref _references) == 0;
}
private class ManagerAtom
{
private static int _counter = -1;
protected static int Acquire() => Interlocked.Increment(ref _counter);
}
private class ManagerAtom<T> : ManagerAtom where T : IResourceManager
{
public static int Atom { get; } = Acquire();
}
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Drawing\Dashboard.Drawing.csproj" />
<ProjectReference Include="..\Dashboard.OpenGL\Dashboard.OpenGL.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Executors\simple.frag" />
<EmbeddedResource Include="Executors\simple.vert" />
<EmbeddedResource Include="Executors\text.vert" />
<EmbeddedResource Include="Executors\text.frag" />
</ItemGroup>
<ItemGroup>
<Folder Include="Text\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,468 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dashboard.OpenGL;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
namespace Dashboard.Drawing.OpenGL
{
public class DrawCallRecorder : IGLDisposable, IInitializer
{
private int _vao = 0;
private int _vbo = 0;
private readonly List<DrawVertex> _vertices = new List<DrawVertex>();
private readonly List<DrawCall> _calls = new List<DrawCall>();
private int _start = 0;
private int _count = 0;
private int _primitives = 0;
private Vector3 _charCoords;
private int _cmdIndex;
private int _texture0, _texture1, _texture2, _texture3;
private TextureTarget _target0, _target1, _target2, _target3;
private Matrix4 _transforms = Matrix4.Identity;
public int CommandModulus = 64;
public int CommandBuffer = 0;
public int CommandSize = 64;
private int CommandByteSize => CommandModulus * CommandSize;
public int TransformsLocation { get; set; }
public void Transforms(in Matrix4 transforms)
{
_transforms = transforms;
}
public void Begin(PrimitiveType type)
{
if (_primitives != 0)
throw new InvalidOperationException("Attempt to begin new draw call before finishing previous one.");
_primitives = (int)type;
_start = _vertices.Count;
_count = 0;
}
public void TexCoords2(Vector2 texCoords)
{
_charCoords = new Vector3(texCoords, 0);
}
public void CharCoords(Vector3 charCoords)
{
_charCoords = charCoords;
}
public void CommandIndex(int index)
{
_cmdIndex = index;
}
public void Vertex3(Vector3 vertex)
{
_vertices.Add(new DrawVertex()
{
Position = vertex,
CharCoords = _charCoords,
CmdIndex = _cmdIndex % CommandModulus,
});
_count++;
}
public void End()
{
if (_primitives == 0)
throw new InvalidOperationException("Attempt to end draw call before starting one.");
_calls.Add(
new DrawCall()
{
Type = (PrimitiveType)_primitives,
Start = _start,
Count = _count,
CmdIndex = _cmdIndex,
Target0 = _target0,
Target1 = _target1,
Target2 = _target2,
Target3 = _target3,
Texture0 = _texture0,
Texture1 = _texture1,
Texture2 = _texture2,
Texture3 = _texture3,
Transforms = _transforms,
});
_primitives = 0;
}
public void BindTexture(TextureTarget target, int texture) => BindTexture(target, 0, texture);
public void BindTexture(TextureTarget target, int unit, int texture)
{
switch (unit)
{
case 0:
_texture0 = 0;
_target0 = target;
break;
case 1:
_texture1 = 0;
_target1 = target;
break;
case 2:
_texture2 = 0;
_target2 = target;
break;
case 3:
_texture3 = 0;
_target3 = target;
break;
default:
throw new ArgumentOutOfRangeException(nameof(unit), "I did not write support for more than 4 textures.");
}
}
public void DrawArrays(PrimitiveType type, int first, int count)
{
throw new NotImplementedException();
}
public void Execute()
{
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
ReadOnlySpan<DrawVertex> vertices = CollectionsMarshal.AsSpan(_vertices);
GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Count * Unsafe.SizeOf<DrawVertex>(), vertices, BufferUsage.DynamicDraw);
foreach (DrawCall call in _calls)
{
GL.BindBufferRange(BufferTarget.UniformBuffer, 0, CommandBuffer, call.CmdIndex / CommandModulus * CommandByteSize, CommandByteSize);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(call.Target0, call.Texture0);
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(call.Target1, call.Texture1);
GL.ActiveTexture(TextureUnit.Texture2);
GL.BindTexture(call.Target2, call.Texture2);
GL.ActiveTexture(TextureUnit.Texture3);
GL.BindTexture(call.Target3, call.Texture3);
Matrix4 transforms = call.Transforms;
GL.UniformMatrix4f(TransformsLocation, 1, true, ref transforms);
GL.DrawArrays(call.Type, call.Start, call.Count);
}
}
public void Clear()
{
_vertices.Clear();
_calls.Clear();
}
public void Dispose()
{
throw new NotImplementedException();
}
public void Dispose(bool safeExit)
{
throw new NotImplementedException();
}
public bool IsInitialized { get; private set; }
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
_vao = GL.CreateVertexArray();
_vbo = GL.CreateBuffer();
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 32, 0);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 32, 16);
GL.VertexAttribIPointer(2, 1, VertexAttribIType.Int, 32, 28);
GL.EnableVertexAttribArray(0);
GL.EnableVertexAttribArray(1);
GL.EnableVertexAttribArray(2);
}
private struct DrawCall
{
public PrimitiveType Type;
public int Start;
public int Count;
public int CmdIndex;
public int Texture0;
public int Texture1;
public int Texture2;
public int Texture3;
public TextureTarget Target0;
public TextureTarget Target1;
public TextureTarget Target2;
public TextureTarget Target3;
public Matrix4 Transforms;
}
[StructLayout(LayoutKind.Explicit, Size = 32)]
private struct DrawVertex
{
[FieldOffset(0)]
public Vector3 Position;
[FieldOffset(16)]
public Vector3 CharCoords;
[FieldOffset(28)]
public int CmdIndex;
}
}
/// <summary>
/// A customizable immediate mode draw call queue, for the modern OpenGL user.
/// </summary>
/// <typeparam name="TCall">The call info type.</typeparam>
/// <typeparam name="TVertex">The vertex structure.</typeparam>
public abstract class DrawCallRecorder<TCall, TVertex> : IGLDisposable, IInitializer
where TVertex : unmanaged
{
/// <summary>
/// The vertex array for this queue.
/// </summary>
public int Vao { get; private set; }
/// <summary>
/// The vertex buffer for this queue.
/// </summary>
public int Vbo { get; private set; }
/// <summary>
/// Number of calls recorded in this queue.
/// </summary>
public int CallCount => Calls.Count;
/// <summary>
/// The number of total vertices recorded.
/// </summary>
public int TotalVertices => Vertices.Count;
/// <summary>
/// The latest draw call info.
/// </summary>
public ref TCall CurrentCall => ref _currentCall;
/// <summary>
/// The latest vertex emitted.
/// </summary>
public ref TVertex CurrentVertex => ref _currentVertex;
/// <summary>
/// True if currently recording a draw call.
/// </summary>
public bool InCall => _primitiveMode != 0;
/// <summary>
/// Size of one vertex.
/// </summary>
protected int VertexSize => Unsafe.SizeOf<TVertex>();
/// <summary>
/// The list of draw calls.
/// </summary>
protected List<DrawCall> Calls { get; } = new List<DrawCall>();
/// <summary>
/// The list of all vertices.
/// </summary>
protected List<TVertex> Vertices { get; } = new List<TVertex>();
/// <summary>
/// The value to write for the draw call info at the start of a call.
/// </summary>
[Pure] protected virtual TCall DefaultCall => default(TCall);
/// <summary>
/// The value to write for last vertex at the start of a call.
/// </summary>
[Pure] protected virtual TVertex DefaultVertex => default;
private int _start = 0;
private int _count = 0;
private int _primitiveMode = 0;
private TCall _currentCall;
private TVertex _currentVertex;
protected DrawCallRecorder()
{
_currentCall = DefaultCall;
_currentVertex = DefaultVertex;
}
/// <summary>
/// Record a draw call directly.
/// </summary>
/// <param name="type">The primitive type to use.</param>
/// <param name="callInfo">The call info structure to use</param>
/// <param name="vertices">The list of vertices to use.</param>
/// <exception cref="InvalidOperationException">You attempted to use this function during another draw call.</exception>
public void DrawArrays(PrimitiveType type, in TCall callInfo, ReadOnlySpan<TVertex> vertices)
{
if (InCall)
throw new InvalidOperationException("Cannot use draw arrays in the middle of an ongoing immediate-mode call.");
DrawCall call = new DrawCall()
{
Type = type,
Start = Vertices.Count,
Count = vertices.Length,
CallInfo = callInfo,
};
Vertices.AddRange(vertices);
Calls.Add(call);
}
/// <summary>
/// Start a draw call.
/// </summary>
/// <param name="type">The primitive type for the call.</param>
public void Begin(PrimitiveType type) => Begin(type, DefaultCall);
/// <summary>
/// Start a draw call.
/// </summary>
/// <param name="type">The primitive type for the call.</param>
/// <param name="callInfo">The call info.</param>
/// <exception cref="InvalidOperationException">You attempted to create a draw call within a draw call.</exception>
public void Begin(PrimitiveType type, TCall callInfo)
{
if (InCall)
throw new InvalidOperationException("Attempt to begin new draw call before finishing previous one.");
_primitiveMode = (int)type;
_start = Vertices.Count;
_count = 0;
CurrentCall = callInfo;
CurrentVertex = DefaultVertex;
}
/// <summary>
/// Emit the latest or modified vertex.
/// </summary>
public void Vertex()
{
Vertex(CurrentVertex);
}
/// <summary>
/// Emit a vertex.
/// </summary>
/// <param name="vertex">The vertex to emit.</param>
public void Vertex(in TVertex vertex)
{
Vertices.Add(CurrentVertex = vertex);
_count++;
}
/// <summary>
/// End the current call.
/// </summary>
/// <exception cref="InvalidOperationException">You tried to end a call that you didn't begin recording.</exception>
public void End()
{
if (!InCall)
throw new InvalidOperationException("Attempt to end draw call before starting one.");
Calls.Add(new DrawCall()
{
Start = _start,
Count = _count,
Type = (PrimitiveType)_primitiveMode,
CallInfo = CurrentCall,
});
_primitiveMode = 0;
}
/// <summary>
/// Called by the execution engine before a draw call is executed.
/// </summary>
/// <param name="call">The call to prepare.</param>
protected abstract void PrepareCall(in TCall call);
/// <summary>
/// Set the vertex format for the <see cref="Vao"/> and <see cref="Vbo"/> created by the recorder.
/// </summary>
protected abstract void SetVertexFormat();
/// <summary>
/// Execute all the recorded draw calls.
/// </summary>
public void Execute()
{
GL.BindVertexArray(Vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo);
ReadOnlySpan<TVertex> vertices = CollectionsMarshal.AsSpan(Vertices);
GL.BufferData(BufferTarget.ArrayBuffer, Vertices.Count * VertexSize, vertices, BufferUsage.DynamicDraw);
foreach (DrawCall call in Calls)
{
PrepareCall(call.CallInfo);
GL.DrawArrays(call.Type, call.Start, call.Count);
}
}
/// <summary>
/// Clear the draw call queue.
/// </summary>
public void Clear()
{
Vertices.Clear();
Calls.Clear();
}
public void Dispose()
{
throw new NotImplementedException();
}
public void Dispose(bool safeExit)
{
throw new NotImplementedException();
}
public bool IsInitialized { get; private set; }
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
Vao = GL.CreateVertexArray();
Vbo = GL.CreateBuffer();
GL.BindVertexArray(Vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo);
SetVertexFormat();
}
protected struct DrawCall
{
public PrimitiveType Type;
public int Start;
public int Count;
public TCall CallInfo;
}
}
}

View File

@@ -0,0 +1,333 @@
using System.Drawing;
using OpenTK.Graphics.OpenGL;
using System.Numerics;
using Dashboard.OpenGL;
using OTK = OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL.Executors
{
public class BaseCommandExecutor : IInitializer, ICommandExecutor
{
private int _program = 0;
private readonly MappableBumpAllocator<CommandInfo> _commands = new MappableBumpAllocator<CommandInfo>();
private readonly DrawCallRecorder _calls = new DrawCallRecorder();
public bool IsInitialized { get; private set; }
public IEnumerable<string> Extensions { get; } = new[] { "DB_base" };
public IContextExecutor Executor { get; private set; } = null!;
public void Initialize()
{
if (IsInitialized) return;
if (Executor == null)
throw new Exception("Executor has not been set.");
IsInitialized = true;
LoadShaders();
}
public void SetContextExecutor(IContextExecutor executor)
{
Executor = executor;
}
public void BeginFrame()
{
}
public void BeginDraw()
{
_commands.Initialize();
_calls.Initialize();
Size size = Executor.Context.FramebufferSize;
Executor.TransformStack.Push(OTK.Matrix4.CreateOrthographicOffCenter(
0,
size.Width,
size.Height,
0,
1,
-1));
GL.Viewport(0, 0, size.Width, size.Height);
}
public void EndDraw()
{
_commands.Unmap();
GL.UseProgram(_program);
_calls.CommandBuffer = _commands.Handle;
_calls.Execute();
}
public void EndFrame()
{
_commands.Clear();
_calls.Clear();
}
public void ProcessCommand(ICommandFrame frame)
{
switch (frame.Command.Name)
{
case "Point":
DrawBasePoint(frame);
break;
case "Line":
DrawBaseLine(frame);
break;
case "RectF":
case "RectS":
case "RectFS":
DrawRect(frame);
break;
}
}
private void DrawBasePoint(ICommandFrame frame)
{
ref CommandInfo info = ref _commands.Take(out int index);
PointCommandArgs args = frame.GetParameter<PointCommandArgs>();
info = new CommandInfo()
{
Type = SimpleDrawCommand.Point,
Arg0 = args.Size,
};
SetCommandCommonBrush(ref info, args.Brush, args.Brush);
_calls.Transforms(Executor.TransformStack.Top);
_calls.Begin(PrimitiveType.Triangles);
_calls.CommandIndex(index);
DrawPoint(args.Position, args.Depth, args.Size);
_calls.End();
}
private void DrawPoint(Vector2 position, float depth, float diameter)
{
// Draw a point as a isocles triangle.
const float adjust = 1.1f;
const float cos30 = 0.8660254038f;
Vector2 top = adjust * new Vector2(0, -cos30);
Vector2 left = adjust * new Vector2(-cos30, 0.5f);
Vector2 right = adjust * new Vector2(cos30, 0.5f);
_calls.TexCoords2(top);
_calls.Vertex3(new Vector3(position + top * diameter, depth));
_calls.TexCoords2(left);
_calls.Vertex3(new Vector3(position + left * diameter, depth));
_calls.TexCoords2(right);
_calls.Vertex3(new Vector3(position + right * diameter, depth));
}
private void DrawBaseLine(ICommandFrame frame)
{
ref CommandInfo info = ref _commands.Take(out int index);
LineCommandArgs args = frame.GetParameter<LineCommandArgs>();
info = new CommandInfo()
{
Type = SimpleDrawCommand.Line,
Arg0 = 0.5f * args.Size / (args.End - args.Start).Length(),
};
SetCommandCommonBrush(ref info, args.Brush, args.Brush);
_calls.Transforms(Executor.TransformStack.Top);
_calls.Begin(PrimitiveType.Triangles);
_calls.CommandIndex(index);
DrawLine(args.Start, args.End, args.Depth, args.Size);
_calls.End();
}
private void DrawLine(Vector2 start, Vector2 end, float depth, float width)
{
float radius = 0.5f * width;
Vector2 segment = end - start;
float length = segment.Length();
float ratio = radius / length;
Vector2 n = ratio * segment;
Vector2 t = new Vector2(-n.Y, n.X);
Vector2 t00 = new Vector2(-ratio, -ratio);
Vector2 t10 = new Vector2(1+ratio, -ratio);
Vector2 t01 = new Vector2(-ratio, +ratio);
Vector2 t11 = new Vector2(1+ratio, +ratio);
Vector3 x00 = new Vector3(start - n - t, depth);
Vector3 x10 = new Vector3(end + n - t, depth);
Vector3 x01 = new Vector3(start - n + t, depth);
Vector3 x11 = new Vector3(end + n + t, depth);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t01);
_calls.Vertex3(x01);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t10);
_calls.Vertex3(x10);
}
private void DrawRect(ICommandFrame frame)
{
ref CommandInfo info = ref _commands.Take(out int index);
RectCommandArgs args = frame.GetParameter<RectCommandArgs>();
Vector2 size = Vector2.Abs(args.End - args.Start);
float aspect = size.X / size.Y;
float border = args.StrikeSize;
float normRad = args.StrikeSize / size.Y;
float wideRad = aspect * normRad;
int flags = 0;
switch (frame.Command.Name)
{
case "RectF":
flags |= 1;
break;
case "RectS":
flags |= 2;
break;
case "RectFS":
flags |= 3;
break;
}
switch (args.BorderKind)
{
case BorderKind.Inset:
flags |= 2 << 2;
break;
case BorderKind.Outset:
flags |= 1 << 2;
break;
}
info = new CommandInfo()
{
Type = SimpleDrawCommand.Rect,
Flags = flags,
Arg0 = aspect,
Arg1 = normRad,
};
SetCommandCommonBrush(ref info, args.FillBrush, args.StrikeBrush);
_calls.Transforms(Executor.TransformStack.Top);
_calls.Begin(PrimitiveType.Triangles);
_calls.CommandIndex(index);
Vector2 t00 = new Vector2(-wideRad, -normRad);
Vector2 t10 = new Vector2(1+wideRad, -normRad);
Vector2 t01 = new Vector2(-wideRad, 1+normRad);
Vector2 t11 = new Vector2(1+wideRad, 1+normRad);
Vector3 x00 = new Vector3(args.Start.X - border, args.Start.Y - border, args.Depth);
Vector3 x10 = new Vector3(args.End.X + border, args.Start.Y - border, args.Depth);
Vector3 x01 = new Vector3(args.Start.X - border, args.End.Y + border, args.Depth);
Vector3 x11 = new Vector3(args.End.X + border, args.End.Y + border, args.Depth);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t01);
_calls.Vertex3(x01);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t10);
_calls.Vertex3(x10);
_calls.End();
}
protected void SetCommandCommonBrush(ref CommandInfo info, IBrush? fill, IBrush? border)
{
switch (fill?.Kind.Name)
{
case "DB_Brush_solid":
SolidBrush solid = (SolidBrush)fill;
Vector4 color = new Vector4(solid.Color.R/255f, solid.Color.G/255f, solid.Color.B/255f, solid.Color.A/255f);
info.FgColor = color;
break;
case "DB_Brush_gradient":
GradientBrush gradient = (GradientBrush)fill;
GradientUniformBuffer gradients = Executor.ResourcePool.GetResourceManager<GradientUniformBuffer>();
gradients.Initialize();
GradientUniformBuffer.Entry entry = gradients.InternGradient(gradient.Gradient);
info.FgGradientIndex = entry.Offset;
info.FgGradientCount = entry.Count;
break;
case null:
// Craete a magenta brush for this.
info.FgColor = new Vector4(1, 0, 1, 1);
break;
}
switch (border?.Kind.Name)
{
case "DB_Brush_solid":
SolidBrush solid = (SolidBrush)border;
Vector4 color = new Vector4(solid.Color.R/255f, solid.Color.G/255f, solid.Color.B/255f, solid.Color.A/255f);
info.BgColor = color;
break;
case "DB_Brush_gradient":
GradientBrush gradient = (GradientBrush)border;
GradientUniformBuffer gradients = Executor.ResourcePool.GetResourceManager<GradientUniformBuffer>();
gradients.Initialize();
GradientUniformBuffer.Entry entry = gradients.InternGradient(gradient.Gradient);
info.BgGradientIndex = entry.Offset;
info.BgGradientCount = entry.Count;
break;
case null:
// Craete a magenta brush for this.
info.BgColor = new Vector4(1, 0, 1, 1);
break;
}
}
private void LoadShaders()
{
using Stream vsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.Executors.simple.vert");
using Stream fsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.Executors.simple.frag");
int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
_program = ShaderUtil.LinkProgram(vs, fs, new []
{
"a_v3Position",
"a_v2TexCoords",
"a_iCmdIndex",
});
GL.DeleteShader(vs);
GL.DeleteShader(fs);
GL.UniformBlockBinding(_program, GL.GetUniformBlockIndex(_program, "CommandBlock"), 0);
}
private static Stream FetchEmbeddedResource(string name)
{
return typeof(BaseCommandExecutor).Assembly.GetManifestResourceStream(name)!;
}
}
}

View File

@@ -0,0 +1,243 @@
using System.Reflection;
using System.Runtime.InteropServices;
using BlurgText;
using Dashboard.OpenGL;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL.Executors
{
public class TextCommandExecutor : ICommandExecutor, IInitializer
{
public IEnumerable<string> Extensions { get; } = new[] { "DB_Text" };
public IContextExecutor Executor { get; private set; }
// private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager<BlurgEngine>();
public bool IsInitialized { get; private set; }
private DrawCallRecorder _recorder;
private int _program = 0;
private int _transformsLocation = -1;
private int _atlasLocation = -1;
private int _borderWidthLocation = -1;
private int _borderColorLocation = -1;
private int _fillColorLocation = -1;
public TextCommandExecutor()
{
Executor = null!;
_recorder = new DrawCallRecorder(this);
}
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
Assembly self = typeof(TextCommandExecutor).Assembly;
using Stream vsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.vert")!;
using Stream fsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.frag")!;
int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
_program = ShaderUtil.LinkProgram(vs, fs, new []
{
"a_v3Position",
"a_v2TexCoords",
});
GL.DeleteShader(vs);
GL.DeleteShader(fs);
_transformsLocation = GL.GetUniformLocation(_program, "m4Transforms");
_atlasLocation = GL.GetUniformLocation(_program, "txAtlas");
_borderWidthLocation = GL.GetUniformLocation(_program, "fBorderWidth");
_borderColorLocation = GL.GetUniformLocation(_program, "v4BorderColor");
_fillColorLocation = GL.GetUniformLocation(_program, "v4FillColor");
_recorder.Initialize();
}
public void SetContextExecutor(IContextExecutor executor)
{
Executor = executor;
}
public void BeginFrame()
{
}
public void BeginDraw()
{
_recorder.Clear();
}
public void EndDraw()
{
GL.UseProgram(_program);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
_recorder.Execute();
GL.Disable(EnableCap.Blend);
}
public void EndFrame()
{
}
public void ProcessCommand(ICommandFrame frame)
{
switch (frame.Command.Name)
{
case "Text":
DrawText(frame);
break;
}
}
private void DrawText(ICommandFrame frame)
{
TextCommandArgs args = frame.GetParameter<TextCommandArgs>();
// DbBlurgFont font = Engine.InternFont(args.Font);
BlurgColor color;
switch (args.TextBrush)
{
case SolidBrush solid:
color = new BlurgColor()
{
R = solid.Color.R,
G = solid.Color.G,
B = solid.Color.B,
A = solid.Color.A,
};
break;
default:
color = new BlurgColor() { R = 255, G = 0, B = 255, A = 255 };
break;
}
//BlurgResult? result = Engine.Blurg.BuildString(font.Font, font.Size, color, args.Text);
// if (result == null)
// return;
//
// Vector3 position = new Vector3(args.Position.X, args.Position.Y, args.Position.Z);
// ExecuteBlurgResult(result, position);
//
// result.Dispose();
}
private void ExecuteBlurgResult(BlurgResult result, Vector3 position)
{
Matrix4 transforms = Executor.TransformStack.Top;
for (int i = 0; i < result.Count; i++)
{
BlurgRect rect = result[i];
int texture = (int)rect.UserData;
Vector4 color = new Vector4(rect.Color.R / 255f, rect.Color.G / 255f, rect.Color.B / 255f,
rect.Color.A / 255f);
if (i == 0)
{
_recorder.Begin(PrimitiveType.Triangles, new Call()
{
Texture = texture,
FillColor = color,
Transforms = transforms,
});
}
else if (
_recorder.CurrentCall.Texture != texture ||
_recorder.CurrentCall.FillColor != color)
{
_recorder.End();
Call call = new Call()
{
Texture = texture,
FillColor = color,
Transforms = transforms,
};
_recorder.Begin(PrimitiveType.Triangles, call);
}
Vector3 p00 = new Vector3(rect.X, rect.Y, 0) + position;
Vector3 p10 = p00 + new Vector3(rect.Width, 0, 0);
Vector3 p11 = p00 + new Vector3(rect.Width, rect.Height, 0);
Vector3 p01 = p00 + new Vector3(0, rect.Height, 0);
Vector2 uv00 = new Vector2(rect.U0, rect.V0);
Vector2 uv10 = new Vector2(rect.U1, rect.V0);
Vector2 uv11 = new Vector2(rect.U1, rect.V1);
Vector2 uv01 = new Vector2(rect.U0, rect.V1);
_recorder.Vertex(p00, uv00);
_recorder.Vertex(p10, uv10);
_recorder.Vertex(p11, uv11);
_recorder.Vertex(p00, uv00);
_recorder.Vertex(p11, uv11);
_recorder.Vertex(p01, uv01);
}
_recorder.End();
}
private struct Call
{
public Matrix4 Transforms = Matrix4.Identity;
public int Texture = 0;
public float BorderWidth = 0f;
public Vector4 FillColor = Vector4.One;
public Vector4 BorderColor = new Vector4(0,0,0,1);
public Call()
{
}
}
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
private struct Vertex
{
[FieldOffset(0)]
public Vector3 Position;
[FieldOffset(4 * sizeof(float))]
public Vector2 TexCoords;
}
private class DrawCallRecorder : DrawCallRecorder<Call, Vertex>
{
private TextCommandExecutor Executor { get; }
public DrawCallRecorder(TextCommandExecutor executor)
{
Executor = executor;
}
public void Vertex(Vector3 position, Vector2 texCoords)
{
Vertex(new Vertex(){Position = position, TexCoords = texCoords});
}
protected override void PrepareCall(in Call call)
{
Matrix4 transforms = call.Transforms;
GL.UniformMatrix4f(Executor._transformsLocation, 1, true, ref transforms);
GL.Uniform1f(Executor._borderWidthLocation, call.BorderWidth);
GL.Uniform4f(Executor._borderColorLocation, 1, in call.BorderColor);
GL.Uniform4f(Executor._fillColorLocation, 1, in call.FillColor);
GL.Uniform1i(Executor._atlasLocation, 0);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2d, call.Texture);
}
protected override void SetVertexFormat()
{
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, VertexSize, 0);
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, VertexSize, 4*sizeof(float));
GL.EnableVertexAttribArray(0);
GL.EnableVertexAttribArray(1);
}
}
}
}

View File

@@ -0,0 +1,55 @@
#ifndef _GRADIENT_GLSL_
#define _GRADIENT_GLSL_
#define DB_GRADIENT_MAX 16
struct Gradient_t {
float fPosition;
float pad0;
float pad1;
float pad2;
vec4 v4Color;
};
uniform GradientBlock
{
Gradient_t vstGradientStops[DB_GRADIENT_MAX];
};
vec4 getGradientColor(float position, int index, int count)
{
position = clamp(position, 0, 1);
int i0 = 0;
float p0 = vstGradientStops[index + i0].fPosition;
int i1 = count - 1;
float p1 = vstGradientStops[index + i1].fPosition;
for (int i = 0; i < count; i++)
{
float px = vstGradientStops[index + i].fPosition;
if (px > p0 && px <= position)
{
p0 = px;
i0 = i;
}
if (px < p1 && px >= position)
{
p1 = px;
i1 = i;
}
}
vec4 c0 = vstGradientStops[index + i0].v4Color;
vec4 c1 = vstGradientStops[index + i1].v4Color;
float l = p1 - p0;
float w = (l > 0) ? (position - p0) / (p1 - p0) : 0;
return mix(c0, c1, w);
}
#endif

View File

@@ -0,0 +1,224 @@
#version 140
#define DB_GRADIENT_MAX 16
#define DB_COMMAND_MAX 64
#define CMD_POINT 1
#define CMD_LINE 2
#define CMD_RECT 3
#define STRIKE_CENTER 0
#define STRIKE_OUTSET 1
#define STRIKE_INSET 2
in vec3 v_v3Position;
in vec2 v_v2TexCoords;
flat in int v_iCmdIndex;
out vec4 f_Color;
uniform sampler2D txForeground;
uniform sampler2D txBackground;
struct Gradient_t {
float fPosition;
float pad0;
float pad1;
float pad2;
vec4 v4Color;
};
uniform GradientBlock
{
Gradient_t vstGradientStops[DB_GRADIENT_MAX];
};
vec4 getGradientColor(float position, int index, int count)
{
position = clamp(position, 0, 1);
int i0 = 0;
float p0 = vstGradientStops[index + i0].fPosition;
int i1 = count - 1;
float p1 = vstGradientStops[index + i1].fPosition;
for (int i = 0; i < count; i++)
{
float px = vstGradientStops[index + i].fPosition;
if (px > p0 && px <= position)
{
p0 = px;
i0 = i;
}
if (px < p1 && px >= position)
{
p1 = px;
i1 = i;
}
}
vec4 c0 = vstGradientStops[index + i0].v4Color;
vec4 c1 = vstGradientStops[index + i1].v4Color;
float l = p1 - p0;
float w = (l > 0) ? (position - p0) / (p1 - p0) : 0;
return mix(c0, c1, w);
}
struct CommandInfo_t {
int iCommand;
int iFlags;
float fArg0;
float fArg1;
int iFgGradientIndex;
int iFgGradientCount;
int iBgGradientIndex;
int iBgGradientCount;
vec4 v4FgColor;
vec4 v4BgColor;
};
uniform CommandBlock
{
CommandInfo_t vstCommandInfo[DB_COMMAND_MAX];
};
CommandInfo_t getCommandInfo()
{
return vstCommandInfo[v_iCmdIndex];
}
vec4 fgColor()
{
return getCommandInfo().v4FgColor;
}
vec4 bgColor()
{
return getCommandInfo().v4BgColor;
}
void Point(void)
{
vec4 fg = fgColor();
if (dot(v_v2TexCoords, v_v2TexCoords) <= 0.25)
f_Color = fg;
else
discard;
}
#define LINE_NORMALIZED_RADIUS(cmd) cmd.fArg0
void Line(void)
{
vec4 fg = fgColor();
CommandInfo_t cmd = getCommandInfo();
float t = clamp(v_v2TexCoords.x, 0, 1);
vec2 dv = v_v2TexCoords - vec2(t, 0);
float d = dot(dv, dv);
float lim = LINE_NORMALIZED_RADIUS(cmd);
lim *= lim;
if (d <= lim)
f_Color = fg;
else
discard;
}
#define RECT_ASPECT_RATIO(cmd) (cmd.fArg0)
#define RECT_BORDER_WIDTH(cmd) (cmd.fArg1)
#define RECT_FILL(cmd) ((cmd.iFlags & (1 << 0)) != 0)
#define RECT_BORDER(cmd) ((cmd.iFlags & (1 << 1)) != 0)
#define RECT_STRIKE_MASK 3
#define RECT_STRIKE_SHIFT 2
#define RECT_STRIKE_KIND(cmd) ((cmd.iFlags & RECT_STRIKE_MASK) >> RECT_STRIKE_SHIFT)
void Rect(void)
{
vec4 fg = fgColor();
vec4 bg = bgColor();
CommandInfo_t cmd = getCommandInfo();
float aspect = RECT_ASPECT_RATIO(cmd);
float border = RECT_BORDER_WIDTH(cmd);
int strikeKind = RECT_STRIKE_KIND(cmd);
vec2 p = abs(2*v_v2TexCoords - vec2(1));
p.x = p.x/aspect;
float m0;
float m1;
if (!RECT_BORDER(cmd))
{
m0 = 1;
m1 = 1;
}
else if (strikeKind == STRIKE_OUTSET)
{
m0 = 1;
m1 = border;
}
else if (strikeKind == STRIKE_INSET)
{
m0 = 1-border;
m1 = 1;
}
else // strikeKind == STRIKE_CENTER
{
float h = 0.5 * border;
m0 = 1-border;
m1 = 1+border;
}
if (p.x > m1*aspect || p.y > m1)
{
discard;
}
if (RECT_FILL(cmd))
{
if (p.x <= 1 && p.y <= 1)
{
f_Color = fg;
}
}
if (RECT_BORDER(cmd))
{
float x = clamp(p.x, aspect*m0, aspect*m1);
float y = clamp(p.y, m0, m1);
if (p.x == x || p.y == y)
{
f_Color = bg;
}
}
}
void main(void)
{
switch (getCommandInfo().iCommand)
{
case CMD_POINT:
Point();
break;
case CMD_LINE:
Line();
break;
case CMD_RECT:
Rect();
break;
default:
// Unimplemented value.
f_Color = vec4(1, 0, 1, 1);
break;
}
}

View File

@@ -0,0 +1,21 @@
#version 140
in vec3 a_v3Position;
in vec2 a_v2TexCoords;
in int a_iCmdIndex;
out vec3 v_v3Position;
out vec2 v_v2TexCoords;
flat out int v_iCmdIndex;
uniform mat4 m4Transforms;
void main(void)
{
vec4 position = vec4(a_v3Position, 1) * m4Transforms;
gl_Position = position;
v_v3Position = position.xyz/position.w;
v_v2TexCoords = a_v2TexCoords;
v_iCmdIndex = a_iCmdIndex;
}

View File

@@ -0,0 +1,21 @@
#version 140
in vec3 v_v3Position;
in vec2 v_v2TexCoords;
out vec4 f_Color;
uniform sampler2D txAtlas;
uniform float fBorderWidth;
uniform vec4 v4BorderColor;
uniform vec4 v4FillColor;
void main() {
// For now just honor the fill color
vec4 color = texture(txAtlas, v_v2TexCoords) * v4FillColor;
if (color.a <= 0.1)
discard;
f_Color = color;
}

View File

@@ -0,0 +1,18 @@
#version 140
in vec3 a_v3Position;
in vec2 a_v2TexCoords;
out vec3 v_v3Position;
out vec2 v_v2TexCoords;
uniform mat4 m4Transforms;
void main(void)
{
vec4 position = vec4(a_v3Position, 1) * m4Transforms;
gl_Position = position;
v_v3Position = position.xyz/position.w;
v_v2TexCoords = a_v2TexCoords;
}

View File

@@ -0,0 +1,40 @@
// using Dashboard.Drawing.OpenGL.Text;
using Dashboard.OpenGL;
using OpenTK;
using OpenTK.Graphics;
namespace Dashboard.Drawing.OpenGL
{
public class GLEngine
{
private readonly Dictionary<IGLContext, ContextExecutor> _executors = new Dictionary<IGLContext, ContextExecutor>();
public bool IsInitialized { get; private set; } = false;
public ContextResourcePoolManager ResourcePoolManager { get; private set; } = new ContextResourcePoolManager();
public void Initialize(IBindingsContext? bindingsContext = null)
{
if (IsInitialized)
return;
IsInitialized = true;
if (bindingsContext != null)
GLLoader.LoadBindings(bindingsContext);
// Typesetter.Backend = BlurgEngine.Global;
}
public ContextExecutor GetExecutor(IGLContext glContext)
{
if (!_executors.TryGetValue(glContext, out ContextExecutor? executor))
{
executor = new ContextExecutor(this, glContext);
executor.Initialize();
_executors.Add(glContext, executor);
}
return executor;
}
}
}

View File

@@ -0,0 +1,90 @@
using System.Runtime.InteropServices;
using Dashboard.OpenGL;
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL
{
public class GradientUniformBuffer : IInitializer, IGLDisposable, IResourceManager
{
private bool _isDisposed;
private int _top = 0;
private readonly MappableBuffer<GradientUniformStruct> _buffer = new MappableBuffer<GradientUniformStruct>();
private readonly Dictionary<Gradient, Entry> _entries = new Dictionary<Gradient, Entry>();
public bool IsInitialized { get; private set; } = false;
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
_buffer.Initialize();
}
public Entry InternGradient(Gradient gradient)
{
if (_entries.TryGetValue(gradient, out Entry entry))
return entry;
int count = gradient.Count;
int offset = _top;
_top += count;
_buffer.EnsureCapacity(_top);
_buffer.Map();
Span<GradientUniformStruct> span = _buffer.AsSpan()[offset.._top];
for (int i = 0; i < count; i++)
{
GradientStop stop = gradient[i];
span[i] = new GradientUniformStruct()
{
Position = stop.Position,
Color = new Vector4(
stop.Color.R / 255f,
stop.Color.G / 255f,
stop.Color.B / 255f,
stop.Color.A / 255f),
};
}
entry = new Entry(offset, count);
_entries.Add(gradient, entry);
return entry;
}
public void Clear()
{
_entries.Clear();
_top = 0;
}
public record struct Entry(int Offset, int Count);
public void Dispose() => Dispose(true);
public void Dispose(bool safeExit)
{
if (_isDisposed)
return;
_isDisposed = true;
_buffer.Dispose(safeExit);
}
string IResourceManager.Name { get; } = nameof(GradientUniformBuffer);
}
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
public struct GradientUniformStruct
{
[FieldOffset(0 * sizeof(float))]
public float Position;
[FieldOffset(4 * sizeof(float))]
public Vector4 Color;
}
}

View File

@@ -0,0 +1,24 @@
namespace Dashboard.Drawing.OpenGL
{
/// <summary>
/// Atomic reference counter.
/// </summary>
public interface IArc : IDisposable
{
/// <summary>
/// The number of references to this.
/// </summary>
int References { get; }
/// <summary>
/// Increment the number of references.
/// </summary>
void IncrementReference();
/// <summary>
/// Decrement the number of references.
/// </summary>
/// <returns>True if this was the last reference.</returns>
bool DecrementReference();
}
}

View File

@@ -0,0 +1,9 @@
namespace Dashboard.Drawing.OpenGL
{
public interface IInitializer
{
bool IsInitialized { get; }
void Initialize();
}
}

View File

@@ -0,0 +1,7 @@
namespace Dashboard.Drawing.OpenGL
{
public interface IResourceManager
{
public string Name { get; }
}
}

View File

@@ -0,0 +1,168 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using Dashboard.OpenGL;
using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL
{
public class MappableBuffer<T> : IInitializer, IGLDisposable where T : struct
{
public int Handle { get; private set; } = 0;
public int Capacity { get; set; } = BASE_CAPACITY;
public IntPtr Pointer { get; private set; } = IntPtr.Zero;
public bool IsInitialized => Handle != 0;
private bool _isDisposed = false;
private const int BASE_CAPACITY = 4 << 10; // 4 KiB
private const int MAX_INCREMENT = 4 << 20; // 4 MiB
~MappableBuffer()
{
Dispose(true, false);
}
public void Initialize()
{
if (IsInitialized)
return;
Handle = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
GL.BufferData(BufferTarget.ArrayBuffer, Capacity, IntPtr.Zero, BufferUsage.DynamicDraw);
}
public void EnsureCapacity(int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (Capacity > count)
return;
SetSize(count, false);
}
public void SetSize(int count, bool clear = false)
{
AssertInitialized();
Unmap();
int sz = Unsafe.SizeOf<T>();
int oldsize = Capacity * sz;
int request = count * sz;
int newsize;
if (request < BASE_CAPACITY)
request = BASE_CAPACITY;
if (request > MAX_INCREMENT)
{
newsize = ((request + MAX_INCREMENT - 1) / MAX_INCREMENT) * MAX_INCREMENT;
}
else
{
newsize = checked((int)BitOperations.RoundUpToPowerOf2((ulong)request));
}
int dest = GL.GenBuffer();
if (clear)
{
GL.BindBuffer(BufferTarget.ArrayBuffer, dest);
GL.BufferData(BufferTarget.ArrayBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
}
else
{
GL.BindBuffer(BufferTarget.CopyWriteBuffer, dest);
GL.BindBuffer(BufferTarget.CopyReadBuffer, Handle);
GL.BufferData(BufferTarget.CopyWriteBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
GL.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer, 0, 0, Math.Min(newsize, oldsize));
}
GL.DeleteBuffer(Handle);
Handle = dest;
Capacity = newsize / Unsafe.SizeOf<T>();
}
public unsafe void Map()
{
if (Pointer != IntPtr.Zero)
return;
AssertInitialized();
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
Pointer = (IntPtr)GL.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);
}
public void Unmap()
{
if (Pointer == IntPtr.Zero)
return;
AssertInitialized();
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
GL.UnmapBuffer(BufferTarget.ArrayBuffer);
Pointer = IntPtr.Zero;
}
public unsafe Span<T> AsSpan()
{
if (Pointer == IntPtr.Zero)
throw new InvalidOperationException("The buffer is not currently mapped.");
AssertInitialized();
return new Span<T>(Pointer.ToPointer(), Capacity);
}
private void AssertInitialized()
{
if (Handle == 0)
throw new InvalidOperationException("The buffer is not initialized.");
}
private void Dispose(bool safeExit, bool disposing)
{
if (_isDisposed)
return;
_isDisposed = true;
if (disposing)
GC.SuppressFinalize(this);
if (safeExit)
ContextCollector.Global.DeleteBufffer(Handle);
}
public void Dispose() => Dispose(true, true);
public void Dispose(bool safeExit) => Dispose(safeExit, true);
}
public class MappableBumpAllocator<T> : MappableBuffer<T> where T : struct
{
private int _top = 0;
private int _previousTop = 0;
public ref T Take(out int index)
{
index = _top;
EnsureCapacity(++_top);
Map();
return ref AsSpan()[index];
}
public ref T Take() => ref Take(out _);
public void Clear()
{
SetSize(0, true);
_previousTop = _top;
_top = 0;
}
}
}

View File

@@ -0,0 +1,51 @@
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL
{
/// <summary>
/// The current stack of transformations.
/// </summary>
public class TransformStack
{
private Matrix4 _top = Matrix4.Identity;
private readonly Stack<Matrix4> _stack = new Stack<Matrix4>();
/// <summary>
/// The top-most transform matrix.
/// </summary>
public ref readonly Matrix4 Top => ref _top;
/// <summary>
/// The number of matrices in the stack.
/// </summary>
public int Count => _stack.Count;
/// <summary>
/// Push a transform.
/// </summary>
/// <param name="transform">The transform to push.</param>
public void Push(in Matrix4 transform)
{
_stack.Push(_top);
_top = transform * _top;
}
/// <summary>
/// Pop a transform.
/// </summary>
public void Pop()
{
if (!_stack.TryPop(out _top))
_top = Matrix4.Identity;
}
/// <summary>
/// Clear the stack of transformations.
/// </summary>
public void Clear()
{
_stack.Clear();
_top = Matrix4.Identity;
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Drawing;
namespace Dashboard.Drawing
{
public class BrushExtension : DrawExtension
{
private BrushExtension() : base("DB_Brush") { }
public static readonly BrushExtension Instance = new BrushExtension();
}
public interface IBrush : IDrawResource
{
}
public readonly struct SolidBrush(Color color) : IBrush
{
public IDrawExtension Kind { get; } = SolidBrushExtension.Instance;
public Color Color { get; } = color;
public override int GetHashCode()
{
return HashCode.Combine(Kind, Color);
}
}
public readonly struct GradientBrush(Gradient gradient) : IBrush
{
public IDrawExtension Kind { get; } = GradientBrushExtension.Instance;
public Gradient Gradient { get; } = gradient;
public override int GetHashCode()
{
return HashCode.Combine(Kind, Gradient);
}
}
public class SolidBrushExtension : DrawExtension
{
private SolidBrushExtension() : base("DB_Brush_solid", new[] { BrushExtension.Instance }) { }
public static readonly SolidBrushExtension Instance = new SolidBrushExtension();
}
public class GradientBrushExtension : DrawExtension
{
private GradientBrushExtension() : base("DB_Brush_gradient", new[] { BrushExtension.Instance }) { }
public static readonly GradientBrushExtension Instance = new GradientBrushExtension();
}
}

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -9,6 +10,10 @@ namespace Dashboard.Drawing
public class DbBaseCommands : DrawExtension
{
public DrawCommand<PointCommandArgs> DrawPoint { get; }
public DrawCommand<LineCommandArgs> DrawLine { get; }
public RectCommand DrawRectF { get; }
public RectCommand DrawRectS { get; }
public RectCommand DrawRectFS { get; }
private DbBaseCommands() : base("DB_base",
new[]
@@ -17,6 +22,10 @@ namespace Dashboard.Drawing
})
{
AddCommand(DrawPoint = new DrawCommand<PointCommandArgs>("Point", this, PointCommandArgs.CommandSize));
AddCommand(DrawLine = new DrawCommand<LineCommandArgs>("Line", this, LineCommandArgs.CommandSize));
AddCommand(DrawRectF = new RectCommand(this, RectCommand.Mode.Fill));
AddCommand(DrawRectS = new RectCommand(this, RectCommand.Mode.Strike));
AddCommand(DrawRectFS = new RectCommand(this, RectCommand.Mode.FillStrike));
}
public static readonly DbBaseCommands Instance = new DbBaseCommands();
@@ -24,17 +33,19 @@ namespace Dashboard.Drawing
public struct PointCommandArgs : IParameterSerializer<PointCommandArgs>
{
public Vector3 Position { get; private set; }
public Vector2 Position { get; private set; }
public float Depth { get; private set; }
public float Size { get; private set; }
public IBrush? Brush { get; private set; }
public PointCommandArgs(Vector3 position, float size, IBrush brush)
public PointCommandArgs(Vector2 position, float depth, float size, IBrush brush)
{
Position = position;
Depth = depth;
Brush = brush;
Size = size;
}
public int Serialize(DrawQueue queue, Span<byte> bytes)
{
if (bytes.Length < CommandSize)
@@ -42,7 +53,7 @@ namespace Dashboard.Drawing
Span<Value> value = stackalloc Value[]
{
new Value(Position, Size, queue.RequireResource(Brush!))
new Value(Position, Depth, Size, queue.RequireResource(Brush!))
};
MemoryMarshal.AsBytes(value).CopyTo(bytes);
@@ -58,12 +69,224 @@ namespace Dashboard.Drawing
Value value = MemoryMarshal.AsRef<Value>(bytes);
Position = value.Position;
Depth = value.Depth;
Size = value.Size;
Brush = (IBrush)queue.Resources[value.BrushIndex];
}
private record struct Value(Vector3 Position, float Size, int BrushIndex);
private record struct Value(Vector2 Position, float Depth, float Size, int BrushIndex);
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
}
public struct LineCommandArgs : IParameterSerializer<LineCommandArgs>
{
public Vector2 Start { get; private set; }
public Vector2 End { get; private set; }
public float Depth { get; private set; }
public float Size { get; private set; }
public IBrush? Brush { get; private set; }
public LineCommandArgs(Vector2 start, Vector2 end, float depth, float size, IBrush brush)
{
Start = start;
End = end;
Depth = depth;
Size = size;
Brush = brush;
}
public int Serialize(DrawQueue queue, Span<byte> bytes)
{
if (bytes.Length < CommandSize)
return CommandSize;
Span<Value> value = stackalloc Value[]
{
new Value(Start, End, Depth, Size, queue.RequireResource(Brush!))
};
MemoryMarshal.AsBytes(value).CopyTo(bytes);
return CommandSize;
}
public void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes)
{
if (bytes.Length < CommandSize)
throw new Exception("Not enough bytes");
Value value = MemoryMarshal.AsRef<Value>(bytes);
Start = value.Start;
End = value.End;
Depth = value.Depth;
Size = value.Size;
Brush = (IBrush)queue.Resources[value.BrushIndex];
}
private record struct Value(Vector2 Start, Vector2 End, float Depth, float Size, int BrushIndex);
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
}
public class RectCommand : IDrawCommand<RectCommandArgs>
{
private readonly Mode _mode;
public string Name { get; }
public IDrawExtension Extension { get; }
public int Length { get; }
public RectCommand(IDrawExtension extension, Mode mode)
{
Extension = extension;
_mode = mode;
switch (mode)
{
case Mode.Fill:
Name = "RectF";
Length = Unsafe.SizeOf<RectF>();
break;
case Mode.Strike:
Name = "RectS";
Length = Unsafe.SizeOf<RectS>();
break;
default:
Name = "RectFS";
Length = Unsafe.SizeOf<RectFS>();
break;
}
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);
}
public RectCommandArgs GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
if (param.Length < Length)
throw new Exception("Not enough bytes");
RectCommandArgs args;
switch (_mode)
{
case Mode.Fill:
ref readonly RectF f = ref MemoryMarshal.AsRef<RectF>(param);
args = new RectCommandArgs(f.Start, f.End, f.Depth, (IBrush)queue.Resources[f.FillBrushIndex]);
break;
case Mode.Strike:
ref readonly RectS s = ref MemoryMarshal.AsRef<RectS>(param);
args = new RectCommandArgs(s.Start, s.End, s.Depth, (IBrush)queue.Resources[s.StrikeBrushIndex], s.StrikeSize, s.BorderKind);
break;
default:
ref readonly RectFS fs = ref MemoryMarshal.AsRef<RectFS>(param);
args = new RectCommandArgs(fs.Start, fs.End, fs.Depth, (IBrush)queue.Resources[fs.FillBrushIndex],
(IBrush)queue.Resources[fs.StrikeBrushIndex], fs.StrikeSize, fs.BorderKind);
break;
}
return args;
}
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return WriteParams(queue, (RectCommandArgs)obj, param);
}
public int WriteParams(DrawQueue queue, RectCommandArgs obj, Span<byte> param)
{
if (param.Length < Length)
return Length;
switch (_mode)
{
case Mode.Fill:
ref RectF f = ref MemoryMarshal.AsRef<RectF>(param);
f.Start = obj.Start;
f.End = obj.End;
f.Depth = obj.Depth;
f.FillBrushIndex = queue.RequireResource(obj.FillBrush!);
break;
case Mode.Strike:
ref RectS s = ref MemoryMarshal.AsRef<RectS>(param);
s.Start = obj.Start;
s.End = obj.End;
s.Depth = obj.Depth;
s.StrikeBrushIndex = queue.RequireResource(obj.StrikeBrush!);
s.StrikeSize = obj.StrikeSize;
s.BorderKind = obj.BorderKind;
break;
default:
ref RectFS fs = ref MemoryMarshal.AsRef<RectFS>(param);
fs.Start = obj.Start;
fs.End = obj.End;
fs.Depth = obj.Depth;
fs.FillBrushIndex = queue.RequireResource(obj.FillBrush!);
fs.StrikeBrushIndex = queue.RequireResource(obj.StrikeBrush!);
fs.StrikeSize = obj.StrikeSize;
fs.BorderKind = obj.BorderKind;
break;
}
return Length;
}
[Flags]
public enum Mode
{
Fill = 1,
Strike = 2,
FillStrike = Fill | Strike,
}
private record struct RectF(Vector2 Start, Vector2 End, float Depth, int FillBrushIndex);
private record struct RectS(Vector2 Start, Vector2 End, float Depth, int StrikeBrushIndex, float StrikeSize, BorderKind BorderKind);
private record struct RectFS(Vector2 Start, Vector2 End, float Depth, int FillBrushIndex, int StrikeBrushIndex, float StrikeSize, BorderKind BorderKind);
}
public struct RectCommandArgs
{
public Vector2 Start { get; private set; }
public Vector2 End { get; private set; }
public float Depth { get; private set; }
public float StrikeSize { get; private set; } = 0f;
public BorderKind BorderKind { get; private set; } = BorderKind.Center;
public IBrush? FillBrush { get; private set; } = null;
public IBrush? StrikeBrush { get; private set; } = null;
public bool IsStruck => StrikeSize != 0;
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush fillBrush)
{
Start = start;
End = end;
Depth = depth;
FillBrush = fillBrush;
}
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush strikeBrush, float strikeSize, BorderKind borderKind)
{
Start = start;
End = end;
Depth = depth;
StrikeBrush = strikeBrush;
StrikeSize = strikeSize;
BorderKind = borderKind;
}
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush fillBrush, IBrush strikeBrush, float strikeSize,
BorderKind borderKind)
{
Start = start;
End = end;
Depth = depth;
FillBrush = fillBrush;
StrikeBrush = strikeBrush;
StrikeSize = strikeSize;
BorderKind = borderKind;
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Numerics;
namespace Dashboard.Drawing
{
public enum DrawPrimitive
{
Point,
Line,
LineStrip,
Triangle,
TriangleFan,
TriangleStrip
}
public record struct DrawVertex(Vector3 Position, Vector3 TextureCoordinate, Vector4 Color);
public record DrawInfo(DrawPrimitive Primitive, int Count)
{
}
public class DrawBuffer
{
}
}

View File

@@ -29,6 +29,8 @@ namespace Dashboard.Drawing
/// <param name="param">The parameter array.</param>
/// <returns>The parameters object.</returns>
object? GetParams(DrawQueue queue, ReadOnlySpan<byte> param);
int WriteParams(DrawQueue queue, object? obj, Span<byte> param);
}
public interface IDrawCommand<T> : IDrawCommand
@@ -39,6 +41,8 @@ namespace Dashboard.Drawing
/// <param name="param">The parameter array.</param>
/// <returns>The parameters object.</returns>
new T? GetParams(DrawQueue queue, ReadOnlySpan<byte> param);
new int WriteParams(DrawQueue queue, T? obj, Span<byte> param);
}
public sealed class DrawCommand : IDrawCommand
@@ -57,6 +61,11 @@ namespace Dashboard.Drawing
{
return null;
}
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return 0;
}
}
public sealed class DrawCommand<T> : IDrawCommand<T>
@@ -80,6 +89,16 @@ namespace Dashboard.Drawing
return t;
}
public int WriteParams(DrawQueue queue, T? obj, Span<byte> param)
{
return obj!.Serialize(queue, param);
}
int IDrawCommand.WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return WriteParams(queue, (T?)obj, param);
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);

View File

@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing
{
@@ -62,11 +64,78 @@ namespace Dashboard.Drawing
return queue.GetController(extension);
}
public static void Point(this DrawQueue queue, Vector3 position, float size, IBrush brush)
public static void Point(this DrawQueue queue, Vector2 position, float depth, float size, IBrush brush)
{
var controller = queue.GetController(DbBaseCommands.Instance);
controller.EnsureSize(position);
controller.Write(DbBaseCommands.Instance.DrawPoint, new PointCommandArgs(position, size, brush));
Vector2 radius = new Vector2(0.5f * size);
Box2d bounds = new Box2d(position - radius, position + radius);
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
controller.EnsureBounds(bounds, depth);
controller.Write(DbBaseCommands.Instance.DrawPoint, new PointCommandArgs(position, depth, size, brush));
}
public static void Line(this DrawQueue queue, Vector2 start, Vector2 end, float depth, float size, IBrush brush)
{
Vector2 radius = new Vector2(size / 2f);
Vector2 min = Vector2.Min(start, end) - radius;
Vector2 max = Vector2.Max(start, end) + radius;
Box2d bounds = new Box2d(min, max);
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
controller.EnsureBounds(bounds, depth);
controller.Write(DbBaseCommands.Instance.DrawLine, new LineCommandArgs(start, end, depth, size, brush));
}
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush fillBrush)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
Vector2 min = Vector2.Min(start, end);
Vector2 max = Vector2.Max(start, end);
controller.EnsureBounds(new Box2d(min, max), depth);
controller.Write(DbBaseCommands.Instance.DrawRectF, new RectCommandArgs(start, end, depth, fillBrush));
}
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush strikeBrush, float strikeSize,
BorderKind kind = BorderKind.Center)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
Vector2 min = Vector2.Min(start, end);
Vector2 max = Vector2.Max(start, end);
controller.EnsureBounds(new Box2d(min, max), depth);
controller.Write(DbBaseCommands.Instance.DrawRectS, new RectCommandArgs(start, end, depth, strikeBrush, strikeSize, kind));
}
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush fillBrush, IBrush strikeBrush,
float strikeSize, BorderKind kind = BorderKind.Center)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
Vector2 min = Vector2.Min(start, end);
Vector2 max = Vector2.Max(start, end);
controller.EnsureBounds(new Box2d(min, max), depth);
controller.Write(DbBaseCommands.Instance.DrawRectFS, new RectCommandArgs(start, end, depth, fillBrush, strikeBrush, strikeSize, kind));
}
public static void Text(this DrawQueue queue, Vector3 position, IBrush brush, string text, IFont font,
Anchor anchor = Anchor.Left)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
SizeF size = Typesetter.MeasureString(font, text);
controller.EnsureBounds(new Box2d(position.X, position.Y, position.X + size.Width, position.Y + size.Height), position.Z);
controller.Write(TextExtension.Instance.TextCommand, new TextCommandArgs(font, brush, anchor, position, text));
}
public static void Text(this DrawQueue queue, Vector3 position, IBrush textBrush, IBrush borderBrush,
float borderRadius, string text, IFont font, Anchor anchor = Anchor.Left, BorderKind borderKind = BorderKind.Outset)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
SizeF size = Typesetter.MeasureString(font, text);
controller.EnsureBounds(new Box2d(position.X, position.Y, position.X + size.Width, position.Y + size.Height), position.Z);
controller.Write(TextExtension.Instance.TextCommand, new TextCommandArgs(font, textBrush, anchor, position, text)
{
BorderBrush = borderBrush,
BorderRadius = borderRadius,
BorderKind = borderKind,
});
}
}
}

View File

@@ -1,12 +1,12 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
namespace Dashboard.Drawing
{
public class DrawQueue : IDisposable
public class DrawQueue : IEnumerable<ICommandFrame>, IDisposable
{
private readonly HashList<IDrawExtension> _extensions = new HashList<IDrawExtension>();
private readonly HashList<IDrawCommand> _commands = new HashList<IDrawCommand>();
@@ -15,9 +15,9 @@ namespace Dashboard.Drawing
private readonly MemoryStream _commandStream = new MemoryStream();
/// <summary>
/// Size of the image in points.
/// The absolute boundary of all graphics objects.
/// </summary>
public Vector3 Size { get; private set; } = Vector3.Zero;
public Box3d Bounds { get; set; }
/// <summary>
/// The extensions required to draw the image.
@@ -47,7 +47,7 @@ namespace Dashboard.Drawing
_resources.Clear();
_commands.Clear();
_extensions.Clear();
_commandStream.Capacity = 0;
_commandStream.SetLength(0);
}
public int RequireExtension(IDrawExtension extension)
@@ -91,7 +91,7 @@ namespace Dashboard.Drawing
sz = ToVlq(cmdIndex, cmd);
cmd[sz++] = 0;
}
_commandStream.Write(cmd[..sz]);
}
@@ -104,6 +104,7 @@ namespace Dashboard.Drawing
int sz = ToVlq(cmdIndex, cmd);
sz += ToVlq(param.Length, cmd[sz..]);
_commandStream.Write(cmd[..sz]);
_commandStream.Write(param);
}
else
{
@@ -119,6 +120,10 @@ namespace Dashboard.Drawing
}
}
public Enumerator GetEnumerator() => new Enumerator(this);
IEnumerator<ICommandFrame> IEnumerable<ICommandFrame>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private static int ToVlq(int value, Span<byte> bytes)
{
if (value < 0)
@@ -126,34 +131,43 @@ namespace Dashboard.Drawing
else if (bytes.Length < 5)
throw new ArgumentOutOfRangeException(nameof(bytes), "Must at least be five bytes long.");
int i;
if (value == 0)
{
bytes[0] = 0;
return 1;
}
int i;
for (i = 0; i < 5 && value != 0; i++, value >>= 7)
{
if (i > 0)
bytes[i - 1] |= 1 << 7;
bytes[i] = (byte)(value & 0x7F);
bytes[i] = (byte)(value & 0x7F);
}
return i;
}
private static int FromVlq(ReadOnlySpan<byte> bytes)
private static int FromVlq(ReadOnlySpan<byte> bytes, out int value)
{
int value = 0;
value = 0;
for (int i = 0; i < bytes.Length; i++)
int i;
for (i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
value = (value << 7) | b;
value |= (b & 0x7F) << (7*i);
if ((b & (1 << 7)) == 0)
{
i++;
break;
}
}
return value;
return i;
}
public void Dispose()
@@ -163,19 +177,9 @@ namespace Dashboard.Drawing
private class DrawController(DrawQueue Queue) : IDrawController
{
public void EnsureSize(Vector3 size)
public void EnsureBounds(Box2d bounds, float depth)
{
Queue.Size = Vector3.Max(Queue.Size, size);
}
public int Require(IDrawExtension extension)
{
return Queue.RequireExtension(extension);
}
public int GetResourceIndex(IDrawResource resource)
{
return Queue._resources.Intern(resource);
Queue.Bounds = Box3d.Union(Queue.Bounds, bounds, depth);
}
public void Write(IDrawCommand command)
@@ -196,49 +200,143 @@ namespace Dashboard.Drawing
param.Serialize(Queue, bytes);
Write(command, bytes);
}
public void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>
{
int length = command.WriteParams(Queue, param, Span<byte>.Empty);
Span<byte> bytes = stackalloc byte[length];
command.WriteParams(Queue, param, bytes);
Write(command, bytes);
}
}
private class HashList<T> : IReadOnlyList<T>
where T : notnull
public class Enumerator : ICommandFrame, IEnumerator<ICommandFrame>
{
private readonly List<T> _list = new List<T>();
private readonly Dictionary<T, int> _map = new Dictionary<T, int>();
private readonly DrawQueue _queue;
private readonly byte[] _stream;
private int _length;
private int _index = -1;
private int _paramsIndex = -1;
private int _paramLength = 0;
private IDrawCommand? _current = null;
public T this[int index] => _list[index];
public ICommandFrame Current => this;
public int Count => _list.Count;
object? IEnumerator.Current => Current;
public int Intern(T value)
public IDrawCommand Command => _current ?? throw new InvalidOperationException();
public bool HasParameters { get; private set; }
public Enumerator(DrawQueue queue)
{
if (_map.TryGetValue(value, out int index))
return index;
index = Count;
_list.Add(value);
_map.Add(value, index);
return index;
_queue = queue;
_stream = queue._commandStream.GetBuffer();
_length = (int)queue._commandStream.Length;
}
public void Clear()
public bool MoveNext()
{
_list.Clear();
_map.Clear();
if (_index == -1)
_index = 0;
if (_index >= _length)
return false;
_index += FromVlq(_stream[_index .. (_index + 5)], out int command);
_current = _queue.Command[command];
HasParameters = _current.Length != 0;
if (!HasParameters)
{
_paramsIndex = -1;
return true;
}
int length;
if (_current.Length < 0)
{
_index += FromVlq(_stream[_index .. (_index + 5)], out length);
}
else
{
length = _current.Length;
}
_paramsIndex = _index;
_paramLength = length;
_index += length;
return true;
}
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
public void Reset()
{
_index = -1;
_current = null;
}
public object? GetParameter()
{
return _current?.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength));
}
public T GetParameter<T>()
{
if (_current is IDrawCommand<T> command)
{
return command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!;
}
else
{
throw new InvalidOperationException();
}
}
public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter)
{
if (_current is IDrawCommand<T> command)
{
parameter = command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!;
return true;
}
else
{
parameter = default;
return false;
}
}
public void Dispose()
{
}
}
}
public interface ICommandFrame
{
public IDrawCommand Command { get; }
public bool HasParameters { get; }
public object? GetParameter();
public T GetParameter<T>();
public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter);
}
public interface IDrawController
{
/// <summary>
/// Ensures that the canvas is at least a certain size.
/// </summary>
/// <param name="size">The minimum size.</param>
void EnsureSize(Vector3 size);
/// <param name="bounds">The bounding box.</param>
void EnsureBounds(Box2d bounds, float depth);
/// <summary>
/// Write into the command stream.
@@ -259,5 +357,12 @@ namespace Dashboard.Drawing
/// <param name="command">The command to write.</param>
/// <param name="param">Any data associated with the command.</param>
void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>;
/// <summary>
/// Write into the command stream.
/// </summary>
/// <param name="command">The command to write.</param>
/// <param name="param">Any data associated with the command.</param>
void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>;
}
}

View File

@@ -0,0 +1,9 @@
using Dashboard.Windowing;
namespace Dashboard.Drawing
{
public interface IDrawQueuePaintable : IPaintable
{
DrawQueue DrawQueue { get; }
}
}

View File

@@ -1,33 +0,0 @@
using System.Drawing;
namespace Dashboard.Drawing
{
public class BrushExtension : DrawExtension
{
private BrushExtension() : base("DB_Brush") { }
public static readonly BrushExtension Instance = new BrushExtension();
}
public interface IBrush : IDrawResource
{
}
public readonly struct SolidBrush : IBrush
{
public IDrawExtension Kind { get; } = SolidBrushExtension.Instance;
public Color Color { get; }
public SolidBrush(Color color)
{
Color = color;
}
}
public class SolidBrushExtension : DrawExtension
{
private SolidBrushExtension() : base("DB_Brush_solid", new[] { BrushExtension.Instance }) { }
public static readonly SolidBrushExtension Instance = new SolidBrushExtension();
}
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing
{
public class TextExtension : DrawExtension
{
public TextCommand TextCommand { get; }
private TextExtension() : base("DB_Text", new [] { BrushExtension.Instance })
{
TextCommand = new TextCommand(this);
}
public static readonly TextExtension Instance = new TextExtension();
}
public class TextCommand : IDrawCommand<TextCommandArgs>
{
public string Name { get; } = "Text";
public IDrawExtension Extension { get; }
public int Length { get; } = -1;
public TextCommand(TextExtension ext)
{
Extension = ext;
}
public int WriteParams(DrawQueue queue, TextCommandArgs obj, Span<byte> param)
{
int size = Unsafe.SizeOf<Header>() + obj.Text.Length * sizeof(char) + sizeof(char);
if (param.Length < size)
return size;
ref Header header = ref MemoryMarshal.Cast<byte, Header>(param[0..Unsafe.SizeOf<Header>()])[0];
Span<char> text = MemoryMarshal.Cast<byte, char>(param[Unsafe.SizeOf<Header>()..]);
header = new Header()
{
// Font = queue.RequireResource(obj.Font),
TextBrush = queue.RequireResource(obj.TextBrush),
BorderBrush = (obj.BorderBrush != null) ? queue.RequireResource(obj.BorderBrush) : -1,
BorderRadius = (obj.BorderBrush != null) ? obj.BorderRadius : 0f,
Anchor = obj.Anchor,
Position = obj.Position,
BorderKind = obj.BorderKind,
};
obj.Text.CopyTo(text);
return size;
}
public TextCommandArgs GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
Header header = MemoryMarshal.Cast<byte, Header>(param[0..Unsafe.SizeOf<Header>()])[0];
ReadOnlySpan<char> text = MemoryMarshal.Cast<byte, char>(param[Unsafe.SizeOf<Header>()..]);
if (header.BorderBrush != -1 && header.BorderRadius != 0)
{
return new TextCommandArgs(
(IFont)queue.Resources[header.Font],
(IBrush)queue.Resources[header.TextBrush],
header.Anchor,
header.Position,
text.ToString())
{
BorderBrush = (IBrush)queue.Resources[header.BorderBrush],
BorderRadius = header.BorderRadius,
BorderKind = header.BorderKind,
};
}
else
{
return new TextCommandArgs(
(IFont)queue.Resources[header.Font],
(IBrush)queue.Resources[header.TextBrush],
header.Anchor,
header.Position,
text.ToString());
}
}
int IDrawCommand.WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return WriteParams(queue, (TextCommandArgs)obj!, param);
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);
}
private struct Header
{
private int _flags;
public int Font;
public int TextBrush;
public int BorderBrush;
public Vector3 Position;
public float BorderRadius;
public Anchor Anchor
{
get => (Anchor)(_flags & 0xF);
set => _flags = (_flags & ~0xF) | (int)value;
}
public BorderKind BorderKind
{
get => (_flags & INSET) switch
{
OUTSET => BorderKind.Outset,
INSET => BorderKind.Inset,
_ => BorderKind.Center,
};
set => _flags = value switch
{
BorderKind.Outset => (_flags & ~INSET) | OUTSET,
BorderKind.Inset => (_flags & ~INSET) | INSET,
_ => (_flags & ~INSET) | CENTER,
};
}
private const int INSET = 0x30;
private const int CENTER = 0x00;
private const int OUTSET = 0x10;
}
}
public record struct TextCommandArgs(IFont Font, IBrush TextBrush, Anchor Anchor, Vector3 Position, string Text)
{
public IBrush? BorderBrush { get; init; } = null;
public float BorderRadius { get; init; } = 0;
public BorderKind BorderKind { get; init; } = BorderKind.Center;
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Reflection.PortableExecutable;
namespace Dashboard.Drawing
{
/// <summary>
/// Interface for registered typesetters.
/// </summary>
public interface ITypeSetter
{
/// <summary>
/// Name of the typesetter.
/// </summary>
string Name { get; }
SizeF MeasureString(IFont font, string value);
IFont LoadFont(Stream stream);
IFont LoadFont(string path);
// IFont LoadFont(NamedFont font);
}
/// <summary>
/// Class for typesetting related functions.
/// </summary>
public static class Typesetter
{
/// <summary>
/// The typesetting backend for this instance.
/// </summary>
public static ITypeSetter Backend { get; set; } = new UndefinedTypeSetter();
public static string Name => Backend.Name;
public static SizeF MeasureString(IFont font, string value)
{
return Backend.MeasureString(font, value);
}
public static IFont LoadFont(Stream stream)
{
return Backend.LoadFont(stream);
}
public static IFont LoadFont(string path)
{
return Backend.LoadFont(path);
}
public static IFont LoadFont(FileInfo file)
{
return Backend.LoadFont(file.FullName);
}
// public static IFont LoadFont(NamedFont font)
// {
// return Backend.LoadFont(font);
// }
public static IFont LoadFont(string family, float size, FontWeight weight = FontWeight.Normal,
FontSlant slant = FontSlant.Normal, FontStretch stretch = FontStretch.Normal)
{
// return LoadFont(new NamedFont(family, size, weight, slant, stretch));
throw new Exception();
}
private class UndefinedTypeSetter : ITypeSetter
{
public string Name { get; } = "Undefined";
[DoesNotReturn]
private void Except()
{
throw new InvalidOperationException("No typesetting backend is loaded.");
}
public SizeF MeasureString(IFont font, string value)
{
Except();
return default;
}
public IFont LoadFont(Stream stream)
{
Except();
return default;
}
public IFont LoadFont(string path)
{
Except();
return default;
}
// public IFont LoadFont(NamedFont font)
// {
// Except();
// return default;
// }
}
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,234 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Text;
using Dashboard.Drawing;
namespace Dashboard.ImmediateUI
{
public class DimUIConfig
{
public Vector2 Margin = new Vector2(8, 4);
public Vector2 Padding = new Vector2(4);
public required IFont Font { get; init; }
public IBrush TextBrush = new SolidBrush(Color.Black);
public IBrush DisabledText = new SolidBrush(Color.Gray);
public float ButtonBorderSize = 2f;
public IBrush ButtonBorderBrush = new SolidBrush(Color.SteelBlue);
public IBrush ButtonFillBrush = new SolidBrush(Color.SlateGray);
public IBrush? ButtonShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
public float ButtonShadowOffset = 2f;
public float InputBorderSize = 2f;
public IBrush InputPlaceholderTextBrush = new SolidBrush(Color.SteelBlue);
public IBrush InputBorderBrush = new SolidBrush(Color.SlateGray);
public IBrush InputFillBrush = new SolidBrush(Color.LightGray);
public IBrush? InputShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
public float InputShadowOffset = -2f;
public float MenuBorderSize = 2f;
public IBrush MenuBorderBrush = new SolidBrush(Color.SteelBlue);
public IBrush MenuFillBrush = new SolidBrush(Color.SlateGray);
public IBrush? MenuShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
public float MenuShadowOffset = 2f;
}
public class DimUI
{
private readonly DimUIConfig _config;
private Vector2 _pen;
private Box2d _bounds;
private bool _firstLine = false;
private bool _sameLine = false;
private float _z = -1;
private float _lineHeight;
private DrawQueue _queue;
public DimUI(DimUIConfig config)
{
_config = config;
}
[MemberNotNull(nameof(_queue))]
public void Begin(Box2d bounds, DrawQueue queue)
{
_bounds = bounds;
_pen = _bounds.Min;
_queue = queue;
_firstLine = true;
_lineHeight = 0;
_z = -1;
}
public void SameLine()
{
_sameLine = true;
}
private void Line()
{
if (!_firstLine && !_sameLine)
{
_pen = new Vector2(_bounds.Left, _pen.Y + _lineHeight);
}
else
{
_firstLine = false;
_sameLine = false;
}
_pen.X += _config.Margin.X;
_lineHeight = 0;
}
private float Z()
{
return _z += 0.001f;
}
public void Text(string text)
{
Line();
SizeF sz = Typesetter.MeasureString(_config.Font, text);
float z = Z();
float h = _config.Margin.Y * 2 + sz.Height;
_queue.Text(new Vector3(_pen + new Vector2(0, _config.Margin.X), z), _config.TextBrush, text, _config.Font);
_lineHeight = Math.Max(_lineHeight, h);
_pen.X += sz.Width;
}
public void DrawBox(
Vector2 position,
Vector2 size,
IBrush fill,
IBrush border, float borderWidth,
IBrush? shadow, float offset)
{
float z = Z();
if (shadow != null)
{
if (offset >= 0)
{
_queue.Rect(position + new Vector2(offset), position + size + new Vector2(offset + borderWidth), z, shadow);
}
else
{
// Inset shadows are draw a bit weirdly.
_queue.Rect(position, position + new Vector2(offset, size.Y), z, shadow);
_queue.Rect(position + new Vector2(offset, 0), position + new Vector2(size.X - offset, offset), z, shadow);
}
}
_queue.Rect(position, position + size, z, fill, border, borderWidth, BorderKind.Outset);
}
public bool Button(string label)
{
Line();
SizeF sz = Typesetter.MeasureString(_config.Font, label);
float h = _config.Margin.Y * 2 + _config.Padding.Y * 2 + sz.Height;
DrawBox(
_pen + new Vector2(0, _config.Margin.Y),
new Vector2(sz.Width + 2 * _config.Padding.X, sz.Height + 2 * _config.Padding.Y),
_config.ButtonFillBrush,
_config.ButtonBorderBrush,
_config.ButtonBorderSize,
_config.ButtonShadowBrush,
_config.ButtonShadowOffset);
float z = Z();
_queue.Text(new Vector3(_pen + new Vector2(_config.Padding.X, _config.Margin.Y + _config.Padding.Y), z),
_config.TextBrush, label, _config.Font);
_lineHeight = Math.Max(_lineHeight, h);
_pen.X += sz.Width + 2 * _config.Padding.X;
return false;
}
public bool Input(string placeholder, StringBuilder value)
{
Line();
IBrush textBrush;
string str;
if (value.Length == 0)
{
textBrush = _config.DisabledText;
str = placeholder;
}
else
{
textBrush = _config.TextBrush;
str = value.ToString();
}
SizeF sz = Typesetter.MeasureString(_config.Font, str);
float h = _config.Margin.Y * 2 + _config.Padding.Y * 2 + sz.Height;
DrawBox(
_pen + new Vector2(0, _config.Margin.Y),
new Vector2(sz.Width + 2 * _config.Padding.X, sz.Height + 2 * _config.Padding.Y),
_config.InputFillBrush,
_config.InputBorderBrush,
_config.InputBorderSize,
_config.InputShadowBrush,
_config.InputShadowOffset);
float z = Z();
_queue.Text(new Vector3(_pen + new Vector2(_config.Padding.X, _config.Margin.Y + _config.Padding.Y), z),
textBrush, str, _config.Font);
_lineHeight = Math.Max(_lineHeight, h);
_pen.X += sz.Width + 2 * _config.Padding.X;
return false;
}
public void BeginMenu()
{
}
public bool MenuItem(string name)
{
return false;
}
public void EndMenu()
{
}
public int Id(ReadOnlySpan<char> str)
{
// Uses the FVN-1A algorithm in 32-bit mode.
const int PRIME = 0x01000193;
const int BASIS = unchecked((int)0x811c9dc5);
int hash = BASIS;
for (int i = 0; i < str.Length; i++)
{
hash ^= str[i] & 0xFF;
hash *= PRIME;
hash ^= str[i] >> 8;
hash *= PRIME;
}
return hash;
}
public void Finish()
{
// TODO:
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Concurrent;
using OpenTK.Graphics.OpenGL;
namespace Dashboard.OpenGL
{
public class ContextCollector : IDisposable
{
private readonly ConcurrentQueue<GLObject> _disposedObjects = new ConcurrentQueue<GLObject>();
public void Dispose()
{
while (_disposedObjects.TryDequeue(out GLObject obj))
{
obj.Dispose();
}
}
void DeleteObject(ObjectIdentifier identifier, int handle) => _disposedObjects.Enqueue(new GLObject(identifier, handle));
public void DeleteTexture(int texture) => DeleteObject(ObjectIdentifier.Texture, texture);
public void DeleteBufffer(int buffer) => DeleteObject(ObjectIdentifier.Buffer, buffer);
public void DeleteFramebuffer(int framebuffer) => DeleteObject(ObjectIdentifier.Framebuffer, framebuffer);
public void DeleteRenderBuffer(int renderbuffer) => DeleteObject(ObjectIdentifier.Renderbuffer, renderbuffer);
public void DeleteSampler(int sampler) => DeleteObject(ObjectIdentifier.Sampler, sampler);
public void DeleteShader(int shader) => DeleteObject(ObjectIdentifier.Shader, shader);
public void DeleteProgram(int program) => DeleteObject(ObjectIdentifier.Program, program);
public void DeleteVertexArray(int vertexArray) => DeleteObject(ObjectIdentifier.VertexArray, vertexArray);
public void DeleteQuery(int query) => DeleteObject(ObjectIdentifier.Query, query);
public void DeleteProgramPipeline(int programPipeline) => DeleteObject(ObjectIdentifier.ProgramPipeline, programPipeline);
public void DeleteTransformFeedback(int transformFeedback) => DeleteObject(ObjectIdentifier.TransformFeedback, transformFeedback);
private readonly record struct GLObject(ObjectIdentifier Type, int Handle)
{
public void Dispose()
{
switch (Type)
{
case ObjectIdentifier.Texture:
GL.DeleteTexture(Handle);
break;
case ObjectIdentifier.Buffer:
GL.DeleteBuffer(Handle);
break;
case ObjectIdentifier.Framebuffer:
GL.DeleteFramebuffer(Handle);
break;
case ObjectIdentifier.Renderbuffer:
GL.DeleteRenderbuffer(Handle);
break;
case ObjectIdentifier.Sampler:
GL.DeleteSampler(Handle);
break;
case ObjectIdentifier.Shader:
GL.DeleteShader(Handle);
break;
case ObjectIdentifier.VertexArray:
GL.DeleteVertexArray(Handle);
break;
case ObjectIdentifier.Program:
GL.DeleteProgram(Handle);
break;
case ObjectIdentifier.Query:
GL.DeleteQuery(Handle);
break;
case ObjectIdentifier.ProgramPipeline:
GL.DeleteProgramPipeline(Handle);
break;
case ObjectIdentifier.TransformFeedback:
GL.DeleteTransformFeedback(Handle);
break;
}
}
}
public static readonly ContextCollector Global = new ContextCollector();
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK.Graphics" Version="[5.0.0-pre.*,5.1)" />
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Drawing\immediate.frag" />
<EmbeddedResource Include="Drawing\immediate.frag" />
<None Remove="Drawing\immediate.vert" />
<EmbeddedResource Include="Drawing\immediate.vert" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,167 @@
using System.Drawing;
using System.Numerics;
using Dashboard.Drawing;
using Dashboard.Pal;
using Dashboard.Windowing;
using OpenTK.Graphics.OpenGL;
using OpenTK.Graphics.Wgl;
using OpenTK.Mathematics;
using ColorBuffer = OpenTK.Graphics.OpenGL.ColorBuffer;
using Vector2 = System.Numerics.Vector2;
namespace Dashboard.OpenGL.Drawing
{
public class DeviceContextBase : IDeviceContextBase
{
private readonly Stack<Matrix4x4> _transforms = new Stack<Matrix4x4>();
private readonly Stack<Box2d> _clipRegions = new Stack<Box2d>();
private readonly Stack<Box2d> _scissorRegions = new Stack<Box2d>();
private int _z = 0;
public DeviceContext Context { get; private set; } = null!;
IContextBase IContextExtensionBase.Context => Context;
public string DriverName => "Dashboard OpenGL Device Context";
public string DriverVendor => "Dashboard";
public Version DriverVersion => new Version(0, 1);
public Box2d ClipRegion => _clipRegions.Peek();
public Box2d ScissorRegion => _scissorRegions.Peek();
public Matrix4x4 Transforms => _transforms.Peek();
public float Scale => ScaleOverride > 0 ? ScaleOverride : (Context.Window as IDpiAwareWindow)?.Scale ?? 1;
public float ScaleOverride { get; set; } = -1f;
public void Dispose()
{
}
public void Require(DeviceContext context)
{
Context = context;
ResetClip();
ResetScissor();
ResetTransforms();
}
void IContextExtensionBase.Require(IContextBase context) => Require((DeviceContext)context);
public void ResetClip()
{
_clipRegions.Clear();
Vector2 size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
_clipRegions.Push(new Box2d(Vector2.Zero, size));
SetClip(ClipRegion);
}
public void PushClip(Box2d clipRegion)
{
clipRegion = new Box2d(ClipRegion.Min.X + clipRegion.Min.X, ClipRegion.Min.Y + clipRegion.Min.Y,
Math.Min(ClipRegion.Max.X, ClipRegion.Min.X + clipRegion.Max.X),
Math.Min(ClipRegion.Max.Y, ClipRegion.Max.Y + clipRegion.Max.Y));
_clipRegions.Push(clipRegion);
SetClip(clipRegion);
}
public void PopClip()
{
_clipRegions.Pop();
SetClip(ClipRegion);
}
public void ResetScissor()
{
GL.Disable(EnableCap.ScissorTest);
_scissorRegions.Clear();
Vector2 size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
_scissorRegions.Push(new Box2d(Vector2.Zero, size));
}
public void PushScissor(Box2d scissorRegion)
{
GL.Enable(EnableCap.ScissorTest);
// scissorRegion = new RectangleF(scissorRegion.X + scissorRegion.X, scissorRegion.Y + scissorRegion.Y,
// Math.Min(ScissorRegion.Right - scissorRegion.X, scissorRegion.Width),
// Math.Min(ScissorRegion.Bottom - scissorRegion.Y, scissorRegion.Height));
_scissorRegions.Push(scissorRegion);
SetScissor(scissorRegion);
}
public void PopScissor()
{
if (_scissorRegions.Count == 1)
GL.Disable(EnableCap.ScissorTest);
_scissorRegions.Pop();
SetScissor(ClipRegion);
}
private void SetClip(Box2d rect)
{
Vector2 size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
GL.Viewport(
(int)Math.Round(rect.Min.X),
(int)Math.Round(size.Y - rect.Min.Y - rect.Size.Y),
(int)Math.Round(rect.Size.X),
(int)Math.Round(rect.Size.Y));
}
void SetScissor(Box2d rect)
{
Vector2 size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
GL.Scissor(
(int)Math.Round(rect.Min.X),
(int)Math.Round(size.Y - rect.Min.Y - rect.Size.Y),
(int)Math.Round(rect.Size.X),
(int)Math.Round(rect.Size.Y));
}
public void ResetTransforms()
{
Vector2 size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
Matrix4x4 m = Matrix4x4.CreateOrthographicOffCenterLeftHanded(0, size.X, size.Y, 0, 1, -1);
_transforms.Clear();
_transforms.Push(m);
}
public void PushTransforms(in Matrix4x4 matrix)
{
Matrix4x4 result = matrix * Transforms;
_transforms.Push(result);
}
public void PopTransforms()
{
_transforms.Pop();
}
public void ClearColor(Color color)
{
GL.ClearColor(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
GL.Clear(ClearBufferMask.ColorBufferBit);
}
public void ClearDepth()
{
GL.Clear(ClearBufferMask.DepthBufferBit);
}
public int IncrementZ()
{
return ++_z;
}
public int DecrementZ()
{
return --_z;
}
}
}

View File

@@ -0,0 +1,240 @@
using System.Drawing;
using System.Numerics;
using System.Runtime;
using System.Runtime.InteropServices;
using Dashboard.Drawing;
using Dashboard.Pal;
using OpenTK.Graphics.OpenGL;
namespace Dashboard.OpenGL.Drawing
{
public class ImmediateMode : IImmediateMode
{
public string DriverName => "Dashboard OpenGL Immediate Mode";
public string DriverVendor => "Dashboard";
public Version DriverVersion { get; } = new Version(1, 0);
public DeviceContext Context { get; private set; } = null!;
private int _program;
private uint _program_apos;
private uint _program_atexcoord;
private uint _program_acolor;
private int _program_transforms;
private int _program_image;
private int _vao;
private int _white;
public void Dispose()
{
}
public void Require(DeviceContext context)
{
Context = context;
_program = GL.CreateProgram();
int vs = GL.CreateShader(ShaderType.VertexShader);
using (StreamReader reader = new StreamReader(GetType().Assembly
.GetManifestResourceStream("Dashboard.OpenGL.Drawing.immediate.vert")!))
{
GL.ShaderSource(vs, reader.ReadToEnd());
}
GL.CompileShader(vs);
GL.AttachShader(_program, vs);
int fs = GL.CreateShader(ShaderType.FragmentShader);
using (StreamReader reader = new StreamReader(GetType().Assembly
.GetManifestResourceStream("Dashboard.OpenGL.Drawing.immediate.frag")!))
{
GL.ShaderSource(fs, reader.ReadToEnd());
}
GL.CompileShader(fs);
GL.AttachShader(_program, fs);
GL.LinkProgram(_program);
GL.DeleteShader(vs); GL.DeleteShader(fs);
_program_apos = (uint)GL.GetAttribLocation(_program, "aPos");
_program_atexcoord = (uint)GL.GetAttribLocation(_program, "aTexCoords");
_program_acolor = (uint)GL.GetAttribLocation(_program, "aColor");
_program_transforms = GL.GetUniformLocation(_program, "transforms");
_program_image = GL.GetUniformLocation(_program, "image");
GL.GenTexture(out _white);
GL.BindTexture(TextureTarget.Texture2d, _white);
GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.Rgb, 1, 1, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgb, PixelType.Byte, IntPtr.Zero);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleA, (int)All.One);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleR, (int)All.One);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleG, (int)All.One);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleB, (int)All.One);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.GenVertexArray(out _vao);
}
public void ClearColor(Color color)
{
GL.ClearColor(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
GL.Clear(ClearBufferMask.ColorBufferBit);
}
public void Line(Vector2 a, Vector2 b, float width, float depth, Vector4 color)
{
Vector2 normal = Vector2.Normalize(b - a);
Vector2 tangent = new Vector2(-normal.Y, normal.X) * width;
Span<ImmediateVertex> vertices =
[
new ImmediateVertex(new Vector3(a-tangent, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(b-tangent, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(b+tangent, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(a-tangent, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(b+tangent, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(a+tangent, depth), Vector2.Zero, color),
];
int buffer = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * ImmediateVertex.Size, ref vertices[0], BufferUsage.StreamDraw);
GL.BindVertexArray(_vao);
GL.VertexAttribPointer(_program_apos, 3, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.PosOffset);
GL.EnableVertexAttribArray(_program_apos);
GL.VertexAttribPointer(_program_atexcoord, 2, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.TexCoordsOffset);
GL.EnableVertexAttribArray(_program_atexcoord);
GL.VertexAttribPointer(_program_acolor, 4, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.ColorOffset);
GL.EnableVertexAttribArray(_program_acolor);
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
GL.UseProgram(_program);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2d, _white);
GL.UniformMatrix4f(_program_transforms, 1, true, ref view);
GL.Uniform1i(_program_image, 0);
GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
GL.DeleteBuffer(buffer);
}
public void Rectangle(Box2d rectangle, float depth, Vector4 color)
{
Span<ImmediateVertex> vertices =
[
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Min.Y, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), Vector2.Zero, color),
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Max.Y, depth), Vector2.Zero, color),
];
int buffer = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * ImmediateVertex.Size, ref vertices[0], BufferUsage.StreamDraw);
GL.BindVertexArray(_vao);
GL.VertexAttribPointer(_program_apos, 3, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.PosOffset);
GL.EnableVertexAttribArray(_program_apos);
GL.VertexAttribPointer(_program_atexcoord, 2, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.TexCoordsOffset);
GL.EnableVertexAttribArray(_program_atexcoord);
GL.VertexAttribPointer(_program_acolor, 4, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.ColorOffset);
GL.EnableVertexAttribArray(_program_acolor);
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
GL.UseProgram(_program);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2d, _white);
GL.UniformMatrix4f(_program_transforms, 1, true, ref view);
GL.Uniform1i(_program_image, 0);
GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
GL.DeleteBuffer(buffer);
}
public void Rectangle(in RectangleDrawInfo rectangle)
{
// TODO: implement this better.
int z = Context.ExtensionRequire<IDeviceContextBase>().IncrementZ();
Color color = (rectangle.Fill as SolidColorBrush)?.Color ?? Color.LightGray;
Vector4 colorV = new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
Vector4 margin = rectangle.Box.Margin;
Vector4 border = rectangle.Box.Border;
Vector2 size = rectangle.Box.Size + new Vector2(border.X + border.Z, border.Y = border.W);
Box2d box = Box2d.FromPositionAndSize(rectangle.Position + new Vector2(margin.X + border.X, margin.Y + border.Y), size);
Rectangle(box, z, colorV);
}
public void Image(Box2d rectangle, Box2d uv, float depth, ITexture texture)
{
Span<ImmediateVertex> vertices =
[
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), new Vector2(uv.Min.X, uv.Min.Y), Vector4.One),
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Min.Y, depth), new Vector2(uv.Max.X, uv.Min.Y), Vector4.One),
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), new Vector2(uv.Max.X, uv.Max.Y), Vector4.One),
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), new Vector2(uv.Min.X, uv.Min.Y), Vector4.One),
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), new Vector2(uv.Max.X, uv.Max.Y), Vector4.One),
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Max.Y, depth), new Vector2(uv.Min.X, uv.Max.Y), Vector4.One),
];
int buffer = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * ImmediateVertex.Size, ref vertices[0], BufferUsage.StreamDraw);
GL.BindVertexArray(_vao);
GL.VertexAttribPointer(_program_apos, 3, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.PosOffset);
GL.EnableVertexAttribArray(_program_apos);
GL.VertexAttribPointer(_program_atexcoord, 2, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.TexCoordsOffset);
GL.EnableVertexAttribArray(_program_atexcoord);
GL.VertexAttribPointer(_program_acolor, 4, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.ColorOffset);
GL.EnableVertexAttribArray(_program_acolor);
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
GL.UseProgram(_program);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2d, ((GLTexture)texture).Handle);
GL.UniformMatrix4f(_program_transforms, 1, true, ref view);
GL.Uniform1i(_program_image, 0);
GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
GL.DeleteBuffer(buffer);
}
IContextBase IContextExtensionBase.Context => Context;
void IContextExtensionBase.Require(IContextBase context)
{
Require((DeviceContext)context);
}
[StructLayout(LayoutKind.Explicit, Pack = sizeof(float) * 4, Size = Size)]
private struct ImmediateVertex(Vector3 position, Vector2 texCoords, Vector4 color)
{
[FieldOffset(PosOffset)] public Vector3 Position = position;
[FieldOffset(TexCoordsOffset)] public Vector2 TexCoords = texCoords;
[FieldOffset(ColorOffset)] public Vector4 Color = color;
public const int Size = 16 * sizeof(float);
public const int PosOffset = 0 * sizeof(float);
public const int TexCoordsOffset = 4 * sizeof(float);
public const int ColorOffset = 8 * sizeof(float);
}
}
}

View File

@@ -0,0 +1,12 @@
#version 130
uniform sampler2D image;
in vec2 vTexCoords;
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor * texture(image, vTexCoords);
}

View File

@@ -0,0 +1,18 @@
#version 130
uniform mat4 transforms;
in vec3 aPos;
in vec2 aTexCoords;
in vec4 aColor;
out vec2 vTexCoords;
out vec4 vColor;
void main() {
vec4 position = vec4(aPos, 1.0) * transforms;
gl_Position = position;
vTexCoords = aTexCoords;
vColor = aColor;
}

View File

@@ -0,0 +1,169 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Dashboard.Drawing;
using Dashboard.OpenGL.Drawing;
using Dashboard.Pal;
using Dashboard.Windowing;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
namespace Dashboard.OpenGL
{
internal class GLContextBindingsContext(IGLContext context) : IBindingsContext
{
public IntPtr GetProcAddress(string procName)
{
return context.GetProcAddress(procName);
}
}
public class GLDeviceContext : DeviceContext
{
public IGLContext GLContext { get; }
public ContextCollector Collector { get; } = new ContextCollector();
public override string DriverName => "Dashboard OpenGL Device Context";
public override string DriverVendor => "Dashboard";
public override Version DriverVersion => new Version(0, 1, 0);
public Version GLVersion { get; }
public string GLRenderer { get; }
public string GLVendor { get; }
public ImmutableHashSet<string> Extensions { get; }
public Thread RendererThread { get; } = Thread.CurrentThread;
public bool IsRenderThread => RendererThread == Thread.CurrentThread;
private readonly ConcurrentQueue<Task> _beforeDrawActions = new ConcurrentQueue<Task>();
private readonly ConcurrentQueue<Task> _afterDrawActions = new ConcurrentQueue<Task>();
public GLDeviceContext(Application app, IWindow? window, IGLContext context) : base(app, window)
{
GLContext = context;
context.MakeCurrent();
GLLoader.LoadBindings(new GLContextBindingsContext(context));
context.Disposed += Dispose;
GL.GetInteger(GetPName.MajorVersion, out int major);
GL.GetInteger(GetPName.MinorVersion, out int minor);
GLVersion = new Version(major, minor);
GLRenderer = GL.GetString(StringName.Renderer) ?? string.Empty;
GLVendor = GL.GetString(StringName.Vendor) ?? string.Empty;
HashSet<string> extensions = new HashSet<string>();
GL.GetInteger(GetPName.NumExtensions, out int extensionCount);
for (uint i = 0; i < extensionCount; i++)
{
string? ext = GL.GetStringi(StringName.Extensions, i);
if (ext != null)
extensions.Add(ext);
}
Extensions = extensions.ToImmutableHashSet();
ExtensionPreload<DeviceContextBase>();
ExtensionPreload<GLTextureExtension>();
ExtensionPreload<ImmediateMode>();
}
public bool IsGLExtensionAvailable(string name)
{
return Extensions.Contains(name);
}
public void AssertGLExtension(string name)
{
if (IsGLExtensionAvailable(name))
return;
throw new NotSupportedException($"The OpenGL extension \"{name}\" is not supported by this context.");
}
public void InvokeBeforeDraw(Task task) => _beforeDrawActions.Enqueue(task);
public void InvokeAfterDraw(Task task) => _afterDrawActions.Enqueue(task);
public Task InvokeBeforeDraw(Action action)
{
Task task = new Task(action);
_beforeDrawActions.Enqueue(task);
return task;
}
public Task InvokeAfterDraw(Action action)
{
Task task = new Task(action);
_afterDrawActions.Enqueue(task);
return task;
}
public Task<T> InvokeBeforeDraw<T>(Func<T> function)
{
Task<T> task = new Task<T>(function);
_beforeDrawActions.Enqueue(task);
return task;
}
public Task<T> InvokeAfterDraw<T>(Func<T> function)
{
Task<T> task = new Task<T>(function);
_afterDrawActions.Enqueue(task);
return task;
}
public Task InvokeOnRenderThread(Action action)
{
if (IsRenderThread)
{
action();
return Task.CompletedTask;
}
return InvokeBeforeDraw(action);
}
public Task<T> InvokeOnRenderThread<T>(Func<T> function)
{
return IsRenderThread ? Task.FromResult(function()) : InvokeBeforeDraw(function);
}
public override void Begin()
{
base.Begin();
GLContext.MakeCurrent();
IDeviceContextBase dc = ExtensionRequire<IDeviceContextBase>();
dc.ResetClip();
dc.ResetTransforms();
while (_beforeDrawActions.TryDequeue(out Task? action))
{
action.RunSynchronously();
}
}
public override void End()
{
base.End();
while (_afterDrawActions.TryDequeue(out Task? action))
{
action.RunSynchronously();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
GLContext.Disposed -= Dispose;
}
}
}
}

View File

@@ -0,0 +1,306 @@
using System.Drawing;
using Dashboard.Drawing;
using Dashboard.Pal;
using OpenTK.Graphics.OpenGL;
using OGL = OpenTK.Graphics.OpenGL;
namespace Dashboard.OpenGL
{
public class GLTextureExtension : ITextureExtension, IContextExtensionBase<GLDeviceContext>
{
public string DriverName => "Dashboard OpenGL Texture Extension";
public string DriverVendor => "Dashboard";
public Version DriverVersion => new Version(0, 1, 0);
public GLDeviceContext Context { get; private set; } = null!;
public bool SupportsArbTextureStorage { get; private set; }
public bool SupportsAnisotropy { get; private set; }
IContextBase IContextExtensionBase.Context => Context;
DeviceContext IContextExtensionBase<DeviceContext>.Context => Context;
private List<GLTexture> _textures = new List<GLTexture>();
public void Dispose()
{
}
public void Require(GLDeviceContext context)
{
Context = context;
SupportsArbTextureStorage = Context.DriverVersion >= new Version(4, 2) ||
Context.IsGLExtensionAvailable("GL_ARB_texture_storage");
SupportsAnisotropy = Context.DriverVersion >= new Version() ||
Context.IsGLExtensionAvailable("GL_EXT_texture_filter_anisotropic") ||
Context.IsGLExtensionAvailable("GL_ARB_texture_filter_anisotropic");
}
public void Require(DeviceContext context) => Require((GLDeviceContext)context);
public void Require(IContextBase context) => Require((GLDeviceContext)context);
public GLTexture CreateTexture(TextureType type)
{
GLTexture texture = new GLTexture(this, type);
lock (_textures) _textures.Add(texture);
return texture;
}
internal void TextureDisposed(GLTexture texture)
{
lock (_textures) _textures.Remove(texture);
}
ITexture ITextureExtension.CreateTexture(TextureType type) => CreateTexture(type);
}
public class GLTexture(GLTextureExtension extension, TextureType type) : ITexture
{
public int Handle { get; private set; } = 0;
public bool IsValid => Handle != 0;
public TextureType Type { get; } = type;
public PixelFormat Format { get; private set; } = PixelFormat.Rgba8I;
public ColorSwizzle Swizzle { get; set; } = ColorSwizzle.Default;
public TextureFilter MinifyFilter { get; set; } = TextureFilter.Linear;
public TextureFilter MagnifyFilter { get; set; } = TextureFilter.Linear;
public Color BorderColor { get; set; } = Color.White;
public TextureRepeat RepeatS { get; set; } = TextureRepeat.Repeat;
public TextureRepeat RepeatT { get; set; } = TextureRepeat.Repeat;
public TextureRepeat RepeatR { get; set; } = TextureRepeat.Repeat;
public int Anisotropy { get; set; } = 0;
public int Width { get; private set; } = 0;
public int Height { get; private set; } = 0;
public int Depth { get; private set; } = 0;
public int Levels { get; private set; } = 0;
public bool Premultiplied { get; set; } = false;
private TextureTarget Target { get; } = type switch
{
TextureType.Texture1D => TextureTarget.Texture1d,
TextureType.Texture2D => TextureTarget.Texture2d,
TextureType.Texture3D => TextureTarget.Texture3d,
TextureType.Texture2DArray => TextureTarget.Texture2dArray,
TextureType.Texture2DCube => TextureTarget.TextureCubeMap,
_ => throw new NotSupportedException()
};
private GLTextureExtension Extension { get; } = extension;
private GLDeviceContext Context => Extension.Context;
~GLTexture()
{
Dispose(false);
}
public void SetStorage(PixelFormat format, int width, int height, int depth, int levels)
{
if (!Context.IsRenderThread)
{
Context.InvokeBeforeDraw(() => SetStorage(format, width, height, depth, levels)).Wait();
return;
}
if (levels == 0)
{
levels = Math.Max(Math.ILogB(width), Math.ILogB(height));
}
Bind();
SizedInternalFormat glFormat = GetFormat(format);
if (Extension.SupportsArbTextureStorage)
{
switch (Type)
{
case TextureType.Texture1D:
GL.TexStorage1D(Target, levels, glFormat, width);
break;
case TextureType.Texture2D:
GL.TexStorage2D(Target, levels, glFormat, width, height);
break;
case TextureType.Texture3D:
case TextureType.Texture2DArray:
case TextureType.Texture2DCube:
GL.TexStorage3D(Target, levels, glFormat, width, height, depth);
break;
}
}
else
{
switch (Type)
{
case TextureType.Texture1D:
GL.TexImage1D(Target, 0, (InternalFormat)glFormat, width, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
break;
case TextureType.Texture2D:
GL.TexImage2D(Target, 0, (InternalFormat)glFormat, width, height, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
break;
case TextureType.Texture3D:
case TextureType.Texture2DArray:
case TextureType.Texture2DCube:
GL.TexImage3D(Target, 0, (InternalFormat)glFormat, width, height, depth, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
break;
}
}
Width = width;
Height = height;
Depth = depth;
Levels = levels;
}
public void Read<T>(Span<T> buffer, int level = 0, int align = 0) where T : unmanaged
{
throw new NotImplementedException();
}
void ITexture.Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level, int align) => Write(format, buffer, level, align, null);
public unsafe void Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level = 0, int align = 4, TimeSpan? timeout = null) where T : unmanaged
{
if (!Context.IsRenderThread)
{
T[] bufferArray = buffer.ToArray();
Task task = Context.InvokeBeforeDraw(() => Write<T>(format, bufferArray, level, align));
if (timeout.HasValue)
task.Wait(timeout.Value);
else
task.Wait();
}
Bind();
OGL::PixelFormat glFormat = format switch
{
PixelFormat.R8I or PixelFormat.R16F => OGL.PixelFormat.Red,
PixelFormat.Rg8I or PixelFormat.Rg16F => OGL.PixelFormat.Rg,
PixelFormat.Rgb8I or PixelFormat.Rgb16F => OGL.PixelFormat.Rgb,
PixelFormat.Rgba8I or PixelFormat.Rgba16F => OGL.PixelFormat.Rgba,
_ => throw new NotSupportedException()
};
PixelType glType = format switch
{
PixelFormat.R8I or PixelFormat.Rg8I or PixelFormat.Rgb8I or PixelFormat.Rgba8I => PixelType.UnsignedByte,
PixelFormat.R16F or PixelFormat.Rg16F or PixelFormat.Rgb16F or PixelFormat.Rgba16F => PixelType.HalfFloat,
_ => throw new NotSupportedException()
};
GL.PixelStorei(PixelStoreParameter.UnpackAlignment, align);
fixed (T* ptr = buffer)
{
switch (Type)
{
case TextureType.Texture1D:
GL.TexSubImage1D(Target, level, 0, Width, glFormat, glType, ptr);
break;
case TextureType.Texture2D:
GL.TexSubImage2D(Target, level, 0, 0, Width, Height, glFormat, glType, ptr);
break;
case TextureType.Texture2DCube:
case TextureType.Texture3D:
case TextureType.Texture2DArray:
GL.TexSubImage3D(Target, level, 0, 0, 0, Width, Height, Depth, glFormat, glType, ptr);
break;
}
}
}
public void Premultiply()
{
throw new NotImplementedException();
}
public void Unmultiply()
{
throw new NotImplementedException();
}
public void GenerateMipmaps()
{
if (!Context.IsRenderThread)
{
Context.InvokeBeforeDraw(GenerateMipmaps).Wait();
return;
}
Bind();
GL.GenerateMipmap(Target);
}
private void Dispose(bool disposing)
{
if (IsDisposed)
return;
IsDisposed = true;
if (disposing)
{
if (Thread.CurrentThread != Context.RendererThread)
{
Context.Collector.DeleteTexture(Handle);
}
else
{
GL.DeleteTexture(Handle);
}
Handle = 0;
GC.SuppressFinalize(this);
}
else
{
Context.Collector.DeleteTexture(Handle);
}
}
public bool IsDisposed { get; private set; }
public void Dispose() => Dispose(false);
private void Bind()
{
if (Handle == 0)
{
Handle = GL.GenTexture();
}
GL.BindTexture(Target, Handle);
}
private static SizedInternalFormat GetFormat(PixelFormat format)
{
return format switch
{
PixelFormat.R8I => SizedInternalFormat.R8,
PixelFormat.R16F => SizedInternalFormat.R16f,
PixelFormat.Rg8I => SizedInternalFormat.Rg8,
PixelFormat.Rg16F => SizedInternalFormat.Rg16f,
PixelFormat.Rgb8I => SizedInternalFormat.Rgb8,
PixelFormat.Rgb16F => SizedInternalFormat.Rgb16f,
PixelFormat.Rgba8I => SizedInternalFormat.Rgba8,
PixelFormat.Rgba16F => SizedInternalFormat.Rgba16f,
_ => throw new ArgumentOutOfRangeException()
};
}
private static PixelFormat GetFormat(SizedInternalFormat format)
{
return format switch
{
SizedInternalFormat.R8 => PixelFormat.R8I,
SizedInternalFormat.R16f => PixelFormat.R16F,
SizedInternalFormat.Rg8 => PixelFormat.Rg8I,
SizedInternalFormat.Rg16f => PixelFormat.Rg16F,
SizedInternalFormat.Rgb8 => PixelFormat.Rgb8I,
SizedInternalFormat.Rgb16f => PixelFormat.Rgb16F,
SizedInternalFormat.Rgba8 => PixelFormat.Rgba8I,
SizedInternalFormat.Rgba16f => PixelFormat.Rgba16F,
_ => throw new ArgumentOutOfRangeException()
};
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Numerics;
using Dashboard.Windowing;
namespace Dashboard.OpenGL
{
/// <summary>
/// Interface for GL context operations
/// </summary>
public interface IGLContext : IDeviceContext
{
/// <summary>
/// The associated group for context sharing.
/// </summary>
/// <remarks>-1 assigns no group.</remarks>
public int ContextGroup { get; }
/// <summary>
/// The size of the framebuffer in pixels.
/// </summary>
public Vector2 FramebufferSize { get; }
/// <summary>
/// Called when the context is disposed.
/// </summary>
event Action Disposed;
/// <summary>
/// Activate this OpenGL Context.
/// </summary>
void MakeCurrent();
IntPtr GetProcAddress(string procName);
}
}

View File

@@ -0,0 +1,16 @@
namespace Dashboard.OpenGL
{
/// <summary>
/// Interface much like <see cref="IDisposable"/> except GL resources are dropped.
/// </summary>
/// <remarks>
/// The main reason this interface exists is that you need a way to differentiate
/// when a context is deleted, versus when the context is to remain present.
/// </remarks>
public interface IGLDisposable : IDisposable
{
/// <inheritdoc cref="IDisposable.Dispose"/>
/// <param name="safeExit">Set to true to spend the time clearing out GL objects.</param>
void Dispose(bool safeExit);
}
}

View File

@@ -0,0 +1,60 @@
using OpenTK.Graphics.OpenGL;
namespace Dashboard.OpenGL
{
public static class ShaderUtil
{
public static int CompileShader(ShaderType type, string source)
{
int shader = GL.CreateShader(type);
GL.ShaderSource(shader, source);
GL.CompileShader(shader);
int compileStatus = 0;
GL.GetShaderi(shader, ShaderParameterName.CompileStatus, out compileStatus);
if (compileStatus == 0)
{
GL.GetShaderInfoLog(shader, out string log);
GL.DeleteShader(shader);
throw new Exception($"{type} Shader compilation failed: " + log);
}
return shader;
}
public static int CompileShader(ShaderType type, Stream stream)
{
using StreamReader reader = new StreamReader(stream, leaveOpen: true);
return CompileShader(type, reader.ReadToEnd());
}
public static int LinkProgram(int s1, int s2, IReadOnlyList<string>? attribLocations = null)
{
int program = GL.CreateProgram();
GL.AttachShader(program, s1);
GL.AttachShader(program, s2);
for (int i = 0; i < attribLocations?.Count; i++)
{
GL.BindAttribLocation(program, (uint)i, attribLocations[i]);
}
GL.LinkProgram(program);
int linkStatus = 0;
GL.GetProgrami(program, ProgramProperty.LinkStatus, out linkStatus);
if (linkStatus == 0)
{
GL.GetProgramInfoLog(program, out string log);
GL.DeleteProgram(program);
throw new Exception("Shader program linking failed: " + log);
}
return program;
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK" Version="[5.0.0-pre*,5.1)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.OpenGL\Dashboard.OpenGL.csproj" />
<ProjectReference Include="..\Dashboard\Dashboard.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,285 @@
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dashboard.Events;
using Dashboard.OpenGL;
using Dashboard.Pal;
using Dashboard.Windowing;
using OpenTK.Platform;
using TK = OpenTK.Platform.Toolkit;
using OPENTK = OpenTK.Platform;
using DB = Dashboard.Events;
namespace Dashboard.OpenTK.PAL2
{
public class Pal2Application : Application
{
public override string DriverName => "Dashboard OpenTK PAL2.0 Driver";
public override string DriverVendor => "Dashboard";
public override Version DriverVersion => new Version(0, 1);
public GraphicsApiHints GraphicsApiHints { get; set; } = new OpenGLGraphicsApiHints();
private readonly List<PhysicalWindow> _windows = new List<PhysicalWindow>();
private readonly ConditionalWeakTable<WindowHandle, WindowExtraInfo> _windowHandleWindowMap =
new ConditionalWeakTable<WindowHandle, WindowExtraInfo>();
private long _tick = Stopwatch.GetTimestamp();
public override IPhysicalWindow CreatePhysicalWindow()
{
PhysicalWindow window = new PhysicalWindow(this, GraphicsApiHints);
_windows.Add(window);
_windowHandleWindowMap.Add(window.WindowHandle, new WindowExtraInfo(window));
return window;
}
public override IWindow CreateWindow()
{
return CreatePhysicalWindow();
}
protected override void InitializeInternal()
{
base.InitializeInternal();
CancellationToken?.Register(() =>
{
TK.Window.PostUserEvent(new ApplicationQuitEventArgs());
});
EventQueue.EventRaised += OnEventRaised;
}
internal void RemoveWindow(PhysicalWindow window)
{
_windows.Remove(window);
}
public override void RunEvents(bool wait)
{
if (_windows.Count == 0)
{
Dispose();
return;
}
TK.Window.ProcessEvents(wait);
long tock = Stopwatch.GetTimestamp();
long elapsed = _tick - tock;
float delta = (float)elapsed / Stopwatch.Frequency;
TickEventArgs tickEvent = new TickEventArgs(delta);
_tick = tock;
for (int i = 0; i < _windows.Count; i++)
{
PhysicalWindow window = _windows[i];
if (window.IsDisposed)
{
_windows.RemoveAt(i);
continue;
}
window.SendEvent(this, tickEvent);
window.SendEvent(this, new PaintEventArgs(window.DeviceContext));
// For now we swap each window individually.
((GLDeviceContext)window.DeviceContext).GLContext.SwapGroup.Swap();
}
}
private void OnEventRaised(PalHandle? handle, PlatformEventType type, EventArgs args)
{
if (handle is WindowHandle window)
{
OnWindowEventRaised(window, type, args);
return;
}
else
{
// System.Diagnostics.Debugger.Break();
}
}
private void OnWindowEventRaised(WindowHandle handle, PlatformEventType type, EventArgs args)
{
if (!_windowHandleWindowMap.TryGetValue(handle, out WindowExtraInfo? info))
{
Debugger?.LogDebug($"Unknown window handle {handle} received from OpenTK.");
return;
}
switch (type)
{
case PlatformEventType.UserMessage:
if (args is ApplicationQuitEventArgs)
{
Quit = true;
return;
}
break;
// Mouse Events
case PlatformEventType.MouseDown:
{
MouseButtonDownEventArgs down = (MouseButtonDownEventArgs)args;
MouseButtons buttons = (MouseButtons)(1 << (int)down.Button);
ModifierKeys modifierKeys = GetModifierKeys(down.Modifiers);
// TODO: modifier keys
MouseButtonEventArgs down2 = new MouseButtonEventArgs(info.MousePosition, buttons, modifierKeys, false);
info.Window.SendEvent(this, down2);
break;
}
case PlatformEventType.MouseUp:
{
MouseButtonUpEventArgs up = (MouseButtonUpEventArgs)args;
MouseButtons buttons = (MouseButtons)(1 << (int)up.Button);
ModifierKeys modifierKeys = GetModifierKeys(up.Modifiers);
// TODO: modifier keys
MouseButtonEventArgs up2 = new MouseButtonEventArgs(info.MousePosition, buttons, modifierKeys, true);
info.Window.SendEvent(this, up2);
break;
}
case PlatformEventType.MouseMove:
{
OPENTK.MouseMoveEventArgs move = (OPENTK.MouseMoveEventArgs)args;
Vector2 position = new Vector2(move.ClientPosition.X, move.ClientPosition.Y);
DB.MouseMoveEventArgs move2 = new DB.MouseMoveEventArgs(position, position - info.MousePosition);
info.MousePosition = position;
info.Window.SendEvent(this, move2);
break;
}
case PlatformEventType.Scroll:
{
ScrollEventArgs scroll = (ScrollEventArgs)args;
Vector2 distance = new Vector2(scroll.Distance.X, scroll.Distance.Y);
Vector2 delta = new Vector2(scroll.Delta.X, scroll.Delta.Y);
MouseScrollEventArgs scroll2 = new MouseScrollEventArgs(distance, delta);
info.Window.SendEvent(this, scroll2);
break;
}
// Keyboard & Text Events
case PlatformEventType.KeyDown:
{
KeyDownEventArgs down = (KeyDownEventArgs)args;
ModifierKeys modifierKeys = GetModifierKeys(down.Modifiers);
KeyCode keyCode = GetKeyCode(down.Key);
ScanCode scanCode = GetScanCode(down.Scancode);
KeyboardButtonEventArgs up2 = new KeyboardButtonEventArgs(keyCode, scanCode, modifierKeys, false);
info.Window.SendEvent(this, up2);
break;
}
case PlatformEventType.KeyUp:
{
KeyUpEventArgs up = (KeyUpEventArgs)args;
ModifierKeys modifierKeys = GetModifierKeys(up.Modifiers);
KeyCode keyCode = GetKeyCode(up.Key);
ScanCode scanCode = GetScanCode(up.Scancode);
KeyboardButtonEventArgs up2 = new KeyboardButtonEventArgs(keyCode, scanCode, modifierKeys, true);
info.Window.SendEvent(this, up2);
break;
}
case PlatformEventType.TextInput:
{
OPENTK.TextInputEventArgs textInput = (OPENTK.TextInputEventArgs)args;
DB.TextInputEventArgs textInput2 = new DB.TextInputEventArgs(textInput.Text);
info.Window.SendEvent(this, textInput2);
break;
}
case PlatformEventType.TextEditing:
{
TextEditingEventArgs textEditing = (TextEditingEventArgs)args;
TextEditEventArgs textEditing2 = new TextEditEventArgs(textEditing.Candidate, textEditing.Cursor, textEditing.Length);
info.Window.SendEvent(this, textEditing2);
break;
}
// Window/Surface related events.
case PlatformEventType.Close:
{
info.Window.SendEvent(this, new WindowCloseEvent());
break;
}
case PlatformEventType.WindowFramebufferResize:
{
var resize = (WindowFramebufferResizeEventArgs)args;
info.Window.SendEvent(this, new ResizeEventArgs());
info.Window.SendEvent(this, new PaintEventArgs(info.Window.DeviceContext));
break;
}
case PlatformEventType.WindowResize:
{
var resize = (WindowResizeEventArgs)args;
info.Window.SendEvent(this, new ResizeEventArgs());
info.Window.SendEvent(this, new PaintEventArgs(info.Window.DeviceContext));
break;
}
default:
Debugger?.LogDebug($"Unknown event type {type} with \"{args}\".");
break;
}
}
private static ModifierKeys GetModifierKeys(KeyModifier modifier)
{
ModifierKeys keys = 0;
keys |= modifier.HasFlag(KeyModifier.NumLock) ? ModifierKeys.NumLock : 0;
keys |= modifier.HasFlag(KeyModifier.CapsLock) ? ModifierKeys.CapsLock : 0;
keys |= modifier.HasFlag(KeyModifier.ScrollLock) ? ModifierKeys.ScrollLock : 0;
keys |= modifier.HasFlag(KeyModifier.LeftShift) ? ModifierKeys.LeftShift : 0;
keys |= modifier.HasFlag(KeyModifier.LeftControl) ? ModifierKeys.LeftControl : 0;
keys |= modifier.HasFlag(KeyModifier.LeftAlt) ? ModifierKeys.LeftAlt : 0;
keys |= modifier.HasFlag(KeyModifier.LeftGUI) ? ModifierKeys.LeftMeta : 0;
keys |= modifier.HasFlag(KeyModifier.RightShift) ? ModifierKeys.RightShift : 0;
keys |= modifier.HasFlag(KeyModifier.RightControl) ? ModifierKeys.RightControl : 0;
keys |= modifier.HasFlag(KeyModifier.RightAlt) ? ModifierKeys.RightAlt : 0;
keys |= modifier.HasFlag(KeyModifier.RightGUI) ? ModifierKeys.RightMeta : 0;
keys |= modifier.HasFlag(KeyModifier.Shift) ? ModifierKeys.Shift : 0;
keys |= modifier.HasFlag(KeyModifier.Control) ? ModifierKeys.Control : 0;
keys |= modifier.HasFlag(KeyModifier.Alt) ? ModifierKeys.Alt : 0;
keys |= modifier.HasFlag(KeyModifier.GUI) ? ModifierKeys.Meta : 0;
// C# makes this cast as annoying as possible.
keys |= (ModifierKeys)((((int)keys >> (int)ModifierKeys.RightBitPos) & 0xF) |
(((int)keys >> (int)ModifierKeys.LeftBitPos) & 0xF));
return keys;
}
private record WindowExtraInfo(PhysicalWindow Window)
{
public Vector2 MousePosition { get; set; } = Vector2.Zero;
}
// TODO: Keycode and scancode tables.
private static KeyCode GetKeyCode(Key key) => key switch
{
_ => (KeyCode)0,
};
private static ScanCode GetScanCode(Scancode scanCode) => scanCode switch
{
_ => (ScanCode)0,
};
}
internal class ApplicationQuitEventArgs() : EventArgs
{
}
}

View File

@@ -0,0 +1,121 @@
using System.Collections.Concurrent;
using System.Drawing;
using Dashboard.OpenGL;
using Dashboard.Pal;
using Dashboard.Windowing;
using OpenTK.Mathematics;
using OpenTK.Platform;
using TK = OpenTK.Platform.Toolkit;
namespace Dashboard.OpenTK.PAL2
{
public class Pal2GLContext : IGLContext, IDisposable
{
public OpenGLContextHandle ContextHandle { get; }
public WindowHandle WindowHandle { get; }
public ISwapGroup SwapGroup { get; }
public int ContextGroup { get; }
public System.Numerics.Vector2 FramebufferSize
{
get
{
TK.Window.GetFramebufferSize(WindowHandle, out Vector2i size);
return new System.Numerics.Vector2(size.X, size.Y);
}
}
public event Action? Disposed;
public Pal2GLContext(WindowHandle window, OpenGLContextHandle context)
{
WindowHandle = window;
ContextHandle = context;
SwapGroup = new DummySwapGroup(context);
ContextGroup = GetContextGroup(ContextHandle);
}
public void MakeCurrent()
{
TK.OpenGL.SetCurrentContext(ContextHandle);
}
public IntPtr GetProcAddress(string procName)
{
return TK.OpenGL.GetProcedureAddress(ContextHandle, procName);
}
public void Dispose() => Dispose(true);
protected void Dispose(bool isDisposing)
{
if (SwapGroup is IGLDisposable glDisposable)
{
glDisposable.Dispose(isDisposing);
}
else if (SwapGroup is IDisposable disposable)
{
disposable.Dispose();
}
Disposed?.Invoke();
}
private static int _contextGroupId = 0;
private static ConcurrentDictionary<OpenGLContextHandle, int> _contextGroupRootContexts = new ConcurrentDictionary<OpenGLContextHandle, int>();
private Size _framebufferSize;
private static int GetContextGroup(OpenGLContextHandle handle)
{
OpenGLContextHandle? shared = TK.OpenGL.GetSharedContext(handle);
if (shared == null)
{
if (_contextGroupRootContexts.TryGetValue(handle, out int group))
return group;
group = Interlocked.Increment(ref _contextGroupId);
_contextGroupRootContexts.TryAdd(handle, group);
return GetContextGroup(handle);
}
else
{
if (_contextGroupRootContexts.TryGetValue(shared, out int group))
return group;
return GetContextGroup(shared);
}
}
public class DummySwapGroup : ISwapGroup
{
public OpenGLContextHandle ContextHandle { get; }
public int SwapInterval
{
get
{
TK.OpenGL.SetCurrentContext(ContextHandle);
return TK.OpenGL.GetSwapInterval();
}
set
{
TK.OpenGL.SetCurrentContext(ContextHandle);
TK.OpenGL.SetSwapInterval(value);
}
}
public DummySwapGroup(OpenGLContextHandle handle)
{
ContextHandle = handle;
}
public void Swap()
{
TK.OpenGL.SwapBuffers(ContextHandle);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More