59 Commits

Author SHA1 Message Date
themixedupstuff aaa79d1878 Add Image member to image brushes. 2026-03-21 20:05:06 +03:00
themixedupstuff f9e374db50 Fix double initialization bug. 2026-03-21 20:04:50 +03:00
themixedupstuff 5bcc2d8214 Write partial scan code translation table. 2026-02-07 15:40:54 +03:00
themixedupstuff 804a3979fd Write out Win32 based Scancode table. 2026-02-07 15:40:38 +03:00
themixedupstuff d299084db5 Fix deafult font structure causing issues when passed to InternFont(). 2026-02-07 15:40:24 +03:00
themixedupstuff d20d929515 Call base method in Form class. 2026-02-02 23:16:17 +03:00
themixedupstuff 3b9fa5c9fb Implement a very botched input scancode mapping. 2026-02-02 23:16:05 +03:00
themixedupstuff 027ebe8dbd Bubble some OpenGL elements up to the DC impl. 2026-02-02 22:02:32 +03:00
themixedupstuff 36dbcac8d9 Make Pal2Application handle Toolkit.Init() instead. 2026-02-01 00:01:08 +03:00
themixedupstuff 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
themixedupstuff e30e50e860 Unfinished work I want to document in the Git history. 2026-01-05 22:26:34 +03:00
themixedupstuff 4f67e0fb75 Create basic controls without much regards to layout rules. 2025-11-21 23:04:37 +03:00
themixedupstuff 5cba1ab7db Give label a proper implementation. 2025-11-19 20:12:52 +03:00
themixedupstuff 9ca309bd52 Remove all old project files. 2025-11-17 22:00:58 +03:00
themixedupstuff 043060db66 Rework the library architecture. 2025-11-17 21:59:07 +03:00
themixedupstuff 66c5eecc26 Move StbImage to its own assembly. 2025-11-14 22:31:54 +03:00
themixedupstuff 9ee6c1180d Add new functions to extension system. 2025-11-14 22:28:21 +03:00
themixedupstuff c4fe4841fe Work on creating a simple message box dialog. 2025-11-13 21:35:01 +03:00
themixedupstuff 2932b3b85e Implement new immediate mode. 2025-11-12 21:24:25 +03:00
themixedupstuff 6e8888df48 Add CreatePhysicalWindow(). 2025-11-09 12:00:21 +03:00
themixedupstuff edc85c3f24 Create a new context system for dashboard. 2025-08-05 23:50:29 +03:00
themixedupstuff 50eda46b13 Fix dependency versions. 2025-07-31 20:59:44 +03:00
themixedupstuff c538dbd56b Implement type dictionaries, with hierarchical sets and unsets. 2025-07-25 23:51:14 +03:00
themixedupstuff 1dcf167022 Implement ReFuel style type atoms and type collections. 2025-07-24 23:16:24 +03:00
themixedupstuff 2690c5bec0 Commit WIP changes. 2025-04-29 23:20:54 +03:00
themixedupstuff 1c3c730e82 Some additional work on dashboard architecture. 2025-02-11 23:29:26 +03:00
themixedupstuff 2c957a0c1a Create some high level classes. 2025-02-02 21:58:49 +03:00
themixedupstuff 49257574f4 Add a few demo controls. 2025-01-25 15:39:18 +03:00
themixedupstuff 1cd005f827 Create the first immediate mode UI base. 2025-01-25 12:34:37 +03:00
themixedupstuff ef860f39bf Patch in BlurgText upstream as well. 2025-01-23 22:00:58 +03:00
themixedupstuff b1aadae0f2 Add initial version of text rendering. 2025-01-22 22:42:43 +03:00
themixedupstuff afa6ed4f4c Add new draw call recorder class. 2025-01-20 23:14:30 +03:00
themixedupstuff 57620f5d5c Fix missing variable command parameter data in DrawQueue. 2025-01-20 23:14:15 +03:00
themixedupstuff 78f06a2359 Fix null reference exception in TextCommand. 2025-01-20 23:13:56 +03:00
themixedupstuff 87ab64f727 Add IDpiAwareGLContext. 2025-01-19 16:48:15 +03:00
themixedupstuff 95cc1648b2 Implement basic commands. 2025-01-18 23:30:56 +03:00
themixedupstuff 5014d218e2 Work on resource classes and generic interfaces. 2025-01-18 20:43:42 +03:00
themixedupstuff 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
themixedupstuff cde0fe2901 Added new mathematical types and enums related to drawing. 2025-01-17 21:32:52 +03:00
themixedupstuff 1a52f17990 Add missing properties. 2025-01-15 20:42:09 +03:00
themixedupstuff b841dbe6c2 Begin implementing the GL engine. 2025-01-14 20:34:47 +03:00
themixedupstuff bb5b87417f Add clone to gradients. 2024-12-25 22:58:23 +03:00
themixedupstuff 165801800e Add basic text support. 2024-12-24 23:20:43 +03:00
themixedupstuff cf7cf9a77a Fix root namespaces. 2024-12-24 22:30:19 +03:00
themixedupstuff 3dff7438b3 Add gradient brushes. 2024-12-24 22:10:31 +03:00
themixedupstuff 6ad11812e2 Add hash function to Gradient. 2024-12-24 22:10:24 +03:00
themixedupstuff f8dea00021 Move basic types to Dashboard.Common. 2024-12-24 22:00:16 +03:00
themixedupstuff 7485ec5eaa Add Gradient. 2024-12-24 21:58:51 +03:00
themixedupstuff 6acaf0a7d5 Rename SolidBrush.cs to Brush.cs 2024-12-24 21:58:43 +03:00
themixedupstuff b5fda1ce3e Create ImageProperties.cs. 2024-12-22 23:27:48 +03:00
themixedupstuff 1067d3e188 Implement command iterator. 2024-12-14 20:44:27 +03:00
themixedupstuff 1b6bf09f95 Move types to Dashboard.Common. 2024-12-14 19:57:23 +03:00
themixedupstuff 509bad221f Add new commands. 2024-12-14 19:46:56 +03:00
themixedupstuff 9ab2edffee Change the IDrawCommand interface. 2024-12-14 19:46:47 +03:00
themixedupstuff 1a1e761326 Add custom hashcode to SolidBrush. 2024-12-14 19:43:50 +03:00
themixedupstuff 34720970ff Make bounds a Box3d. 2024-12-14 16:27:40 +03:00
themixedupstuff c706e16db1 Create Box3d structure. 2024-12-14 16:27:13 +03:00
themixedupstuff 8bc2685206 First code commit 2024-12-13 21:57:07 +03:00
themixedupstuff fb76258e41 Initial commit of the Dashboard rewrite. 2024-12-11 20:56:39 +03:00
194 changed files with 10947 additions and 8798 deletions
@@ -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;
}
}
}
@@ -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>
+21
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;
}
+18
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;
}
-27
View File
@@ -1,27 +0,0 @@
using BlurgText;
using Dashboard.ImmediateDraw;
using OpenTK.Mathematics;
namespace Dashboard.BlurgText
{
public static class BlurgCommand
{
public static void PutBlurgText(this DrawList list, DashboardBlurg blurg, BlurgResult result, Vector2 origin)
{
for (int i = 0; i < result.Count; i++)
{
BlurgRect rect = result[i];
Rectangle pos = new Rectangle()
{
Min = origin + new Vector2(rect.X, rect.Y),
Size = new Vector2(rect.Width, rect.Height)
};
Rectangle uv = new Rectangle(rect.U1, rect.V1, rect.U0, rect.V0);
list.Image(blurg.Images[(int)rect.UserData], pos, uv);
}
}
}
}
+21
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()
{
}
}
}
+214
View File
@@ -0,0 +1,214 @@
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 Font xfont)
font = xfont.Base;
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));
}
}
}
+10 -9
View File
@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard\Dashboard.csproj" />
<PackageReference Include="BlurgText" Version="0.1.0-nightly-4" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
</ItemGroup>
</Project>
-63
View File
@@ -1,63 +0,0 @@
using BlurgText;
using Dashboard.Media;
using Dashboard.Media.Color;
namespace Dashboard.BlurgText
{
public class DashboardBlurg : IDisposable
{
private readonly List<QImageBuffer> images = new List<QImageBuffer>();
public Blurg Blurg { get; }
public IReadOnlyList<QImageBuffer> Images => images.AsReadOnly();
public DashboardBlurg()
{
Blurg = new Blurg(TextureAllocationCallback, TextureUpdateCallback);
}
private nint TextureAllocationCallback(int width, int height)
{
QImageBuffer image = new QImageBuffer(ImageFormat.RgbaU8, width, height);
images.Add(image);
return images.Count - 1;
}
private void TextureUpdateCallback(nint userData, nint buffer, int x, int y, int width, int height)
{
if (width == 0 || height == 0)
return;
QImageLock src = new QImageLock(ImageFormat.RgbaU8, width, height, 1, buffer);
QImageBuffer image = images[(int)userData];
image.LockBits2d(out QImageLock dest, QImageLockOptions.Default);
src.CopyTo(dest, x, y);
image.UnlockBits();
}
private bool _isDisposed = false;
private void Dispose(bool disposing)
{
if (_isDisposed) return;
if (disposing)
{
Blurg.Dispose();
foreach (QImageBuffer image in images)
{
image.Dispose();
}
images.Clear();
}
_isDisposed = true;
}
public void Dispose() => Dispose(true);
}
}
+15
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
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
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);
}
}
}
+95
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() { }
}
}
@@ -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();
}
}
@@ -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();
}
}
+10
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>
+27
View File
@@ -0,0 +1,27 @@
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 Image Image { get; set; } = image;
public Box2d TextureCoordinates { get; set; } = new Box2d(0, 0, 1, 1);
}
public class NinePatchImageBrush(Image image) : Brush
{
public Image Image { get; set; } = image;
public Box2d CenterCoordinates { get; set; } = new Box2d(0, 0, 1, 1);
public Vector4 Extents { get; set; } = Vector4.Zero;
}
}
+42
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);
}
}
}
@@ -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();
}
}
+27
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);
}
}
+61
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);
}
}
@@ -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);
}
}
+12
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);
}
}
@@ -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();
}
}
+72
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));
}
}
}
+213
View File
@@ -0,0 +1,213 @@
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
{
// This scan code table is based on the Win32 scan code table.
// See https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
Error = 0xFF,
SystemPowerDown = 0xE05E,
SystemSleep = 0xE05F,
SystemWakeUp = 0xE063,
A = 0x1E,
B = 0x30,
C = 0x2E,
D = 0x20,
E = 0x12,
F = 0x21,
G = 0x22,
H = 0x23,
I = 0x17,
J = 0x24,
K = 0x25,
L = 0x26,
M = 0x32,
N = 0x31,
O = 0x18,
P = 0x19,
Q = 0x10,
R = 0x13,
S = 0x1F,
T = 0x14,
U = 0x16,
V = 0x2F,
W = 0x11,
X = 0x2D,
Y = 0x15,
Z = 0x2C,
D1 = 0x02,
D2 = 0x03,
D3 = 0x04,
D4 = 0x05,
D5 = 0x06,
D6 = 0x07,
D7 = 0x08,
D8 = 0x09,
D9 = 0x0A,
D0 = 0x0B,
Return = 0x1C,
Esc = 0x01,
Delete = 0x0E,
Tab = 0x0F,
Space = 0x39,
Dash = 0x0C,
Equals = 0x0D,
LBracket = 0x1A,
RBracket = 0x1B,
Backslash = 0x2B,
Semicolon = 0x27,
Apostrophe = 0x28,
Grave = 0x29,
Comma = 0x33,
Period = 0x34,
ForwardSlash = 0x35,
CapsLock = 0x3A,
F1 = 0x3B,
F2 = 0x3C,
F3 = 0x3D,
F4 = 0x3E,
F5 = 0x3F,
F6 = 0x40,
F7 = 0x41,
F8 = 0x42,
F9 = 0x43,
F10 = 0x44,
F11 = 0x45,
F12 = 0x46,
PrintScr = 0xE037,
ScrollLock = 0x46,
Pause = 0xE046,
Insert = 0xE052,
Home = 0xE047,
PageUp = 0xE049,
Backspace = 0xE053,
End = 0xE04F,
PageDown = 0xE051,
RightArrow = 0xE04D,
LeftArrow = 0xE04B,
DownArrow = 0xE050,
UpArrow = 0xE048,
NumLock = 0xE045,
NumDiv = 0xE035,
NumMul = 0x37,
NumSub = 0x4A,
NumAdd = 0x4E,
NumEnter = 0xE01C,
Num1 = 0x4F,
Num2 = 0x50,
Num3 = 0x51,
Num4 = 0x4B,
Num5 = 0x4C,
Num6 = 0x4D,
Num7 = 0x47,
Num8 = 0x48,
Num9 = 0x49,
Num0 = 0x52,
NumDecimal = 0x53,
NumBackslash = 0x56,
NumEquals = 0x59,
Application = 0xE05D,
F13 = 0x64,
F14 = 0x65,
F15 = 0x66,
F16 = 0x67,
F17 = 0x68,
F18 = 0x69,
F19 = 0x6A,
F20 = 0x6B,
F21 = 0x6C,
F22 = 0x6D,
F23 = 0x6E,
F24 = 0x76,
NumComma = 0x7E,
International1 = 0x73,
International2 = 0x70,
International3 = 0x7D,
International4 = 0x79,
International5 = 0x7B,
International6 = 0x5C,
Lang1 = 0x72,
Lang2 = 0x71,
Lang3 = 0x78,
Lang4 = 0x77,
LCtrl = 0x1D,
LShift = 0x2A,
LWin = 0xE05B,
LAlt = 0x38,
RAlt = 0xE038,
RWin = 0xE05C,
RCtrl = 0xE01D,
RShift = 0x36,
NextTrack = 0xE019,
PrevTrack = 0xE010,
Stop = 0xE024,
PlayPause = 0xE022,
VolUp = 0xE030,
VolDown = 0xE029,
Configuration = 0xE06D,
Email = 0xE06C,
Calculator = 0xE021,
Browser = 0xE06B,
Search = 0xE065,
HomePage = 0xE032,
Back = 0xE06A,
Forward = 0xE69,
Halt = 0xE068,
Refresh = 0xE67,
Bookmarks = 0xE066,
}
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;
}
}
+43
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;
}
}
+15
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;
}
}
}
+82
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)
{
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Dashboard.Events
{
public class WindowCloseEvent() : UiEventArgs(UiEventType.WindowClose)
{
public bool Cancel { get; set; } = false;
}
}
+41
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,
}
}
+227
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);
}
}
}
+38
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();
}
}
+211
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;
}
}
+18
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; }
}
}
+435
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;
}
}
}
+171
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;
}
}
}
+364
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;
// }
// }
}
}
+30
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,
}
}
+23
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
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}'.");
}
}
+30
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;
}
}
}
+17
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,
}
}
+202
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);
Initialize();
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;
}
}
}
+152
View File
@@ -0,0 +1,152 @@
using System.Numerics;
using Dashboard.Collections;
using Dashboard.Windowing;
namespace Dashboard.Pal
{
public abstract class DeviceContext : IContextBase<DeviceContext, IDeviceContextExtension>, IDeviceContext
{
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 abstract ISwapGroup SwapGroup { get; }
public abstract Vector2 FramebufferSize { get; }
public virtual bool DoubleBuffered { get; } = false;
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);
}
}
@@ -0,0 +1,7 @@
namespace Dashboard.Pal
{
public interface IApplicationExtension : IContextExtensionBase<Application>
{
}
}
+122
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);
}
}
+10
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);
}
}
@@ -0,0 +1,6 @@
namespace Dashboard.Pal
{
public interface IDeviceContextExtension : IContextExtensionBase<DeviceContext>
{
}
}
+43
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);
}
}
@@ -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; }
}
}
@@ -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);
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace Dashboard.Windowing
{
public interface IForm : IEventListener, IDisposable
{
public IWindow Window { get; }
public string Title { get; }
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace Dashboard.Windowing
{
public interface IPaintable
{
event EventHandler Painting;
/// <summary>
/// Paint this paintable object.
/// </summary>
void Paint();
}
}
+18
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();
}
}
+93
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; }
}
}
+48
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;
}
}
+184
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);
}
}
@@ -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();
}
}
}
@@ -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>
@@ -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;
}
}
}
@@ -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)!;
}
}
}
@@ -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);
}
}
}
}
@@ -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
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
+40
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;
}
}
}
@@ -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;
}
}
+24
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();
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace Dashboard.Drawing.OpenGL
{
public interface IInitializer
{
bool IsInitialized { get; }
void Initialize();
}
}
@@ -0,0 +1,7 @@
namespace Dashboard.Drawing.OpenGL
{
public interface IResourceManager
{
public string Name { get; }
}
}
+168
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;
}
}
}
@@ -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;
}
}
}
+52
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();
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
</ItemGroup>
</Project>
+292
View File
@@ -0,0 +1,292 @@
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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[]
{
BrushExtension.Instance,
})
{
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();
}
public struct PointCommandArgs : IParameterSerializer<PointCommandArgs>
{
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(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)
return CommandSize;
Span<Value> value = stackalloc Value[]
{
new Value(Position, Depth, Size, queue.RequireResource(Brush!))
};
MemoryMarshal.AsBytes(value).CopyTo(bytes);
return CommandSize;
}
[MemberNotNull(nameof(Brush))]
public void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes)
{
if (bytes.Length < CommandSize)
throw new Exception("Not enough bytes");
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(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;
}
}
}
+26
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
{
}
}
+107
View File
@@ -0,0 +1,107 @@
using System;
namespace Dashboard.Drawing
{
public interface IDrawCommand
{
/// <summary>
/// Name of the command.
/// </summary>
string Name { get; }
/// <summary>
/// The draw extension that defines this command.
/// </summary>
IDrawExtension Extension { get; }
/// <summary>
/// The length of the command data segment, in bytes.
/// </summary>
/// <remarks>
/// Must be 0 for simple commands. For commands that are variadic, the
/// value must be less than 0. Any other positive value, otherwise.
/// </remarks>
int Length { get; }
/// <summary>
/// Get the parameters object for this command.
/// </summary>
/// <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
{
/// <summary>
/// Get the parameters object for this command.
/// </summary>
/// <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
{
public string Name { get; }
public IDrawExtension Extension { get; }
public int Length { get; } = 0;
public DrawCommand(string name, IDrawExtension extension)
{
Name = name;
Extension = extension;
}
public object? GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return null;
}
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return 0;
}
}
public sealed class DrawCommand<T> : IDrawCommand<T>
where T : IParameterSerializer<T>, new()
{
public string Name { get; }
public IDrawExtension Extension { get; }
public int Length { get; }
public DrawCommand(string name, IDrawExtension extension, int length)
{
Name = name;
Extension = extension;
Length = length;
}
public T? GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
T t = new T();
t.Deserialize(queue, param);
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);
}
}
}
+141
View File
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing
{
/// <summary>
/// Interface for all drawing extensions.
/// </summary>
public interface IDrawExtension
{
/// <summary>
/// Name of this extension.
/// </summary>
public string Name { get; }
public IReadOnlyList<IDrawExtension> Requires { get; }
/// <summary>
/// The list of commands this extension defines, if any.
/// </summary>
public IReadOnlyList<IDrawCommand> Commands { get; }
}
/// <summary>
/// A simple draw extension.
/// </summary>
public class DrawExtension : IDrawExtension
{
private readonly List<IDrawCommand> _drawCommands = new List<IDrawCommand>();
public string Name { get; }
public IReadOnlyList<IDrawCommand> Commands { get; }
public IReadOnlyList<IDrawExtension> Requires { get; }
public DrawExtension(string name, IEnumerable<IDrawExtension>? requires = null)
{
Name = name;
Commands = _drawCommands.AsReadOnly();
Requires = (requires ?? Enumerable.Empty<IDrawExtension>()).ToImmutableList();
}
protected void AddCommand(IDrawCommand command)
{
_drawCommands.Add(command);
}
}
public static class DrawExtensionClass
{
/// <summary>
/// Get the draw controller for the given queue.
/// </summary>
/// <param name="extension">The extension instance.</param>
/// <param name="queue">The draw queue.</param>
/// <returns>The draw controller for this queue.</returns>
public static IDrawController GetController(this IDrawExtension extension, DrawQueue queue)
{
return queue.GetController(extension);
}
public static void Point(this DrawQueue queue, Vector2 position, float depth, float size, IBrush 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,
});
}
}
}
+368
View File
@@ -0,0 +1,368 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Dashboard.Drawing
{
public class DrawQueue : IEnumerable<ICommandFrame>, IDisposable
{
private readonly HashList<IDrawExtension> _extensions = new HashList<IDrawExtension>();
private readonly HashList<IDrawCommand> _commands = new HashList<IDrawCommand>();
private readonly HashList<IDrawResource> _resources = new HashList<IDrawResource>();
private readonly DrawController _controller;
private readonly MemoryStream _commandStream = new MemoryStream();
/// <summary>
/// The absolute boundary of all graphics objects.
/// </summary>
public Box3d Bounds { get; set; }
/// <summary>
/// The extensions required to draw the image.
/// </summary>
public IReadOnlyList<IDrawExtension> Extensions => _extensions;
/// <summary>
/// The resources used by this draw queue.
/// </summary>
public IReadOnlyList<IDrawResource> Resources => _resources;
/// <summary>
/// The list of commands used by the extension.
/// </summary>
public IReadOnlyList<IDrawCommand> Command => _commands;
public DrawQueue()
{
_controller = new DrawController(this);
}
/// <summary>
/// Clear the queue.
/// </summary>
public void Clear()
{
_resources.Clear();
_commands.Clear();
_extensions.Clear();
_commandStream.SetLength(0);
}
public int RequireExtension(IDrawExtension extension)
{
foreach (IDrawExtension super in extension.Requires)
RequireExtension(super);
return _extensions.Intern(extension);
}
public int RequireResource(IDrawResource resource)
{
RequireExtension(resource.Kind);
return _resources.Intern(resource);
}
internal IDrawController GetController(IDrawExtension extension)
{
_extensions.Intern(extension);
return _controller;
}
private void Write(IDrawCommand command)
{
if (command.Length > 0)
throw new InvalidOperationException("This command has a finite length argument.");
int cmdIndex = _commands.Intern(command);
Span<byte> cmd = stackalloc byte[6];
int sz;
if (command.Length == 0)
{
// Write a fixed command.
sz = ToVlq(cmdIndex, cmd);
}
else
{
// Write a variadic with zero length.
sz = ToVlq(cmdIndex, cmd);
cmd[sz++] = 0;
}
_commandStream.Write(cmd[..sz]);
}
private void Write(IDrawCommand command, ReadOnlySpan<byte> param)
{
if (command.Length < 0)
{
Span<byte> cmd = stackalloc byte[10];
int cmdIndex = _commands.Intern(command);
int sz = ToVlq(cmdIndex, cmd);
sz += ToVlq(param.Length, cmd[sz..]);
_commandStream.Write(cmd[..sz]);
_commandStream.Write(param);
}
else
{
if (command.Length != param.Length)
throw new ArgumentOutOfRangeException(nameof(param.Length), "Length of the parameter does not match the command.");
Span<byte> cmd = stackalloc byte[5];
int cmdIndex = _commands.Intern(command);
int sz = ToVlq(cmdIndex, cmd);
_commandStream.Write(cmd[..sz]);
_commandStream.Write(param);
}
}
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)
throw new ArgumentOutOfRangeException(nameof(value), "Must be a positive integer.");
else if (bytes.Length < 5)
throw new ArgumentOutOfRangeException(nameof(bytes), "Must at least be five bytes long.");
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);
}
return i;
}
private static int FromVlq(ReadOnlySpan<byte> bytes, out int value)
{
value = 0;
int i;
for (i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
value |= (b & 0x7F) << (7*i);
if ((b & (1 << 7)) == 0)
{
i++;
break;
}
}
return i;
}
public void Dispose()
{
throw new NotImplementedException();
}
private class DrawController(DrawQueue Queue) : IDrawController
{
public void EnsureBounds(Box2d bounds, float depth)
{
Queue.Bounds = Box3d.Union(Queue.Bounds, bounds, depth);
}
public void Write(IDrawCommand command)
{
Queue.Write(command);
}
public void Write(IDrawCommand command, ReadOnlySpan<byte> bytes)
{
Queue.Write(command, bytes);
}
public void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>
{
int length = param.Serialize(Queue, Span<byte>.Empty);
Span<byte> bytes = stackalloc byte[length];
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);
}
}
public class Enumerator : ICommandFrame, IEnumerator<ICommandFrame>
{
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 ICommandFrame Current => this;
object? IEnumerator.Current => Current;
public IDrawCommand Command => _current ?? throw new InvalidOperationException();
public bool HasParameters { get; private set; }
public Enumerator(DrawQueue queue)
{
_queue = queue;
_stream = queue._commandStream.GetBuffer();
_length = (int)queue._commandStream.Length;
}
public bool MoveNext()
{
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 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="bounds">The bounding box.</param>
void EnsureBounds(Box2d bounds, float depth);
/// <summary>
/// Write into the command stream.
/// </summary>
/// <param name="command">The command to write.</param>
void Write(IDrawCommand command);
/// <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(IDrawCommand command, ReadOnlySpan<byte> param);
/// <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<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>;
}
}
+9
View File
@@ -0,0 +1,9 @@
using Dashboard.Windowing;
namespace Dashboard.Drawing
{
public interface IDrawQueuePaintable : IPaintable
{
DrawQueue DrawQueue { get; }
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Dashboard.Drawing
{
/// <summary>
/// Interface for draw resources.
/// </summary>
public interface IDrawResource
{
/// <summary>
/// The extension for this kind of resource.
/// </summary>
IDrawExtension Kind { get; }
}
}
+14
View File
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dashboard.Drawing
{
public interface IParameterSerializer<T>
{
int Serialize(DrawQueue queue, Span<byte> bytes);
void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes);
}
}
+140
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;
}
}
+105
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;
// }
}
}
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+234
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:
}
}
}
@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LanguageVersion>7.3</LanguageVersion>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ReFuel.FreeType" Version="0.1.0-rc.5" />
<PackageReference Include="ReFuel.StbImage" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard\Dashboard.csproj" />
</ItemGroup>
</Project>
@@ -1,13 +0,0 @@
namespace Dashboard.Media.Defaults
{
internal static class EnvironmentVariables
{
public const string SerifFont = "QUIK_SERIF_FONT";
public const string SansFont = "QUIK_SANS_FONT";
public const string MonospaceFont = "QUIK_MONOSPACE_FONT";
public const string CursiveFont = "QUIK_CURSIVE_FONT";
public const string FantasyFont = "QUIK_FANTASY_FONT";
public const string FallbackFontDatabase = "QUIK_FALLBACK_FONT_DB";
}
}
-16
View File
@@ -1,16 +0,0 @@
using System;
using ReFuel.FreeType;
namespace Dashboard.Media.Defaults
{
public static class FTProvider
{
private static FTLibrary _ft;
public static FTLibrary Ft => _ft;
static FTProvider()
{
FT.InitFreeType(out _ft);
}
}
}
@@ -1,269 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.Json;
using ReFuel.FreeType;
using Dashboard.Media.Fonts;
using Dashboard.PAL;
using Dashboard.Media.Defaults.Linux;
namespace Dashboard.Media.Defaults.Fallback
{
public class FallbackFontDatabase : IFontDataBase
{
private readonly string DbPath =
Environment.GetEnvironmentVariable(EnvironmentVariables.FallbackFontDatabase) ??
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "QUIK/fontdb.json");
private Dictionary<FontFace, FileInfo> FilesMap { get; } = new Dictionary<FontFace, FileInfo>();
private Dictionary<string, List<FontFace>> ByFamily { get; } = new Dictionary<string, List<FontFace>>();
private Dictionary<SystemFontFamily, FontFace> SystemFonts { get; } = new Dictionary<SystemFontFamily, FontFace>();
private List<FontFace> All { get; } = new List<FontFace>();
IEnumerable<FontFace> IFontDataBase.All => this.All;
public FallbackFontDatabase(bool rebuild = false)
{
// Load existing database if desired.
List<DbEntry> database;
if(!rebuild)
{
database = LoadDatabase();
}
else
{
database = new List<DbEntry>();
}
VerifyDatabase(database);
FlushDatabase(database);
database.ForEach(x => AddFont(x.Face, new FileInfo(x.FilePath)));
(FontFace, FileInfo) serif = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SerifFont, LinuxFonts.DefaultSerifFamilies, this);
SystemFonts[SystemFontFamily.Serif] = serif.Item1;
(FontFace, FileInfo) sans = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SansFont, LinuxFonts.DefaultSansFamilies, this);
SystemFonts[SystemFontFamily.Sans] = sans.Item1;
(FontFace, FileInfo) mono = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.MonospaceFont, LinuxFonts.DefaultMonospaceFamilies, this);
SystemFonts[SystemFontFamily.Monospace] = mono.Item1;
(FontFace, FileInfo) cursive = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.CursiveFont, LinuxFonts.DefaultCursiveFamilies, this);
SystemFonts[SystemFontFamily.Cursive] = cursive.Item1;
(FontFace, FileInfo) fantasy = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.FantasyFont, LinuxFonts.DefaultFantasyFamilies, this);
SystemFonts[SystemFontFamily.Fantasy] = fantasy.Item1;
}
public FileInfo FontFileInfo(FontFace face)
{
if (FilesMap.TryGetValue(face, out FileInfo info))
return info;
else
return null;
}
public Stream Open(FontFace face)
{
return FontFileInfo(face)?.OpenRead() ?? throw new FileNotFoundException();
}
public IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All)
{
// A bit scuffed and LINQ heavy but it should work.
IEnumerable<FontFace> candidates;
if (criteria.HasFlag(FontMatchCriteria.Family))
{
List<FontFace> siblings;
if (!ByFamily.TryGetValue(prototype.Family, out siblings))
{
return Enumerable.Empty<FontFace>();
}
candidates = siblings;
}
else
{
candidates = All;
}
return
candidates
.Where(x =>
implies(criteria.HasFlag(FontMatchCriteria.Slant), prototype.Slant == x.Slant) ||
implies(criteria.HasFlag(FontMatchCriteria.Weight), prototype.Weight == x.Weight) ||
implies(criteria.HasFlag(FontMatchCriteria.Stretch), prototype.Stretch == x.Stretch)
)
.OrderByDescending(x =>
(prototype.Slant == x.Slant ? 1 : 0) +
(prototype.Weight == x.Weight ? 1 : 0) +
(prototype.Stretch == x.Stretch ? 1 : 0) +
confidence(prototype.Family, x.Family) * 3
);
// a => b = a'+b
static bool implies(bool a, bool b)
{
return !a || b;
}
static int confidence(string target, string testee)
{
int i;
for (i = 0; i < target.Length && i < testee.Length && target[i] == testee[i]; i++);
return i;
}
}
public FontFace GetSystemFontFace(SystemFontFamily family)
{
return SystemFonts[family];
}
private void AddFont(FontFace face, FileInfo file)
{
if (!All.Contains(face))
All.Add(face);
FilesMap.TryAdd(face, file);
if (!ByFamily.TryGetValue(face.Family, out List<FontFace> siblings))
{
siblings = new List<FontFace>();
ByFamily.Add(face.Family, siblings);
}
if (!siblings.Contains(face))
siblings.Add(face);
}
private List<DbEntry> LoadDatabase()
{
FileInfo info = new FileInfo(DbPath);
if (!info.Exists)
return new List<DbEntry>();
using Stream str = info.OpenRead();
try
{
return JsonSerializer.Deserialize<List<DbEntry>>(str);
}
catch
{
return new List<DbEntry>();
}
}
private void VerifyDatabase(List<DbEntry> db)
{
// Very slow way to do this but how many fonts could a system have on average?
Dictionary<string, DbEntry> entires = new Dictionary<string, DbEntry>();
foreach (DbEntry entry in db)
{
FileInfo info = new FileInfo(entry.FilePath);
// Reprocess fonts that appear like this.
if (!info.Exists) continue;
else if (info.LastWriteTime > entry.AccessTime) continue;
}
string fontpath = null;
try
{
fontpath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
if (string.IsNullOrEmpty(fontpath))
throw new Exception();
}
catch
{
foreach (string path in FontPaths)
{
if (Directory.Exists(path))
{
fontpath = path;
break;
}
}
// rip
if (string.IsNullOrEmpty(fontpath))
return;
}
SearchPathForFonts(entires, fontpath);
db.Clear();
db.AddRange(entires.Values);
}
private static void SearchPathForFonts(Dictionary<string, DbEntry> entries, string path)
{
DirectoryInfo dir = new DirectoryInfo(path);
foreach (FileInfo file in dir.EnumerateFiles())
{
SearchFileForFonts(entries, file);
}
foreach (DirectoryInfo directory in dir.EnumerateDirectories())
{
SearchPathForFonts(entries, directory.FullName);
}
}
private static void SearchFileForFonts(Dictionary<string, DbEntry> entries, FileInfo file)
{
if (entries.ContainsKey(file.FullName))
return;
if (FT.NewFace(FTProvider.Ft, file.FullName, 0, out FTFace face) != FTError.None)
return;
FontFace facename = FontFace.Parse(face.FamilyName, face.StyleName);
DbEntry entry = new DbEntry(facename, file.FullName);
entries.Add(file.FullName, entry);
FT.DoneFace(face);
}
private void FlushDatabase(List<DbEntry> db)
{
FileInfo info = new FileInfo(DbPath);
Directory.CreateDirectory(Path.GetDirectoryName(DbPath));
using Stream str = info.Open(FileMode.Create);
JsonSerializer.Serialize(str, db);
}
private static readonly string[] FontPaths = new string[] {
"/usr/share/fonts",
};
[JsonSerializable(typeof(DbEntry))]
private class DbEntry
{
[JsonIgnore] public FontFace Face => new FontFace(Family, Slant, Weight, Stretch);
public string Family { get; set; }
public FontSlant Slant { get; set; }
public FontWeight Weight { get; set; }
public FontStretch Stretch { get; set; }
public string FilePath { get; set; }
public DateTime AccessTime { get; set; }
public DbEntry() {}
public DbEntry(FontFace face, string path)
{
Family = face.Family;
Slant = face.Slant;
Weight = face.Weight;
Stretch = face.Stretch;
FilePath = path;
AccessTime = DateTime.Now;
}
}
}
}
@@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ReFuel.FreeType;
using Dashboard.Media.Defaults.Fallback;
using Dashboard.Media.Defaults.Linux;
using Dashboard.Media.Fonts;
using Dashboard.PAL;
namespace Dashboard.Media.Defaults
{
public static class FontDataBaseProvider
{
public static IFontDataBase Instance { get; }
static FontDataBaseProvider()
{
try
{
// TODO: add as other operating systems are supported.
if (OperatingSystem.IsLinux())
{
Instance = new FontConfigFontDatabase();
}
else
{
Instance = new FallbackFontDatabase();
}
}
catch (Exception ex)
{
throw new NotSupportedException("Could not load a suitable font database implementation.", ex);
}
}
public static (FontFace, FileInfo) ResolveSystemFont(string envVar, string defaults, IFontDataBase db)
{
StringBuilder builder = new StringBuilder();
string user = Environment.GetEnvironmentVariable(envVar);
if (user != null)
{
builder.Append(user);
builder.Append(':');
}
builder.Append(defaults);
string[] list = builder.ToString().Split(':');
foreach (string item in list)
{
if (File.Exists(item))
{
// Process file.
if (FT.NewFace(FTProvider.Ft, item, 0, out FTFace ftface) != FTError.None)
{
continue;
}
FontFace face = FontFace.Parse(ftface.FamilyName, ftface.StyleName);
FT.DoneFace(ftface);
return (face, new FileInfo(item));
}
else
{
IEnumerable<FontFace> faces = db.Search(
new FontFace(item, FontSlant.Normal, FontWeight.Normal, FontStretch.Normal),
FontMatchCriteria.Family);
if (faces.Any())
{
FontFace face = faces.First();
return (face, db.FontFileInfo(face));
}
}
}
try
{
FontFace face = db.GetSystemFontFace(SystemFontFamily.Sans);
return (face, db.FontFileInfo(face));
}
catch (Exception ex)
{
throw new NotImplementedException("No fallback font yet.", ex);
}
}
}
}
-84
View File
@@ -1,84 +0,0 @@
using System;
using System.Buffers;
using System.IO;
using ReFuel.FreeType;
using Dashboard.Media.Color;
using Dashboard.Media.Fonts;
using OpenTK.Mathematics;
namespace Dashboard.Media.Defaults
{
public class FontFreeType : Font
{
private MemoryStream ms;
private FTFace face;
public override FontFace Face => throw new NotImplementedException();
public FontFreeType(Stream stream)
{
ms = new MemoryStream();
stream.CopyTo(ms);
FTError e = FT.NewMemoryFace(Ft, ms.GetBuffer(), ms.Length, 0, out face);
if (e != FTError.None)
{
throw new Exception("Could not load font face from stream.");
}
}
public override bool HasRune(int rune)
{
return FT.GetCharIndex(face, (ulong)rune) != 0;
}
protected override Image Render(out GlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options)
{
FT.SetCharSize(face, 0, (long)Math.Round(64*size), 0, (uint)Math.Round(options.Resolution));
uint index = FT.GetCharIndex(face, (ulong)codepoint);
FT.LoadGlyph(face, index, FTLoadFlags.Default);
ref readonly FTGlyphMetrics ftmetrics = ref face.Glyph.Metrics;
metrics = new GlyphMetrics(codepoint,
new Vector2(ftmetrics.Width/64f, ftmetrics.Height/64f),
new Vector2(ftmetrics.HorizontalBearingX/64f, ftmetrics.HorizontalBearingY/64f),
new Vector2(ftmetrics.VerticalBearingX/64f, ftmetrics.VerticalBearingY/64f),
new Vector2(ftmetrics.HorizontalAdvance/64f, ftmetrics.VerticalAdvance/64f)
);
FT.RenderGlyph(face.Glyph, options.Sdf ? FTRenderMode.Sdf : FTRenderMode.Normal);
ref readonly FTBitmap bitmap = ref face.Glyph.Bitmap;
if (bitmap.Width == 0 || bitmap.Pitch == 0 || bitmap.Buffer == IntPtr.Zero)
{
return null;
}
QImageBuffer image = new QImageBuffer(ImageFormat.AlphaU8, (int)bitmap.Width, (int)bitmap.Rows);
image.LockBits2d(out QImageLock lk, QImageLockOptions.Default);
unsafe
{
Buffer.MemoryCopy((void*)bitmap.Buffer, (void*)lk.ImagePtr, lk.Width * lk.Height, bitmap.Width * bitmap.Rows);
}
image.UnlockBits();
return image;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
ms.Dispose();
}
FT.DoneFace(face);
}
private static FTLibrary Ft => FTProvider.Ft;
}
}
@@ -1,24 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Dashboard.PAL;
namespace Dashboard.Media.Defaults
{
public class FreeTypeFontFactory : IFontFactory
{
public bool TryOpen(Stream stream, [NotNullWhen(true)] out Font font)
{
try
{
font = new FontFreeType(stream);
return true;
}
catch
{
font = null;
return false;
}
}
}
}
-98
View File
@@ -1,98 +0,0 @@
using System;
using System.IO;
using Dashboard.Media.Color;
using ReFuel.Stb;
namespace Dashboard.Media.Defaults
{
public unsafe class ImageStbi : Image
{
private readonly StbImage image;
private QImageBuffer buffer;
private bool isSdf = false;
public override int Width => image.Width;
public override int Height => image.Height;
public override int Depth => 1;
public override bool IsSdf => isSdf;
public override ImageFormat InternalFormat => Stb2QImageFormat(image.Format);
public ImageStbi(Stream source)
{
// According to the stbi documentation, only a specific type of PNG
// files are premultiplied out of the box (iPhone PNG). Take the
// precision loss L and move on.
StbImage.FlipVerticallyOnLoad = true;
StbImage.UnpremultiplyOnLoad = true;
image = StbImage.Load(source);
}
public static ImageFormat Stb2QImageFormat(StbiImageFormat src)
{
switch (src)
{
case StbiImageFormat.Grey: return ImageFormat.RedU8;
case StbiImageFormat.Rgb: return ImageFormat.RgbU8;
case StbiImageFormat.Rgba: return ImageFormat.RgbaU8;
case StbiImageFormat.GreyAlpha: return ImageFormat.RaU8;
default: return ImageFormat.Undefined;
}
}
public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options)
{
if (options.MipLevel > 0) throw new Exception("This image has no mip levels.");
buffer?.Dispose();
buffer = new QImageBuffer(options.Format, Width, Height);
buffer.LockBits2d(out QImageLock dst, QImageLockOptions.Default);
byte *srcPtr = (byte*)image.ImagePointer;
QImageLock src = new QImageLock(InternalFormat, Width, Height, 1, (IntPtr)srcPtr);
FormatConvert.Convert(dst, src);
if (options.Premultiply)
{
FormatConvert.Premultiply(dst);
}
imageLock = dst;
}
public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options)
{
LockBits2d(out imageLock, options);
}
public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth)
{
if (depth != 1) throw new ArgumentOutOfRangeException(nameof(depth));
LockBits2d(out imageLock, options);
}
public override void UnlockBits()
{
buffer.UnlockBits();
}
public void SdfHint(bool value = true)
{
isSdf = value;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
buffer?.Dispose();
image.Dispose();
}
}
}
}
@@ -1,230 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Text;
using Dashboard;
namespace Dashboard.Media.Defaults
{
public static unsafe class FontConfig
{
private const string fontconfig = "fontconfig";
public static bool Exists { get; }
public static IntPtr FAMILY { get; } = Marshal.StringToHGlobalAnsi("family");
public static IntPtr STYLE { get; } = Marshal.StringToHGlobalAnsi("style");
public static IntPtr FILE { get; } = Marshal.StringToHGlobalAnsi("file");
public static IntPtr WEIGHT { get; } = Marshal.StringToHGlobalAnsi("weight");
public static IntPtr SLANT { get; } = Marshal.StringToHGlobalAnsi("slant");
static FontConfig()
{
try
{
if (FcInitLoadConfigAndFonts() == null)
{
Exists = false;
}
Exists = true;
}
catch
{
Exists = false;
}
}
[DllImport(fontconfig, EntryPoint = "FcInitLoadConfigAndFonts")]
public static extern FcConfig* FcInitLoadConfigAndFonts();
[DllImport(fontconfig, EntryPoint = "FcConfigGetCurrent")]
public static extern FcConfig ConfigGetCurrent();
[DllImport(fontconfig, EntryPoint = "FcPatternCreate")]
public static extern FcPattern PatternCreate();
[DllImport(fontconfig, EntryPoint = "FcPatternCreate")]
public static extern bool FcPatternAdd(FcPattern pattern, IntPtr what, FcValue value, bool append);
[DllImport(fontconfig, EntryPoint = "FcObjectSetBuild", CallingConvention = CallingConvention.Cdecl)]
public static extern FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4, IntPtr i5, IntPtr i6);
public static FcObjectSet ObjectSetBuild(IntPtr i1)
{
return ObjectSetBuild(i1, IntPtr.Zero);
}
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2)
{
return ObjectSetBuild(i1, i2, IntPtr.Zero);
}
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3)
{
return ObjectSetBuild(i1, i2, i3, IntPtr.Zero);
}
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4)
{
return ObjectSetBuild(i1, i2, i3, i4, IntPtr.Zero);
}
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4, IntPtr i5)
{
return ObjectSetBuild(i1, i2, i3, i4, i5, IntPtr.Zero);
}
[DllImport(fontconfig, EntryPoint = "FcFontList")]
public static extern FcFontSet FontList(FcConfig config, FcPattern pattern, FcObjectSet objectSet);
[DllImport(fontconfig, EntryPoint = "FcNameUnparse")]
public static extern IntPtr NameUnparse(FcPattern pat);
public static string NameUnparseStr(FcPattern pat) => Marshal.PtrToStringAnsi(NameUnparse(pat));
[DllImport(fontconfig, EntryPoint = "FcPatternGetString")]
public static extern FcResult PatternGetString(FcPattern p, IntPtr what, int n, out IntPtr val);
public static FcResult PatternGetString(FcPattern p, IntPtr what, out string str)
{
FcResult i = PatternGetString(p, what, 0, out IntPtr ptr);
if (i == FcResult.Match)
{
str = Marshal.PtrToStringAnsi(ptr);
}
else
{
str = null;
}
Marshal.FreeHGlobal(ptr);
return i;
}
[DllImport(fontconfig, EntryPoint = "FcPatternGet")]
public static extern FcResult PatternGet(FcPattern p, IntPtr what, int id, out FcValue value);
[DllImport(fontconfig, EntryPoint = "FcFontSetDestroy")]
public static extern void FontSetDestroy(FcFontSet fs);
[DllImport(fontconfig, EntryPoint = "FcObjectSetDestroy")]
public static extern void ObjectSetDestroy (FcObjectSet os);
[DllImport(fontconfig, EntryPoint = "FcConfigDestroy")]
public static extern void ConfigDestroy (FcConfig cfg);
[DllImport(fontconfig, EntryPoint = "FcPatternDestroy")]
public static extern void PatternDestroy (FcPattern os);
#region Range
[DllImport(fontconfig, EntryPoint = "FcRangeCreateDouble")]
public static extern IntPtr RangeCreateDouble(double begin, double end);
[DllImport(fontconfig, EntryPoint = "FcRangeCreateInteger")]
public static extern IntPtr RangeCreateInteger (int begin, int end);
[DllImport(fontconfig, EntryPoint = "FcRangeDestroy")]
public static extern void RangeDestroy(IntPtr range);
[DllImport(fontconfig, EntryPoint = "FcRangeCopy")]
public static extern IntPtr RangeCopy (IntPtr range);
[DllImport(fontconfig, EntryPoint = "FcRangeGetDouble")]
public static extern bool RangeGetDouble(IntPtr range, out double start, out double end);
#endregion
}
public enum FcResult
{
Match,
NoMatch,
TypeMismatch,
NoId,
OutOfMemory
}
public struct FcConfig
{
public readonly IntPtr Handle;
}
public struct FcPattern
{
public readonly IntPtr Handle;
}
public unsafe struct FcObjectSet
{
public readonly IntPtr Handle;
private Accessor* AsPtr => (Accessor*)Handle;
public int NObject => AsPtr->nobject;
public int SObject => AsPtr->sobject;
#pragma warning disable CS0649 // Will always have default value.
private struct Accessor
{
public int nobject;
public int sobject;
public byte** objects;
}
#pragma warning restore CS0649
}
public unsafe struct FcFontSet
{
public readonly IntPtr Handle;
private Accessor* AsPtr => (Accessor*)Handle;
public int NFont => AsPtr->nfont;
public int SFont => AsPtr->sfont;
public FcPattern this[int i]
{
get
{
if (i < 0 || i >= NFont)
throw new IndexOutOfRangeException();
return AsPtr->fonts[i];
}
}
#pragma warning disable CS0649 // Will always have default value.
private struct Accessor
{
public int nfont;
public int sfont;
public FcPattern* fonts;
}
#pragma warning restore CS0649
}
public enum FcType
{
Unknown = -1,
Void,
Integer,
Double,
String,
Bool,
Matrix,
CharSet,
FTFace,
LangSet,
Range
}
[StructLayout(LayoutKind.Explicit)]
public readonly struct FcValue
{
[FieldOffset(0)] public readonly FcType Type;
[FieldOffset(sizeof(FcType))] public readonly IntPtr Pointer;
[FieldOffset(sizeof(FcType))] public readonly int Int;
[FieldOffset(sizeof(FcType))] public readonly double Double;
}
}
@@ -1,168 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using ReFuel.FreeType;
using Dashboard.Media.Fonts;
using Dashboard.PAL;
namespace Dashboard.Media.Defaults.Linux
{
/// <summary>
/// Font database for Linux libfontconfig systems.
/// </summary>
public class FontConfigFontDatabase : IFontDataBase
{
private Dictionary<FontFace, FileInfo> FilesMap { get; } = new Dictionary<FontFace, FileInfo>();
private Dictionary<string, List<FontFace>> ByFamily { get; } = new Dictionary<string, List<FontFace>>();
private Dictionary<SystemFontFamily, FontFace> SystemFonts { get; } = new Dictionary<SystemFontFamily, FontFace>();
private List<FontFace> All { get; } = new List<FontFace>();
IEnumerable<FontFace> IFontDataBase.All => this.All;
public FontConfigFontDatabase()
{
if (!FontConfig.Exists)
{
throw new NotSupportedException("This host doesn't have fontconfig installed.");
}
FcConfig config = FontConfig.ConfigGetCurrent();
FcPattern pattern = FontConfig.PatternCreate();
FcObjectSet os = FontConfig.ObjectSetBuild(FontConfig.FAMILY, FontConfig.STYLE, FontConfig.FILE);
FcFontSet fs = FontConfig.FontList(config, pattern, os);
for (int i = 0; i < fs.NFont; i++)
{
FcPattern current = fs[i];
if (
FontConfig.PatternGetString(current, FontConfig.FAMILY, 0, out IntPtr pFamily) != FcResult.Match ||
FontConfig.PatternGetString(current, FontConfig.STYLE, 0, out IntPtr pStyle) != FcResult.Match)
{
continue;
}
string family = Marshal.PtrToStringUTF8(pFamily);
string style = Marshal.PtrToStringUTF8(pStyle);
FontFace face = FontFace.Parse(family, style);
FontConfig.PatternGetString(current, FontConfig.FILE, 0, out IntPtr pFile);
string file = Marshal.PtrToStringAnsi(pFile);
AddFont(face, new FileInfo(file));
}
FontConfig.FontSetDestroy(fs);
FontConfig.ObjectSetDestroy(os);
FontConfig.PatternDestroy(pattern);
(FontFace, FileInfo) serif = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SerifFont, LinuxFonts.DefaultSerifFamilies, this);
SystemFonts[SystemFontFamily.Serif] = serif.Item1;
(FontFace, FileInfo) sans = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SansFont, LinuxFonts.DefaultSansFamilies, this);
SystemFonts[SystemFontFamily.Sans] = sans.Item1;
(FontFace, FileInfo) mono = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.MonospaceFont, LinuxFonts.DefaultMonospaceFamilies, this);
SystemFonts[SystemFontFamily.Monospace] = mono.Item1;
(FontFace, FileInfo) cursive = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.CursiveFont, LinuxFonts.DefaultCursiveFamilies, this);
SystemFonts[SystemFontFamily.Cursive] = cursive.Item1;
(FontFace, FileInfo) fantasy = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.FantasyFont, LinuxFonts.DefaultFantasyFamilies, this);
SystemFonts[SystemFontFamily.Fantasy] = fantasy.Item1;
AddFont(serif.Item1, serif.Item2);
AddFont(sans.Item1, sans.Item2);
AddFont(mono.Item1, mono.Item2);
AddFont(cursive.Item1, cursive.Item2);
AddFont(fantasy.Item1, fantasy.Item2);
}
private void AddFont(FontFace face, FileInfo file)
{
if (!All.Contains(face))
All.Add(face);
FilesMap.TryAdd(face, file);
if (!ByFamily.TryGetValue(face.Family, out List<FontFace> siblings))
{
siblings = new List<FontFace>();
ByFamily.Add(face.Family, siblings);
}
if (!siblings.Contains(face))
siblings.Add(face);
}
public IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All)
{
// A bit scuffed and LINQ heavy but it should work.
IEnumerable<FontFace> candidates;
if (criteria.HasFlag(FontMatchCriteria.Family))
{
List<FontFace> siblings;
if (!ByFamily.TryGetValue(prototype.Family, out siblings))
{
return Enumerable.Empty<FontFace>();
}
candidates = siblings;
}
else
{
candidates = All;
}
return
candidates
.Where(x =>
implies(criteria.HasFlag(FontMatchCriteria.Slant), prototype.Slant == x.Slant) ||
implies(criteria.HasFlag(FontMatchCriteria.Weight), prototype.Weight == x.Weight) ||
implies(criteria.HasFlag(FontMatchCriteria.Stretch), prototype.Stretch == x.Stretch)
)
.OrderByDescending(x =>
(prototype.Slant == x.Slant ? 1 : 0) +
(prototype.Weight == x.Weight ? 1 : 0) +
(prototype.Stretch == x.Stretch ? 1 : 0) +
confidence(prototype.Family, x.Family) * 3
);
// a => b = a'+b
static bool implies(bool a, bool b)
{
return !a || b;
}
static int confidence(string target, string testee)
{
int i;
for (i = 0; i < target.Length && i < testee.Length && target[i] == testee[i]; i++);
return i;
}
}
public FileInfo FontFileInfo(FontFace face)
{
if (FilesMap.TryGetValue(face, out FileInfo info))
return info;
else
return null;
}
public Stream Open(FontFace face)
{
return FontFileInfo(face)?.OpenRead() ?? throw new FileNotFoundException();
}
public FontFace GetSystemFontFace(SystemFontFamily family)
{
return SystemFonts[family];
}
}
}
@@ -1,11 +0,0 @@
namespace Dashboard.Media.Defaults.Linux
{
internal static class LinuxFonts
{
public const string DefaultSerifFamilies = "Noto Serif:Nimbus Roman:Liberation Serif:FreeSerif:Times:Times New Roman";
public const string DefaultSansFamilies = "Noto Sans:Nimbus Sans:Droid Sans:Liberation Sans:FreeSans:Helvetica Neue:Helvetica:Arial";
public const string DefaultMonospaceFamilies = "Noto Mono:Nimbus Mono PS:Liberation Mono:DejaVu Mono:FreeMono:Lucida Console:Consolas:Courier:Courier New";
public const string DefaultCursiveFamilies = "";
public const string DefaultFantasyFamilies = "";
}
}

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