142 Commits

Author SHA1 Message Date
themixedupstuff c6a9dd8008 Change the way container controls work and add Grid layout. 2024-08-04 13:21:00 +03:00
themixedupstuff ff3b3ee961 Rename UserData to Attribute. 2024-08-02 21:20:35 +03:00
themixedupstuff 9b6a15c6ec Add userdata fields to all control hierarchy. 2024-07-30 21:54:18 +03:00
themixedupstuff 50552914c1 Rename various types. 2024-07-28 14:19:13 +03:00
themixedupstuff d959c42e99 Rename various objects. 2024-07-28 14:11:23 +03:00
themixedupstuff 9b2f0859e5 Replace own matrix type with OpenTK. 2024-07-28 14:07:42 +03:00
themixedupstuff 831c93b916 Replace QColor and QColorF with OpenTK.Mathematics.Color4. 2024-07-28 13:59:35 +03:00
themixedupstuff 42782b8a71 Replace QVec2 with OpenTK.Mathematics.Vector2. 2024-07-28 13:50:33 +03:00
themixedupstuff 1301868269 Remove old OpenGL bindings and just pull in OpenTK. 2024-07-28 13:34:53 +03:00
themixedupstuff 21591c3d11 Go over vertex generator. 2024-07-28 12:37:33 +03:00
themixedupstuff 3856b3c66e Refactoring some names I don't like. 2024-07-28 12:34:22 +03:00
themixedupstuff 92fb8f06a7 Rename QuikCommandHandler. 2024-07-28 12:24:24 +03:00
themixedupstuff 468648de95 Rename a few files and classes. 2024-07-28 12:21:18 +03:00
themixedupstuff 3f1875252e Merge branch 'master' into blurg-test 2024-07-28 11:36:20 +03:00
themixedupstuff 65609bf685 Fix buffer size in QImageBuffer.cs 2024-07-28 11:33:12 +03:00
themixedupstuff a95ddb46ad Add some blurg test code. 2024-07-28 11:16:07 +03:00
themixedupstuff dadd9f137a Add utility for custom commands into command lists. 2024-07-26 23:48:48 +03:00
themixedupstuff 23fc485b2a Create Dashboard.BlurgText 2024-07-26 22:20:44 +03:00
themixedupstuff c6c76aa8be Update dependencies. 2024-07-18 21:54:44 +03:00
themixedupstuff 6670667ff6 Fix some broken dependencies. 2024-07-18 20:34:39 +03:00
themixedupstuff d06264e424 Rename some classes to fit the new names. 2024-07-18 20:28:02 +03:00
themixedupstuff a1f4e6a4dc Rename from QUIK to Dashboard
Due to unforseen naming conflicts, the project has been rebranded under the ReFuel
umbrealla and will now be referred to as Dashboard from now on. Other changes will
occur to suit the library more for the engine whilst keeping the freestanding
nature of the library.

Rename folder.

Rename to Dashboard.OpenTK

Rename to Dashboard.Media.Defaults.

Do the last renames and path fixes.
2024-07-17 23:26:58 +03:00
themixedupstuff 470475092a Rename namespace to Dashboard. 2024-07-17 23:18:20 +03:00
themixedupstuff 1ee492ccd4 Push WIP changes before rename. 2024-07-17 23:12:07 +03:00
themixedupstuff 3b52649ad2 Change over to the new nuget package and change namespaces. 2024-06-24 22:28:41 +03:00
themixedupstuff f8a4c73367 Fix load order in FallbackFontDatabase. 2024-06-21 11:59:04 +03:00
themixedupstuff 88e060a92b Update Stbi to new rebranded package. 2024-06-21 09:52:29 +03:00
themixedupstuff 1ccab1c85a Converted project to ^nullable enable. 2024-06-09 22:54:33 +03:00
themixedupstuff 55e9d775cd Some changes to the UI elements. 2024-06-09 22:06:13 +03:00
themixedupstuff 8fd00257d3 Fix GL21 driver bug. 2024-06-09 19:55:13 +03:00
themixedupstuff 82d2027cf3 Add measure string function to Typesetter. 2024-06-09 19:20:35 +03:00
themixedupstuff 9b16e015f6 Fix depth test issue in demo. 2024-06-06 22:33:55 +03:00
themixedupstuff ff4a158cdb Update stbi. 2024-06-01 14:33:20 +03:00
themixedupstuff 29829fd299 Made font rendering easier. 2024-05-27 21:49:25 +03:00
themixedupstuff 19cc765955 Implement Label control. 2024-05-16 22:58:22 +03:00
themixedupstuff ce2a569a20 Add texture swizzle parameters. 2024-05-16 22:57:55 +03:00
themixedupstuff ab1849a891 Add missing commands. 2024-05-16 22:57:38 +03:00
themixedupstuff 3ae107c83e Fix translation matrix to be column major and implement multiplication. 2024-05-16 22:57:14 +03:00
themixedupstuff bd69c0d93f Preliminary typesetting work. 2024-05-15 23:17:01 +03:00
themixedupstuff 279e619c3b Remove extra accidental copy of one file. 2024-05-15 23:16:37 +03:00
themixedupstuff 7cb47c721b Fix 90 degree rotation error in Image2D 2024-05-05 16:45:43 +03:00
themixedupstuff 9105b16df8 Add singleton support for QuikApplication. 2024-05-05 16:45:25 +03:00
themixedupstuff 3b73090f79 Fix error in the mouse button enum. 2024-05-05 16:45:12 +03:00
themixedupstuff 3484dce8c5 Create a font atlaser. 2024-05-01 16:53:30 +03:00
themixedupstuff 3418537b43 Add image copy functions to QImage. 2024-05-01 16:52:20 +03:00
themixedupstuff d831c9b72d Added better font search stuff. 2024-05-01 15:34:21 +03:00
themixedupstuff 66644be699 Remove popz command in UIBase.cs 2024-05-01 14:15:47 +03:00
themixedupstuff 39dfca18f2 Remove the readme under lib/ 2024-05-01 14:13:16 +03:00
themixedupstuff 75a11adbe7 Remove old submodules from the repository. 2024-05-01 14:12:12 +03:00
themixedupstuff 21233c8173 Add fontconfig support. 2024-04-28 15:39:12 +03:00
themixedupstuff ccb0c6ffe7 Add new font search criterea. 2024-04-28 13:45:46 +03:00
themixedupstuff bc3dcff3ea Remove the old Quik texture API. 2024-04-28 13:44:40 +03:00
themixedupstuff 2aa1066a9d Fix one of the image command lists. 2024-04-14 23:21:31 +03:00
themixedupstuff fe3c2d0550 Image support for vertex generator. 2024-04-14 23:21:08 +03:00
themixedupstuff 5aa9a2f4e6 Support for 3d images in the renderer. 2024-04-14 22:51:10 +03:00
themixedupstuff 4d5e0dd8f2 Add unit vectors to QVec2. 2024-04-14 22:35:54 +03:00
themixedupstuff f07208ebe9 Replace textures with QImage. 2024-04-14 22:35:30 +03:00
themixedupstuff b57d7bed41 Texture 3d entry points and enums. 2024-04-14 22:34:51 +03:00
themixedupstuff a1573d3786 Merge branch 'master' of https://git.mixedup.dev/QUIK/Quik 2024-04-11 19:35:30 +03:00
themixedupstuff 20c126fb88 New Image3D command. 2024-04-11 19:33:42 +03:00
themixedupstuff 7ce474d92a Changes to platform abstraction layer. 2024-04-11 19:09:00 +03:00
themixedupstuff 2eb5663ee9 Renamed the classes in the command engine. 2024-04-11 19:06:58 +03:00
themixedupstuff 09ce8d3229 Add invalid cast exceptions to frame conversion operators. 2024-04-09 10:50:52 +03:00
themixedupstuff 98aee237bd Update README.md 2024-04-08 22:55:37 +02:00
themixedupstuff d8beb2a274 Update README.md 2024-04-08 22:54:26 +02:00
themixedupstuff a8d805b461 Update README.md 2024-04-08 22:54:02 +02:00
themixedupstuff 4dff6eba91 Math library changes and fixes. 2024-04-08 23:51:03 +03:00
themixedupstuff 9c9efc6eeb Convert command queue to a command list. 2024-04-08 23:50:05 +03:00
themixedupstuff 51e9018a22 These dependencies have been moved into their own repositories. 2024-04-06 23:36:32 +03:00
themixedupstuff 032a38e13b Small fixes on OpenTK support. 2024-03-04 21:41:16 +03:00
themixedupstuff cea243a3b8 Finish backporting Freetype from in development quik. 2023-11-10 20:57:33 +03:00
themixedupstuff 6240f5921b Add new assets for the quik logo. 2023-10-17 22:26:14 +03:00
themixedupstuff 959788563f Fix small errors in driver. 2023-09-22 21:23:59 +03:00
themixedupstuff 118b50cee2 Add font rendering. 2023-09-22 19:30:17 +03:00
themixedupstuff 1f6a3a55e1 Ressurrect old freetype library. 2023-09-16 09:59:51 +03:00
themixedupstuff ac0a70cefc Remove auto-generated freetype bindings. 2023-09-16 09:50:14 +03:00
themixedupstuff 845ed1c27a Rename Quik.Media.Stb to *.Defaults 2023-09-16 09:24:57 +03:00
themixedupstuff 4a35f18737 Add partial FreeType bindings. 2023-09-15 20:40:30 +03:00
themixedupstuff ae0c9e742c Begin working on freetype bindings, again. 2023-09-13 21:38:38 +03:00
themixedupstuff ebb2ee7fee Add freetype as a submodule. 2023-07-28 22:45:24 +03:00
themixedupstuff 65fb6dcb87 Add file and lineno to exception msg. 2023-07-28 22:38:55 +03:00
themixedupstuff d65bb8ad0a Add new demo application template. 2023-07-28 22:37:49 +03:00
themixedupstuff 2bcac4a83e Remove references to old control types, at least for now. 2023-07-28 22:37:13 +03:00
themixedupstuff 6b8b3f2f0d Create a super basic test for fonts. 2023-07-16 18:31:51 +03:00
themixedupstuff ecd6e8cab7 Disabled obsolete warning. 2023-07-15 16:23:42 +03:00
themixedupstuff 99277153d2 Add the stbimage test. 2023-07-15 16:23:22 +03:00
themixedupstuff f0132e3459 Delete .idea directory 2023-07-09 17:49:09 +03:00
themixedupstuff c3a815171b Begin work on Quik.Media.Stb 2023-07-09 17:46:35 +03:00
themixedupstuff 648f44f12d Create nicer wrapper of Stbi. 2023-07-09 17:33:35 +03:00
themixedupstuff 3e7de074c6 Temporarily fix Stbi. 2023-07-09 17:05:01 +03:00
themixedupstuff afd73d1622 Move stbtt to it's own 2023-07-09 16:44:49 +03:00
themixedupstuff 03c477e98f Cannot get the nuget package to work properly. 2023-07-09 16:33:09 +03:00
themixedupstuff 5f859adab9 Create preliminary bindings for stb libraries. 2023-07-08 15:32:01 +03:00
themixedupstuff aea346180b Reference the redist nuget packages. 2023-07-08 14:22:18 +03:00
themixedupstuff 17950b6870 Add the wrapper packages. 2023-07-08 14:17:05 +03:00
themixedupstuff 10dcbeb55e Added stb redistributables. 2023-07-08 14:08:11 +03:00
themixedupstuff 7d2dee5a4d Create stbi redist. 2023-07-08 13:25:44 +03:00
themixedupstuff fad876c1b3 Add stb back again. 2023-07-08 13:22:26 +03:00
themixedupstuff 1b9e50da82 Remove submodules for now. 2023-07-08 13:22:03 +03:00
themixedupstuff 33427146e1 Design a docker container for cross compiling native dependencies. 2023-07-08 13:17:14 +03:00
themixedupstuff 79242127e5 Add stb into project directory. 2023-07-03 13:24:16 +03:00
themixedupstuff 3906222506 Add alpha premultiplication. 2023-07-02 11:39:02 +03:00
themixedupstuff 1676184118 Add image buffers and color conversion. 2023-07-02 11:22:52 +03:00
themixedupstuff 995943d83b Fix incorrect nomenclature in interface name. 2023-07-02 10:38:46 +03:00
themixedupstuff f830626579 Add RedAlpha image formats. 2023-07-02 10:38:32 +03:00
themixedupstuff 98883db604 Add float color structure. 2023-07-02 10:38:19 +03:00
themixedupstuff 8e2db62e56 Add a very simple matrix class. 2023-06-30 19:57:04 +03:00
themixedupstuff d72a07354a We are going real old school GL. 2023-06-30 19:35:24 +03:00
themixedupstuff 1297365b38 Add missing stuff. 2023-06-30 16:34:16 +03:00
themixedupstuff 72d0f02440 Purge all old files. 2023-06-29 14:17:32 +03:00
themixedupstuff 8d2afd0955 Remove old test projects. 2023-06-29 14:04:32 +03:00
themixedupstuff 41b7bebba5 Rename geometry types with Q prefix instead of Quik. 2023-06-29 10:42:02 +03:00
themixedupstuff fe71028573 Add GL_UNSIGNED_INT 2023-06-29 10:37:23 +03:00
themixedupstuff 46c18d97d2 Small fixes in the command engine components. 2023-06-29 10:31:28 +03:00
themixedupstuff 5c71afe9c7 Rectangles added :) 2023-06-28 15:22:48 +03:00
themixedupstuff 61e4d2bd16 Begin work on new command engine. 2023-06-23 22:56:07 +03:00
themixedupstuff 78c71054b4 Add more useful stuff into frames. 2023-06-23 22:55:41 +03:00
themixedupstuff 2e07b0ffc9 Add pretty colors and new functions into the math types. 2023-06-23 22:53:22 +03:00
themixedupstuff a50650e54a Create custom OpenGL bindings.
This removes the hard dependency on OpenTK for a core component of QUIK.
Although not all environments are going to be using OpenGL, having it be
independent from a vendor library would be more benefitial for more
people.
2023-06-22 23:11:32 +03:00
themixedupstuff b57677a6c2 Rename namespace 'CommandQueue' as 'CommandMachine'. 2023-05-20 19:37:51 +03:00
themixedupstuff 4b2271dd29 Change textures from interface to abstract class. 2023-05-15 21:49:48 +03:00
themixedupstuff e731e8af49 Add new style classes. 2023-05-15 12:07:12 +03:00
themixedupstuff 9eadc26f2f New command queue implementation with hopefully less GC invocations. 2023-05-15 10:22:09 +03:00
themixedupstuff 39a9f67367 Add new commands. 2023-05-14 23:32:32 +03:00
themixedupstuff 3de9de4e2d New conversion operators and make some explicit. 2023-05-14 23:30:26 +03:00
themixedupstuff fc1cc6a1ba Add intersect to rectangle. 2023-05-14 23:29:52 +03:00
themixedupstuff 9339295378 Push all uncommitted changes.
I have had a long break from this project due to other higher priority
things going on in my life. Big changes inbound.
2023-05-13 16:17:57 +03:00
themixedupstuff 98b1c1a277 New in development queue system. 2023-05-13 16:17:23 +03:00
themixedupstuff 3d672022e1 Update .gitignore 2023-05-13 15:43:03 +03:00
themixedupstuff 1d3f22f6ce Clean up the font test code slightly. 2022-08-20 12:57:57 +03:00
themixedupstuff bf47915491 Very quick test text implementation. 2022-08-19 16:13:19 +03:00
themixedupstuff a0473d9e83 Change over draw call propagation in the vertex generator. 2022-08-18 13:58:03 +03:00
themixedupstuff 7c0c6d9a75 Remove stroke stippling for a good long time. 2022-08-16 21:38:18 +03:00
themixedupstuff 72cad718cb Implement rectangle rendering. 2022-08-16 21:35:12 +03:00
themixedupstuff 16bb3f41a7 Implement bezier lines. 2022-08-08 10:52:03 +03:00
themixedupstuff 502eb95278 Create joint generating code for multi-segment lines and refactor code for end caps. 2022-08-06 10:05:27 +03:00
themixedupstuff 772906bec7 Add single line rendering. 2022-08-04 16:40:58 +03:00
themixedupstuff a6465730c1 Design a simple logo and add it to README.md 2022-08-04 10:38:41 +03:00
themixedupstuff ecd31a5cde Create a quick and dirty style system. 2022-08-03 22:54:16 +03:00
themixedupstuff c26dcfe3f3 Implement own vector types to releive the System.Numerics dependency. 2022-08-03 17:52:00 +03:00
themixedupstuff 9838540a8f Move project into its own subfolder to make room for other components. 2022-08-03 17:30:31 +03:00
themixedupstuff 4c42b68033 Create initial draw command list. 2022-08-03 15:04:40 +03:00
88 changed files with 8855 additions and 682 deletions
+27
View File
@@ -0,0 +1,27 @@
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);
}
}
}
}
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dashboard.Drawing\Dashboard.Drawing.csproj" />
<ProjectReference Include="..\Dashboard\Dashboard.csproj" />
<PackageReference Include="BlurgText" Version="0.1.0-nightly-4" />
</ItemGroup>
</Project>
+63
View File
@@ -0,0 +1,63 @@
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);
}
}
@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
-69
View File
@@ -1,69 +0,0 @@
using System;
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; }
private DbBaseCommands() : base("DB_base",
new[]
{
BrushExtension.Instance,
})
{
AddCommand(DrawPoint = new DrawCommand<PointCommandArgs>("Point", this, PointCommandArgs.CommandSize));
}
public static readonly DbBaseCommands Instance = new DbBaseCommands();
}
public struct PointCommandArgs : IParameterSerializer<PointCommandArgs>
{
public Vector3 Position { get; private set; }
public float Size { get; private set; }
public IBrush? Brush { get; private set; }
public PointCommandArgs(Vector3 position, float size, IBrush brush)
{
Position = position;
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, 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;
Size = value.Size;
Brush = (IBrush)queue.Resources[value.BrushIndex];
}
private record struct Value(Vector3 Position, float Size, int BrushIndex);
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
}
}
-88
View File
@@ -1,88 +0,0 @@
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);
}
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);
}
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 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;
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);
}
}
}
-72
View File
@@ -1,72 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
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, Vector3 position, float size, IBrush brush)
{
var controller = queue.GetController(DbBaseCommands.Instance);
controller.EnsureSize(position);
controller.Write(DbBaseCommands.Instance.DrawPoint, new PointCommandArgs(position, size, brush));
}
}
}
-263
View File
@@ -1,263 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
namespace Dashboard.Drawing
{
public class DrawQueue : 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>
/// Size of the image in points.
/// </summary>
public Vector3 Size { get; private set; } = Vector3.Zero;
/// <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.Capacity = 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]);
}
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);
}
}
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.");
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)
{
int value = 0;
for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
value = (value << 7) | b;
if ((b & (1 << 7)) == 0)
break;
}
return value;
}
public void Dispose()
{
throw new NotImplementedException();
}
private class DrawController(DrawQueue Queue) : IDrawController
{
public void EnsureSize(Vector3 size)
{
Queue.Size = Vector3.Max(Queue.Size, size);
}
public int Require(IDrawExtension extension)
{
return Queue.RequireExtension(extension);
}
public int GetResourceIndex(IDrawResource resource)
{
return Queue._resources.Intern(resource);
}
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);
}
}
private 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();
}
}
public interface IDrawController
{
/// <summary>
/// Ensures that the canvas is at least a certain size.
/// </summary>
/// <param name="size">The minimum size.</param>
void EnsureSize(Vector3 size);
/// <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>;
}
}
-13
View File
@@ -1,13 +0,0 @@
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
@@ -1,14 +0,0 @@
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);
}
}
-33
View File
@@ -1,33 +0,0 @@
using System.Drawing;
namespace Dashboard.Drawing
{
public class BrushExtension : DrawExtension
{
private BrushExtension() : base("DB_Brush") { }
public static readonly BrushExtension Instance = new BrushExtension();
}
public interface IBrush : IDrawResource
{
}
public readonly struct SolidBrush : IBrush
{
public IDrawExtension Kind { get; } = SolidBrushExtension.Instance;
public Color Color { get; }
public SolidBrush(Color color)
{
Color = color;
}
}
public class SolidBrushExtension : DrawExtension
{
private SolidBrushExtension() : base("DB_Brush_solid", new[] { BrushExtension.Instance }) { }
public static readonly SolidBrushExtension Instance = new SolidBrushExtension();
}
}
@@ -0,0 +1,19 @@
<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>
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,16 @@
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);
}
}
}
@@ -0,0 +1,269 @@
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;
}
}
}
}
@@ -0,0 +1,93 @@
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
@@ -0,0 +1,84 @@
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;
}
}
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,98 @@
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();
}
}
}
}
@@ -0,0 +1,230 @@
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;
}
}
@@ -0,0 +1,168 @@
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];
}
}
}
@@ -0,0 +1,11 @@
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 = "";
}
}
+155
View File
@@ -0,0 +1,155 @@
using System;
using System.Buffers;
using System.Collections;
using System.IO;
using System.Linq;
using System.Net;
using Dashboard.Media.Fonts;
// WebRequest is obsolete but runs on .NET framework.
#pragma warning disable SYSLIB0014
namespace Dashboard.Media.Defaults
{
public class StbMediaLoader : MediaLoader<string>, MediaLoader<Uri>, MediaLoader<FileInfo>, MediaLoader<FontFace>
{
public bool AllowRemoteTransfers { get; set; } = false;
private readonly ArrayPool<byte> ByteArrays = ArrayPool<byte>.Create();
public IDisposable GetMedia(object key, MediaHint hint)
{
Type t = key.GetType();
/**/ if (t == typeof(string))
{
return GetMedia((string)key, hint);
}
else if (t == typeof(Uri))
{
return GetMedia((Uri)key, hint);
}
else if (t == typeof(FileInfo))
{
return GetMedia((FileInfo)key, hint);
}
else if (t == typeof(FontFace))
{
return GetMedia((FontFace)key, hint);
}
else
{
return null;
}
}
public IDisposable GetMedia(Uri uri, MediaHint hint)
{
throw new NotImplementedException();
}
public IDisposable GetMedia(string str, MediaHint hint)
{
throw new NotImplementedException();
}
public IDisposable GetMedia(FileInfo file, MediaHint hint)
{
throw new NotImplementedException();
}
public IDisposable GetMedia(FontFace key, MediaHint hint)
{
throw new NotImplementedException();
}
public Stream OpenResource(FileInfo file)
{
if (file.Exists)
{
return file.Open(FileMode.Open);
}
else
{
return null;
}
}
public Stream OpenResource(Uri uri)
{
switch (uri.Scheme)
{
case "http":
case "https":
if (!AllowRemoteTransfers) return null;
try
{
WebRequest request = HttpWebRequest.Create(uri);
WebResponse response = request.GetResponse();
MemoryStream stream = new MemoryStream();
response.GetResponseStream().CopyTo(stream);
response.Close();
stream.Position = 0;
return stream;
}
catch
{
return null;
}
case "file":
return OpenResource(new FileInfo(uri.AbsolutePath));
default:
return null;
}
}
public Stream OpenResource(string key)
{
if (File.Exists(key))
{
return File.Open(key, FileMode.Open);
}
else if (Uri.TryCreate(key, UriKind.RelativeOrAbsolute, out Uri uri))
{
return OpenResource(uri);
}
else
{
return null;
}
}
MediaHint InferMedia(Stream str, MediaHint hint)
{
if (hint != MediaHint.None)
{
return hint;
}
byte[] array = ByteArrays.Rent(4);
str.Read(array, 0, 4);
str.Position = 0;
foreach (var(type, seq) in MediaTypes)
{
if (seq.SequenceEqual(array))
return hint;
}
return MediaHint.None;
}
private readonly (MediaHint, byte[])[] MediaTypes = new (MediaHint, byte[])[] {
(MediaHint.Image, new byte[] { 0x42, 0x4d }), /* .bmp `BM` */
(MediaHint.Image, new byte[] { 0x47, 0x49, 0x46, 0x38 }), /* .gif `GIF8` */
(MediaHint.Image, new byte[] { 0xff, 0xd8, 0xff, 0xe0 }), /* .jpg (JFIF) */
(MediaHint.Image, new byte[] { 0xff, 0xd8, 0xff, 0xe1 }), /* .jpg (EXIF) */
(MediaHint.Image, new byte[] { 0x89, 0x50, 0x4e, 0x47 }), /* .png `.PNG `*/
(MediaHint.Image, new byte[] { 0x4d, 0x4d, 0x00, 0x2a }), /* .tif (motorola) */
(MediaHint.Image, new byte[] { 0x49, 0x49, 0x2a, 0x00 }), /* .tif (intel) */
(MediaHint.Font, new byte[] { 0x00, 0x01, 0x00, 0x00 }), /* .ttf */
(MediaHint.Font, new byte[] { 0x4F, 0x54, 0x54, 0x4F }), /* .otf */
};
}
}
@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
#if false
namespace Quik.Media.Defaults.Win32
{
public class EnumerateFonts
{
private const byte DEFAULT_CHARSET = 1;
public static void Enumerate(FontFace font)
{
/* It's windows, just borrow the desktop window. */
IntPtr hdc = GetDC(GetDesktopWindow());
List<(LogFontA, TextMetricA)> list = new List<(LogFontA, TextMetricA)>();
LogFontA font2 = new LogFontA()
{
//FaceName = font.Family,
Weight = ((font.Style & FontStyle.Bold) != 0) ? FontWeight.Bold : FontWeight.Regular,
Italic = (font.Style & FontStyle.Italic) != 0,
CharSet = DEFAULT_CHARSET
};
Console.WriteLine(font2.FaceName);
EnumFontFamiliesExProc proc = (in LogFontA font, in TextMetricA metric, int type, IntPtr lparam) =>
{
list.Add((font, metric));
return 0;
};
EnumFontFamiliesExA(hdc, font2, proc, IntPtr.Zero, 0);
}
private const string gdi32 = "Gdi32.dll";
private const string user32 = "User32.dll";
[DllImport(gdi32)]
private static extern int EnumFontFamiliesExA(
IntPtr hdc,
in LogFontA font,
[MarshalAs(UnmanagedType.FunctionPtr)] EnumFontFamiliesExProc proc,
IntPtr lparam,
int flags /* Should be zero. */);
[DllImport(user32)]
private static extern IntPtr /* HWND */ GetDesktopWindow();
[DllImport(user32)]
private static extern IntPtr /* HDC */ GetDC(IntPtr hwnd);
private delegate int EnumFontFamiliesExProc(in LogFontA font, in TextMetricA metric, int fontType, IntPtr lParam);
private struct LogFontA
{
public long Height;
public long Width;
public long Escapement;
public long Orientation;
public FontWeight Weight;
[MarshalAs(UnmanagedType.U1)]
public bool Italic;
[MarshalAs(UnmanagedType.U1)]
public bool Underline;
[MarshalAs(UnmanagedType.U1)]
public bool StrikeOut;
public byte CharSet;
public byte OutPrecision;
public byte ClipPrecision;
public byte PitchAndFamily;
private unsafe fixed byte aFaceName[32];
public unsafe string FaceName
{
get
{
fixed (byte* str = aFaceName)
{
int len = 0;
for (; str[len] != 0 && len < 32; len++) ;
return Encoding.UTF8.GetString(str, len);
}
}
set
{
fixed (byte *str = aFaceName)
{
Span<byte> span = new Span<byte>(str, 32);
Encoding.UTF8.GetBytes(value, span);
span[31] = 0;
}
}
}
}
private struct TextMetricA
{
public long Height;
public long Ascent;
public long Descent;
public long InternalLeading;
public long ExternalLeading;
public long AveCharWidth;
public long MaxCharWidth;
public long Weight;
public long Overhang;
public long DigitizedAspectX;
public long DigitizedAspectY;
public byte FirstChar;
public byte LastChar;
public byte DefaultChar;
public byte BreakChar;
public byte Italic;
public byte Underlined;
public byte StruckOut;
public byte PitchAndFamily;
public byte CharSet;
}
private enum FontWeight : long
{
DontCare = 0,
Thin = 100,
ExtraLight = 200,
UltraLight = 200,
Light = 300,
Normal = 400,
Regular = 400,
Medium = 500,
Semibold = 600,
Demibold = 600,
Bold = 700,
Extrabold = 800,
Ultrabold = 800,
Heavy = 900,
Black = 900
}
}
}
#endif
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK" Version="4.8.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard\Dashboard.csproj" />
<EmbeddedResource Include="glsl\**"/>
</ItemGroup>
</Project>
+105
View File
@@ -0,0 +1,105 @@
using Dashboard.ImmediateDraw;
using Dashboard.Media;
using Dashboard.PAL;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
using System;
using System.Collections.Generic;
namespace Dashboard.OpenTK
{
public class OpenTKPlatform : IDbPlatform
{
private readonly List<OpenTKPort> _ports = new List<OpenTKPort>();
// These shall remain a sad nop for now.
public string? Title { get; set; }
public Media.Image? Icon { get; set; } = null;
public event EventHandler? EventRaised;
public NativeWindowSettings DefaultSettings { get; set; } = NativeWindowSettings.Default;
public IReadOnlyList<OpenTKPort> Ports => _ports;
private bool IsGLInitialized = false;
public IDashHandle CreatePort()
{
NativeWindow window = new NativeWindow(DefaultSettings);
OpenTKPort port = new OpenTKPort(window);
_ports.Add(port);
if (!IsGLInitialized)
{
window.Context.MakeCurrent();
GL.LoadBindings(new GLFWBindingsContext());
IsGLInitialized = true;
}
window.Closing += (ea) =>
{
Environment.Exit(0);
};
return port;
}
public void Dispose()
{
// FIXME: dispose pattern here!
// Copy the array to prevent collection modification exceptions.
foreach (OpenTKPort port in _ports.ToArray())
{
port.Dispose();
}
}
public void ProcessEvents(bool block)
{
NativeWindow.ProcessWindowEvents(block);
}
public void DestroyPort(IDashHandle port) => ((OpenTKPort)port).Dispose();
public string PortGetTitle(IDashHandle port) => ((OpenTKPort)port).Title;
public void PortSetTitle(IDashHandle port, string title) => ((OpenTKPort)port).Title = title;
public Vector2 PortGetSize(IDashHandle port) => ((OpenTKPort)port).Size;
public void PortSetSize(IDashHandle port, Vector2 size) => ((OpenTKPort)port).Size = size;
public Vector2 PortGetPosition(IDashHandle port) => ((OpenTKPort)port).Position;
public void PortSetPosition(IDashHandle port, Vector2 position) => ((OpenTKPort)port).Position = position;
public bool PortIsValid(IDashHandle port) => ((OpenTKPort)port).IsValid;
public void PortSubscribeEvent(IDashHandle port, EventHandler handler) => ((OpenTKPort)port).EventRaised += handler;
public void PortUnsubscribeEvent(IDashHandle port, EventHandler handler) => ((OpenTKPort)port).EventRaised -= handler;
public void PortFocus(IDashHandle port) => ((OpenTKPort)port).Focus();
public void PortShow(IDashHandle port, bool shown = true) => ((OpenTKPort)port).Show(shown);
public void PortPaint(IDashHandle port, DrawList commands) => ((OpenTKPort)port).Paint(commands);
public void GetMaximumImage(out int width, out int height)
{
GL.GetInteger(GetPName.MaxTextureSize, out int value);
width = height = value;
}
public void GetMaximumImage(out int width, out int height, out int depth)
{
GetMaximumImage(out width, out height);
GL.GetInteger(GetPName.MaxArrayTextureLayers, out int value);
depth = value;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
using System;
using OpenTK.Mathematics;
using OpenTK.Windowing.Desktop;
using OpenTK.Graphics.OpenGL4;
using Dashboard.OpenGL;
using Dashboard.ImmediateDraw;
using Dashboard.PAL;
using Dashboard.VertexGenerator;
namespace Dashboard.OpenTK
{
public class OpenTKPort : IDashHandle
{
private readonly NativeWindow _window;
private readonly GL21Driver _glDriver;
private readonly VertexDrawingEngine _vertexEngine;
public string Title
{
get => _window.Title;
set => _window.Title = value;
}
public Vector2 Size
{
get
{
Vector2i size = _window.ClientSize;
return new Vector2(size.X, size.Y);
}
set
{
// OpenTK being OpenTK as usual, you can't set the client size.
Vector2i extents = _window.Size - _window.ClientSize;
Vector2i size = extents + new Vector2i((int)value.X, (int)value.Y);
_window.Size = size;
}
}
public Vector2 Position
{
get
{
Vector2i location = _window.Location;
return new Vector2(location.X, location.Y);
}
set
{
Vector2i location = new Vector2i((int)value.X, (int)value.Y);
_window.Location = location;
}
}
public bool IsValid => !isDisposed;
public event EventHandler? EventRaised;
public OpenTKPort(NativeWindow window)
{
_window = window;
_glDriver = new GL21Driver();
_vertexEngine = new VertexDrawingEngine();
}
public void Focus()
{
_window.Focus();
}
public void Paint(DrawList queue)
{
Rectangle view = new Rectangle(Size, new Vector2(0, 0));
_vertexEngine.Reset();
_vertexEngine.ProcessCommands(view, queue);
if (!_window.Context.IsCurrent)
_window.Context.MakeCurrent();
if (!_glDriver.IsInit)
_glDriver.Init();
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_glDriver.Draw(_vertexEngine.DrawQueue, view);
_window.Context.SwapBuffers();
}
public void Show(bool shown = true)
{
_window.IsVisible = shown;
}
private bool isDisposed;
private void Dispose(bool disposing)
{
if (isDisposed) return;
if (disposing)
{
_window?.Dispose();
GC.SuppressFinalize(this);
}
isDisposed = true;
}
public void Dispose() => Dispose(true);
}
}
+31 -23
View File
@@ -3,41 +3,49 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard", "Dashboard\Dashboard.csproj", "{49A62F46-AC1C-4240-8615-020D4FBBF964}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard", "Dashboard\Dashboard.csproj", "{4FE772DD-F424-4EAC-BF88-CB8F751B4926}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.Drawing", "Dashboard.Drawing\Dashboard.Drawing.csproj", "{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.Media.Defaults", "Dashboard.Media.Defaults\Dashboard.Media.Defaults.csproj", "{3798F6DD-8F84-4B7D-A810-B0D4B5ACB672}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.OpenTK", "Dashboard.OpenTK\Dashboard.OpenTK.csproj", "{2013470A-915C-46F2-BDD3-FCAA39C845EE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.TestApplication", "tests\Dashboard.TestApplication\Dashboard.TestApplication.csproj", "{7C90B90B-DF31-439B-9080-CD805383B014}"
ProjectSection(ProjectDependencies) = postProject
{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5} = {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}
{49A62F46-AC1C-4240-8615-020D4FBBF964} = {49A62F46-AC1C-4240-8615-020D4FBBF964}
EndProjectSection
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{40F3B724-88A1-4D4F-93AB-FE0DC07A347E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.Demo", "tests\Dashboard.Demo\Dashboard.Demo.csproj", "{EAA5488E-ADF0-4D68-91F4-FAE98C8691FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.BlurgText", "Dashboard.BlurgText\Dashboard.BlurgText.csproj", "{D05A9DEA-A5D1-43DC-AB41-36B07598B749}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{49A62F46-AC1C-4240-8615-020D4FBBF964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49A62F46-AC1C-4240-8615-020D4FBBF964}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49A62F46-AC1C-4240-8615-020D4FBBF964}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49A62F46-AC1C-4240-8615-020D4FBBF964}.Release|Any CPU.Build.0 = Release|Any CPU
{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Release|Any CPU.Build.0 = Release|Any CPU
{7C90B90B-DF31-439B-9080-CD805383B014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C90B90B-DF31-439B-9080-CD805383B014}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C90B90B-DF31-439B-9080-CD805383B014}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C90B90B-DF31-439B-9080-CD805383B014}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4FE772DD-F424-4EAC-BF88-CB8F751B4926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FE772DD-F424-4EAC-BF88-CB8F751B4926}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FE772DD-F424-4EAC-BF88-CB8F751B4926}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FE772DD-F424-4EAC-BF88-CB8F751B4926}.Release|Any CPU.Build.0 = Release|Any CPU
{3798F6DD-8F84-4B7D-A810-B0D4B5ACB672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3798F6DD-8F84-4B7D-A810-B0D4B5ACB672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3798F6DD-8F84-4B7D-A810-B0D4B5ACB672}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3798F6DD-8F84-4B7D-A810-B0D4B5ACB672}.Release|Any CPU.Build.0 = Release|Any CPU
{2013470A-915C-46F2-BDD3-FCAA39C845EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2013470A-915C-46F2-BDD3-FCAA39C845EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2013470A-915C-46F2-BDD3-FCAA39C845EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2013470A-915C-46F2-BDD3-FCAA39C845EE}.Release|Any CPU.Build.0 = Release|Any CPU
{EAA5488E-ADF0-4D68-91F4-FAE98C8691FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAA5488E-ADF0-4D68-91F4-FAE98C8691FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAA5488E-ADF0-4D68-91F4-FAE98C8691FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAA5488E-ADF0-4D68-91F4-FAE98C8691FC}.Release|Any CPU.Build.0 = Release|Any CPU
{D05A9DEA-A5D1-43DC-AB41-36B07598B749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D05A9DEA-A5D1-43DC-AB41-36B07598B749}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D05A9DEA-A5D1-43DC-AB41-36B07598B749}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D05A9DEA-A5D1-43DC-AB41-36B07598B749}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7C90B90B-DF31-439B-9080-CD805383B014} = {9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}
{EAA5488E-ADF0-4D68-91F4-FAE98C8691FC} = {40F3B724-88A1-4D4F-93AB-FE0DC07A347E}
EndGlobalSection
EndGlobal
-6
View File
@@ -1,6 +0,0 @@
namespace Dashboard;
public class Class1
{
}
+56
View File
@@ -0,0 +1,56 @@
using System;
namespace Dashboard.Controls
{
public enum Dock
{
None,
Top,
Left,
Bottom,
Right,
Center
}
[Flags]
public enum Anchor
{
None = 0,
Top = 1 << 0,
Left = 1 << 1,
Bottom = 1 << 2,
Right = 1 << 3,
All = Top | Left | Bottom | Right
}
public enum Direction
{
Vertical,
Horizontal
}
public enum TextAlignment
{
Left,
Center,
Right,
Justify,
}
public enum VerticalAlignment
{
Top,
Center,
Bottom,
Justify,
}
public enum HorizontalAlignment
{
Left,
Center,
Right,
Justify
}
}
+86
View File
@@ -0,0 +1,86 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Dashboard.Controls
{
public abstract class ContainerControl : Control, ICollection<Control>
{
private readonly List<Control> children = new List<Control>();
public int Count => children.Count;
public bool IsReadOnly => false;
public event EventHandler<ChildEventArgs>? ChildAdded;
public event EventHandler<ChildEventArgs>? ChildRemoved;
public void Add(Control item)
{
children.Add(item);
OnChildAdded(new ChildEventArgs(item));
}
public void Clear()
{
foreach (Control child in this)
{
OnChildRemoved(new ChildEventArgs(child));
}
children.Clear();
}
public bool Contains(Control item)
{
return children.Contains(item);
}
public void CopyTo(Control[] array, int arrayIndex)
{
children.CopyTo(array, arrayIndex);
}
public IEnumerator<Control> GetEnumerator()
{
return children.GetEnumerator();
}
public bool Remove(Control item)
{
if (children.Remove(item))
{
OnChildRemoved(new ChildEventArgs(item));
return true;
}
return false;
}
public virtual void OnChildAdded(ChildEventArgs ea)
{
Adopt(ea.Child, this);
ChildAdded?.Invoke(this, ea);
}
public virtual void OnChildRemoved(ChildEventArgs ea)
{
Adopt(ea.Child, null);
ChildRemoved?.Invoke(this, ea);
}
IEnumerator IEnumerable.GetEnumerator()
{
return children.GetEnumerator();
}
}
public class ChildEventArgs : EventArgs
{
public Control Child { get; }
public ChildEventArgs(Control child)
{
Child = child;
}
}
}
+142
View File
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using Dashboard.ImmediateDraw;
using OpenTK.Mathematics;
namespace Dashboard.Controls
{
public abstract class Control : UIBase
{
private readonly DrawList drawCommands = new DrawList();
public string? Id { get; set; }
public override Vector2 Position
{
get => base.Position;
set
{
base.Position = value;
InvalidateLayout();
}
}
public Style Style { get; set; } = new Style();
public float Padding
{
get => (float)(Style["padding"] ?? 0.0f);
set => Style["padding"] = value;
}
public bool IsVisualsValid { get; private set; } = false;
public bool IsLayoutValid { get; private set; } = false;
protected bool IsLayoutSuspended { get; private set; } = false;
protected ResizedEventArgs? LastResizeEvent { get; private set; }
public void InvalidateVisual()
{
IsVisualsValid = false;
OnVisualsInvalidated(this, EventArgs.Empty);
}
public void InvalidateLayout()
{
IsLayoutValid = false;
OnLayoutInvalidated(this, EventArgs.Empty);
}
public void SuspendLayout()
{
IsLayoutSuspended = true;
}
public void ResumeLayout()
{
IsLayoutSuspended = false;
InvalidateLayout();
}
protected abstract void ValidateVisual(DrawList cmd);
protected abstract void ValidateLayout();
protected override void PaintBegin(DrawList cmd)
{
base.PaintBegin(cmd);
if (!IsLayoutValid && !IsLayoutSuspended)
{
ValidateLayout();
OnLayoutValidated(this, EventArgs.Empty);
IsLayoutValid = true;
InvalidateVisual();
}
if (!IsVisualsValid)
{
ValidateVisual(drawCommands);
OnVisualsValidated(this, EventArgs.Empty);
IsVisualsValid = true;
}
cmd.PushStyle(Style);
cmd.PushViewport();
cmd.StoreViewport(AbsoluteBounds);
cmd.Splice(drawCommands);
if (this is IEnumerable<Control> children)
{
foreach (Control child in children)
{
child.Paint(cmd);
}
}
cmd.PopViewport();
cmd.PopStyle();
}
public event EventHandler? StyleChanged;
public event EventHandler? VisualsInvalidated;
public event EventHandler? VisualsValidated;
public event EventHandler? LayoutInvalidated;
public event EventHandler? LayoutValidated;
protected virtual void OnStyleChanged(object sender, EventArgs ea)
{
StyleChanged?.Invoke(sender, ea);
InvalidateLayout();
}
protected virtual void OnVisualsInvalidated(object sender, EventArgs ea)
{
VisualsInvalidated?.Invoke(sender, ea);
}
protected virtual void OnVisualsValidated(object sender, EventArgs ea)
{
VisualsValidated?.Invoke(sender, ea);
}
protected virtual void OnLayoutInvalidated(object sender, EventArgs ea)
{
LayoutInvalidated?.Invoke(sender, ea);
}
protected virtual void OnLayoutValidated(object sender, EventArgs ea)
{
LayoutValidated?.Invoke(sender, ea);
}
public override void OnResized(object sender, ResizedEventArgs ea)
{
base.OnResized(sender, ea);
LastResizeEvent = ea;
InvalidateLayout();
}
}
}
+100
View File
@@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Dashboard.ImmediateDraw;
using OpenTK.Mathematics;
namespace Dashboard.Controls
{
public class FlowBox : ContainerControl
{
public Direction FlowDirection { get; set; }
public bool AllowWrap { get; set; }
public VerticalAlignment VerticalAlignment { get; set; }
public HorizontalAlignment HorizontalAlignment { get; set; }
public float ItemPadding { get; set; } = 4f;
protected override void ValidateLayout()
{
}
protected override void ValidateVisual(DrawList cmd)
{
throw new NotImplementedException();
}
private void FlowHorizontal()
{
IEnumerator<Control> controls = this.GetEnumerator();
List<Control> row;
}
// Enumerate a row.
private bool EnumerateRows(IEnumerator<Control> iterator, List<Control> row)
{
float width = 0;
do
{
if (width + iterator.Current.Size.X < Size.X)
{
row.Add(iterator.Current);
width += iterator.Current.Size.X + ItemPadding;
}
else
{
return true;
}
} while (iterator.MoveNext());
return false;
}
// Flows a row of children.
private void FlowRow(List<Control> line, Vector2 offset, Vector2 size, float packedWidth)
{
Vector2 pointer = offset;
pointer.X += hstart();
foreach (Control child in line)
{
child.Position = pointer;
pointer += new Vector2(child.Size.X + hoffset(child), voffset(child));
}
float hstart()
{
return HorizontalAlignment switch {
HorizontalAlignment.Center => (size.Y - packedWidth) / 2,
HorizontalAlignment.Right => size.Y - packedWidth,
_ => 0f
};
}
float hoffset(Control child)
{
if (line.Count == 1)
return 0;
else if (HorizontalAlignment == HorizontalAlignment.Justify)
{
return ItemPadding + ((size.Y - packedWidth) / (line.Count - 1));
}
else
{
return ItemPadding;
}
}
float voffset(Control child)
{
return VerticalAlignment switch {
VerticalAlignment.Top => 0f,
VerticalAlignment.Bottom => size.Y - child.Size.Y,
_ => (size.Y - child.Size.Y) / 2,
};
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
using Dashboard.ImmediateDraw;
using Dashboard.Layout;
using OpenTK.Mathematics;
namespace Dashboard.Controls
{
public class GridBox : ContainerControl
{
protected override void ValidateLayout()
{
if (LastResizeEvent != null)
{
foreach (Control child in this)
{
GridLayoutAttribute attribute = GridLayoutAttribute.GetGridLayout(child);
attribute.Evaluate(LastResizeEvent, child.Bounds, out Rectangle newBounds);
child.Bounds = newBounds;
child.InvalidateLayout();
}
}
}
protected override void ValidateVisual(DrawList cmd)
{
cmd.Rectangle(new Rectangle(Size, Vector2.Zero));
}
}
}
+32
View File
@@ -0,0 +1,32 @@
using Dashboard.ImmediateDraw;
using Dashboard.Media;
using Dashboard.Typography;
using OpenTK.Mathematics;
namespace Dashboard.Controls
{
public class Label : Control
{
public string Text { get; set; } = string.Empty;
public Font? Font { get; set; }
public float TextSize { get; set; }
public bool AutoSize { get; set; } = true;
protected override void ValidateLayout()
{
if (AutoSize)
{
Vector2 size = Typesetter.MeasureHorizontal(Text, TextSize, Font!);
Size = size;
}
}
protected override void ValidateVisual(DrawList cmd)
{
float padding = Padding;
Vector2 origin = new Vector2(padding, padding);
cmd.TypesetHorizontalDirect(Text, origin, TextSize, Font!);
}
}
}
+151
View File
@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using Dashboard.ImmediateDraw;
using OpenTK.Mathematics;
namespace Dashboard.Controls
{
/// <summary>
/// Bases for all UI elements.
/// </summary>
public abstract class UIBase : IDbAttribute
{
private Vector2 size;
public UIBase? Parent { get; protected set; }
public Rectangle Bounds
{
get => new Rectangle(Position + Size, Position);
set
{
Size = value.Size;
Position = value.Min;
}
}
public virtual Vector2 Position { get; set; }
public Vector2 Size
{
get => size;
set
{
Vector2 oldSize = size;
size = value;
OnResized(this, new ResizedEventArgs(size, oldSize));
}
}
public Rectangle AbsoluteBounds
{
get
{
if (Parent == null)
{
return Bounds;
}
else
{
return new Rectangle(Bounds.Max + Parent.Position, Bounds.Min + Parent.Position);
}
}
}
public Vector2 MaximumSize { get; set; } = new Vector2(-1, -1);
public Vector2 MinimumSize { get; set; } = new Vector2(-1, -1);
public bool IsMaximumSizeSet => MaximumSize != new Vector2(-1, -1);
public bool IsMinimumSizeSet => MinimumSize != new Vector2(-1, -1);
public Dictionary<string, object> Attributes { get; } = new Dictionary<string, object>();
public virtual void NotifyEvent(object? sender, EventArgs args)
{
}
protected virtual void PaintBegin(DrawList cmd)
{
cmd.PushViewport();
cmd.StoreViewport(AbsoluteBounds);
cmd.PushZ();
}
protected virtual void PaintEnd(DrawList cmd)
{
cmd.PopViewport();
}
public void Paint(DrawList cmd)
{
PaintBegin(cmd);
PaintEnd(cmd);
}
public event EventHandler<ResizedEventArgs>? Resized;
public event EventHandler<AdoptedEventArgs>? Adopted;
public virtual void OnResized(object sender, ResizedEventArgs ea)
{
Resized?.Invoke(sender, ea);
}
protected virtual void OnAdopted(UIBase? parent)
{
Adopted?.Invoke(this, new AdoptedEventArgs(parent));
}
public bool IsDisposed { get; private set; } = false;
protected virtual void Dispose(bool disposing)
{
foreach (object userdata in Attributes.Values)
{
if (userdata is IDisposable disposable)
disposable.Dispose();
}
}
protected void DisposeInvoker(bool disposing)
{
if (IsDisposed)
return;
IsDisposed = true;
Dispose(disposing);
}
public void Dispose() => DisposeInvoker(true);
protected static void Adopt(UIBase child, UIBase? parent)
{
child.Parent = parent;
child.OnAdopted(parent);
}
}
public class ResizedEventArgs : EventArgs
{
public Vector2 NewSize { get; }
public Vector2 OldSize { get; }
public ResizedEventArgs(Vector2 newSize, Vector2 oldSize)
{
NewSize = newSize;
OldSize = oldSize;
}
}
public class AdoptedEventArgs : EventArgs
{
public UIBase? Parent { get; }
public AdoptedEventArgs(UIBase? parent = null)
{
Parent = parent;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
using System;
namespace Dashboard.Controls
{
public class View : UIBase
{
}
}
+13 -3
View File
@@ -1,9 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="res/**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OpenTK.Graphics" Version="4.8.2" />
<PackageReference Include="OpenTK.Mathematics" Version="4.8.2" />
</ItemGroup>
</Project>
+133
View File
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Dashboard.ImmediateDraw;
using Dashboard.Controls;
using Dashboard.Media;
using Dashboard.PAL;
using Dashboard.Typography;
namespace Dashboard
{
/// <summary>
/// Main class for Dashboard applications.
/// </summary>
public class DbApplication
{
/// <summary>
/// The application platform driver.
/// </summary>
public IDbPlatform Platform { get; }
/// <summary>
/// Title of the application.
/// </summary>
public string? Title
{
get => Platform.Title;
set => Platform.Title = value;
}
/// <summary>
/// Application icon.
/// </summary>
public Image? Icon
{
get => Platform.Icon;
set => Platform.Icon = value;
}
public PAL.Dash? MainPort { get; private set; } = null;
public FontProvider FontProvider { get; }
/// <summary>
/// List of media loaders, drivers that load media such as images and fonts.
/// </summary>
public List<MediaLoader> MediaLoaders { get; } = new List<MediaLoader>();
public DbApplication(IDbPlatform platform)
{
Platform = platform;
FontProvider = new FontProvider(this);
Current = this;
}
public IDisposable? GetMedia(object key, MediaHint hint)
{
IDisposable? disposable = null;
foreach (MediaLoader loader in MediaLoaders)
{
disposable = loader.GetMedia(key, hint);
if (disposable != null)
break;
}
return disposable;
}
public IDisposable? GetMedia<T>(T key, MediaHint hint)
{
IDisposable? disposable = null;
foreach (MediaLoader loader in MediaLoaders)
{
if (loader is MediaLoader<T> typedLoader)
{
disposable = typedLoader.GetMedia(key, hint);
if (disposable != null)
break;
}
}
return disposable;
}
private DrawList cmd = new DrawList();
public void Run(View mainView, bool yield = true)
{
Init(mainView);
while (RunSync())
{
if (yield)
{
Thread.Yield();
}
}
}
public void Init(View mainView)
{
MainPort = new PAL.Dash(Platform) { UIElement = mainView };
MainPort.EventRaised += (sender, ea) => mainView.NotifyEvent(sender, ea);
}
public bool RunSync()
{
if (!MainPort!.IsValid)
return false;
Platform.ProcessEvents(false);
if (MainPort.IsValid)
{
cmd.Clear();
MainPort.Paint(cmd);
}
return true;
}
public static DbApplication Current { get; private set; } = null!;
public static void SetCurrentApplication(DbApplication application)
{
Current = application;
}
}
}
+268
View File
@@ -0,0 +1,268 @@
using OpenTK.Mathematics;
using System;
using System.Diagnostics;
namespace Dashboard
{
/// <summary>
/// A bezier curve segment.
/// </summary>
[DebuggerDisplay("{Start} -- {ControlA} -- {ControlB} -- {End}")]
public struct Bezier
{
/// <summary>
/// Segment start point.
/// </summary>
public Vector2 Start;
/// <summary>
/// Start point control point.
/// </summary>
public Vector2 ControlA;
/// <summary>
/// End point control point.
/// </summary>
public Vector2 ControlB;
/// <summary>
/// Segment end point.
/// </summary>
public Vector2 End;
/// <summary>
/// An approximation of the arc length of the bezier curve, for calculating rasterization resolution.
/// </summary>
public float RasterizationArc =>
0.5f * (End - Start).Length +
0.5f * ((ControlA - Start).Length + (ControlB - ControlA).Length + (End - ControlB).Length);
public Bezier(Vector2 start, Vector2 controlA, Vector2 controlB, Vector2 end)
{
Start = start;
ControlA = controlA;
ControlB = controlB;
End = end;
}
public Bezier(
float startX,
float startY,
float controlAx,
float controlAy,
float controlBx,
float controlBy,
float endX,
float endY)
: this(
new Vector2(startX, startY),
new Vector2(controlAx, controlAy),
new Vector2(controlBx, controlBy),
new Vector2(endX, endY))
{
}
/// <summary>
/// Get a point in the curve segment.
/// </summary>
/// <param name="t">Control parameter (between 0 and 1)</param>
/// <returns>The point on the curve.</returns>
public Vector2 GetBezierPoint(float t)
{
float T = 1 - t;
return
T * T * T * Start +
3 * T * T * t * ControlA +
3 * T * t * t * ControlB +
t * t * t * End;
}
/// <summary>
/// Get the tangent on the curve.
/// </summary>
/// <param name="t">Control parameter (between 0 and 1)</param>
/// <returns>The tangent curve.</returns>
public Vector2 GetBezierTangent(float t)
{
float T = 1 - t;
return
(
3 * T * T * (ControlA - Start) +
6 * T * t * (ControlB - ControlA) +
3 * t * t * (End - ControlB)
).Normalized();
}
internal Vector2 GetBezierNormal(float t)
{
Vector2 tangent = GetBezierTangent(t);
return new Vector2(-tangent.Y, tangent.X);
}
}
/// <summary>
/// A line segment.
/// </summary>
[DebuggerDisplay("{Start} -- {End}")]
public struct Line
{
/// <summary>
/// Start point.
/// </summary>
public Vector2 Start;
/// <summary>
/// End point.
/// </summary>
public Vector2 End;
public Line(Vector2 start, Vector2 end)
{
Start = start;
End = end;
}
public Line(float startX, float startY, float endX, float endY)
{
Start.X = startX;
Start.Y = startY;
End.X = endX;
End.Y = endY;
}
public Vector2 Normal()
{
Vector2 tangent = Tangent();
return new Vector2(-tangent.Y, tangent.X);
}
public Vector2 Tangent()
{
return (End - Start).Normalized();
}
}
/// <summary>
/// A rectangle.
/// </summary>
[DebuggerDisplay("({Left}, {Top}, {Right}, {Bottom})")]
public struct Rectangle
{
/// <summary>
/// Position maximum point.
/// </summary>
public Vector2 Max;
/// <summary>
/// Position minimum point.
/// </summary>
public Vector2 Min;
public float Left
{
get => Min.X;
set => Min.X = value;
}
public float Right
{
get => Max.X;
set => Max.X = value;
}
public float Top
{
get => Min.Y;
set => Min.Y = value;
}
public float Bottom
{
get => Max.Y;
set => Max.Y = value;
}
public Vector2 Size
{
get => Max - Min;
set => Max = Min + value;
}
public Rectangle(Vector2 max, Vector2 min)
{
Max = max;
Min = min;
}
public Rectangle(float r, float b, float l, float t)
{
Max = new Vector2(r, b);
Min = new Vector2(l, t);
}
public bool Contains(Vector2 point)
{
return
point.X > Left && point.X < Right &&
point.Y > Bottom && point.Y < Top;
}
internal void Translate(in Vector2 offset)
{
Min += offset;
Max += offset;
}
public static Rectangle Intersect(in Rectangle a, in Rectangle b) =>
new Rectangle(
Math.Max(a.Right, b.Right),
Math.Max(a.Bottom, b.Bottom)
,
Math.Min(a.Left, b.Left),
Math.Min(a.Top, b.Top));
}
/// <summary>
/// An ellipse.
/// </summary>
/// <remarks>It is undefined to have an ellipse with non-orthogonal axes.</remarks>
[DebuggerDisplay("{Center} ellipse {AxisA}; {AxisB}")]
public struct Ellipse
{
/// <summary>
/// Ellipse center point.
/// </summary>
public Vector2 Center;
/// <summary>
/// First ellipse axis.
/// </summary>
public Vector2 AxisA;
/// <summary>
/// Second ellipse axis.
/// </summary>
public Vector2 AxisB;
}
/// <summary>
/// A triangle.
/// </summary>
[DebuggerDisplay("{A} -- {B} -- {C}")]
public struct Triangle
{
/// <summary>
/// First vertex.
/// </summary>
public Vector2 A;
/// <summary>
/// Second vertex.
/// </summary>
public Vector2 B;
/// <summary>
/// Third vertex.
/// </summary>
public Vector2 C;
}
}
+21
View File
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace Dashboard
{
/// <summary>
/// Common interface for Dashboard objects that accept attributes.
/// </summary>
/// <remarks>
/// Dashboard will call dispose on any and all objects which implement IDisposable.
/// If this is an issue, please guard your object against this using a wrapper.
/// </remarks
public interface IDbAttribute : IDisposable
{
/// <summary>
/// Attribute dictionary.
/// </summary>
public Dictionary<string, object> Attributes { get; }
}
}
+62
View File
@@ -0,0 +1,62 @@
namespace Dashboard.ImmediateDraw
{
/// <summary>
/// Enumeration of built-in Quik commands.
/// </summary>
public enum Command
{
#region Control Commands
/// <summary>
/// Invoke a function directly.
/// </summary>
Invoke,
/// <summary>
/// Begin conditional rendering segment.
/// </summary>
ConditionalBegin,
/// <summary>
/// End conditional rendering segment.
/// </summary>
ConditionalEnd,
PushViewport,
IntersectViewport,
StoreViewport,
PopViewport,
PushZ,
IncrementZ,
AddZ,
StoreZ,
DecrementZ,
PopZ,
PushMatrix,
StoreIdentityMatrix,
StoreMatrix,
PopMatrix,
PushStyle,
StoreStyle,
PopStyle,
#endregion
#region Draw Commands
Line,
Bezier,
Rectangle,
Ellipse,
Triangle,
Polygon,
Image,
#endregion
/// <summary>
/// Start index for custom commands.
/// </summary>
CustomCommandBase = 1024,
}
}
@@ -0,0 +1,8 @@
namespace Dashboard.ImmediateDraw
{
/// <summary>
/// A delegate for a Dashboard engine command.
/// </summary>
/// <param name="stack">The current stack.</param>
public delegate void CommandHandler(DrawingEngine state, DrawQueue queue);
}
+233
View File
@@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using OpenTK.Mathematics;
namespace Dashboard.ImmediateDraw
{
public class DrawingEngine
{
private int _zIndex = 0;
private readonly Stack<int> _zStack = new Stack<int>();
public int ZIndex => _zIndex;
private Rectangle _viewport;
private readonly Stack<Rectangle> _viewportStack = new Stack<Rectangle>();
private readonly Stack<Matrix4> _matrixStack = new Stack<Matrix4>();
private Command _customCommandBase = Command.CustomCommandBase;
private readonly List<CommandHandler> _customCommands = new List<CommandHandler>();
public Rectangle Viewport => _viewport;
public Matrix4 ActiveTransforms { get; }
public StyleStack Style { get; } = new StyleStack(new Style());
protected DrawingEngine()
{
Reset();
}
public Command RegisterCustomCommand(CommandHandler handler)
{
Command id = _customCommandBase++;
_customCommands.Insert(id - Command.CustomCommandBase, handler);
return id;
}
public void ProcessCommands(Rectangle bounds, DrawList queue)
{
DrawQueue iterator = queue.GetEnumerator();
if (!iterator.Peek().IsCommand)
throw new ArgumentException("The first element in the iterator must be a command frame.");
Reset();
_viewport = bounds;
_viewportStack.Push(_viewport);
Frame frame;
while (iterator.TryDequeue(out frame))
{
Command cmd = (Command)frame;
switch (cmd)
{
default:
if (cmd > Command.CustomCommandBase)
{
_customCommands[cmd - Command.CustomCommandBase].Invoke(this, iterator);
}
else
{
ChildProcessCommand(cmd, iterator);
}
break;
case Command.ConditionalBegin: ConditionalHandler(iterator); break;
case Command.ConditionalEnd: /* nop */ break;
case Command.Invoke:
iterator.Dequeue().As<CommandHandler>().Invoke(this, iterator);
break;
case Command.PushViewport:
_viewportStack.Push(_viewport);
break;
case Command.IntersectViewport:
_viewport = Rectangle.Intersect((Rectangle)iterator.Dequeue(), _viewport);
break;
case Command.StoreViewport:
_viewport = (Rectangle)iterator.Dequeue();
break;
case Command.PopViewport:
_viewport = _viewportStack.TryPop(out Rectangle viewport) ? viewport : bounds;
break;
case Command.PushZ:
_zStack.Push(_zIndex);
break;
case Command.IncrementZ:
_zIndex++;
break;
case Command.AddZ:
_zIndex += (int)iterator.Dequeue();
break;
case Command.StoreZ:
_zIndex = (int)iterator.Dequeue();
break;
case Command.DecrementZ:
_zIndex--;
break;
case Command.PopZ:
_zIndex = _zStack.TryPop(out int zindex) ? zindex : 0;
break;
case Command.PushStyle:
Style.Push(iterator.Dequeue().As<Style>());
break;
case Command.StoreStyle:
Style.Pop();
Style.Push(iterator.Dequeue().As<Style>());
break;
case Command.PopStyle:
Style.Pop();
break;
}
}
}
protected virtual void ChildProcessCommand(Command name, DrawQueue queue)
{
}
public virtual void Reset()
{
_zIndex = 0;
_zStack.Clear();
_viewport = new Rectangle(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue);
_viewportStack.Clear();
_matrixStack.Clear();
_matrixStack.Push(Matrix4.Identity);
}
private void ConditionalHandler(DrawQueue iterator)
{
Frame frame = iterator.Dequeue();
if (
frame.IsInteger && (int)frame != 0 ||
frame.As<Func<bool>>().Invoke())
{
// Take this branch.
return;
}
// Skip this branch.
int depth = 1;
while (iterator.TryPeek(out frame))
{
if (!frame.IsCommand)
{
iterator.Dequeue();
continue;
}
switch ((Command)frame)
{
case Command.ConditionalBegin:
// Increment conditional depth.
depth++;
break;
case Command.ConditionalEnd:
// Decrement condional depth, exit if zero.
if (--depth == 0)
{
iterator.Dequeue();
return;
}
break;
}
iterator.Dequeue();
}
}
private static readonly Dictionary<Type, IDrawListSerializer> s_serializers = new Dictionary<Type, IDrawListSerializer>();
/// <summary>
/// Add a custom serializer to the command engine.
/// </summary>
/// <typeparam name="T">Type object type.</typeparam>
/// <param name="serializer">The serializer.</param>
/// <param name="overwrite">True to allow overwriting.</param>
public static void AddSerializer<T>(IDrawListSerializer serializer, bool overwrite = false)
{
if (overwrite)
{
s_serializers[typeof(T)] = serializer;
}
else
{
s_serializers.Add(typeof(T), serializer);
}
}
/// <summary>
/// Add a custom serializer to the command engine.
/// </summary>
/// <typeparam name="T">Type object type.</typeparam>
/// <param name="serializer">The serializer.</param>
/// <param name="overwrite">True to allow overwriting.</param>
public static void AddSerializer<T>(IDrawListSerializer<T> serializer, bool overwrite = false)
=> AddSerializer<T>((IDrawListSerializer)serializer, overwrite);
/// <summary>
/// Get a serializer for the given object.
/// </summary>
/// <typeparam name="T">The object type.</typeparam>
/// <param name="value">Required parameter for the C# type inference to work.</param>
/// <returns>The serializer.</returns>
public static IDrawListSerializer<T> GetSerializer<T>(IDrawListSerializable<T>? value)
where T : IDrawListSerializable<T>, new()
{
if (!s_serializers.TryGetValue(typeof(T), out var serializer))
{
serializer = new DrawListSerializableSerializer<T>();
AddSerializer<T>(serializer);
}
return (IDrawListSerializer<T>)serializer;
}
/// <summary>
/// Get a serializer for the given object.
/// </summary>
/// <typeparam name="T">The object type.</typeparam>
/// <param name="value">Required parameter for the C# type inference to work.</param>
/// <returns>The serializer.</returns>
public static IDrawListSerializer<T> GetSerializer<T>(T? value)
{
return (IDrawListSerializer<T>)s_serializers[typeof(T)];
}
}
}
+434
View File
@@ -0,0 +1,434 @@
using Dashboard.Media;
using OpenTK.Mathematics;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Dashboard.ImmediateDraw
{
public class DrawList : IEnumerable<Frame>
{
private readonly List<Frame> _frames = new List<Frame>();
public void Clear()
{
_frames.Clear();
}
protected void Enqueue(in Frame frame)
{
_frames.Add(frame);
}
public void Invoke(CommandHandler handler)
{
Enqueue(Command.Invoke);
Enqueue(new Frame(handler));
}
public void ConditionalBegin(bool value)
{
Enqueue(Command.ConditionalBegin);
Enqueue((Frame)(value ? 1 : 0));
}
public void ConditionalBegin(Func<bool> condition)
{
Enqueue(Command.ConditionalBegin);
Enqueue(new Frame(condition));
}
public void ConditionalEnd()
{
Enqueue(Command.ConditionalEnd);
}
public void PushViewport()
{
Enqueue(Command.PushViewport);
}
public void IntersectViewport(in Rectangle viewport)
{
Enqueue(Command.IntersectViewport);
Enqueue(viewport);
}
public void StoreViewport(in Rectangle viewport)
{
Enqueue(Command.StoreViewport);
Enqueue(viewport);
}
public void PopViewport()
{
Enqueue(Command.PopViewport);
}
public void PushZ()
{
Enqueue(Command.PushZ);
}
public void IncrementZ()
{
Enqueue(Command.IncrementZ);
}
public void AddZ(int value)
{
if (value == 1)
{
IncrementZ();
}
else if (value == -1)
{
DecrementZ();
}
else
{
Enqueue(Command.AddZ);
Enqueue((Frame)value);
}
}
public void StoreZ(int value)
{
Enqueue(Command.StoreZ);
Enqueue((Frame)value);
}
public void DecrementZ()
{
Enqueue(Command.DecrementZ);
}
public void PopZ()
{
Enqueue(Command.PopZ);
}
public void PushStyle(Style style)
{
Enqueue(Command.PushStyle);
Enqueue(new Frame(style));
}
public void StoreStyle(Style style)
{
Enqueue(Command.StoreStyle);
Enqueue(new Frame(style));
}
public void PopStyle()
{
Enqueue(Command.PopStyle);
}
public void Line(in Line line)
{
Enqueue(Command.Line);
Enqueue(line);
}
public void Line(params Line[] lines)
{
Enqueue(Command.Line);
Enqueue((Frame)lines.Length);
foreach (Line line in lines)
Enqueue(line);
}
public void Bezier(in Bezier bezier)
{
Frame a, b;
Frame.Create(bezier, out a, out b);
Enqueue(Command.Bezier);
Enqueue(a);
Enqueue(b);
}
public void Bezier(params Bezier[] beziers)
{
Frame a, b;
Enqueue(Command.Bezier);
Enqueue((Frame)beziers.Length);
foreach (Bezier bezier in beziers)
{
Frame.Create(bezier, out a, out b);
Enqueue(a);
Enqueue(b);
}
}
public void Rectangle(in Rectangle rectangle)
{
Enqueue(Command.Rectangle);
Enqueue(rectangle);
}
public void Rectangle(Rectangle[] rectangles)
{
Enqueue(Command.Rectangle);
Enqueue((Frame)rectangles.Length);
foreach (Rectangle rectangle in rectangles)
Enqueue(rectangle);
}
public void Ellipse(in Ellipse ellipse)
{
Frame a, b;
Frame.Create(ellipse, out a, out b);
Enqueue(Command.Ellipse);
Enqueue(a);
Enqueue(b);
}
public void Ellipse(params Ellipse[] ellipses)
{
Frame a, b;
Enqueue(Command.Ellipse);
Enqueue((Frame)ellipses.Length);
foreach (Ellipse ellipse in ellipses)
{
Frame.Create(ellipse, out a, out b);
Enqueue(a);
Enqueue(b);
}
}
public void Triangle(in Triangle triangle)
{
Enqueue(Command.Triangle);
Enqueue(triangle.A);
Enqueue(triangle.B);
Enqueue(triangle.C);
}
public void Triangle(params Triangle[] triangles)
{
Enqueue(Command.Triangle);
Enqueue((Frame)triangles.Length);
foreach (Triangle triangle in triangles)
{
Enqueue(triangle.A);
Enqueue(triangle.B);
Enqueue(triangle.C);
}
}
public void Polygon(params Vector2[] polygon)
{
Enqueue(Command.Polygon);
Enqueue((Frame)polygon.Length);
foreach (Vector2 vertex in polygon)
{
Enqueue(vertex);
}
}
public void Image(Image texture, in Rectangle rectangle)
{
Enqueue(Command.Image);
Enqueue((Frame)(int)ImageCommandFlags.Single);
Enqueue(new Frame(texture));
Enqueue(rectangle);
}
public void Image(Image texture, in Rectangle rectangle, in Rectangle uv)
{
Enqueue(Command.Image);
Enqueue((Frame)(int)(ImageCommandFlags.Single | ImageCommandFlags.UVs));
Enqueue(new Frame(texture));
Enqueue(rectangle);
Enqueue(uv);
}
public void Image(Image texture, ReadOnlySpan<Rectangle> rectangles, bool interleavedUV = false)
{
int count = rectangles.Length;
ImageCommandFlags flags = ImageCommandFlags.None;
if (interleavedUV)
{
count /= 2;
flags |= ImageCommandFlags.UVs;
}
Enqueue(Command.Image);
Enqueue(new Frame((int)flags, count));
Enqueue(new Frame(texture));
foreach (Rectangle rectangle in rectangles)
{
Enqueue(rectangle);
}
}
public void Image(Image texture, ReadOnlySpan<Rectangle> rectangles, ReadOnlySpan<Rectangle> uvs)
{
int count = Math.Min(rectangles.Length, uvs.Length);
Enqueue(Command.Image);
Enqueue(new Frame((int)ImageCommandFlags.UVs, count));
Enqueue(new Frame(texture));
for (int i = 0; i < count; i++)
{
Enqueue(rectangles[i]);
Enqueue(uvs[i]);
}
}
public void Image3D(Image texture, in Image3DCall call)
{
Enqueue(Command.Image);
Enqueue(new Frame(ImageCommandFlags.Image3d | ImageCommandFlags.Single));
Enqueue(new Frame(texture));
Enqueue(call.Rectangle);
Enqueue(call.UVs);
Enqueue(new Frame(call.Layer));
}
public void Image3D(Image texture, ReadOnlySpan<Image3DCall> calls)
{
Enqueue(Command.Image);
Enqueue(new Frame((int)ImageCommandFlags.Image3d, calls.Length));
Enqueue(new Frame(texture));
foreach (Image3DCall call in calls)
{
Enqueue(call.Rectangle);
Enqueue(call.UVs);
Enqueue(new Frame(call.Layer));
}
}
public void Splice(DrawList list)
{
foreach (Frame frame in list)
{
Enqueue(frame);
}
}
/// <summary>
/// Serialize an object into the command list.
/// </summary>
/// <typeparam name="T">The type of the value to serialize.</typeparam>
/// <param name="value">What to write into the command list.</param>
public void Write<T>(T value)
{
DrawingEngine.GetSerializer(value).Serialize(value, this);
}
/// <summary>
/// Serialize an object into the command list.
/// </summary>
/// <typeparam name="T">The type of the value to serialize.</typeparam>
/// <param name="value">What to write into the command list.</param>
public void Write<T>(IDrawListSerializable<T> value)
where T : IDrawListSerializable<T>, new()
{
DrawingEngine.GetSerializer(value).Serialize((T)value, this);
}
public DrawQueue GetEnumerator() => new DrawQueue(_frames);
IEnumerator<Frame> IEnumerable<Frame>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class DrawQueue : IEnumerator<Frame>
{
private readonly IReadOnlyList<Frame> _frames;
private int _current;
public Frame Current => _frames[_current];
object IEnumerator.Current => Current;
public DrawQueue(IReadOnlyList<Frame> frames)
{
_current = -1;
_frames = frames;
}
public void Dispose()
{
}
public bool TryDequeue([NotNullWhen(true)] out Frame frame)
{
if (MoveNext())
{
frame = Current;
return true;
}
else
{
frame = default;
return false;
}
}
public Frame Dequeue() => TryDequeue(out Frame frame) ? frame : throw new Exception("No more frames left.");
public bool TryPeek([NotNullWhen(true)] out Frame frame)
{
if (_current + 1 < _frames.Count)
{
frame = _frames[_current + 1];
return true;
}
else
{
frame = default;
return false;
}
}
public Frame Peek() => TryPeek(out Frame frame) ? frame : throw new Exception("No more frames left.");
/// <summary>
/// Deserialize an object from the command queue.
/// </summary>
/// <typeparam name="T">Type of the object to deserialize.</typeparam>
/// <param name="value">The deserialized value.</param>
public void Read<T>([NotNull] out T? value)
{
value = DrawingEngine.GetSerializer(default(T)).Deserialize(this);
}
/// <summary>
/// Deserialize an object from the command queue.
/// </summary>
/// <typeparam name="T">Type of the object to deserialize.</typeparam>
/// <param name="value">The deserialized value.</param>
public void Read<T>([NotNull] out IDrawListSerializable<T>? value)
where T : IDrawListSerializable<T>, new()
{
value = DrawingEngine.GetSerializer(value = null).Deserialize(this);
}
/// <inheritdoc/>
public bool MoveNext()
{
if (_current + 1 < _frames.Count)
{
_current++;
return true;
}
return false;
}
/// <inheritdoc/>
public void Reset()
{
_current = -1;
}
}
}
+362
View File
@@ -0,0 +1,362 @@
using System;
using System.Runtime.InteropServices;
using OpenTK.Mathematics;
namespace Dashboard.ImmediateDraw
{
[StructLayout(LayoutKind.Explicit)]
public struct Frame
{
[FieldOffset(0)]
private FrameType _type;
[FieldOffset(sizeof(FrameType) + 0 * sizeof(int))]
private int _i1;
[FieldOffset(sizeof(FrameType) + 1 * sizeof(int))]
private int _i2;
[FieldOffset(sizeof(FrameType) + 2 * sizeof(int))]
private int _i3;
[FieldOffset(sizeof(FrameType) + 3 * sizeof(int))]
private int _i4;
[FieldOffset(sizeof(FrameType) + 0 * sizeof(float))]
private float _f1;
[FieldOffset(sizeof(FrameType) + 1 * sizeof(float))]
private float _f2;
[FieldOffset(sizeof(FrameType) + 2 * sizeof(float))]
private float _f3;
[FieldOffset(sizeof(FrameType) + 3 * sizeof(float))]
private float _f4;
[FieldOffset(24)]
private object? _object = null;
public bool IsCommand => _type == FrameType.Command;
public bool IsInteger =>
_type == FrameType.IVec1 ||
_type == FrameType.IVec2 ||
_type == FrameType.IVec3 ||
_type == FrameType.IVec4;
public bool IsFloat =>
_type == FrameType.Vec1 ||
_type == FrameType.Vec2 ||
_type == FrameType.Vec3 ||
_type == FrameType.Vec4;
public int VectorSize
{
get
{
switch (_type)
{
case FrameType.None:
return 0;
default:
return 1;
case FrameType.Vec2: case FrameType.IVec2:
return 2;
case FrameType.Vec3: case FrameType.IVec3:
return 3;
case FrameType.Vec4: case FrameType.IVec4:
return 4;
}
}
}
public FrameType Type => _type;
public int I1 => _i1;
public int I2 => _i2;
public int I3 => _i3;
public int I4 => _i4;
public float F1 => _f1;
public float F2 => _f2;
public float F3 => _f3;
public float F4 => _f4;
public static Frame None { get; } = new Frame() {
_type = FrameType.None
};
#region Constructors
public Frame(Command command) : this()
{
_type = FrameType.Command;
_i1 = (int)command;
}
public Frame(object o)
{
_type = FrameType.Object;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_object = o;
}
public Frame(int i1)
{
_type = FrameType.IVec1;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_i1 = i1;
}
public Frame(int i1, int i2)
{
_type = FrameType.IVec2;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_i1 = i1;
_i2 = i2;
}
public Frame(int i1, int i2, int i3)
{
_type = FrameType.IVec3;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_i1 = i1;
_i2 = i2;
_i3 = i3;
}
public Frame(int i1, int i2, int i3, int i4)
{
_type = FrameType.IVec4;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_i1 = i1;
_i2 = i2;
_i3 = i3;
_i4 = i4;
}
public Frame(float f1)
{
_type = FrameType.Vec1;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_f1 = f1;
}
public Frame(float f1, float f2)
{
_type = FrameType.Vec2;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_f1 = f1;
_f2 = f2;
}
public Frame(float f1, float f2, float f3)
{
_type = FrameType.Vec3;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_f1 = f1;
_f2 = f2;
_f3 = f3;
}
public Frame(float f1, float f2, float f3, float f4)
{
_type = FrameType.Vec4;
_i1 = _i2 = _i3 = _i4 = default;
_f1 = _f2 = _f3 = _f4 = default;
_object = null;
_f1 = f1;
_f2 = f2;
_f3 = f3;
_f4 = f4;
}
#endregion
public T As<T>()
{
return (T)_object!;
}
public float GetF(int i)
{
switch (i)
{
case 0: return _f1;
case 1: return _f2;
case 2: return _f3;
case 3: return _f4;
default:
throw new ArgumentOutOfRangeException();
}
}
public int GetI(int i)
{
switch (i)
{
case 0: return _i1;
case 1: return _i2;
case 2: return _i3;
case 3: return _i4;
default:
throw new ArgumentOutOfRangeException();
}
}
#region Frame->T Conversion
public static explicit operator int(in Frame frame)
{
switch (frame.Type)
{
default:
throw new InvalidCastException();
case FrameType.Command:
case FrameType.IVec1:
case FrameType.IVec2:
case FrameType.IVec3:
case FrameType.IVec4:
return frame._i1;
case FrameType.Vec1:
case FrameType.Vec2:
case FrameType.Vec3:
case FrameType.Vec4:
return (int)frame._f1;
}
}
public static explicit operator float(in Frame frame)
{
switch (frame.Type)
{
default:
throw new InvalidCastException();
case FrameType.IVec1:
case FrameType.IVec2:
case FrameType.IVec3:
case FrameType.IVec4:
return frame._i1;
case FrameType.Vec1:
case FrameType.Vec2:
case FrameType.Vec3:
case FrameType.Vec4:
return frame._f1;
}
}
public static explicit operator Command(in Frame frame)
{
if (frame.Type != FrameType.Command)
{
throw new InvalidCastException("Not a command frame.");
}
return (Command)frame._i1;
}
public static explicit operator Vector2(in Frame frame)
{
switch (frame.Type)
{
default:
throw new InvalidCastException();
case FrameType.IVec2:
case FrameType.IVec3:
case FrameType.IVec4:
return new Vector2(frame._i1, frame._i2);
case FrameType.Vec2:
case FrameType.Vec3:
case FrameType.Vec4:
return new Vector2(frame._f1, frame._f2);
}
}
public static explicit operator Color4(in Frame frame)
{
switch (frame.Type)
{
case FrameType.IVec4:
return new Color4((byte)frame._i1, (byte)frame._i2, (byte)frame._i3, (byte)frame._i4);
case FrameType.Vec4:
return new Color4(frame._f1, frame._f2, frame._f3, frame._f4);
default:
throw new InvalidCastException();
}
}
public static explicit operator Rectangle(in Frame frame)
{
switch (frame.Type)
{
default:
throw new InvalidCastException();
case FrameType.IVec4:
return new Rectangle(frame._i1, frame._i2, frame._i3, frame._i4);
case FrameType.Vec4:
return new Rectangle(frame._f1, frame._f2, frame._f3, frame._f4);
}
}
public static explicit operator Line(in Frame frame)
{
switch (frame.Type)
{
default:
throw new InvalidCastException();
case FrameType.IVec4:
return new Line(frame._i1, frame._i2, frame._i3, frame._i4);
case FrameType.Vec4:
return new Line(frame._f1, frame._f2, frame._f3, frame._f4);
}
}
#endregion
public static explicit operator Frame(int i) => new Frame(i);
public static explicit operator Frame(float f) => new Frame(f);
public static implicit operator Frame(Command cmd) => new Frame(cmd);
public static implicit operator Frame(in Vector2 vector) => new Frame(vector.X, vector.Y);
public static implicit operator Frame(in Color4 color) => new Frame(color.R, color.G, color.B, color.A);
public static implicit operator Frame(in Rectangle rect) => new Frame(rect.Max.X, rect.Max.Y, rect.Min.X, rect.Min.Y);
public static implicit operator Frame(in Line line) => new Frame(line.Start.X, line.Start.Y, line.End.X, line.Start.Y);
public static void Create(in Bezier bezier, out Frame a, out Frame b)
{
a = new Frame(bezier.Start.X, bezier.Start.Y, bezier.End.X, bezier.End.Y);
b = new Frame(bezier.ControlA.X, bezier.ControlA.Y, bezier.ControlB.X, bezier.ControlB.Y);
}
public static void Create(in Ellipse ellipse, out Frame a, out Frame b)
{
a = new Frame(ellipse.Center.X, ellipse.Center.Y);
b = new Frame(ellipse.AxisA.X, ellipse.AxisA.Y, ellipse.AxisB.X, ellipse.AxisB.Y);
}
}
}
+68
View File
@@ -0,0 +1,68 @@
namespace Dashboard.ImmediateDraw
{
/// <summary>
/// Enumeration of command types in the Dashboard command lists.
/// </summary>
public enum FrameType
{
/// <summary>
/// A null value.
/// </summary>
None,
/// <summary>
/// A command frame.
/// </summary>
Command,
/// <summary>
/// An integer frame.
/// </summary>
IVec1,
/// <summary>
/// A two dimensional integer vector frame.
/// </summary>
IVec2,
/// <summary>
/// A three dimensional integer vector frame.
/// </summary>
IVec3,
/// <summary>
/// A four dimensional integer vector frame.
/// </summary>
IVec4,
/// <summary>
/// A floating point frame.
/// </summary>
Vec1,
/// <summary>
/// A two dimensional floating point vector frame.
/// </summary>
Vec2,
/// <summary>
/// A three dimensional floating point vector frame.
/// </summary>
Vec3,
/// <summary>
/// A four dimensional floating point vector frame.
/// </summary>
Vec4,
/// <summary>
/// A serialized object frame.
/// </summary>
Serialized,
/// <summary>
/// A .Net object frame.
/// </summary>
Object,
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace Dashboard.ImmediateDraw
{
public enum ImageCommandFlags
{
None = 0,
Single = 1 << 0,
UVs = 1 << 1,
Image3d = 1 << 2,
}
public struct Image3DCall
{
public Rectangle Rectangle;
public Rectangle UVs;
public int Layer;
}
}
+68
View File
@@ -0,0 +1,68 @@
using System.Diagnostics.CodeAnalysis;
namespace Dashboard.ImmediateDraw
{
public interface IDrawListSerializable { }
/// <summary>
/// Interface for objects that can be serialized into the Dashboard command stream.
/// </summary>
public interface IDrawListSerializable<T> : IDrawListSerializable
{
/// <summary>
/// Seralize object.
/// </summary>
/// <param name="list">The object to serialize into.</param>
void Serialize(DrawList list);
/// <summary>
/// Deserialize object.
/// </summary>
/// <param name="queue">The command queue to deserialize from.</param>
void Deserialize(DrawQueue queue);
}
/// <summary>
/// Base interface for all Command List serializers.
/// </summary>
public interface IDrawListSerializer { }
public interface IDrawListSerializer<T> : IDrawListSerializer
{
/// <summary>
/// Serialize an object into the command list.
/// </summary>
/// <param name="value">The object to serialize.</param>
/// <param name="list">The command list to serialize into.</param>
void Serialize(T value, DrawList list);
/// <summary>
/// Deserialize an object from the command queue.
/// </summary>
/// <param name="queue">The command queue.</param>
/// <returns>The object deserialized from the command queue.</returns>
[return: NotNull]
T Deserialize(DrawQueue queue);
}
/// <summary>
/// Class for automatic serialization of <see cref="IDrawListSerializable"/> objects.
/// </summary>
/// <typeparam name="T">The object type to convert.</typeparam>
internal class DrawListSerializableSerializer<T> : IDrawListSerializer<T>
where T : IDrawListSerializable<T>, new()
{
public T Deserialize(DrawQueue queue)
{
T value = new T();
value.Deserialize(queue);
return value;
}
public void Serialize(T value, DrawList list)
{
value.Serialize(list);
}
}
}
+116
View File
@@ -0,0 +1,116 @@
using OpenTK.Mathematics;
using Dashboard.Controls;
using System.Reflection.Metadata;
namespace Dashboard.Layout
{
/// <summary>
/// Control attributes for grid layout.
/// </summary>
public class GridLayoutAttribute
{
/// <summary>
/// An anchor will keep the relative distance of a control's edges the same during resizes.
/// </summary>
/// <remarks><see cref="Dock"/> has higher precedence.</remarks>
public Anchor Anchor { get; set; } = Anchor.Left | Anchor.Top;
/// <summary>
/// Dock will strongly attach a control to its container or its edges.
/// </summary>
/// <remarks>Has more precedence than <see cref="Anchor"/></remarks>
public Dock Dock { get; set; } = Dock.None;
public void Evaluate(
ResizedEventArgs parentResizeEvent,
in Rectangle oldBounds,
out Rectangle newBounds
)
{
switch (Dock)
{
default:
case Dock.None:
break;
case Dock.Top:
newBounds = new Rectangle(
parentResizeEvent.NewSize.X,
oldBounds.Size.Y,
0,
0
);
return;
case Dock.Bottom:
newBounds = new Rectangle(
parentResizeEvent.NewSize.X,
parentResizeEvent.NewSize.Y,
0,
oldBounds.Size.Y
);
return;
case Dock.Left:
newBounds = new Rectangle(
oldBounds.Size.X,
parentResizeEvent.NewSize.Y,
0,
0
);
return;
case Dock.Right:
newBounds = new Rectangle(
parentResizeEvent.NewSize.X,
parentResizeEvent.NewSize.Y,
parentResizeEvent.NewSize.Y - oldBounds.Size.Y,
0
);
return;
}
Vector2 scale = parentResizeEvent.NewSize / parentResizeEvent.OldSize;
newBounds = oldBounds;
if (Anchor.HasFlag(Anchor.Top))
{
newBounds.Top = scale.Y * oldBounds.Top;
}
if (Anchor.HasFlag(Anchor.Left))
{
newBounds.Left = scale.X * oldBounds.Left;
}
if (Anchor.HasFlag(Anchor.Bottom))
{
float margin = scale.Y * (parentResizeEvent.OldSize.Y - newBounds.Bottom);
newBounds.Bottom = parentResizeEvent.NewSize.Y - margin;
}
if (Anchor.HasFlag(Anchor.Right))
{
float margin = scale.X * (parentResizeEvent.OldSize.X - newBounds.Right);
newBounds.Right = parentResizeEvent.NewSize.X - margin;
}
}
/// <summary>
/// Get the grid layout attribute associated with the given UI-base.
/// </summary>
/// <param name="uiBase">The UI-base to query.</param>
/// <returns>It's grid layout attribute, if any.</returns>
public static GridLayoutAttribute GetGridLayout(UIBase uiBase)
{
const string GRID_LAYOUT_ATTRIBUTE_KEY = "486ddf8c-b75f-4ad4-a51d-5ba20db9bd0e";
if (
!uiBase.Attributes.TryGetValue(GRID_LAYOUT_ATTRIBUTE_KEY, out object? attribute)
|| attribute is not GridLayoutAttribute)
{
attribute = new GridLayoutAttribute();
uiBase.Attributes[GRID_LAYOUT_ATTRIBUTE_KEY] = attribute;
}
return (GridLayoutAttribute)attribute;
}
}
}
+110
View File
@@ -0,0 +1,110 @@
using System;
using OpenTK.Mathematics;
namespace Dashboard.Media.Color
{
public static class FormatConvert
{
public static void Premultiply(QImageLock image)
{
switch (image.Format)
{
case ImageFormat.RaF:
case ImageFormat.RaU8:
case ImageFormat.RgbF:
case ImageFormat.RgbU8:
case ImageFormat.RgbaF:
case ImageFormat.RgbaU8:
break;
default:
return;
}
int count = image.Width * image.Height * image.Depth;
if (image.Format.IsFloat())
{
LockIOF io = new LockIOF(image);
for (int i = 0; i < count; i++)
{
Color4 color = io[i];
color.R *= color.A;
color.G *= color.A;
color.G *= color.A;
io[i] = color;
}
}
else
{
LockIO io = new LockIO(image);
for (int i = 0; i < count; i++)
{
Color4 color = io[i];
float a = color.A;
color.R = (byte)(color.R * a);
color.G = (byte)(color.G * a);
color.B = (byte)(color.B * a);
io[i] = color;
}
}
}
public static void Convert(QImageLock dst, QImageLock src)
{
if (dst.Format.IsU8() && src.Format.IsU8())
{
LockIO dstIO = new LockIO(dst);
LockIO srcIO = new LockIO(src);
int count = dst.Width * dst.Height * dst.Depth;
for (int i = 0; i < count; i++)
{
dstIO[i] = srcIO[i];
}
}
else if (dst.Format.IsU8() && src.Format.IsFloat())
{
LockIO dstIO = new LockIO(dst);
LockIOF srcIO = new LockIOF(src);
int count = dst.Width * dst.Height * dst.Depth;
for (int i = 0; i < count; i++)
{
dstIO[i] = srcIO[i];
}
}
else if (dst.Format.IsFloat() && src.Format.IsU8())
{
LockIOF dstIO = new LockIOF(dst);
LockIO srcIO = new LockIO(src);
int count = dst.Width * dst.Height * dst.Depth;
for (int i = 0; i < count; i++)
{
dstIO[i] = srcIO[i];
}
}
else if (dst.Format.IsFloat() && src.Format.IsFloat())
{
LockIOF dstIO = new LockIOF(dst);
LockIOF srcIO = new LockIOF(src);
int count = dst.Width * dst.Height * dst.Depth;
for (int i = 0; i < count; i++)
{
dstIO[i] = srcIO[i];
}
}
else
{
throw new Exception("Congratulations you have broken image formats!");
}
}
}
}
+69
View File
@@ -0,0 +1,69 @@
using System;
using System.Runtime.InteropServices;
namespace Dashboard.Media.Color
{
public class QImageBuffer : Image
{
private byte[] buffer;
GCHandle handle;
private bool isSdf = false;
public override ImageFormat InternalFormat { get; }
public override int Width { get; }
public override int Height { get; }
public override int Depth { get; }
public override bool IsSdf => isSdf;
public QImageBuffer(ImageFormat format, int width, int height, int depth = 1)
{
InternalFormat = format;
Width = width;
Height = height;
Depth = depth;
buffer = new byte[(long)width * height * depth * format.BytesPerPixel()];
}
~QImageBuffer()
{
Dispose(false);
}
private QImageLock Lock()
{
if (handle.IsAllocated) handle.Free();
handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0);
return new QImageLock(InternalFormat, Width, Height, Depth, ptr);
}
protected override void Dispose(bool disposing)
{
if (handle.IsAllocated) handle.Free();
GC.SuppressFinalize(this);
}
public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options)
{
imageLock = Lock();
}
public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options)
{
imageLock = Lock();
}
public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth)
{
imageLock = Lock();
}
public override void UnlockBits()
{
handle.Free();
}
public void SetSdf(bool value = true) => isSdf = value;
}
}
+151
View File
@@ -0,0 +1,151 @@
using System;
using OpenTK.Mathematics;
namespace Dashboard.Media.Color
{
public unsafe struct LockIO
{
public QImageLock Lock { get; }
public int Width => Lock.Width;
public int Height => Lock.Height;
public int Depth => Depth;
public ImageFormat Format => Lock.Format;
public LockIO(QImageLock imageLock)
{
if (!imageLock.Format.IsU8())
throw new Exception("Can only read/write U8 format images");
Lock = imageLock;
}
public Color4 this[int index]
{
get
{
int chan = Format.Channels();
byte *ptr = (byte*)Lock.ImagePtr + chan * index;
switch (Format)
{
default:
case ImageFormat.RedU8: return new Color4(ptr[0], 0, 0, 255);
case ImageFormat.AlphaU8: return new Color4(0, 0, 0, ptr[0]);
case ImageFormat.RaU8: return new Color4(ptr[0], 0, 0, ptr[1]);
case ImageFormat.RgbU8: return new Color4(ptr[0], ptr[1], ptr[2], 255);
case ImageFormat.RgbaU8: return new Color4(ptr[0], ptr[1], ptr[2], ptr[3]);
}
}
set
{
int chan = Format.Channels();
byte *ptr = (byte*)Lock.ImagePtr + chan * index;
switch (Format)
{
default:
case ImageFormat.RedU8:
ptr[0] = (byte)(value.R * 255);
break;
case ImageFormat.AlphaU8:
ptr[0] = (byte)(value.A * 255);
break;
case ImageFormat.RaU8:
ptr[0] = (byte)(value.R * 255);
ptr[1] = (byte)(value.A * 255);
break;
case ImageFormat.RgbU8:
ptr[0] = (byte)(value.R * 255);
ptr[1] = (byte)(value.G * 255);
ptr[2] = (byte)(value.B * 255);
break;
case ImageFormat.RgbaU8:
ptr[0] = (byte)(value.R * 255);
ptr[1] = (byte)(value.G * 255);
ptr[2] = (byte)(value.B * 255);
ptr[3] = (byte)(value.A * 255);
break;
}
}
}
public Color4 this[int x, int y, int z = 0]
{
get => this[x + y * Width + z * Width * Height];
set => this[x + y * Width + z * Width * Height] = value;
}
}
public unsafe struct LockIOF
{
public QImageLock Lock { get; }
public int Width => Lock.Width;
public int Height => Lock.Height;
public int Depth => Depth;
public ImageFormat Format => Lock.Format;
public LockIOF(QImageLock imageLock)
{
if (!imageLock.Format.IsFloat())
throw new Exception("Can only read/write U8 format images");
Lock = imageLock;
}
public Color4 this[int index]
{
get
{
int chan = Format.Channels();
float *ptr = (float*)Lock.ImagePtr + chan * index;
switch (Format)
{
default:
case ImageFormat.RedU8: return new Color4(ptr[0], 0, 0, 1);
case ImageFormat.AlphaU8: return new Color4(0, 0, 0, ptr[0]);
case ImageFormat.RaU8: return new Color4(ptr[0], 0, 0, ptr[1]);
case ImageFormat.RgbU8: return new Color4(ptr[0], ptr[1], ptr[2], 1);
case ImageFormat.RgbaU8: return new Color4(ptr[0], ptr[1], ptr[2], ptr[3]);
}
}
set
{
int chan = Format.Channels();
float *ptr = (float*)Lock.ImagePtr + chan * index;
switch (Format)
{
default:
case ImageFormat.RedU8:
ptr[0] = value.R;
break;
case ImageFormat.AlphaU8:
ptr[0] = value.A;
break;
case ImageFormat.RaU8:
ptr[0] = value.R;
ptr[1] = value.A;
break;
case ImageFormat.RgbU8:
ptr[0] = value.R;
ptr[1] = value.G;
ptr[2] = value.B;
break;
case ImageFormat.RgbaU8:
ptr[0] = value.R;
ptr[1] = value.G;
ptr[2] = value.B;
ptr[3] = value.A;
break;
}
}
}
public Color4 this[int x, int y, int z = 0]
{
get => this[x + y * Width + z * Width * Height];
set => this[x + y * Width + z * Width * Height] = value;
}
}
}
+71
View File
@@ -0,0 +1,71 @@
namespace Dashboard.Media
{
public static class Extensions
{
public static bool IsU8(this ImageFormat format)
{
switch (format)
{
case ImageFormat.AlphaU8:
case ImageFormat.RedU8:
case ImageFormat.RaU8:
case ImageFormat.RgbU8:
case ImageFormat.RgbaU8:
return true;
default:
return false;
}
}
public static bool IsFloat(this ImageFormat format)
{
switch (format)
{
case ImageFormat.AlphaF:
case ImageFormat.RedF:
case ImageFormat.RaF:
case ImageFormat.RgbF:
case ImageFormat.RgbaF:
return true;
default:
return false;
}
}
public static int BytesPerPixel(this ImageFormat format)
{
switch (format)
{
case ImageFormat.AlphaU8: return sizeof(byte);
case ImageFormat.RedU8: return sizeof(byte);
case ImageFormat.RaU8: return 2 * sizeof(byte);
case ImageFormat.RgbU8: return 3 * sizeof(byte);
case ImageFormat.RgbaU8: return 4 * sizeof(byte);
case ImageFormat.AlphaF: return sizeof(float);
case ImageFormat.RedF: return sizeof(float);
case ImageFormat.RaF: return 2 * sizeof(float);
case ImageFormat.RgbF: return 3 * sizeof(float);
case ImageFormat.RgbaF: return 4 * sizeof(float);
default: return 0;
}
}
public static int Channels(this ImageFormat format)
{
switch (format)
{
case ImageFormat.AlphaU8: return 1;
case ImageFormat.RedU8: return 1;
case ImageFormat.RaU8: return 2;
case ImageFormat.RgbU8: return 3;
case ImageFormat.RgbaU8: return 4;
case ImageFormat.AlphaF: return 1;
case ImageFormat.RedF: return 1;
case ImageFormat.RaF: return 2;
case ImageFormat.RgbF: return 3;
case ImageFormat.RgbaF: return 4;
default: return 0;
}
}
}
}
+129
View File
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dashboard.Media;
using Dashboard.Media.Fonts;
namespace Dashboard.Media
{
/// <summary>
/// Abstract class that represents a font.
/// </summary>
public abstract class Font : IDisposable
{
public abstract FontFace Face { get; }
public string Family => Face.Family;
public FontSlant Slant => Face.Slant;
public FontWeight Weight => Face.Weight;
public FontStretch Stretch => Face.Stretch;
public abstract bool HasRune(int rune);
protected abstract Image Render(out GlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options);
private readonly Dictionary<float, SizedFontCollection> _atlasses = new Dictionary<float, SizedFontCollection>();
public void Get(int codepoint, float size, out FontGlyph glyph)
{
SizedFontCollection? collection;
if (!_atlasses.TryGetValue(size, out collection))
{
collection = new SizedFontCollection(size);
_atlasses.Add(size, collection);
}
collection.Get(codepoint, out glyph, this);
}
// IDisposable
private bool isDisposed = false;
private void DisposePrivate(bool disposing)
{
if (isDisposed) return;
Dispose(disposing);
isDisposed = true;
}
protected virtual void Dispose(bool disposing) { }
public void Dispose() => DisposePrivate(true);
private class SizedFontCollection
{
public float Size { get; }
private readonly Dictionary<int, FontGlyph> glyphs = new Dictionary<int, FontGlyph>();
private readonly FontAtlas atlas;
public SizedFontCollection(float size)
{
Size = size;
DbApplication.Current.Platform.GetMaximumImage(out int height, out int width);
// Do no allow to create a texture that is greater than 16 square characters at 200 DPI.
width = Math.Min(width, (int)(size * 200 * 16));
height = Math.Min(height, (int)(size * 200 * 16));
// width = height = 256;
atlas = new FontAtlas(width, height, DbApplication.Current.FontProvider.RasterizerOptions.Sdf);
}
public void Get(int codepoint, out FontGlyph glyph, Font font)
{
if (glyphs.TryGetValue(codepoint, out glyph))
return;
Image image = font.Render(
out GlyphMetrics metrics,
codepoint,
Size,
DbApplication.Current.FontProvider.RasterizerOptions);
if (image != null)
{
image.LockBits2d(out QImageLock l, QImageLockOptions.Default);
atlas.PutGlyph(codepoint, l, out FontAtlasGlyphInfo glyphInfo);
image.UnlockBits();
image.Dispose();
glyph = new FontGlyph(codepoint, glyphInfo.Image, metrics, glyphInfo.UVs);
}
else
{
glyph = new FontGlyph(codepoint, null, metrics, default);
}
glyphs[codepoint] = glyph;
}
}
}
public readonly struct FontGlyph
{
public readonly int CodePoint;
public readonly Image? Image;
public readonly GlyphMetrics Metrics;
public readonly Rectangle UVs;
public FontGlyph(int codepoint, Image? image, in GlyphMetrics metrics, in Rectangle uvs)
{
CodePoint = codepoint;
Image = image;
Metrics = metrics;
UVs = uvs;
}
}
public struct FontRasterizerOptions
{
public float Resolution { get; set; }
public bool Sdf { get; set; }
public static readonly FontRasterizerOptions Default = new FontRasterizerOptions()
{
Resolution = 96.0f,
Sdf = false
};
}
}
+185
View File
@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using Dashboard.Media.Color;
using OpenTK.Mathematics;
namespace Dashboard.Media.Fonts
{
public struct FontAtlasGlyphInfo
{
public int Codepoint;
public Image Image;
public Rectangle UVs;
}
public class FontAtlas
{
private readonly int width, height;
private readonly List<AtlasPage> atlases = new List<AtlasPage>();
private readonly Dictionary<int, FontAtlasGlyphInfo> glyphs = new Dictionary<int, FontAtlasGlyphInfo>();
private int index = 0;
private AtlasPage? last = null;
private bool isSdf = false;
private int expansion;
public bool IsSdf
{
get => isSdf;
set
{
foreach (AtlasPage page in atlases)
{
((QImageBuffer)page.Image).SetSdf(value);
}
isSdf = value;
}
}
public FontAtlas(int width, int height, bool isSdf, int expansion = 4)
{
this.width = width;
this.height = height;
IsSdf = isSdf;
this.expansion = expansion;
}
public bool GetGlyph(int codepoint, out FontAtlasGlyphInfo info)
{
return glyphs.TryGetValue(codepoint, out info);
}
public void PutGlyph(int codepoint, QImageLock source, out FontAtlasGlyphInfo info)
{
info = new FontAtlasGlyphInfo() { Codepoint = codepoint };
if (last == null || !last.WouldFit(source))
{
AddPage();
}
last!.PutGlyph(source, ref info);
}
private void AddPage()
{
index++;
if (index < atlases.Count)
{
last = atlases[index];
}
else
{
last = new AtlasPage(width, height, expansion);
((QImageBuffer)last.Image).SetSdf(IsSdf);
atlases.Add(last);
}
}
public void Clear()
{
// Trim any pages that were not used yet.
for (int i = atlases.Count -1; i >= 0; i--)
{
if (atlases[i].PointerX != 0 && atlases[i].PointerY != 0)
{
for (int j = i + 1; j < atlases.Count; j++)
{
atlases[j].Dispose();
}
if (i != atlases.Count - 1)
atlases.RemoveRange(i+1, atlases.Count - i - 1);
break;
}
}
if (atlases.Count > 0)
{
last = atlases[0];
}
else
{
last = null;
}
index = -1;
glyphs.Clear();
}
private class AtlasPage : IDisposable
{
public Image Image;
public int PointerX, PointerY;
public int RowHeight;
public int Expansion;
public bool IsFull => PointerX > Image.Width || PointerY > Image.Height;
public AtlasPage(int width, int height, int expansion)
{
Image = new QImageBuffer(ImageFormat.AlphaU8, width, height);
Expansion = expansion;
Reset();
}
public void PutGlyph(QImageLock src, ref FontAtlasGlyphInfo prototype)
{
if (IsFull)
throw new Exception("Page is full!");
Image.LockBits2d(out QImageLock dst, QImageLockOptions.Default);
src.CopyTo(dst, PointerX, PointerY);
Image.UnlockBits();
Vector2 min = new Vector2((float)PointerX/Image.Width, (float)PointerY/Image.Height);
Vector2 size = new Vector2((float)src.Width/Image.Width, (float)src.Height/Image.Height);
prototype.Image = Image;
prototype.UVs = new Rectangle(min + size, min);
AdvanceColumn(src.Width, src.Height);
}
public void Reset()
{
RowHeight = PointerX = PointerY = 0;
}
public void AdvanceRow()
{
PointerX = 0;
PointerY += RowHeight + Expansion;
RowHeight = 0;
}
public void AdvanceColumn(int width, int height)
{
RowHeight = Math.Max(RowHeight, height);
PointerX += width + Expansion;
if (PointerX > Image.Width)
{
AdvanceRow();
}
}
private bool isDisposed = false;
public void Dispose()
{
if (isDisposed)
return;
Image?.Dispose();
isDisposed = true;
}
internal bool WouldFit(QImageLock source)
{
return !IsFull || PointerX + source.Width > Image.Width || PointerY + source.Height > Image.Height;
}
}
}
}
+238
View File
@@ -0,0 +1,238 @@
using System;
using System.Text;
namespace Dashboard.Media.Fonts
{
public readonly struct FontFace : IEquatable<FontFace>
{
public string Family { get; }
public FontSlant Slant { get; }
public FontWeight Weight { get; }
public FontStretch Stretch { get; }
public FontFace(string family, FontSlant slant, FontWeight weight, FontStretch stretch)
{
Family = family;
Slant = slant;
Weight = weight;
Stretch = stretch;
}
public override string ToString()
{
StringBuilder builder = new StringBuilder(Family);
if (Slant != FontSlant.Normal)
{
builder.Append(' ');
builder.Append(Slant);
}
if (Stretch != FontStretch.Normal)
{
builder.Append(' ');
builder.Append(Stretch);
}
if (Weight != FontWeight.Normal)
{
builder.Append(' ');
builder.Append(Weight);
}
if (Slant == FontSlant.Normal &&
Stretch == FontStretch.Normal &&
Weight == FontWeight.Normal)
{
builder.Append(" Regular");
}
return builder.ToString();
}
public override int GetHashCode()
{
return HashCode.Combine(Family, Slant, Weight, Stretch);
}
public static bool operator==(FontFace a, FontFace b)
{
return (a.Slant == b.Slant) &&
(a.Weight == b.Weight) &&
(a.Stretch == b.Stretch) &&
(a.Family == a.Family);
}
public static bool operator!=(FontFace a, FontFace b)
{
return (a.Slant != b.Slant) ||
(a.Weight != b.Weight) ||
(a.Stretch != b.Stretch) ||
(a.Family != b.Family);
}
public bool Equals(FontFace other)
{
return this == other;
}
public override bool Equals(object? obj)
{
return (obj?.GetType() == typeof(FontFace)) &&
this == (FontFace)obj;
}
public static FontFace Parse(string family, string style)
{
FontSlant slant = FontSlant.Normal;
FontWeight weight = FontWeight.Normal;
FontStretch stretch = FontStretch.Normal;
string[] tokens = style.Split(' ');
foreach (string token in tokens)
{
/**/ if (TryParseSlant(token, out FontSlant xslant)) slant = xslant;
else if (TryParseWeight(token, out FontWeight xweight)) weight = xweight;
else if (TryParseStretch(token, out FontStretch xstretch)) stretch = xstretch;
}
return new FontFace(family, slant, weight, stretch);
}
public static FontFace Parse(string face)
{
StringBuilder family = new StringBuilder();
FontSlant slant = FontSlant.Normal;
FontWeight weight = FontWeight.Normal;
FontStretch stretch = FontStretch.Normal;
string[] tokens = face.Split(' ');
foreach (string token in tokens)
{
string xtoken = token.ToLower();
if (xtoken == "regular" || xtoken == "normal")
{
continue;
}
else if (TryParseSlant(xtoken, out FontSlant xslant)) slant = xslant;
else if (TryParseWeight(xtoken, out FontWeight xweight)) weight = xweight;
else if (TryParseStretch(xtoken, out FontStretch xstretch)) stretch = xstretch;
else
{
family.Append(token);
}
}
return new FontFace(family.ToString(), slant, weight, stretch);
}
/// <summary>
/// Try to convert a token that represents a font slant into its enum.
/// </summary>
/// <param name="token">The token to interpret.</param>
/// <param name="slant">The resulting slant.</param>
/// <returns>True if it matched any.</returns>
public static bool TryParseSlant(string token, out FontSlant slant)
{
switch (token.ToLower())
{
case "italic":
slant = FontSlant.Italic;
return true;
case "oblique":
slant = FontSlant.Oblique;
return true;
default:
slant = FontSlant.Normal;
return false;
}
}
/// <summary>
/// Try to convert a token that represents a font weight into its enum.
/// </summary>
/// <param name="token">The token to interpret.</param>
/// <param name="weight">The resulting weight.</param>
/// <returns>True if it matched any.</returns>
public static bool TryParseWeight(string token, out FontWeight weight)
{
switch (token.ToLower())
{
case "thin":
weight = FontWeight.Thin;
return true;
case "extralight":
case "ultralight":
weight = FontWeight._200;
return true;
case "light":
case "demilight":
case "semilight":
weight = FontWeight._300;
return true;
case "demibold":
case "semibold":
weight = FontWeight._600;
return true;
case "bold":
weight = FontWeight._700;
return true;
case "extrabold":
case "ultrabold":
weight = FontWeight._800;
return true;
case "heavy":
case "extrablack":
case "black":
case "ultrablack":
weight = FontWeight._900;
return true;
default:
weight = FontWeight.Normal;
return false;
}
}
/// <summary>
/// Try to convert a token that represents a font stretch into its enum.
/// </summary>
/// <param name="token">The token to interpret.</param>
/// <param name="stretch">The resulting stretch.</param>
/// <returns>True if it matched any.</returns>
public static bool TryParseStretch(string token, out FontStretch stretch)
{
switch (token.ToLower())
{
case "ultracondensed":
stretch = FontStretch.UltraCondensed;
return true;
case "extracondensed":
stretch = FontStretch.ExtraCondensed;
return true;
case "condensed":
stretch = FontStretch.Condensed;
return true;
case "semicondensed":
case "demicondensed":
stretch = FontStretch.SemiCondensed;
return true;
case "semiexpanded":
case "demiexpanded":
stretch = FontStretch.SemiExpanded;
return true;
case "expanded":
stretch = FontStretch.Expanded;
return true;
case "extraexpanded":
stretch = FontStretch.ExtraExpanded;
return true;
case "ultraexpanded":
stretch = FontStretch.UltraExpanded;
return true;
default:
stretch = FontStretch.Normal;
return false;
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace Dashboard.Media.Fonts
{
public enum FontSlant
{
Normal = 0,
Italic = 1,
Oblique = 2,
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace Dashboard.Media.Fonts
{
/// <summary>
/// Enumeration of font stretch values.
/// </summary>
public enum FontStretch
{
UltraCondensed = 500,
ExtraCondensed = 625,
Condensed = 750,
SemiCondensed = 875,
Normal = 1000,
SemiExpanded = 1125,
Expanded = 1250,
ExtraExpanded = 1500,
UltraExpanded = 2000,
}
}
+22
View File
@@ -0,0 +1,22 @@
using System;
namespace Dashboard.Media.Fonts
{
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 = _700,
Heavy = _900,
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Dashboard.Media.Fonts
{
public enum SystemFontFamily
{
/// <summary>
/// A font with serifs, like Times New Roman.
/// </summary>
Serif,
/// <summary>
/// A font without serifs, like Helvetica or Arial.
/// </summary>
Sans,
/// <summary>
/// A monospace font like Courier New.
/// </summary>
Monospace,
/// <summary>
/// A cursive font like Lucida Handwriting.
/// </summary>
Cursive,
/// <summary>
/// An immature font like Comic Sans or Papyrus, nghehehehe.
/// </summary>
Fantasy
}
}
+49
View File
@@ -0,0 +1,49 @@
using OpenTK.Mathematics;
namespace Dashboard.Media
{
/// <summary>
/// Glyph properties with metrics based on FreeType glyph metrics.
/// </summary>
public struct GlyphMetrics
{
/// <summary>
/// The code point for the character.
/// </summary>
public int Rune { get; }
/// <summary>
/// Size of the glyph in units.
/// </summary>
public Vector2 Size { get; }
/// <summary>
/// Bearing vector for horizontal layout.
/// </summary>
public Vector2 HorizontalBearing { get; }
/// <summary>
/// Bearing vector for vertical layout.
/// </summary>
public Vector2 VerticalBearing { get; }
/// <summary>
/// Advance vector for vertical and horizontal layouts.
/// </summary>
public Vector2 Advance { get; }
public GlyphMetrics(
int character,
Vector2 size,
Vector2 horizontalBearing,
Vector2 verticalBearing,
Vector2 advance)
{
Rune = character;
Size = size;
HorizontalBearing = horizontalBearing;
VerticalBearing = verticalBearing;
Advance = advance;
}
}
}
+120
View File
@@ -0,0 +1,120 @@
using System;
namespace Dashboard.Media
{
public abstract class Image : IDisposable
{
public abstract int Width { get; }
public abstract int Height { get; }
public abstract int Depth { get; }
public abstract ImageFormat InternalFormat { get; }
public virtual int MipMapLevels => 0;
public virtual bool Premultiplied => false;
public virtual bool IsSdf => false;
public abstract void LockBits2d(out QImageLock imageLock, QImageLockOptions options);
public abstract void LockBits3d(out QImageLock imageLock, QImageLockOptions options);
public abstract void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth);
public abstract void UnlockBits();
// IDisposable
private bool isDisposed = false;
private void DisposePrivate(bool disposing)
{
if (isDisposed) return;
Dispose(disposing);
isDisposed = true;
}
protected virtual void Dispose(bool disposing) { }
public void Dispose() => DisposePrivate(true);
}
public struct QImageLockOptions
{
public ImageFormat Format { get; }
public bool Premultiply { get; }
public int MipLevel { get; }
public static QImageLockOptions Default { get; } = new QImageLockOptions(ImageFormat.RgbaU8, true, 0);
public QImageLockOptions(ImageFormat format, bool premultiply, int level)
{
Format = format;
Premultiply = premultiply;
MipLevel = level;
}
}
public struct QImageLock
{
public ImageFormat Format { get; }
public int Width { get; }
public int Height { get; }
public int Depth { get; }
public IntPtr ImagePtr { get; }
public QImageLock(ImageFormat format, int width, int height, int depth, IntPtr ptr)
{
Format = format;
Width = width;
Height = height;
Depth = depth;
ImagePtr = ptr;
}
public unsafe void CopyTo(QImageLock destination, int x, int y)
{
if (
Width + x >= destination.Width ||
Height + y >= destination.Height)
{
throw new Exception("Image falls outside the bounds of the destination.");
}
else if (Format != destination.Format)
{
throw new Exception("Image formats must be the same.");
}
int bpp = Format.BytesPerPixel();
for (int i = 0; i < Height; i++)
{
IntPtr srcPtr = (IntPtr)((long)ImagePtr + i * Width * bpp);
long dstPos = x + i * destination.Width;
IntPtr dstPtr = (IntPtr)((long)destination.ImagePtr + dstPos * bpp);
Buffer.MemoryCopy((void*)srcPtr, (void*)dstPtr, Width * bpp, Width * bpp);
}
}
public unsafe void ExtractFrom(QImageLock destination, int x, int y, int width, int height)
{
if (
width != destination.Width ||
height != destination.Height)
{
throw new Exception("Destination is not the same size as the subregion.");
}
else if (x + width > Width || y + height > Height)
{
throw new Exception("The subregion is larger than this image.");
}
else if (Format != destination.Format)
{
throw new Exception("Image formats must be the same.");
}
int bpp = Format.BytesPerPixel();
for (int i = 0; i < height; i++)
{
long srcPos = x + y * i;
IntPtr srcPtr = (IntPtr)((long)ImagePtr + srcPos * bpp);
long dstPos = i * destination.Width;
IntPtr dstPtr = (IntPtr)((long)destination.ImagePtr + dstPos * bpp);
Buffer.MemoryCopy((void*)srcPtr, (void*)dstPtr, width * bpp, width * bpp);
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
using System;
namespace Dashboard.Media
{
public enum ImageFormat
{
Undefined,
RedU8,
RgbU8,
RgbaU8,
RedF,
RgbF,
RgbaF,
AlphaU8,
AlphaF,
RaU8,
RaF,
}
}
+22
View File
@@ -0,0 +1,22 @@
using System;
using System.IO;
namespace Dashboard.Media
{
public enum MediaHint
{
None,
Image,
Font
}
public interface MediaLoader
{
IDisposable GetMedia(object key, MediaHint hint);
}
public interface MediaLoader<T> : MediaLoader
{
IDisposable GetMedia(T key, MediaHint hint);
}
}
+75
View File
@@ -0,0 +1,75 @@
using OpenTK.Mathematics;
using System;
namespace Dashboard
{
public enum MouseButton : byte
{
Primary = 1 << 0,
Secondary = 1 << 1,
Tertiary = 1 << 2,
Auxilliary1 = 1 << 3,
Auxilliary2 = 1 << 4,
Auxilliary3 = 1 << 5,
Auxilliary4 = 1 << 6,
Auxilliary5 = 1 << 7,
}
public struct MouseState
{
public readonly Vector2 AbsolutePosition;
public readonly MouseButton ButtonsDown;
public MouseState(Vector2 position, MouseButton down)
{
AbsolutePosition = position;
ButtonsDown = down;
}
}
public class MouseButtonEventArgs : EventArgs
{
public Vector2 AbsolutePosition { get; }
public MouseButton Buttons { get; }
public MouseButtonEventArgs(Vector2 position, MouseButton buttons)
{
AbsolutePosition = position;
Buttons = buttons;
}
public Vector2 RelativePosition(Vector2 origin)
{
return AbsolutePosition - origin;
}
// public QVec2 RelativePosition(Controls.Control control)
// {
// return AbsolutePosition - control.AbsoluteBounds.Min;
// }
}
public class MouseMoveEventArgs : EventArgs
{
public Vector2 AbsolutePosition { get; }
public Vector2 LastPosition { get; }
public Vector2 Motion { get; }
public MouseMoveEventArgs(Vector2 position, Vector2 lastPosition)
{
AbsolutePosition = position;
LastPosition = lastPosition;
Motion = position - lastPosition;
}
public Vector2 RelativePosition(Vector2 origin)
{
return AbsolutePosition - origin;
}
// public QVec2 RelativePosition(Controls.Control control)
// {
// return AbsolutePosition - control.AbsoluteBounds.Min;
// }
}
}
+460
View File
@@ -0,0 +1,460 @@
using Dashboard.VertexGenerator;
using Dashboard.Media;
using OpenTK.Graphics.OpenGL4;
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using OpenTK.Mathematics;
namespace Dashboard.OpenGL
{
public class GL21Driver : IDisposable
{
private int program;
private int v2Position;
private int fZIndex;
private int v2TexPos;
private int fTexLayer;
private int v4Color;
private int m4Transforms;
private int fMaxZ;
private int iEnableSdf;
private int iEnableTexture;
private int iAlphaDiscard;
private int fSdfThreshold;
private int tx2d;
private int tx2darray;
private bool isDiposed;
private readonly Dictionary<DrawCallQueue, DrawData> data = new Dictionary<DrawCallQueue, DrawData>();
private readonly TextureManager textures = new TextureManager();
public bool IsInit { get; private set; } = false;
public event Action<GL21Driver>? OnGCDispose;
public GL21Driver()
{
}
~GL21Driver()
{
Dispose(false);
}
public void Init()
{
if (IsInit) return;
int vs = CreateShader(ShaderType.VertexShader, "Dashboard.res.gl21.vert");
int fs = CreateShader(ShaderType.FragmentShader, "Dashboard.res.gl21.frag");
program = GL.CreateProgram();
GL.AttachShader(program, vs);
GL.AttachShader(program, fs);
GL.LinkProgram(program);
if (CheckProgram(program, out string msg) == false)
{
GraphicsException ex = new GraphicsException("Could not link shader program.");
ex.Data.Add("Program Info Log", msg);
}
GL.DeleteShader(vs);
GL.DeleteShader(fs);
v2Position = GL.GetAttribLocation(program, nameof(v2Position));
fZIndex = GL.GetAttribLocation(program, nameof(fZIndex));
v2TexPos = GL.GetAttribLocation(program, nameof(v2TexPos));
fTexLayer = GL.GetAttribLocation(program, nameof(fTexLayer));
v4Color = GL.GetAttribLocation(program, nameof(v4Color));
m4Transforms = GL.GetUniformLocation(program, nameof(m4Transforms));
fMaxZ = GL.GetUniformLocation(program, nameof(fMaxZ));
fSdfThreshold = GL.GetUniformLocation(program, nameof(fSdfThreshold));
iEnableSdf = GL.GetUniformLocation(program, nameof(iEnableSdf));
iEnableTexture = GL.GetUniformLocation(program, nameof(iEnableTexture));
iAlphaDiscard = GL.GetUniformLocation(program, nameof(iAlphaDiscard));
tx2d = GL.GetUniformLocation(program, nameof(tx2d));
tx2darray = GL.GetUniformLocation(program, nameof(tx2darray));
IsInit = true;
}
private void AssertInit()
{
if (!IsInit) throw new InvalidOperationException("Initialize the driver first.");
}
public void Draw(DrawCallQueue queue, in Rectangle view)
{
AssertInit();
if (!data.TryGetValue(queue, out DrawData? draw))
{
draw = new DrawData(this, queue);
data.Add(queue, draw);
}
// This already binds the vertex array for me.
draw.PrepareFrame();
Vector2 size = view.Size;
Matrix4 viewMatrix = Matrix4.CreateOrthographicOffCenter(view.Left, view.Right, view.Bottom, view.Top, 1, -1);
GL.Viewport(0, 0, (int)view.Size.X, (int)view.Size.Y);
GL.UseProgram(program);
GL.Uniform1(fMaxZ, (float)(queue.ZDepth+1));
GL.Uniform1(fSdfThreshold, 0.5f);
GL.Uniform1(tx2d, 0);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Enable(EnableCap.ScissorTest);
GL.Enable(EnableCap.DepthTest);
GL.DepthFunc(DepthFunction.Less);
foreach (DrawCall call in queue)
{
GL.Scissor(
(int)MathF.Round(call.Bounds.Min.X),
(int)MathF.Round(size.Y - call.Bounds.Max.Y),
(int)MathF.Round(call.Bounds.Size.X),
(int)MathF.Round(call.Bounds.Size.Y));
Matrix4 modelMatrix = Matrix4.CreateTranslation(call.Bounds.Min.X, call.Bounds.Min.Y, 0);
Matrix4 modelView = viewMatrix * modelMatrix;
GL.UniformMatrix4(m4Transforms, false, ref modelView);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, 0);
if (call.Texture != null)
{
GL.Uniform1(iEnableSdf, call.Texture.IsSdf ? 1 : 0);
GL.Uniform1(iAlphaDiscard, 1);
if (call.Texture.Depth > 1)
{
GL.Uniform1(iEnableTexture, 3);
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(TextureTarget.Texture2D, textures.GetTexture(call.Texture));
}
else
{
GL.Uniform1(iEnableTexture, 2);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, textures.GetTexture(call.Texture));
}
}
else
{
GL.Uniform1(iEnableTexture, 0);
}
GL.DrawElements(PrimitiveType.Triangles, call.Count, DrawElementsType.UnsignedInt, sizeof(int)*call.Start);
}
GL.Disable(EnableCap.ScissorTest);
GL.Disable(EnableCap.DepthTest);
GL.Disable(EnableCap.Blend);
}
public void ClearDrawQueue(DrawCallQueue queue)
{
AssertInit();
if (!data.TryGetValue(queue, out DrawData? draw))
return;
draw.Dispose();
data.Remove(queue);
}
private static int CreateShader(ShaderType type, string name)
{
StreamReader source = new StreamReader(typeof(GL21Driver).Assembly.GetManifestResourceStream(name) ?? throw new Exception("Resource not found."));
string text = source.ReadToEnd();
source.Dispose();
int shader = GL.CreateShader(type);
GL.ShaderSource(shader, text);
GL.CompileShader(shader);
if (CheckShader(shader, out string msg) == false)
{
GraphicsException ex = new GraphicsException($"Failed to compile {type} shader stage.");
ex.Data.Add("Shader Stage", type);
ex.Data.Add("Shader Info Log", msg);
ex.Data.Add("Shader Source", text);
throw ex;
}
return shader;
}
private static bool CheckShader(int shader, out string message)
{
message = string.Empty;
GL.GetShader(shader, ShaderParameter.CompileStatus, out int i);
if (i == 0)
{
message = GL.GetShaderInfoLog(shader);
return false;
}
return true;
}
private static bool CheckProgram(int program, out string message)
{
message = string.Empty;
GL.GetProgram(program, GetProgramParameterName.LinkStatus, out int i);
if (i == 0)
{
message = GL.GetProgramInfoLog(program);
return false;
}
return true;
}
private void Dispose(bool disposing)
{
if (isDiposed) return;
if (!IsInit)
{
isDiposed = true;
return;
}
if (!disposing)
{
if (OnGCDispose == null)
{
throw new Exception("This object must strictly be disposed from the owning thread, not GC");
}
else
{
OnGCDispose(this);
return;
}
}
GL.DeleteProgram(program);
foreach (DrawData datum in data.Values)
{
datum.Dispose();
}
isDiposed = true;
GC.SuppressFinalize(this);
}
public void Dispose() => Dispose(true);
private class DrawData : IDisposable
{
public DrawCallQueue Queue { get; }
public int VertexArray { get; }
private readonly GL21Driver driver;
private int vbo1, vbo2;
private int ebo1, ebo2;
public DrawData(GL21Driver driver, DrawCallQueue queue)
{
Queue = queue;
this.driver = driver;
VertexArray = GL.GenVertexArray();
GL.GenBuffers(1, out vbo1);
GL.GenBuffers(1, out vbo2);
GL.GenBuffers(1, out ebo1);
GL.GenBuffers(1, out ebo2);
isDisposed = false;
}
public void PrepareFrame()
{
int vbo, ebo;
vbo = Swap(ref vbo1, ref vbo2);
ebo = Swap(ref ebo1, ref ebo2);
if (Queue.VertexCount == 0 || Queue.ElementCount == 0)
return;
GL.BindVertexArray(VertexArray);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BufferData(BufferTarget.ArrayBuffer, DbVertex.Stride * Queue.VertexCount, Queue.VertexArray, BufferUsageHint.StreamDraw);
GL.VertexAttribPointer(driver.v2Position, 2, VertexAttribPointerType.Float, false, DbVertex.Stride, DbVertex.PositionOffset);
GL.VertexAttribPointer(driver.fZIndex, 1, VertexAttribPointerType.UnsignedInt, false, DbVertex.Stride, DbVertex.ZIndexOffset);
GL.VertexAttribPointer(driver.v2TexPos, 2, VertexAttribPointerType.Float, false, DbVertex.Stride, DbVertex.TextureCoordinatesOffset);
GL.VertexAttribPointer(driver.fTexLayer, 1, VertexAttribPointerType.Float, false, DbVertex.Stride, DbVertex.TextureLayerOffset);
GL.VertexAttribPointer(driver.v4Color, 4, VertexAttribPointerType.Float, true, DbVertex.Stride, DbVertex.ColorOffset);
GL.EnableVertexAttribArray(driver.v2Position);
GL.EnableVertexAttribArray(driver.fZIndex);
GL.EnableVertexAttribArray(driver.v2TexPos);
GL.EnableVertexAttribArray(driver.v4Color);
GL.EnableVertexAttribArray(driver.fTexLayer);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
GL.BufferData(BufferTarget.ElementArrayBuffer, Queue.ElementCount * sizeof(int), Queue.ElementArray, BufferUsageHint.StreamDraw);
int Swap(ref int a, ref int b)
{
a ^= b;
b ^= a;
a ^= b;
return a;
}
}
private bool isDisposed;
public void Dispose()
{
if (isDisposed) return;
GL.DeleteVertexArray(VertexArray);
GL.DeleteBuffer(vbo1);
GL.DeleteBuffer(vbo2);
GL.DeleteBuffer(ebo1);
GL.DeleteBuffer(ebo2);
}
}
}
internal class TextureManager : IDisposable
{
private readonly Dictionary<Image, int> textures = new Dictionary<Image, int>();
private readonly HashSet<Image> imagesNotUsed = new HashSet<Image>();
private bool isDisposed = false;
public void BeginFrame()
{
if (imagesNotUsed.Count > 0)
{
foreach (Image image in imagesNotUsed)
{
GL.DeleteTexture(textures[image]);
}
imagesNotUsed.Clear();
}
foreach (Image image in textures.Keys)
{
imagesNotUsed.Add(image);
}
}
public int GetTexture(Image image)
{
if (textures.TryGetValue(image, out int texture))
{
return texture;
}
if (image.Depth > 1)
{
texture = UploadTexture3d(image);
}
else
{
texture = UploadTexture2d(image);
}
return textures[image] = texture;
}
public int UploadTexture3d(Image image3d)
{
int texture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2DArray, texture);
image3d.LockBits3d(out QImageLock lck, QImageLockOptions.Default);
GL.TexImage3D(TextureTarget.Texture2DArray, 0, PixelInternalFormat.Rgba, lck.Width, lck.Height, lck.Depth, 0, s_InternalFormat[lck.Format], s_PixelType[lck.Format], lck.ImagePtr);
image3d.UnlockBits();
GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Linear);
return texture;
}
public int UploadTexture2d(Image image2d)
{
int texture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, texture);
image2d.LockBits2d(out QImageLock lck, QImageLockOptions.Default);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, lck.Width, lck.Height, 0, s_InternalFormat[lck.Format], s_PixelType[lck.Format], lck.ImagePtr);
image2d.UnlockBits();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Linear);
switch (image2d.InternalFormat)
{
case ImageFormat.RedU8:
case ImageFormat.RedF:
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int)All.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int)All.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int)All.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int)All.One);
break;
case ImageFormat.AlphaU8:
case ImageFormat.AlphaF:
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int)All.One);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int)All.One);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int)All.One);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int)All.Alpha);
break;
}
return texture;
}
public void Dispose()
{
if (isDisposed)
return;
isDisposed = true;
int[] ids = textures.Values.ToArray();
GL.DeleteTextures(ids.Length, ref ids[0]);
}
private static readonly Dictionary<ImageFormat, PixelFormat> s_InternalFormat = new Dictionary<ImageFormat, PixelFormat>()
{
[ImageFormat.AlphaF] = PixelFormat.Alpha,
[ImageFormat.AlphaU8] = PixelFormat.Alpha,
[ImageFormat.RedF] = PixelFormat.Red,
[ImageFormat.RedU8] = PixelFormat.Red,
[ImageFormat.RgbF] = PixelFormat.Rgb,
[ImageFormat.RgbU8] = PixelFormat.Rgb,
[ImageFormat.RgbaU8] = PixelFormat.Rgba,
[ImageFormat.RgbaF] = PixelFormat.Rgba,
};
private static readonly Dictionary<ImageFormat, PixelType> s_PixelType = new Dictionary<ImageFormat, PixelType>()
{
[ImageFormat.AlphaF] = PixelType.Float,
[ImageFormat.RedF] = PixelType.Float,
[ImageFormat.RgbF] = PixelType.Float,
[ImageFormat.RgbaF] = PixelType.Float,
[ImageFormat.AlphaU8] = PixelType.UnsignedByte,
[ImageFormat.RedU8] = PixelType.UnsignedByte,
[ImageFormat.RgbU8] = PixelType.UnsignedByte,
[ImageFormat.RgbaU8] = PixelType.UnsignedByte,
};
}
}
+46
View File
@@ -0,0 +1,46 @@
using System;
using OpenTK.Graphics.OpenGL4;
namespace Dashboard.OpenGL
{
[System.Serializable]
public class GraphicsException : Exception
{
public GraphicsException()
{
AddExtraData();
}
public GraphicsException(string message) : base(message)
{
AddExtraData();
}
public GraphicsException(string message, Exception inner) : base(message, inner)
{
AddExtraData();
}
protected GraphicsException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
AddExtraData();
}
private void AddExtraData()
{
GL.GetInteger(GetPName.MajorVersion, out int major);
GL.GetInteger(GetPName.MinorVersion, out int minor);
string version = GL.GetString(StringName.Version);
string vendor = GL.GetString(StringName.Vendor);
string renderer = GL.GetString(StringName.Renderer);
Data.Add("OpenGL Version", new Version(major, minor));
Data.Add("OpenGL Version String", version);
Data.Add("OpenGL Vendor", vendor);
Data.Add("OpenGL Renderer", renderer);
}
}
}
+88
View File
@@ -0,0 +1,88 @@
using Dashboard.ImmediateDraw;
using Dashboard.Controls;
using OpenTK.Mathematics;
using System;
namespace Dashboard.PAL
{
/// <summary>
/// An abstraction layer over the UI input and output.
/// </summary>
public class Dash
{
private readonly IDashHandle handle;
private readonly IDbPlatform platform;
public string Title
{
get => platform.PortGetTitle(handle);
set => platform.PortSetTitle(handle, value);
}
public Vector2 Size
{
get => platform.PortGetSize(handle);
set => platform.PortSetSize(handle, value);
}
public Vector2 Position
{
get => platform.PortGetPosition(handle);
set => platform.PortSetPosition(handle, value);
}
public UIBase? UIElement { get; set; }
public bool IsValid => platform.PortIsValid(handle);
public event EventHandler EventRaised
{
add
{
platform.PortSubscribeEvent(handle, value);
}
remove
{
platform.PortUnsubscribeEvent(handle, value);
}
}
public Dash(IDbPlatform platform)
{
this.platform = platform;
handle = platform.CreatePort();
}
bool isDisposed = false;
public void Dispose()
{
if (isDisposed) return;
platform.DestroyPort(handle);
isDisposed = true;
}
public void Focus()
{
platform.PortFocus(handle);
}
public void Paint(DrawList? list = null)
{
if (UIElement == null)
return;
list ??= new DrawList();
list.Clear();
UIElement.Bounds = new Rectangle(Size, new Vector2(0,0));
UIElement.Paint(list);
platform.PortPaint(handle, list);
}
public void Show(bool shown = true)
{
platform.PortShow(handle, shown);
}
}
}
+62
View File
@@ -0,0 +1,62 @@
using Dashboard.ImmediateDraw;
using Dashboard.Media;
using OpenTK.Mathematics;
using System;
namespace Dashboard.PAL
{
/// <summary>
/// An empty interface to statically type Quik port handles.
/// </summary>
public interface IDashHandle
{
}
/// <summary>
/// The primary primary platform abstraction interface for dashboard hosts.
/// </summary>
public interface IDbPlatform : IDisposable
{
/// <summary>
/// The title of the application.
/// </summary>
string? Title { get; set; }
/// <summary>
/// The default icon for the application.
/// </summary>
Image? Icon { get; set; }
/// <summary>
/// The event raised when an event is received.
/// </summary>
event EventHandler? EventRaised;
/// <summary>
/// Raise the events that have been enqueued.
/// </summary>
/// <param name="block">True to block until a new event arrives.</param>
void ProcessEvents(bool block);
/// <summary>
/// Create a window.
/// </summary>
/// <returns>The window instance.</returns>
IDashHandle CreatePort();
void DestroyPort(IDashHandle port);
string PortGetTitle(IDashHandle port);
void PortSetTitle(IDashHandle port, string title);
Vector2 PortGetSize(IDashHandle port);
void PortSetSize(IDashHandle port, Vector2 size);
Vector2 PortGetPosition(IDashHandle port);
void PortSetPosition(IDashHandle port, Vector2 position);
bool PortIsValid(IDashHandle port);
void PortSubscribeEvent(IDashHandle port, EventHandler handler);
void PortUnsubscribeEvent(IDashHandle port, EventHandler handler);
void PortFocus(IDashHandle port);
void PortShow(IDashHandle port, bool shown = true);
void PortPaint(IDashHandle port, DrawList commands);
void GetMaximumImage(out int width, out int height);
void GetMaximumImage(out int width, out int height, out int depth);
}
}
+67
View File
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dashboard.Media.Fonts;
namespace Dashboard.PAL
{
/// <summary>
/// Flags that effect font search criterea.
/// </summary>
[Flags]
public enum FontMatchCriteria
{
None = 0,
Family = 1 << 0,
Slant = 1 << 1,
Weight = 1 << 2,
Stretch = 1 << 3,
All = Family | Slant | Weight | Stretch,
}
/// <summary>
/// An abstraction over the system font database.
/// </summary>
public interface IFontDataBase
{
/// <summary>
/// All the fonts installed in the system.
/// </summary>
IEnumerable<FontFace> All { get; }
public FontFace Serif => GetSystemFontFace(SystemFontFamily.Serif);
public FontFace Sans => GetSystemFontFace(SystemFontFamily.Sans);
public FontFace Monospace => GetSystemFontFace(SystemFontFamily.Monospace);
public FontFace Cursive => GetSystemFontFace(SystemFontFamily.Cursive);
public FontFace Fantasy => GetSystemFontFace(SystemFontFamily.Fantasy);
/// <summary>
/// Search for the given font face.
/// </summary>
/// <param name="prototype">The font face prototype.</param>
/// <param name="criteria">The match criteria</param>
/// <returns>A list of fonts sorted by the closest match first.</returns>
IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All);
/// <summary>
/// Get the font face file info if it exists.
/// </summary>
/// <param name="face">The face to look for.</param>
/// <returns>The file info if it exists.</returns>
FileInfo FontFileInfo(FontFace face);
/// <summary>
/// Open a font face.
/// </summary>
/// <param name="face">The font face to open.</param>
/// <returns>The stream to the font face.</returns>
Stream Open(FontFace face);
/// <summary>
/// Get a system font family.
/// </summary>
/// <param name="family">The family type to look up.</param>
/// <returns>The name of a font in this family.</returns>
FontFace GetSystemFontFace(SystemFontFamily family);
}
}
+12
View File
@@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Dashboard.Media;
namespace Dashboard.PAL
{
public interface IFontFactory
{
bool TryOpen(Stream stream, [NotNullWhen(true)] out Font font);
}
}
+276
View File
@@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using Dashboard.Media;
using Dashboard.Media.Fonts;
using OpenTK.Mathematics;
namespace Dashboard
{
public enum TextAlignment
{
Left,
Center,
Right,
Justify
}
public enum TextDecoration
{
None,
Underline,
Overline,
Strikethrough
}
public enum TextTransform
{
None,
Title,
Upper,
Lower,
}
public enum ListMarkerType
{
None,
Disc,
Circle,
FilledSquare,
Square,
Dash,
ArabicNumeral,
RomanNumeralUpper,
RomanNumeralLower,
AlphabetUpper,
AlphabetLower,
Image
}
public enum ListMarkerPosition
{
Inside,
Outside,
}
public abstract class StyleBase
{
public abstract object? this[string key] { get; set; }
public Color4? Color
{
get => (Color4?)this["color"];
set => this["color"] = value;
}
public float? LineHeight
{
get => (float?)this["line-height"];
set => this["line-height"] = value;
}
public float? LetterSpacing
{
get => (float?)this["letter-spacing"];
set => this["letter-spacing"] = value;
}
public TextAlignment? TextAlignment
{
get => (TextAlignment?)this["text-alignment"];
set => this["text-alignment"] = value;
}
public TextDecoration? TextDecoration
{
get => (TextDecoration?)this["text-decoration"];
set => this["text-decoration"] = value;
}
public float? TextIndent
{
get => (float?)this["text-indent"];
set => this["text-indent"] = value;
}
public TextTransform? TextTransform
{
get => (TextTransform?)this["text-transform"];
set => this["text-transform"] = value;
}
public ListMarkerType? ListMarker
{
get => (ListMarkerType?)this["list-marker"];
set => this["list-marker"] = value;
}
public ListMarkerPosition? ListMarkerPosition
{
get => (ListMarkerPosition?)this["list-marker-position"];
set => this["list-marker-position"] = value;
}
public Image? ListMarkerImage
{
get => (Image?)this["list-marker-image"];
set => this["list-marker-image"] = value;
}
public float? StrokeWidth
{
get => (float?)this["stroke-width"];
set => this["stroke-width"] = value;
}
public OpenTK.Mathematics.Color4? StrokeColor
{
get => (Color4?)this["stroke-color"];
set => this["stroke-color"] = value;
}
public FontFace? Font
{
get => (FontFace?)this["font"];
set => this["font"] = value;
}
public int? ZIndex
{
get => (int?)this["z-index"];
set => this["z-index"] = value;
}
}
public class Style : StyleBase
{
private readonly Dictionary<string, object> _keys = new Dictionary<string, object>();
public override object? this[string styleKey]
{
get => _keys.TryGetValue(styleKey, out object? value) ? value : null;
set
{
if (value == null)
_keys.Remove(styleKey);
else
_keys[styleKey] = value;
}
}
}
public class StyleStack : StyleBase
{
public readonly List<Style> _styles = new List<Style>();
public Style BaseStyle { get; }
public override object? this[string key]
{
get
{
object? value = null;
for (int i = _styles.Count; i != 0 && value == null; i--)
{
value = _styles[i-1][key];
}
return value;
}
set => throw new InvalidOperationException();
}
public StyleStack(Style baseStyle)
{
BaseStyle = baseStyle;
Push(BaseStyle);
}
public void Clear()
{
_styles.Clear();
Push(BaseStyle);
}
public void Push(Style style)
{
_styles.Add(style);
}
public void Pop()
{
if (_styles.Count == 1)
return;
_styles.RemoveAt(_styles.Count - 1);
}
}
/// <summary>
/// A line stipple pattern.
/// </summary>
public struct StipplePattern
{
/// <summary>
/// The stipple pitch value.
/// </summary>
public float Pitch;
/// <summary>
/// The stipple duty cycle.
/// </summary>
public float DutyCycle;
public StipplePattern(float pitch, float dutyCycle)
{
Pitch = pitch;
DutyCycle = dutyCycle;
}
public static StipplePattern None => new StipplePattern(0.0f, 1.0f);
}
/// <summary>
/// Stroke style for lines and borders.
/// </summary>
public class StrokeStyle
{
/// <summary>
/// Stroke color.
/// </summary>
public Color4 Color { get; set; }
/// <summary>
/// Stroke width.
/// </summary>
public float Width { get; set; }
// /// <summary>
// /// Stroke stipple pattern.
// /// </summary>
// public QuikStipplePattern StipplePattern { get; set; }
public StrokeStyle()
{
}
public StrokeStyle(Color4 color, float width /*, QuikStipplePattern pattern*/)
{
Color = color;
Width = width;
// StipplePattern = pattern;
}
// public QuikStrokeStyle(QuikColor color, float width) : this(color, width, QuikStipplePattern.None)
// {
// }
}
/// <summary>
/// Fill style for rectangles and the like.
/// </summary>
public class FillStyle
{
public Color4 Color { get; set; }
}
}
+108
View File
@@ -0,0 +1,108 @@
using Dashboard.Media;
using Dashboard.Media.Fonts;
using Dashboard.PAL;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Dashboard.Typography
{
/// <summary>
/// The font provider is a caching object that provides fonts for typesetting classes.
/// </summary>
public class FontProvider : IDisposable
{
private Dictionary<FontFace, Font> Fonts { get; } = new Dictionary<FontFace, Font>();
private HashSet<Font> UsedFonts { get; } = new HashSet<Font>();
public readonly FontRasterizerOptions RasterizerOptions;
public IFontDataBase? Database { get; set; }
public IFontFactory? FontFactory { get; set; }
private readonly DbApplication App;
public Font this[FontFace info]
{
get
{
if (!Fonts.TryGetValue(info, out Font? font))
{
using Stream str = Database?.Open(info) ?? throw new Exception("Font could not be found.");
if (FontFactory?.TryOpen(str, out font) ?? false)
{
Fonts.Add(info, font);
}
else
{
throw new Exception("Font not found.");
}
}
UsedFonts.Add(font);
return font;
}
}
public Font this[SystemFontFamily family]
{
get
{
return this[Database?.GetSystemFontFace(family) ?? throw new Exception("No font database.")];
}
}
public FontProvider(DbApplication app, in FontRasterizerOptions options)
{
RasterizerOptions = options;
App = app;
Type? fdb = Type.GetType("Quik.Media.Defaults.FontDataBaseProvider, Quik.Media.Defaults");
if (fdb != null)
{
PropertyInfo? instanceProperty = fdb.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.GetProperty);
if (instanceProperty != null)
{
Database = (IFontDataBase)instanceProperty.GetValue(null)!;
}
}
Type? ffact = Type.GetType("Quik.Media.Defaults.FreeTypeFontFactory, Quik.Media.Defaults");
if (ffact != null)
{
ConstructorInfo? ctor = ffact.GetConstructor(Array.Empty<Type>());
FontFactory = (IFontFactory?)ctor?.Invoke(null);
}
}
public FontProvider(DbApplication app)
: this(app, FontRasterizerOptions.Default)
{
}
/// <summary>
/// Tracks the use of fonts used by this typesetter and removes any that haven't been referenced since the last cycle.
/// </summary>
public void Collect()
{
// foreach (FontJar jar in Fonts.Values.ToArray())
// {
// if (!UsedFonts.Contains(jar))
// {
// Fonts.Remove(jar.Info);
// }
// }
// UsedFonts.Clear();
}
private bool isDisposed = false;
public void Dispose()
{
if (isDisposed) return;
isDisposed = true;
foreach (Font font in Fonts.Values)
{
font.Dispose();
}
}
}
}
+510
View File
@@ -0,0 +1,510 @@
using OpenTK.Mathematics;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Dashboard.Media;
namespace Dashboard.Typography
{
/// <summary>
/// An atomic horizontal block of text which cannot be further divided.
/// </summary>
public struct HorizontalTextBlock
{
/// <summary>
/// The font associated with the text block.
/// </summary>
/// <value></value>
// public QuikFont Font { get; }
/// <summary>
/// Textual contents of the text block.
/// </summary>
public string Text { get; }
/// <summary>
/// Indicates this text block should be layed out right to left.
/// </summary>
public bool IsRTL { get; }
/// <summary>
/// Indicates this is a whitespace block.
/// </summary>
public bool IsWhitespace => string.IsNullOrWhiteSpace(Text);
public float Width { get; }
public float Ascend { get; }
public float Descend { get; }
public float Height => Ascend - Descend;
public HorizontalTextBlock(object font, string text, bool rtl = false)
{
// Font = font;
Text = text;
IsRTL = rtl;
float width = 0.0f;
float ascend = 0.0f;
float descend = 0.0f;
foreach (char chr in text)
{
// font.GetCharacter(chr, out _, out QGlyphMetrics glyph);
// width += glyph.Advance.X;
// ascend = Math.Max(ascend, glyph.HorizontalBearing.Y);
// descend = Math.Min(descend, glyph.HorizontalBearing.Y - glyph.Size.Y);
}
Width = width;
Ascend = ascend;
Descend = descend;
}
public HorizontalTextBlock(float width)
{
// Font = null;
Text = string.Empty;
IsRTL = false;
Width = width;
Ascend = Descend = 0.0f;
}
}
/// <summary>
/// An atomic vertical block of text which cannot be further divided.
/// </summary>
public struct VerticalTextBlock
{
// public QuikFont Font { get; }
public string Text { get; }
public bool IsWhitespace => string.IsNullOrWhiteSpace(Text);
public float Width { get; }
public float Height { get; }
public VerticalTextBlock(object font, string text)
{
// Font = font;
Text = text;
float width = 0.0f;
float height = 0.0f;
foreach(char chr in text)
{
// font.GetCharacter(chr, out _, out QGlyphMetrics glyph);
// width = Math.Max(width, - glyph.VerticalBearing.X * 2);
// height += glyph.Advance.Y;
}
Width = width;
Height = height;
}
public VerticalTextBlock(float height)
{
// Font = null;
Text = string.Empty;
Width = 0.0f;
Height = height;
}
}
public abstract class Paragraph
{
public abstract bool IsVertical { get; }
public float JustifyLimit { get; set; } = 30.0f;
public TextAlignment Alignment { get; set; } = TextAlignment.Default;
public float PreSpace { get; set; } = 0.0f;
public float PostSpace { get; set; } = 0.0f;
public float FirstLineInset { get; set; } = 0.0f;
public float LineGap { get; set; } = 12.0f;
public abstract void Typeset(TypesetGroup group, float width);
protected abstract void AppendBlock(object font, string text, bool rtl = false);
public void ConsumeText(object font, string text)
{
StringBuilder segment = new StringBuilder();
bool rtl = false;
bool ws = false;
foreach(char chr in text)
{
UnicodeCategory cat = char.GetUnicodeCategory(chr);
// FIXME: don't ignore control characters like a barbarian.
// TODO: how do I detect text flow direction???
if (char.IsWhiteSpace(chr) && chr != '\u00a0')
{
if (ws)
{
segment.Append(chr);
}
else
{
AppendBlock(font, segment.ToString());
segment.Clear();
segment.Append(chr);
ws = true;
}
}
else
{
if (ws)
{
AppendBlock(font, segment.ToString(), rtl);
segment.Clear();
segment.Append(chr);
ws = false;
}
else
{
segment.Append(chr);
}
}
}
if (segment.Length > 0)
{
AppendBlock(font, segment.ToString(), rtl);
}
}
}
public class HorizontalParagraph : Paragraph
{
public override bool IsVertical => false;
public List<HorizontalTextBlock> Blocks { get; } = new List<HorizontalTextBlock>();
public override void Typeset(TypesetGroup group, float width)
{
Queue<HorizontalTextBlock> line = new Queue<HorizontalTextBlock>();
int index = 0;
bool firstLine = true;
Vector2 pen = new Vector2(0, -PreSpace);
while (index < Blocks.Count)
{
index
+= GatherLine(
index,
width - (firstLine ? FirstLineInset : 0),
line,
out float excess,
out float ascend,
out float descend);
firstLine = false;
pen.Y -= ascend;
float interblockWs =
Alignment.HasFlag(TextAlignment.Justify) && excess < JustifyLimit
? excess / (line.Count - 1)
: 0.0f;
switch (Alignment & TextAlignment.HorizontalMask)
{
default:
case TextAlignment.AlignLeft:
if (firstLine) pen.X += FirstLineInset;
break;
case TextAlignment.AlignCenterH:
pen.X += excess / 2;
break;
case TextAlignment.AlignRight:
pen.X += excess;
break;
}
PutBlock(group, line, interblockWs, ref pen);
pen.Y -= LineGap - descend;
pen.X = 0.0f;
}
pen.Y -= PostSpace;
group.BoundingBox = new Rectangle(width, pen.Y, 0, 0);
group.Translate(-pen);
}
private int GatherLine(
int index,
float width,
Queue<HorizontalTextBlock> line,
out float excess,
out float ascend,
out float descend)
{
float currentWidth = 0.0f;
ascend = descend = 0.0f;
for (int i = index; i < Blocks.Count; i++)
{
HorizontalTextBlock block = Blocks[i];
if (currentWidth + block.Width > width)
{
break;
}
ascend = Math.Max(ascend, block.Ascend);
descend = Math.Min(descend, block.Descend);
currentWidth += block.Width;
line.Enqueue(block);
}
excess = width - currentWidth;
return line.Count;
}
public void PutBlock(
TypesetGroup group,
Queue<HorizontalTextBlock> line,
float interblockWs,
ref Vector2 pen)
{
Vector2 penpal = pen;
while (line.TryDequeue(out HorizontalTextBlock block))
{
if (block.IsWhitespace)
{
penpal.X += block.Width + interblockWs;
continue;
}
if (block.IsRTL)
{
for (int i = block.Text.Length - 1; i >= 0; i--)
{
char chr = block.Text[i];
// block.Font.GetCharacter(chr, out QuikTexture texture, out QGlyphMetrics metrics);
// group.Add(
// new TypesetCharacter(
// chr,
// texture,
// new QRectangle(
// penpal.X + metrics.Advance.X,
// penpal.Y + metrics.HorizontalBearing.Y,
// penpal.X + metrics.HorizontalBearing.X,
// penpal.Y - metrics.Size.Y + metrics.HorizontalBearing.Y),
// metrics.Location
// )
// );
// penpal.X += metrics.Advance.X;
}
}
else
{
for (int i = 0; i < block.Text.Length; i++)
{
char chr = block.Text[i];
// block.Font.GetCharacter(chr, out QuikTexture texture, out QGlyphMetrics metrics);
// group.Add(
// new TypesetCharacter(
// chr,
// texture,
// new QRectangle(
// penpal.X + metrics.Advance.X,
// penpal.Y + metrics.HorizontalBearing.Y,
// penpal.X + metrics.HorizontalBearing.X,
// penpal.Y - metrics.Size.Y + metrics.HorizontalBearing.Y),
// metrics.Location
// )
// );
// penpal.X += metrics.Advance.X;
}
}
penpal.X += interblockWs;
}
penpal.X -= interblockWs;
pen = penpal;
}
protected override void AppendBlock(object font, string text, bool rtl = false)
{
Blocks.Add(new HorizontalTextBlock(font, text, rtl));
}
}
public class VerticalParagraph : Paragraph
{
public override bool IsVertical => true;
public List<VerticalTextBlock> Blocks { get; } = new List<VerticalTextBlock>();
public override void Typeset(TypesetGroup group, float width)
{
throw new NotImplementedException();
}
protected override void AppendBlock(object font, string text, bool rtl = false)
{
Blocks.Add(new VerticalTextBlock(font, text));
}
}
public struct TypesetCharacter
{
public int Character;
public Image Texture;
public Rectangle Position;
public Rectangle UV;
public TypesetCharacter(
int chr,
Image texture,
in Rectangle position,
in Rectangle uv)
{
Character = chr;
Texture = texture;
Position = position;
UV = uv;
}
}
public class TypesetGroup : ICollection<TypesetCharacter>
{
private int _count = 0;
private TypesetCharacter[] _array = Array.Empty<TypesetCharacter>();
public Rectangle BoundingBox;
public int Count => _count;
public bool IsReadOnly => false;
public void Add(TypesetCharacter item)
{
if (_count == _array.Length)
{
Array.Resize(ref _array, _array.Length + 256);
}
_array[_count++] = item;
}
public void Clear()
{
_count = 0;
}
public void Translate(Vector2 offset)
{
BoundingBox.Translate(offset);
for (int i = 0; i < _count; i++)
{
_array[i].Position.Translate(offset);
}
}
public bool Contains(TypesetCharacter item)
{
throw new NotSupportedException();
}
public void CopyTo(TypesetCharacter[] array, int arrayIndex)
{
_array.CopyTo(array, arrayIndex);
}
public IEnumerator<TypesetCharacter> GetEnumerator()
{
for (int i = 0; i < _count; i++)
{
yield return _array[i];
}
}
public bool Remove(TypesetCharacter item)
{
throw new NotSupportedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
}
public void SortBy(IComparer<TypesetCharacter> comparer)
{
Array.Sort(_array, 0, _count, comparer);
}
public static IComparer<TypesetCharacter> SortByTexture { get; } = new SortByTextureComparer();
private class SortByTextureComparer : IComparer<TypesetCharacter>
{
public int Compare(TypesetCharacter x, TypesetCharacter y)
{
return y.Texture.GetHashCode() - x.Texture.GetHashCode();
}
}
}
/// <summary>
/// An enumeration of possible text alignments.
/// </summary>
[Flags]
public enum TextAlignment
{
/// <summary>
/// Align to the left margin, horizontally.
/// </summary>
AlignLeft = 0x01,
/// <summary>
/// Align text to center of left and right margins.
/// </summary>
AlignCenterH = 0x03,
/// <summary>
/// Align to the right margin, horizontally.
/// </summary>
AlignRight = 0x02,
/// <summary>
/// A bitmask for values relating to horizontal alignment.
/// </summary>
HorizontalMask = 0x03,
/// <summary>
/// Align text to the top margin.
/// </summary>
AlignTop = 0x00,
/// <summary>
/// Align text between the top and bottom margins.
/// <summary>
AlignCenterV = 0x04,
/// <summary>
/// Align text to the bottom margin.
/// </summary>
AlignBottom = 0x08,
/// <summary>
/// A bitmask for values relating to the vertical alignment.
/// </summary>
VerticalMask = 0x0C,
/// <summary>
/// Distribute characters uniformly on the line, when possible.
/// <summary>
Justify = 0x10,
/// <summary>
/// The default text alignment value.
/// </summary>
Default = AlignTop | AlignLeft
}
}
+182
View File
@@ -0,0 +1,182 @@
using OpenTK.Mathematics;
using Dashboard.ImmediateDraw;
using Dashboard.Media;
using System;
using System.Collections.Generic;
using System.Text;
namespace Dashboard.Typography
{
public static class Typesetter
{
private ref struct LineEnumerator
{
private ReadOnlySpan<char> Entire, Segment;
private bool Final;
public ReadOnlySpan<char> Current => Segment;
public LineEnumerator(ReadOnlySpan<char> value)
{
Entire = value;
Segment = ReadOnlySpan<char>.Empty;
Final = false;
}
public void Reset()
{
Segment = ReadOnlySpan<char>.Empty;
Final = false;
}
public bool MoveNext()
{
if (Final)
{
return false;
}
else if (Segment == ReadOnlySpan<char>.Empty)
{
int index = Entire.IndexOf('\n');
if (index == -1)
{
Segment = Entire;
}
else
{
Segment = Entire.Slice(0, index);
}
return true;
}
else
{
Entire.Overlaps(Segment, out int offset);
if (offset + Segment.Length >= Entire.Length)
{
return false;
}
ReadOnlySpan<char> rest = Entire.Slice(offset + Segment.Length + 1);
int index = rest.IndexOf('\n');
if (index == -1)
{
Segment = rest;
Final = true;
}
else
{
Segment = rest.Slice(0, index);
}
return true;
}
}
}
public static Vector2 MeasureHorizontal(ReadOnlySpan<char> str, float size, Font font)
{
var enumerator = new LineEnumerator(str);
float width = 0.0f;
float height = 0.0f;
while (enumerator.MoveNext())
{
ReadOnlySpan<char> line = enumerator.Current;
float lineHeight = 0.0f;
foreach (Rune r in line.EnumerateRunes())
{
int codepoint = r.Value;
font.Get(codepoint, size, out FontGlyph glyph);
width += glyph.Metrics.Advance.X;
lineHeight = Math.Max(lineHeight, glyph.Metrics.Size.Y);
}
height += lineHeight;
}
return new Vector2(width, height);
}
public static void TypesetHorizontalDirect(this DrawList list, ReadOnlySpan<char> str, Vector2 origin, float size, Font font)
{
Dictionary<Image, FontDrawInfo> drawInfo = new Dictionary<Image, FontDrawInfo>();
var enumerator = new LineEnumerator(str);
Vector2 pen = origin;
while (enumerator.MoveNext())
{
ReadOnlySpan<char> line = enumerator.Current;
float rise = 0.0f;
float fall = 0.0f;
// Find out all the code pages required, and the line height.
foreach (Rune r in line.EnumerateRunes())
{
int codepoint = r.Value;
font.Get(codepoint, size, out FontGlyph glyph);
float crise = glyph.Metrics.HorizontalBearing.Y;
float cfall = glyph.Metrics.Size.Y - crise;
rise = Math.Max(crise, rise);
fall = Math.Max(cfall, fall);
}
pen += new Vector2(0, rise);
foreach (Rune r in line.EnumerateRunes())
{
FontDrawInfo info;
int codepoint = r.Value;
font.Get(codepoint, size, out FontGlyph glyph);
ref readonly GlyphMetrics metrics = ref glyph.Metrics;
Image? image = glyph.Image;
if (image == null)
{
pen += new Vector2(metrics.Advance.X, 0);
continue;
}
if (!drawInfo.TryGetValue(image, out info))
{
info = new FontDrawInfo();
info.Image = image;
info.rectangles = new List<Rectangle>();
drawInfo[image] = info;
}
Rectangle dest = new Rectangle(
pen + new Vector2(metrics.HorizontalBearing.X + metrics.Size.X, metrics.Size.Y - metrics.HorizontalBearing.Y),
pen + new Vector2(metrics.HorizontalBearing.X, -metrics.HorizontalBearing.Y));
info.rectangles.Add(dest);
info.rectangles.Add(glyph.UVs);
pen.X += metrics.Advance.X;
}
pen.X = origin.X;
pen.Y += fall;
}
// Now for each rectangle we can dispatch draw calls.
foreach (FontDrawInfo info in drawInfo.Values)
{
list.Image(info.Image, info.rectangles.ToArray(), true);
}
}
private struct FontDrawInfo
{
public Image Image;
public List<Rectangle> rectangles;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
using System;
using System.Text;
namespace Dashboard.Typography
{
public static class UnicodeUtil
{
public static bool IsWhiteSpace(int chr)
{
switch (chr)
{
case '\t': case '\n': case '\v': case '\f': case '\r': case ' ':
case '\u0085': case '\u00A0': case '\u1680': case '\u2000':
case '\u2001': case '\u2002': case '\u2003': case '\u2004':
case '\u2005': case '\u2006': case '\u2007': case '\u2008':
case '\u2009': case '\u200A': case '\u2028': case '\u2029':
case '\u202F': case '\u205F': case '\u3000': case '\u180E':
case '\u200B': case '\u200C': case '\u200D': case '\u2060':
return true;
default:
return false;
}
}
public static bool IsUnicodeWhitespace(int chr)
{
switch (chr)
{
case '\t': case '\n': case '\v': case '\f': case '\r': case ' ':
case '\u0085': case '\u00A0': case '\u1680': case '\u2000':
case '\u2001': case '\u2002': case '\u2003': case '\u2004':
case '\u2005': case '\u2006': case '\u2007': case '\u2008':
case '\u2009': case '\u200A': case '\u2028': case '\u2029':
case '\u202F': case '\u205F': case '\u3000':
return true;
default:
return false;
}
}
public static bool IsNewLine(int chr)
{
switch (chr)
{
case '\n': case '\v': case '\f': case '\r':
case '\u0085': case '\u2028': case '\u2029':
return true;
default:
return false;
}
}
public static WhitespaceInfo GetWhitespaceInfo(int chr)
{
switch (chr)
{
// CHAR ------------BOILERPLATE----------- BREAK NEWLINE
case '\t': return new WhitespaceInfo('\t', true, false);
case '\n': return new WhitespaceInfo('\n', false, true);
case '\v': return new WhitespaceInfo('\v', false, true);
case '\f': return new WhitespaceInfo('\f', false, true);
case '\r': return new WhitespaceInfo('\r', false, true);
case ' ': return new WhitespaceInfo(' ', true, false);
case '\u0085': return new WhitespaceInfo('\u0085', false, true);
case '\u00A0': return new WhitespaceInfo('\u00A0', false, false);
case '\u1680': return new WhitespaceInfo('\u1689', true, false);
case '\u2000': return new WhitespaceInfo('\u2000', true, false);
case '\u2001': return new WhitespaceInfo('\u2001', true, false);
case '\u2002': return new WhitespaceInfo('\u2002', true, false);
case '\u2003': return new WhitespaceInfo('\u2003', true, false);
case '\u2004': return new WhitespaceInfo('\u2004', true, false);
case '\u2005': return new WhitespaceInfo('\u2005', true, false);
case '\u2006': return new WhitespaceInfo('\u2006', true, false);
case '\u2007': return new WhitespaceInfo('\u2007', false, false);
case '\u2008': return new WhitespaceInfo('\u2008', true, false);
case '\u2009': return new WhitespaceInfo('\u2009', true, false);
case '\u200A': return new WhitespaceInfo('\u200A', true, false);
case '\u2028': return new WhitespaceInfo('\u2028', false, true);
case '\u2029': return new WhitespaceInfo('\u2029', false, true);
case '\u202F': return new WhitespaceInfo('\u202F', false, false);
case '\u205F': return new WhitespaceInfo('\u205F', true, false);
case '\u3000': return new WhitespaceInfo('\u3000', true, false);
case '\u180E': return new WhitespaceInfo('\u180E', true, false);
case '\u200B': return new WhitespaceInfo('\u200B', true, false);
case '\u200C': return new WhitespaceInfo('\u200C', true, false);
case '\u200D': return new WhitespaceInfo('\u200D', true, false);
case '\u2060': return new WhitespaceInfo('\u2060', false, false);
default:
throw new ArgumentException("Character is not a whitespace character.", nameof(chr));
}
}
}
public struct WhitespaceInfo
{
public int Character { get; }
public bool IsBreaking { get; }
public bool IsNewline { get; }
public WhitespaceInfo(int chr, bool breaking, bool nl)
{
Character = chr;
IsBreaking = breaking;
IsNewline = nl;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
using OpenTK.Mathematics;
using System.Diagnostics;
namespace Dashboard.VertexGenerator
{
/// <summary>
/// Represents a GPU vertex.
/// </summary>
[DebuggerDisplay("XY={Position} RGBA={Color}, UV={TextureCoordinates}")]
public struct DbVertex
{
/// <summary>
/// Position value.
/// </summary>
public Vector2 Position;
/// <summary>
/// Texture Coordinates.
/// </summary>
public Vector2 TextureCoordinates;
/// <summary>
/// Per vertex color value.
/// </summary>
public Color4 Color;
/// <summary>
/// Per vertex depth index value.
/// </summary>
public int ZIndex;
/// <summary>
/// The texture layer to draw for 3d images.
/// </summary>
public float TextureLayer;
public static int PositionOffset => 0;
public static unsafe int TextureCoordinatesOffset => sizeof(Vector2);
public static unsafe int ColorOffset => 2 * sizeof(Vector2);
public static unsafe int ZIndexOffset => ColorOffset + sizeof(Color4);
public static unsafe int TextureLayerOffset => ZIndexOffset + sizeof(int);
public static unsafe int Stride => sizeof(DbVertex);
}
}
+113
View File
@@ -0,0 +1,113 @@
using Dashboard.Media;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Dashboard.VertexGenerator
{
public class DrawCallQueue : IEnumerable<DrawCall>
{
private readonly RefList<DbVertex> _vertices = new RefList<DbVertex>();
private readonly RefList<int> _elements = new RefList<int>();
private readonly List<DrawCall> _drawCalls = new List<DrawCall>();
private int _start;
private int _baseOffset;
private Rectangle _bounds;
private Image? _texture;
public int ZDepth { get; private set; }
public DbVertex[] VertexArray => _vertices.InternalArray;
public int VertexCount => _vertices.Count;
public int[] ElementArray => _elements.InternalArray;
public int ElementCount => _elements.Count;
public int DrawCallCount => _drawCalls.Count;
public int BaseOffset => _baseOffset;
public void Clear()
{
_vertices.Clear();
_elements.Clear();
_drawCalls.Clear();
ZDepth = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void StartDrawCall(in Rectangle bounds, Image? texture, int baseOffset)
{
_start = ElementCount;
_texture = texture;
_bounds = bounds;
_baseOffset = baseOffset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void StartDrawCall(in Rectangle bounds) => StartDrawCall(bounds, null, _vertices.Count);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void StartDrawCall(in Rectangle bounds, int baseOffset) => StartDrawCall(bounds, null, baseOffset);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void StartDrawCall(in Rectangle bounds, Image texture) => StartDrawCall(bounds, texture, _vertices.Count);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddVertex(in DbVertex vertex)
{
_vertices.Add(in vertex);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddElement(int offset)
{
_elements.Add(offset + _baseOffset);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RestoreOffset(int baseOffset)
{
int old = _baseOffset;
_baseOffset = baseOffset;
return old;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RestoreOffset() => RestoreOffset(_vertices.Count);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int AbsoluteElement(int offset)
{
return _baseOffset + offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RelativeElement(int baseOffset, int offset)
{
return AbsoluteElement(offset) - baseOffset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EndDrawCall()
{
int count = ElementCount - _start;
_drawCalls.Add(new DrawCall(_start, count, _bounds, _texture));
}
public IEnumerator<DrawCall> GetEnumerator() => _drawCalls.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _drawCalls.GetEnumerator();
}
public struct DrawCall
{
public int Start { get; }
public int Count { get; }
public Rectangle Bounds { get; }
public Image? Texture { get; }
public DrawCall(int start, int count, in Rectangle bounds, Image? texture)
{
Start = start;
Count = count;
Bounds = bounds;
Texture = texture;
}
}
}
+48
View File
@@ -0,0 +1,48 @@
using System;
namespace Dashboard.VertexGenerator
{
/// <summary>
/// A small list which whose items can be used by reference.
/// </summary>
/// <typeparam name="T">Container type.</typeparam>
internal class RefList<T>
{
private T[] _array = Array.Empty<T>();
private int _count = 0;
public T[] InternalArray => _array;
public ref T this[int index] => ref _array[index];
public int Count => _count;
public int Capacity => _array.Length;
public void Add(in T item)
{
EnsureCapacity(Count + 1);
this[_count++] = item;
}
public void Add(T item)
{
EnsureCapacity(Count + 1);
this[_count++] = item;
}
public void Clear()
{
Array.Resize(ref _array, 0);
_count = 0;
}
private void EnsureCapacity(int needed)
{
while (_array.Length < needed)
{
Array.Resize(ref _array, Math.Max(1, _array.Length) * 2);
}
}
}
}
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
/**
* QUIK: User Interface Kit
* Copyright (C) 2023 Halit Utku Maden, et al.
*/
#version 130
in vec2 fv2TexPos;
in vec4 fv4Color;
in float ffTexLayer;
out vec4 fragColor;
uniform int iEnableSdf;
uniform int iEnableTexture;
uniform int iAlphaDiscard;
uniform float fSdfThreshold = 0.5;
uniform sampler2D tx2d;
const float fAlphaThreshold = 0.01;
vec4 getTexture()
{
if (iEnableTexture == 3)
{
// return texture(tx2dArray, vec3(fv2TexPos, ffTexLayer));
}
else if (iEnableSdf == 1)
{
vec2 texelSz = 1.0/vec2(textureSize(tx2d, 0));
vec2 txCoord2 = fv2TexPos + texelSz * (1 - mod(fv2TexPos, texelSz));
return texture(tx2d, txCoord2);
}
else
{
return texture(tx2d, fv2TexPos);
}
}
void main(void)
{
vec4 albedo = fv4Color;
if (iEnableTexture != 0)
{
vec4 value = getTexture();
if (iEnableSdf != 0)
{
value = vec4(vec3(1.0), smoothstep(fSdfThreshold-0.1, fSdfThreshold+0.1, value.r));
}
if (iAlphaDiscard != 0 && value.a <= fAlphaThreshold)
{
discard;
}
albedo = albedo * value;
}
fragColor = albedo;
}
+35
View File
@@ -0,0 +1,35 @@
/**
* QUIK: User Interface Kit
* Copyright (C) 2023 Halit Utku Maden, et al.
*/
#version 130
in vec2 v2Position; /**< The vertex position.*/
in float fZIndex; /**< The z index. */
in vec2 v2TexPos; /**< The texture coorindates. */
in float fTexLayer; /**< The texture layer for 3D textures. */
in vec4 v4Color; /**< The vertex color. */
out vec2 fv2TexPos;
out float ffTexLayer;
out vec4 fv4Color;
uniform mat4 m4Transforms; /**< The view matrix. */
uniform float fMaxZ; /**< Highest Z coordinate. */
const mat4 m4BaseTransforms = mat4(
vec4( 2.0, 0.0, 0.0, 0.0),
vec4( 0.0, -2.0, 0.0, 0.0),
vec4( 0.0, 0.0, 1.0, 0.0),
vec4(-1.0, 1.0, 0.0, 1.0)
);
void main(void)
{
vec4 v = vec4(v2Position, fZIndex/fMaxZ, 1);
gl_Position = m4Transforms * v;
fv2TexPos = v2TexPos;
fv4Color = v4Color;
ffTexLayer = fTexLayer;
}
+20 -74
View File
@@ -1,77 +1,23 @@
Dashboard
=========
Dashboard is a modular immediate rendering and UI toolkit that is intended to
develop modular graphics applications in .NET. The library comes in layers,
with no tight coupling between each stage. It makes Dashboard an excellent
library to embed into your OpenGL, Vulkan or DirectX applications as a UI or
HUD rendering toolkit.
![QUIK](https://git.mixedup.dev/QUIK/Quik.Common/raw/branch/master/assets/quik.svg) QUIK: User Interface Kit
====================================================================================================================
QUIK is an immediate mode user interface kit intended for embedding in other
C# applications. When rendering UI elements, a command buffer is generated may
can be converted to any other rendering commands using built-in or self made
procedures.
### Scope
Generally speaking, this library is not going to replace a professional UI
toolkit for most .NET programmers. The 1st and 3rd party frameworks available
are already excellent for most people. As such, the library does not follow
any UI development discipline, like MVVM. This is supposed to be easy to
integrate, customize and deliver. You are free to pick and choose which
components you like, hate, include and exclude.
Why QUIK?
---------
QUIK was inspired by Dear ImGUI and Nuklear projects, however these libraries
being implemented in C++ and C respectively make it harder to use them in a
C#.NET environment, either requiring excessive P/Invoke or custom made, error
prone marshalling.
### Availability
This library is currently under development and is not intended for end users
until further notice. Things are subject to change without notice, or
deprecation. You have been warned. And we do not talk about the original
QUIK/Dashboard code base. It's a mess.
Dashboard.Drawing - The Immediate Rendering System
----------------------------------------------------
The Dashboard immediate rendering system provides a framework for issuing
rendering commands independtly of the graphics backend present. In order
to draw anything, you must issue enqueue drawing commands into a queue.
This queue is later consumed by the graphics backend in order to reach a
final image.
The main benefit of the graphics queue is that you can write your graphics calls
once and expect the same results, at least approximately. As an advanced
example, you could write a DrawQueue to PostScript transpiler, which can be
consumed by a tool like ghostscript to conver it into a PDF file. And with the
same DrawQueue contents, you can also render the same thing with OpenGL, or your
custom software renderer.
Each draw queue is going to contain the following main components:
- The image bounding box.
- The list of extensions required to draw the image.
- The list of resources referenced by the draw queue.
- The list of commands that have been used in the draw queue.
- The stream of bytes that make up the draw queue.
This creates an extremely de/serializable command queue that can be generated
and interpreted with ease.
### Coordinate Systems and Measures.
The coordinate system is assumed to be left handed and with a top-left origin.
The length unit is device independent points (pt). Without accounting for high
pixel density displays, 1pt is 96/72 pixels (~1.3). The angle unit is degrees.
These set of conventions set herein follow the conventions set by traditional
computer aided graphics design. Following these conventions is entirely
optional, if and when you see fit.
### Extensions
A drawing extension defines a set of features and commands that need to be
understood to generate the image correctly. Extensions are simple strings that
are valid ISO C99 identifiers. This means that they must only contain
alphanumeric characters, or an underscore, and must not begin with a number.
The prefix `DB_` is reserved for the Dashboard library itself. Please refrain
from making an extension with this prefix. We recommend naming your extensions
with an underscore case name. Also please consider including a vendor prefix,
such as our own `DB_`. If you do not wish to do so, please consider your vendor
prefix as `X_`.
## Special Thanks
I would like to thank the following people, in no particular order, for their
contributions in this endevour:
- Callum McGing for BlurgText, the text rendering library.
- "BoyBayKiller", for providing valuable feedback for the libraries that spun off of Dashboard.
- "Froil" for coining the name Dashboard, fitting with the suite of ReFuel libraries, after the inevitable issues that the name QUIK has caused.
QUIK is not intended to replace the aforementioned libraries for the C or C++
developer, however it is intended to make a similar library available to C#
users without having to battle the un/managed barrier. It also comes with the
advantage of not requiring any platform specific native libraries.
On top of that, QUIK targets not just .NET 6.0, but old-school .NET framework
in mind as well. Whilst I do not promise the golden .NET Framework 2.0 support
I do wish to make the library available for .NET Framework 4.7.2 in the future.
Therefore the language version of the project is strictly limited to 7.3.
+7
View File
@@ -0,0 +1,7 @@
services:
cmdline:
image: quik/quik
user: quik
working_dir: /home/quik
volumes:
- .:/home/quik/src
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\Dashboard\Dashboard.csproj" />
<ProjectReference Include="..\..\Dashboard.Media.Defaults\Dashboard.Media.Defaults.csproj" />
<ProjectReference Include="..\..\Dashboard.OpenTK\Dashboard.OpenTK.csproj" />
<ProjectReference Include="..\..\Dashboard.BlurgText\Dashboard.BlurgText.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+50
View File
@@ -0,0 +1,50 @@
using Dashboard.ImmediateDraw;
using Dashboard.Controls;
using Dashboard.OpenTK;
using Dashboard.Media;
using Dashboard.BlurgText;
using BlurgText;
using OpenTK.Mathematics;
namespace Dashboard.Demo
{
public static class Program
{
public static readonly DbApplication Application = new DbApplication(new OpenTKPlatform());
public static void Main(string[] args)
{
Application.Run(new EmptyView());
}
}
public class EmptyView : View
{
private Font? font;
DashboardBlurg dblurg = new DashboardBlurg();
BlurgResult? result;
// private readonly Label Label = new Label() { Text = "Hello world!", Position = new QVec2(300, 300) };
protected override void PaintBegin(DrawList cmd)
{
base.PaintBegin(cmd);
if (result == null)
{
// IFontDataBase db = FontDataBaseProvider.Instance;
// font = new QFontFreeType(db.FontFileInfo(db.Sans).OpenRead());
// Label.Font = font;
// Label.TextSize = 12;
// Label.InvalidateVisual();
var font = dblurg.Blurg.AddFontFile("/usr/share/fonts/google-noto/NotoSans-Regular.ttf");
result = dblurg.Blurg.BuildString(font!, 12, BlurgColor.White, "Hello World");
}
cmd.PutBlurgText(dblurg, result!, new Vector2(300, 300));
cmd.Rectangle(new Rectangle(16, 16, 0, 0));
// Label.Paint(cmd);
}
}
}
@@ -1,10 +0,0 @@
using Dashboard.Drawing;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
DrawQueue queue = new DrawQueue();
queue.Point(Vector3.Zero, 2, new SolidBrush(Color.White));
Debugger.Break();