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
128 changed files with 8854 additions and 5134 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);
}
}
}
}
@@ -2,12 +2,13 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.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);
}
}
-15
View File
@@ -1,15 +0,0 @@
namespace Dashboard
{
[Flags]
public enum Anchor
{
Auto = 0,
Right = (1 << 0),
Left = (1 << 1),
HCenter = Left | Right,
Top = (1 << 2),
Bottom = (1 << 3),
VCenter = Top | Bottom,
Middle = HCenter | VCenter,
}
}
-65
View File
@@ -1,65 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Numerics;
namespace Dashboard
{
public readonly record struct Box2d(Vector2 Min, Vector2 Max)
{
public float Left => Min.X;
public float Right => Max.X;
public float Top => Min.Y;
public float Bottom => Max.Y;
public Vector2 Size => Max - Min;
public Vector2 Center => (Min + Max) * 0.5f;
public Box2d(RectangleF rectangle)
: this(new Vector2(rectangle.Left, rectangle.Top), new Vector2(rectangle.Right, rectangle.Bottom))
{
}
public Box2d(float x0, float y0, float x1, float y1)
: this(new Vector2(x0, y0), new Vector2(x1, y1))
{
}
public static Box2d FromPositionAndSize(Vector2 position, Vector2 size, Origin anchor = Origin.Center)
{
Vector2 half = size * 0.5f;
switch (anchor)
{
case Origin.Center:
return new Box2d(position - half, position + half);
case Origin.TopLeft:
return new Box2d(position, position + size);
default:
throw new NotImplementedException();
}
}
public static Box2d Union(Box2d left, Box2d right)
{
Vector2 min = Vector2.Min(left.Min, right.Min);
Vector2 max = Vector2.Max(left.Max, right.Max);
return new Box2d(min, max);
}
public static Box2d Intersect(Box2d left, Box2d right)
{
Vector2 min = Vector2.Max(left.Min, right.Min);
Vector2 max = Vector2.Min(left.Max, right.Max);
return new Box2d(min, max);
}
public static explicit operator RectangleF(Box2d box2d)
{
return new RectangleF((PointF)box2d.Center, (SizeF)box2d.Size);
}
public static explicit operator Box2d(RectangleF rectangle)
{
return new Box2d(rectangle);
}
}
}
-46
View File
@@ -1,46 +0,0 @@
using System.Drawing;
using System.Numerics;
namespace Dashboard
{
public readonly record struct Box3d(Vector3 Min, Vector3 Max)
{
public float Left => Min.X;
public float Right => Max.X;
public float Top => Min.Y;
public float Bottom => Max.Y;
public float Far => Min.Z;
public float Near => Max.Z;
public Vector3 Size => Max - Min;
public Vector3 Center => Min + Size * 0.5f;
public static Box3d Union(Box3d left, Box3d right)
{
Vector3 min = Vector3.Min(left.Min, right.Min);
Vector3 max = Vector3.Max(left.Max, right.Max);
return new Box3d(min, max);
}
public static Box3d Union(Box3d box, Box2d bounds, float depth)
{
Vector3 min = Vector3.Min(box.Min, new Vector3(bounds.Left, bounds.Top, depth));
Vector3 max = Vector3.Max(box.Max, new Vector3(bounds.Right, bounds.Bottom, depth));
return new Box3d(min, max);
}
public static Box3d Intersect(Box3d left, Box3d right)
{
Vector3 min = Vector3.Max(left.Min, right.Min);
Vector3 max = Vector3.Min(left.Max, right.Max);
return new Box3d(min, max);
}
public static Box3d Intersect(Box3d box, Box2d bounds, float depth)
{
Vector3 min = Vector3.Max(box.Min, new Vector3(bounds.Min, depth));
Vector3 max = Vector3.Min(box.Max, new Vector3(bounds.Max, depth));
return new Box3d(min, max);
}
}
}
-10
View File
@@ -1,10 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Dashboard</RootNamespace>
</PropertyGroup>
</Project>
-40
View File
@@ -1,40 +0,0 @@
namespace Dashboard
{
public enum FontWeight
{
_100 = 100,
_200 = 200,
_300 = 300,
_400 = 400,
_500 = 500,
_600 = 600,
_800 = 800,
_900 = 900,
Thin = _100,
Normal = _400,
Bold = _600,
Heavy = _900,
}
public enum FontSlant
{
Normal,
Italic,
Oblique,
}
public enum FontStretch
{
UltraCondensed = 500,
ExtraCondensed = 625,
Condensed = 750,
SemiCondensed = 875,
Normal = 1000,
SemiExpanded = 1125,
Expanded = 1250,
ExtraExpanded = 1500,
UltraExpanded = 2000,
}
}
-227
View File
@@ -1,227 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Numerics;
namespace Dashboard
{
/// <summary>
/// Enumeration of the kinds of gradients available.
/// </summary>
public enum GradientType
{
/// <summary>
/// A gradient which transitions over a set axis.
/// </summary>
Axial,
/// <summary>
/// A gradient which transitions along elliptical curves.
/// </summary>
Radial,
}
/// <summary>
/// A single gradient stop.
/// </summary>
/// <param name="Position">The position of the gradient stop. Must be [0,1].</param>
/// <param name="Color">The color value for the stop.</param>
public record struct GradientStop(float Position, Color Color);
/// <summary>
/// Represents a linear gradient.
/// </summary>
public struct Gradient : ICollection<GradientStop>, ICloneable, IEquatable<Gradient>
{
private readonly List<GradientStop> _stops = new List<GradientStop>();
/// <summary>
/// Gradient type.
/// </summary>
public GradientType Type { get; set; } = GradientType.Axial;
/// <summary>
/// First gradient control point.
/// </summary>
public Vector2 C0 { get; set; } = Vector2.Zero;
/// <summary>
/// Second gradient control point.
/// </summary>
public Vector2 C1 { get; set; } = Vector2.One;
/// <summary>
/// Number of stops in a gradient.
/// </summary>
public int Count => _stops.Count;
public bool IsReadOnly => false;
/// <summary>
/// Get a gradient control point.
/// </summary>
/// <param name="index">The index to get the control point for.</param>
public GradientStop this[int index]
{
get => _stops[index];
set
{
RemoveAt(index);
Add(value);
}
}
public Gradient()
{
}
public Gradient(Color a, Color b)
{
Add(new GradientStop(0, a));
Add(new GradientStop(1, b));
}
public Gradient(IEnumerable<GradientStop> stops)
{
_stops.AddRange(stops);
if (_stops.Any(x => x.Position < 0 || x.Position > 1))
throw new Exception("Gradient stop positions must be in the range [0, 1].");
_stops.Sort((a, b) => a.Position.CompareTo(b.Position));
}
public Color GetColor(float position)
{
if (Count == 0)
return Color.Black;
else if (Count == 1)
return _stops[0].Color;
int pivot = _stops.FindIndex(x => x.Position < position);
GradientStop left, right;
if (pivot == -1)
{
left = right = _stops[^1];
}
else if (pivot == 0)
{
left = right = _stops[0];
}
else
{
left = _stops[pivot-1];
right = _stops[pivot];
}
float weight = (position - left.Position) / (right.Position - left.Position);
Vector4 lcolor = new Vector4(left.Color.R, left.Color.G, left.Color.B, left.Color.A) * (1-weight);
Vector4 rcolor = new Vector4(right.Color.R, right.Color.G, right.Color.B, right.Color.A) * weight;
Vector4 color = lcolor + rcolor;
return Color.FromArgb((byte)color.W, (byte)color.X, (byte)color.Y, (byte)color.Z);
}
public Gradient Clone()
{
Gradient gradient = new Gradient()
{
Type = Type,
C0 = C0,
C1 = C1,
};
foreach (GradientStop stop in _stops)
{
gradient.Add(stop);
}
return gradient;
}
object ICloneable.Clone()
{
return Clone();
}
public IEnumerator<GradientStop> GetEnumerator()
{
return _stops.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_stops).GetEnumerator();
}
public void Add(GradientStop item)
{
if (item.Position < 0 || item.Position > 1)
throw new Exception("Gradient stop positions must be in the range [0, 1].");
int index = _stops.FindIndex(x => x.Position > item.Position);
if (index == -1)
index = _stops.Count;
_stops.Insert(index, item);
}
public void Clear()
{
_stops.Clear();
}
public bool Contains(GradientStop item)
{
return _stops.Contains(item);
}
public void CopyTo(GradientStop[] array, int arrayIndex)
{
_stops.CopyTo(array, arrayIndex);
}
public bool Remove(GradientStop item)
{
return _stops.Remove(item);
}
public void RemoveAt(int index)
{
_stops.RemoveAt(index);
}
public override int GetHashCode()
{
HashCode code = new HashCode();
code.Add(Count);
foreach (GradientStop item in this)
code.Add(item.GetHashCode());
return code.ToHashCode();
}
public bool Equals(Gradient other)
{
return
Type == other.Type &&
C0 == other.C0 &&
C1 == other.C1 &&
_stops.Equals(other._stops);
}
public override bool Equals(object? obj)
{
return obj is Gradient other && Equals(other);
}
public static bool operator ==(Gradient left, Gradient right)
{
return left.Equals(right);
}
public static bool operator !=(Gradient left, Gradient right)
{
return !left.Equals(right);
}
}
}
-38
View File
@@ -1,38 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace Dashboard
{
public class HashList<T> : IReadOnlyList<T>
where T : notnull
{
private readonly List<T> _list = new List<T>();
private readonly Dictionary<T, int> _map = new Dictionary<T, int>();
public T this[int index] => _list[index];
public int Count => _list.Count;
public int Intern(T value)
{
if (_map.TryGetValue(value, out int index))
return index;
index = Count;
_list.Add(value);
_map.Add(value, index);
return index;
}
public void Clear()
{
_list.Clear();
_map.Clear();
}
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
}
}
-196
View File
@@ -1,196 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Dashboard
{
/// <summary>
/// Pixel format for images.
/// </summary>
public enum PixelFormat
{
R8I,
Rg8I,
Rgb8I,
Rgba8I,
R16F,
Rg816F,
Rgb16F,
Rgba16F,
}
/// <summary>
/// Color channels for images.
/// </summary>
public enum ColorChannel
{
/// <summary>
/// The zero channel. Used for swizzle masks.
/// </summary>
Zero = 0,
/// <summary>
/// The one channel. Used for swizzle masks.
/// </summary>
One = 1,
/// <summary>
/// An invalid swizzle mask.
/// </summary>
Unknown = 2,
/// <summary>
/// The red channel.
/// </summary>
Red = 4,
/// <summary>
/// The green channel.
/// </summary>
Green = 5,
/// <summary>
/// The blue channel.
/// </summary>
Blue = 6,
/// <summary>
/// The alpha channel.
/// </summary>
Alpha = 7,
}
/// <summary>
/// Defines a image swizzle mask.
/// </summary>
public struct ColorSwizzle : IEquatable<ColorSwizzle>
{
public short Mask;
private const int MASK = 7;
private const int RBIT = 0;
private const int GBIT = 3;
private const int BBIT = 6;
private const int ABIT = 9;
/// <summary>
/// Swizzle the red channel.
/// </summary>
public ColorChannel R
{
get => (ColorChannel)((Mask >> RBIT) & MASK);
set => Mask = (short)(((int)value << RBIT) | (Mask & ~(MASK << RBIT)));
}
/// <summary>
/// Swizzle the green channel.
/// </summary>
public ColorChannel G
{
get => (ColorChannel)((Mask >> GBIT) & MASK);
set => Mask = (short)(((int)value << GBIT) | (Mask & ~(MASK << GBIT)));
}
/// <summary>
/// Swizzle the blue channel.
/// </summary>
public ColorChannel B
{
get => (ColorChannel)((Mask >> BBIT) & MASK);
set => Mask = (short)(((int)value << BBIT) | (Mask & ~(MASK << BBIT)));
}
/// <summary>
/// Swizzle the alpha channel.
/// </summary>
public ColorChannel A
{
get => (ColorChannel)((Mask >> ABIT) & MASK);
set => Mask = (short)(((int)value << ABIT) | (Mask & ~(MASK << ABIT)));
}
public ColorSwizzle(short mask)
{
Mask = mask;
}
public ColorSwizzle(ColorChannel r, ColorChannel g, ColorChannel b, ColorChannel a)
{
Mask = (short)(((int)r << RBIT) | ((int)g << GBIT) | ((int)b << BBIT) | ((int)a << ABIT));
}
public override string ToString()
{
return $"{GetChannelChar(R)}{GetChannelChar(G)}{GetChannelChar(B)}{GetChannelChar(A)}";
char GetChannelChar(ColorChannel channel) => channel switch
{
ColorChannel.Zero => '0',
ColorChannel.Red => 'R',
ColorChannel.Green => 'G',
ColorChannel.Blue => 'B',
ColorChannel.Alpha => 'A',
ColorChannel.One => '1',
_ => '?',
};
}
public override int GetHashCode()
{
return Mask.GetHashCode();
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is ColorSwizzle other && Equals(other);
}
public bool Equals(ColorSwizzle other)
{
return Mask == other.Mask;
}
public static readonly ColorSwizzle Default = Parse("RGBA");
public static readonly ColorSwizzle White = Parse("1111");
public static readonly ColorSwizzle Black = Parse("0001");
public static readonly ColorSwizzle Transparent = Parse("0000");
public static readonly ColorSwizzle RedToGrayscale = Parse("RRR1");
public static readonly ColorSwizzle RedToWhiteAlpha = Parse("111A");
public static bool TryParse(ReadOnlySpan<char> str, out ColorSwizzle value)
{
if (str.Length < 4)
{
value = default;
return false;
}
ColorChannel r = GetChannelFromChar(str[0]);
ColorChannel g = GetChannelFromChar(str[1]);
ColorChannel b = GetChannelFromChar(str[2]);
ColorChannel a = GetChannelFromChar(str[3]);
value = new ColorSwizzle(r, g, b, a);
return true;
ColorChannel GetChannelFromChar(char chr) => chr switch
{
'0' => ColorChannel.Zero,
'R' => ColorChannel.Red,
'G' => ColorChannel.Green,
'B' => ColorChannel.Blue,
'A' => ColorChannel.Alpha,
'1' => ColorChannel.One,
_ => ColorChannel.Unknown,
};
}
public static ColorSwizzle Parse(ReadOnlySpan<char> str) =>
TryParse(str, out ColorSwizzle value) ? value : throw new FormatException(nameof(str));
public static bool operator ==(ColorSwizzle left, ColorSwizzle right) =>
left.Mask == right.Mask;
public static bool operator !=(ColorSwizzle left, ColorSwizzle right) =>
left.Mask != right.Mask;
}
}
-23
View File
@@ -1,23 +0,0 @@
namespace Dashboard
{
public enum BorderKind
{
Inset = -1,
Center = 0,
Outset = 1,
}
public enum CapType
{
None,
Circular,
Rectangular,
}
public enum CuspType
{
None,
Circular,
Rectangular,
}
}
-17
View File
@@ -1,17 +0,0 @@
namespace Dashboard
{
public enum Origin
{
Center = 0,
Left = (1 << 0),
Top = (1 << 1),
Right = (1 << 2),
Bottom = (1 << 3),
TopLeft = Top | Left,
BottomLeft = Bottom | Left,
BottomRight = Bottom | Right,
TopRight = Top | Right,
}
}
-48
View File
@@ -1,48 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing.OpenGL
{
public enum SimpleDrawCommand : int
{
Point = 1,
Line = 2,
Rect = 3,
/// <summary>
/// Make sure your custom commands have values higher than this if you plan on using the default command
/// buffer.
/// </summary>
CustomCommandStart = 4096
}
[StructLayout(LayoutKind.Explicit, Size = 64)]
public struct CommandInfo
{
[FieldOffset(0)]
public SimpleDrawCommand Type;
[FieldOffset(4)]
public int Flags;
[FieldOffset(8)]
public float Arg0;
[FieldOffset(12)]
public float Arg1;
[FieldOffset(16)]
public int FgGradientIndex;
[FieldOffset(20)]
public int FgGradientCount;
[FieldOffset(24)]
public int BgGradientIndex;
[FieldOffset(28)]
public int BgGradientCount;
[FieldOffset(32)]
public Vector4 FgColor;
[FieldOffset(48)]
public Vector4 BgColor;
}
}
@@ -1,77 +0,0 @@
using System.Collections.Concurrent;
using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL
{
public class ContextCollector : IDisposable
{
private readonly ConcurrentQueue<GLObject> _disposedObjects = new ConcurrentQueue<GLObject>();
public void Dispose()
{
while (_disposedObjects.TryDequeue(out GLObject obj))
{
obj.Dispose();
}
}
void DeleteObject(ObjectIdentifier identifier, int handle) => _disposedObjects.Enqueue(new GLObject(identifier, handle));
public void DeleteTexture(int texture) => DeleteObject(ObjectIdentifier.Texture, texture);
public void DeleteBufffer(int buffer) => DeleteObject(ObjectIdentifier.Buffer, buffer);
public void DeleteFramebuffer(int framebuffer) => DeleteObject(ObjectIdentifier.Framebuffer, framebuffer);
public void DeleteRenderBuffer(int renderbuffer) => DeleteObject(ObjectIdentifier.Renderbuffer, renderbuffer);
public void DeleteSampler(int sampler) => DeleteObject(ObjectIdentifier.Sampler, sampler);
public void DeleteShader(int shader) => DeleteObject(ObjectIdentifier.Shader, shader);
public void DeleteProgram(int program) => DeleteObject(ObjectIdentifier.Program, program);
public void DeleteVertexArray(int vertexArray) => DeleteObject(ObjectIdentifier.VertexArray, vertexArray);
public void DeleteQuery(int query) => DeleteObject(ObjectIdentifier.Query, query);
public void DeleteProgramPipeline(int programPipeline) => DeleteObject(ObjectIdentifier.ProgramPipeline, programPipeline);
public void DeleteTransformFeedback(int transformFeedback) => DeleteObject(ObjectIdentifier.TransformFeedback, transformFeedback);
private readonly record struct GLObject(ObjectIdentifier Type, int Handle)
{
public void Dispose()
{
switch (Type)
{
case ObjectIdentifier.Texture:
GL.DeleteTexture(Handle);
break;
case ObjectIdentifier.Buffer:
GL.DeleteBuffer(Handle);
break;
case ObjectIdentifier.Framebuffer:
GL.DeleteFramebuffer(Handle);
break;
case ObjectIdentifier.Renderbuffer:
GL.DeleteRenderbuffer(Handle);
break;
case ObjectIdentifier.Sampler:
GL.DeleteSampler(Handle);
break;
case ObjectIdentifier.Shader:
GL.DeleteShader(Handle);
break;
case ObjectIdentifier.VertexArray:
GL.DeleteVertexArray(Handle);
break;
case ObjectIdentifier.Program:
GL.DeleteProgram(Handle);
break;
case ObjectIdentifier.Query:
GL.DeleteQuery(Handle);
break;
case ObjectIdentifier.ProgramPipeline:
GL.DeleteProgramPipeline(Handle);
break;
case ObjectIdentifier.TransformFeedback:
GL.DeleteTransformFeedback(Handle);
break;
}
}
}
public static readonly ContextCollector Global = new ContextCollector();
}
}
-179
View File
@@ -1,179 +0,0 @@
using System.Drawing;
using Dashboard.Drawing.OpenGL.Executors;
namespace Dashboard.Drawing.OpenGL
{
public interface ICommandExecutor
{
IEnumerable<string> Extensions { get; }
IContextExecutor Executor { get; }
void SetContextExecutor(IContextExecutor executor);
void BeginFrame();
void BeginDraw();
void EndDraw();
void EndFrame();
void ProcessCommand(ICommandFrame frame);
}
public interface IContextExecutor : IInitializer, IGLDisposable
{
GLEngine Engine { get; }
IGLContext Context { get; }
ContextResourcePool ResourcePool { get; }
TransformStack TransformStack { get; }
}
public class ContextExecutor : IContextExecutor
{
public GLEngine Engine { get; }
public IGLContext Context { get; }
public ContextResourcePool ResourcePool { get; }
public TransformStack TransformStack { get; } = new TransformStack();
protected bool IsDisposed { get; private set; } = false;
public bool IsInitialized { get; private set; } = false;
private readonly List<ICommandExecutor> _executorsList = new List<ICommandExecutor>();
private readonly Dictionary<string, ICommandExecutor> _executorsMap = new Dictionary<string, ICommandExecutor>();
public ContextExecutor(GLEngine engine, IGLContext context)
{
Engine = engine;
Context = context;
ResourcePool = Engine.ResourcePoolManager.Get(context);
ResourcePool.IncrementReference();
AddExecutor(new BaseCommandExecutor());
AddExecutor(new TextCommandExecutor());
}
~ContextExecutor()
{
DisposeInvoker(true, false);
}
public void AddExecutor(ICommandExecutor executor, bool overwrite = false)
{
if (IsInitialized)
throw new Exception("This context executor is already initialized. Cannot add new command executors.");
IInitializer? initializer = executor as IInitializer;
if (initializer?.IsInitialized == true)
throw new InvalidOperationException("This command executor has already been initialized, cannot add here.");
if (!overwrite)
{
foreach (string extension in executor.Extensions)
{
if (_executorsMap.ContainsKey(extension))
throw new InvalidOperationException("An executor already handles this extension.");
}
}
foreach (string extension in executor.Extensions)
{
_executorsMap[extension] = executor;
}
_executorsList.Add(executor);
executor.SetContextExecutor(this);
}
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
foreach (ICommandExecutor executor in _executorsList)
{
if (executor is IInitializer initializer)
initializer.Initialize();
}
}
public virtual void BeginFrame()
{
foreach (ICommandExecutor executor in _executorsList)
executor.BeginFrame();
}
protected virtual void BeginDraw()
{
foreach (ICommandExecutor executor in _executorsList)
executor.BeginDraw();
}
protected virtual void EndDraw()
{
foreach (ICommandExecutor executor in _executorsList)
executor.EndDraw();
}
public virtual void EndFrame()
{
ResourcePool.Collector.Dispose();
TransformStack.Clear();
foreach (ICommandExecutor executor in _executorsList)
executor.EndFrame();
}
public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize));
public virtual void Draw(DrawQueue drawQueue, RectangleF bounds)
{
BeginDraw();
foreach (ICommandFrame frame in drawQueue)
{
if (_executorsMap.TryGetValue(frame.Command.Extension.Name, out ICommandExecutor? executor))
executor.ProcessCommand(frame);
}
EndDraw();
}
private void DisposeInvoker(bool safeExit, bool disposing)
{
if (!IsDisposed)
return;
IsDisposed = true;
if (disposing)
GC.SuppressFinalize(this);
Dispose(safeExit, disposing);
}
protected virtual void Dispose(bool safeExit, bool disposing)
{
if (disposing)
{
foreach (ICommandExecutor executor in _executorsList)
{
if (executor is IGLDisposable glDisposable)
glDisposable.Dispose(safeExit);
else if (executor is IDisposable disposable)
disposable.Dispose();
}
if (ResourcePool.DecrementReference())
Dispose();
}
}
public void Dispose() => DisposeInvoker(true, true);
public void Dispose(bool safeExit) => DisposeInvoker(safeExit, true);
}
}
@@ -1,131 +0,0 @@
namespace Dashboard.Drawing.OpenGL
{
public class ContextResourcePoolManager
{
private readonly Dictionary<IGLContext, ContextResourcePool> _unique = new Dictionary<IGLContext, ContextResourcePool>();
private readonly Dictionary<int, ContextResourcePool> _shared = new Dictionary<int, ContextResourcePool>();
public ContextResourcePool Get(IGLContext context)
{
if (context.ContextGroup == -1)
{
if (!_unique.TryGetValue(context, out ContextResourcePool? pool))
{
pool = new ContextResourcePool(this, context);
_unique.Add(context, pool);
}
return pool;
}
else
{
if (!_shared.TryGetValue(context.ContextGroup, out ContextResourcePool? pool))
{
pool = new ContextResourcePool(this, context.ContextGroup);
_shared.Add(context.ContextGroup, pool);
}
return pool;
}
}
internal void Disposed(ContextResourcePool pool)
{
// TODO:
}
}
public class ContextResourcePool : IGLDisposable, IArc
{
private int _references = 0;
private bool _isDisposed = false;
private readonly Dictionary<int, IResourceManager> _managers = new Dictionary<int, IResourceManager>();
public ContextResourcePoolManager Manager { get; }
public IGLContext? Context { get; private set; } = null;
public int ContextGroup { get; private set; } = -1;
public int References => _references;
public ContextCollector Collector { get; } = new ContextCollector();
internal ContextResourcePool(ContextResourcePoolManager manager, int contextGroup)
{
Manager = manager;
ContextGroup = contextGroup;
}
internal ContextResourcePool(ContextResourcePoolManager manager, IGLContext context)
{
Manager = manager;
Context = context;
}
public T GetResourceManager<T>(bool init = true) where T : IResourceManager, new()
{
int index = ManagerAtom<T>.Atom;
if (!_managers.TryGetValue(index, out IResourceManager? resourceClass))
{
_managers[index] = resourceClass = new T();
}
if (init && resourceClass is IInitializer initializer)
{
initializer.Initialize();
}
return (T)resourceClass;
}
~ContextResourcePool()
{
Dispose(true, false);
}
public void Dispose() => Dispose(true, false);
public void Dispose(bool safeExit) => Dispose(safeExit, true);
private void Dispose(bool safeExit, bool disposing)
{
if (_isDisposed)
return;
_isDisposed = true;
Manager.Disposed(this);
if (disposing)
{
foreach ((int _, IResourceManager manager) in _managers)
{
if (manager is IGLDisposable glDisposable)
glDisposable.Dispose(safeExit);
else if (manager is IDisposable disposable)
disposable.Dispose();
}
GC.SuppressFinalize(this);
}
}
public void IncrementReference()
{
Interlocked.Increment(ref _references);
}
public bool DecrementReference()
{
return Interlocked.Decrement(ref _references) == 0;
}
private class ManagerAtom
{
private static int _counter = -1;
protected static int Acquire() => Interlocked.Increment(ref _counter);
}
private class ManagerAtom<T> : ManagerAtom where T : IResourceManager
{
public static int Atom { get; } = Acquire();
}
}
}
@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
<PackageReference Include="OpenTK.Graphics" Version="5.0.0-pre.13" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Drawing\Dashboard.Drawing.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Executors\simple.frag" />
<EmbeddedResource Include="Executors\simple.vert" />
<EmbeddedResource Include="Executors\text.vert" />
<EmbeddedResource Include="Executors\text.frag" />
</ItemGroup>
</Project>
@@ -1,467 +0,0 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
namespace Dashboard.Drawing.OpenGL
{
public class DrawCallRecorder : IGLDisposable, IInitializer
{
private int _vao = 0;
private int _vbo = 0;
private readonly List<DrawVertex> _vertices = new List<DrawVertex>();
private readonly List<DrawCall> _calls = new List<DrawCall>();
private int _start = 0;
private int _count = 0;
private int _primitives = 0;
private Vector3 _charCoords;
private int _cmdIndex;
private int _texture0, _texture1, _texture2, _texture3;
private TextureTarget _target0, _target1, _target2, _target3;
private Matrix4 _transforms = Matrix4.Identity;
public int CommandModulus = 64;
public int CommandBuffer = 0;
public int CommandSize = 64;
private int CommandByteSize => CommandModulus * CommandSize;
public int TransformsLocation { get; set; }
public void Transforms(in Matrix4 transforms)
{
_transforms = transforms;
}
public void Begin(PrimitiveType type)
{
if (_primitives != 0)
throw new InvalidOperationException("Attempt to begin new draw call before finishing previous one.");
_primitives = (int)type;
_start = _vertices.Count;
_count = 0;
}
public void TexCoords2(Vector2 texCoords)
{
_charCoords = new Vector3(texCoords, 0);
}
public void CharCoords(Vector3 charCoords)
{
_charCoords = charCoords;
}
public void CommandIndex(int index)
{
_cmdIndex = index;
}
public void Vertex3(Vector3 vertex)
{
_vertices.Add(new DrawVertex()
{
Position = vertex,
CharCoords = _charCoords,
CmdIndex = _cmdIndex % CommandModulus,
});
_count++;
}
public void End()
{
if (_primitives == 0)
throw new InvalidOperationException("Attempt to end draw call before starting one.");
_calls.Add(
new DrawCall()
{
Type = (PrimitiveType)_primitives,
Start = _start,
Count = _count,
CmdIndex = _cmdIndex,
Target0 = _target0,
Target1 = _target1,
Target2 = _target2,
Target3 = _target3,
Texture0 = _texture0,
Texture1 = _texture1,
Texture2 = _texture2,
Texture3 = _texture3,
Transforms = _transforms,
});
_primitives = 0;
}
public void BindTexture(TextureTarget target, int texture) => BindTexture(target, 0, texture);
public void BindTexture(TextureTarget target, int unit, int texture)
{
switch (unit)
{
case 0:
_texture0 = 0;
_target0 = target;
break;
case 1:
_texture1 = 0;
_target1 = target;
break;
case 2:
_texture2 = 0;
_target2 = target;
break;
case 3:
_texture3 = 0;
_target3 = target;
break;
default:
throw new ArgumentOutOfRangeException(nameof(unit), "I did not write support for more than 4 textures.");
}
}
public void DrawArrays(PrimitiveType type, int first, int count)
{
throw new NotImplementedException();
}
public void Execute()
{
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
ReadOnlySpan<DrawVertex> vertices = CollectionsMarshal.AsSpan(_vertices);
GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Count * Unsafe.SizeOf<DrawVertex>(), vertices, BufferUsage.DynamicDraw);
foreach (DrawCall call in _calls)
{
GL.BindBufferRange(BufferTarget.UniformBuffer, 0, CommandBuffer, call.CmdIndex / CommandModulus * CommandByteSize, CommandByteSize);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(call.Target0, call.Texture0);
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(call.Target1, call.Texture1);
GL.ActiveTexture(TextureUnit.Texture2);
GL.BindTexture(call.Target2, call.Texture2);
GL.ActiveTexture(TextureUnit.Texture3);
GL.BindTexture(call.Target3, call.Texture3);
Matrix4 transforms = call.Transforms;
GL.UniformMatrix4f(TransformsLocation, 1, true, ref transforms);
GL.DrawArrays(call.Type, call.Start, call.Count);
}
}
public void Clear()
{
_vertices.Clear();
_calls.Clear();
}
public void Dispose()
{
throw new NotImplementedException();
}
public void Dispose(bool safeExit)
{
throw new NotImplementedException();
}
public bool IsInitialized { get; private set; }
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
_vao = GL.CreateVertexArray();
_vbo = GL.CreateBuffer();
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 32, 0);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 32, 16);
GL.VertexAttribIPointer(2, 1, VertexAttribIType.Int, 32, 28);
GL.EnableVertexAttribArray(0);
GL.EnableVertexAttribArray(1);
GL.EnableVertexAttribArray(2);
}
private struct DrawCall
{
public PrimitiveType Type;
public int Start;
public int Count;
public int CmdIndex;
public int Texture0;
public int Texture1;
public int Texture2;
public int Texture3;
public TextureTarget Target0;
public TextureTarget Target1;
public TextureTarget Target2;
public TextureTarget Target3;
public Matrix4 Transforms;
}
[StructLayout(LayoutKind.Explicit, Size = 32)]
private struct DrawVertex
{
[FieldOffset(0)]
public Vector3 Position;
[FieldOffset(16)]
public Vector3 CharCoords;
[FieldOffset(28)]
public int CmdIndex;
}
}
/// <summary>
/// A customizable immediate mode draw call queue, for the modern OpenGL user.
/// </summary>
/// <typeparam name="TCall">The call info type.</typeparam>
/// <typeparam name="TVertex">The vertex structure.</typeparam>
public abstract class DrawCallRecorder<TCall, TVertex> : IGLDisposable, IInitializer
where TVertex : unmanaged
{
/// <summary>
/// The vertex array for this queue.
/// </summary>
public int Vao { get; private set; }
/// <summary>
/// The vertex buffer for this queue.
/// </summary>
public int Vbo { get; private set; }
/// <summary>
/// Number of calls recorded in this queue.
/// </summary>
public int CallCount => Calls.Count;
/// <summary>
/// The number of total vertices recorded.
/// </summary>
public int TotalVertices => Vertices.Count;
/// <summary>
/// The latest draw call info.
/// </summary>
public ref TCall CurrentCall => ref _currentCall;
/// <summary>
/// The latest vertex emitted.
/// </summary>
public ref TVertex CurrentVertex => ref _currentVertex;
/// <summary>
/// True if currently recording a draw call.
/// </summary>
public bool InCall => _primitiveMode != 0;
/// <summary>
/// Size of one vertex.
/// </summary>
protected int VertexSize => Unsafe.SizeOf<TVertex>();
/// <summary>
/// The list of draw calls.
/// </summary>
protected List<DrawCall> Calls { get; } = new List<DrawCall>();
/// <summary>
/// The list of all vertices.
/// </summary>
protected List<TVertex> Vertices { get; } = new List<TVertex>();
/// <summary>
/// The value to write for the draw call info at the start of a call.
/// </summary>
[Pure] protected virtual TCall DefaultCall => default(TCall);
/// <summary>
/// The value to write for last vertex at the start of a call.
/// </summary>
[Pure] protected virtual TVertex DefaultVertex => default;
private int _start = 0;
private int _count = 0;
private int _primitiveMode = 0;
private TCall _currentCall;
private TVertex _currentVertex;
protected DrawCallRecorder()
{
_currentCall = DefaultCall;
_currentVertex = DefaultVertex;
}
/// <summary>
/// Record a draw call directly.
/// </summary>
/// <param name="type">The primitive type to use.</param>
/// <param name="callInfo">The call info structure to use</param>
/// <param name="vertices">The list of vertices to use.</param>
/// <exception cref="InvalidOperationException">You attempted to use this function during another draw call.</exception>
public void DrawArrays(PrimitiveType type, in TCall callInfo, ReadOnlySpan<TVertex> vertices)
{
if (InCall)
throw new InvalidOperationException("Cannot use draw arrays in the middle of an ongoing immediate-mode call.");
DrawCall call = new DrawCall()
{
Type = type,
Start = Vertices.Count,
Count = vertices.Length,
CallInfo = callInfo,
};
Vertices.AddRange(vertices);
Calls.Add(call);
}
/// <summary>
/// Start a draw call.
/// </summary>
/// <param name="type">The primitive type for the call.</param>
public void Begin(PrimitiveType type) => Begin(type, DefaultCall);
/// <summary>
/// Start a draw call.
/// </summary>
/// <param name="type">The primitive type for the call.</param>
/// <param name="callInfo">The call info.</param>
/// <exception cref="InvalidOperationException">You attempted to create a draw call within a draw call.</exception>
public void Begin(PrimitiveType type, TCall callInfo)
{
if (InCall)
throw new InvalidOperationException("Attempt to begin new draw call before finishing previous one.");
_primitiveMode = (int)type;
_start = Vertices.Count;
_count = 0;
CurrentCall = callInfo;
CurrentVertex = DefaultVertex;
}
/// <summary>
/// Emit the latest or modified vertex.
/// </summary>
public void Vertex()
{
Vertex(CurrentVertex);
}
/// <summary>
/// Emit a vertex.
/// </summary>
/// <param name="vertex">The vertex to emit.</param>
public void Vertex(in TVertex vertex)
{
Vertices.Add(CurrentVertex = vertex);
_count++;
}
/// <summary>
/// End the current call.
/// </summary>
/// <exception cref="InvalidOperationException">You tried to end a call that you didn't begin recording.</exception>
public void End()
{
if (!InCall)
throw new InvalidOperationException("Attempt to end draw call before starting one.");
Calls.Add(new DrawCall()
{
Start = _start,
Count = _count,
Type = (PrimitiveType)_primitiveMode,
CallInfo = CurrentCall,
});
_primitiveMode = 0;
}
/// <summary>
/// Called by the execution engine before a draw call is executed.
/// </summary>
/// <param name="call">The call to prepare.</param>
protected abstract void PrepareCall(in TCall call);
/// <summary>
/// Set the vertex format for the <see cref="Vao"/> and <see cref="Vbo"/> created by the recorder.
/// </summary>
protected abstract void SetVertexFormat();
/// <summary>
/// Execute all the recorded draw calls.
/// </summary>
public void Execute()
{
GL.BindVertexArray(Vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo);
ReadOnlySpan<TVertex> vertices = CollectionsMarshal.AsSpan(Vertices);
GL.BufferData(BufferTarget.ArrayBuffer, Vertices.Count * VertexSize, vertices, BufferUsage.DynamicDraw);
foreach (DrawCall call in Calls)
{
PrepareCall(call.CallInfo);
GL.DrawArrays(call.Type, call.Start, call.Count);
}
}
/// <summary>
/// Clear the draw call queue.
/// </summary>
public void Clear()
{
Vertices.Clear();
Calls.Clear();
}
public void Dispose()
{
throw new NotImplementedException();
}
public void Dispose(bool safeExit)
{
throw new NotImplementedException();
}
public bool IsInitialized { get; private set; }
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
Vao = GL.CreateVertexArray();
Vbo = GL.CreateBuffer();
GL.BindVertexArray(Vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo);
SetVertexFormat();
}
protected struct DrawCall
{
public PrimitiveType Type;
public int Start;
public int Count;
public TCall CallInfo;
}
}
}
@@ -1,332 +0,0 @@
using System.Drawing;
using OpenTK.Graphics.OpenGL;
using System.Numerics;
using OTK = OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL.Executors
{
public class BaseCommandExecutor : IInitializer, ICommandExecutor
{
private int _program = 0;
private readonly MappableBumpAllocator<CommandInfo> _commands = new MappableBumpAllocator<CommandInfo>();
private readonly DrawCallRecorder _calls = new DrawCallRecorder();
public bool IsInitialized { get; private set; }
public IEnumerable<string> Extensions { get; } = new[] { "DB_base" };
public IContextExecutor Executor { get; private set; } = null!;
public void Initialize()
{
if (IsInitialized) return;
if (Executor == null)
throw new Exception("Executor has not been set.");
IsInitialized = true;
LoadShaders();
}
public void SetContextExecutor(IContextExecutor executor)
{
Executor = executor;
}
public void BeginFrame()
{
}
public void BeginDraw()
{
_commands.Initialize();
_calls.Initialize();
Size size = Executor.Context.FramebufferSize;
Executor.TransformStack.Push(OTK.Matrix4.CreateOrthographicOffCenter(
0,
size.Width,
size.Height,
0,
1,
-1));
GL.Viewport(0, 0, size.Width, size.Height);
}
public void EndDraw()
{
_commands.Unmap();
GL.UseProgram(_program);
_calls.CommandBuffer = _commands.Handle;
_calls.Execute();
}
public void EndFrame()
{
_commands.Clear();
_calls.Clear();
}
public void ProcessCommand(ICommandFrame frame)
{
switch (frame.Command.Name)
{
case "Point":
DrawBasePoint(frame);
break;
case "Line":
DrawBaseLine(frame);
break;
case "RectF":
case "RectS":
case "RectFS":
DrawRect(frame);
break;
}
}
private void DrawBasePoint(ICommandFrame frame)
{
ref CommandInfo info = ref _commands.Take(out int index);
PointCommandArgs args = frame.GetParameter<PointCommandArgs>();
info = new CommandInfo()
{
Type = SimpleDrawCommand.Point,
Arg0 = args.Size,
};
SetCommandCommonBrush(ref info, args.Brush, args.Brush);
_calls.Transforms(Executor.TransformStack.Top);
_calls.Begin(PrimitiveType.Triangles);
_calls.CommandIndex(index);
DrawPoint(args.Position, args.Depth, args.Size);
_calls.End();
}
private void DrawPoint(Vector2 position, float depth, float diameter)
{
// Draw a point as a isocles triangle.
const float adjust = 1.1f;
const float cos30 = 0.8660254038f;
Vector2 top = adjust * new Vector2(0, -cos30);
Vector2 left = adjust * new Vector2(-cos30, 0.5f);
Vector2 right = adjust * new Vector2(cos30, 0.5f);
_calls.TexCoords2(top);
_calls.Vertex3(new Vector3(position + top * diameter, depth));
_calls.TexCoords2(left);
_calls.Vertex3(new Vector3(position + left * diameter, depth));
_calls.TexCoords2(right);
_calls.Vertex3(new Vector3(position + right * diameter, depth));
}
private void DrawBaseLine(ICommandFrame frame)
{
ref CommandInfo info = ref _commands.Take(out int index);
LineCommandArgs args = frame.GetParameter<LineCommandArgs>();
info = new CommandInfo()
{
Type = SimpleDrawCommand.Line,
Arg0 = 0.5f * args.Size / (args.End - args.Start).Length(),
};
SetCommandCommonBrush(ref info, args.Brush, args.Brush);
_calls.Transforms(Executor.TransformStack.Top);
_calls.Begin(PrimitiveType.Triangles);
_calls.CommandIndex(index);
DrawLine(args.Start, args.End, args.Depth, args.Size);
_calls.End();
}
private void DrawLine(Vector2 start, Vector2 end, float depth, float width)
{
float radius = 0.5f * width;
Vector2 segment = end - start;
float length = segment.Length();
float ratio = radius / length;
Vector2 n = ratio * segment;
Vector2 t = new Vector2(-n.Y, n.X);
Vector2 t00 = new Vector2(-ratio, -ratio);
Vector2 t10 = new Vector2(1+ratio, -ratio);
Vector2 t01 = new Vector2(-ratio, +ratio);
Vector2 t11 = new Vector2(1+ratio, +ratio);
Vector3 x00 = new Vector3(start - n - t, depth);
Vector3 x10 = new Vector3(end + n - t, depth);
Vector3 x01 = new Vector3(start - n + t, depth);
Vector3 x11 = new Vector3(end + n + t, depth);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t01);
_calls.Vertex3(x01);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t10);
_calls.Vertex3(x10);
}
private void DrawRect(ICommandFrame frame)
{
ref CommandInfo info = ref _commands.Take(out int index);
RectCommandArgs args = frame.GetParameter<RectCommandArgs>();
Vector2 size = Vector2.Abs(args.End - args.Start);
float aspect = size.X / size.Y;
float border = args.StrikeSize;
float normRad = args.StrikeSize / size.Y;
float wideRad = aspect * normRad;
int flags = 0;
switch (frame.Command.Name)
{
case "RectF":
flags |= 1;
break;
case "RectS":
flags |= 2;
break;
case "RectFS":
flags |= 3;
break;
}
switch (args.BorderKind)
{
case BorderKind.Inset:
flags |= 2 << 2;
break;
case BorderKind.Outset:
flags |= 1 << 2;
break;
}
info = new CommandInfo()
{
Type = SimpleDrawCommand.Rect,
Flags = flags,
Arg0 = aspect,
Arg1 = normRad,
};
SetCommandCommonBrush(ref info, args.FillBrush, args.StrikeBrush);
_calls.Transforms(Executor.TransformStack.Top);
_calls.Begin(PrimitiveType.Triangles);
_calls.CommandIndex(index);
Vector2 t00 = new Vector2(-wideRad, -normRad);
Vector2 t10 = new Vector2(1+wideRad, -normRad);
Vector2 t01 = new Vector2(-wideRad, 1+normRad);
Vector2 t11 = new Vector2(1+wideRad, 1+normRad);
Vector3 x00 = new Vector3(args.Start.X - border, args.Start.Y - border, args.Depth);
Vector3 x10 = new Vector3(args.End.X + border, args.Start.Y - border, args.Depth);
Vector3 x01 = new Vector3(args.Start.X - border, args.End.Y + border, args.Depth);
Vector3 x11 = new Vector3(args.End.X + border, args.End.Y + border, args.Depth);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t01);
_calls.Vertex3(x01);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t00);
_calls.Vertex3(x00);
_calls.TexCoords2(t11);
_calls.Vertex3(x11);
_calls.TexCoords2(t10);
_calls.Vertex3(x10);
_calls.End();
}
protected void SetCommandCommonBrush(ref CommandInfo info, IBrush? fill, IBrush? border)
{
switch (fill?.Kind.Name)
{
case "DB_Brush_solid":
SolidBrush solid = (SolidBrush)fill;
Vector4 color = new Vector4(solid.Color.R/255f, solid.Color.G/255f, solid.Color.B/255f, solid.Color.A/255f);
info.FgColor = color;
break;
case "DB_Brush_gradient":
GradientBrush gradient = (GradientBrush)fill;
GradientUniformBuffer gradients = Executor.ResourcePool.GetResourceManager<GradientUniformBuffer>();
gradients.Initialize();
GradientUniformBuffer.Entry entry = gradients.InternGradient(gradient.Gradient);
info.FgGradientIndex = entry.Offset;
info.FgGradientCount = entry.Count;
break;
case null:
// Craete a magenta brush for this.
info.FgColor = new Vector4(1, 0, 1, 1);
break;
}
switch (border?.Kind.Name)
{
case "DB_Brush_solid":
SolidBrush solid = (SolidBrush)border;
Vector4 color = new Vector4(solid.Color.R/255f, solid.Color.G/255f, solid.Color.B/255f, solid.Color.A/255f);
info.BgColor = color;
break;
case "DB_Brush_gradient":
GradientBrush gradient = (GradientBrush)border;
GradientUniformBuffer gradients = Executor.ResourcePool.GetResourceManager<GradientUniformBuffer>();
gradients.Initialize();
GradientUniformBuffer.Entry entry = gradients.InternGradient(gradient.Gradient);
info.BgGradientIndex = entry.Offset;
info.BgGradientCount = entry.Count;
break;
case null:
// Craete a magenta brush for this.
info.BgColor = new Vector4(1, 0, 1, 1);
break;
}
}
private void LoadShaders()
{
using Stream vsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.Executors.simple.vert");
using Stream fsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.Executors.simple.frag");
int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
_program = ShaderUtil.LinkProgram(vs, fs, new []
{
"a_v3Position",
"a_v2TexCoords",
"a_iCmdIndex",
});
GL.DeleteShader(vs);
GL.DeleteShader(fs);
GL.UniformBlockBinding(_program, GL.GetUniformBlockIndex(_program, "CommandBlock"), 0);
}
private static Stream FetchEmbeddedResource(string name)
{
return typeof(BaseCommandExecutor).Assembly.GetManifestResourceStream(name)!;
}
}
}
@@ -1,243 +0,0 @@
using System.Reflection;
using System.Runtime.InteropServices;
using BlurgText;
using Dashboard.Drawing.OpenGL.Text;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL.Executors
{
public class TextCommandExecutor : ICommandExecutor, IInitializer
{
public IEnumerable<string> Extensions { get; } = new[] { "DB_Text" };
public IContextExecutor Executor { get; private set; }
private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager<BlurgEngine>();
public bool IsInitialized { get; private set; }
private DrawCallRecorder _recorder;
private int _program = 0;
private int _transformsLocation = -1;
private int _atlasLocation = -1;
private int _borderWidthLocation = -1;
private int _borderColorLocation = -1;
private int _fillColorLocation = -1;
public TextCommandExecutor()
{
Executor = null!;
_recorder = new DrawCallRecorder(this);
}
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
Assembly self = typeof(TextCommandExecutor).Assembly;
using Stream vsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.vert")!;
using Stream fsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.frag")!;
int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
_program = ShaderUtil.LinkProgram(vs, fs, new []
{
"a_v3Position",
"a_v2TexCoords",
});
GL.DeleteShader(vs);
GL.DeleteShader(fs);
_transformsLocation = GL.GetUniformLocation(_program, "m4Transforms");
_atlasLocation = GL.GetUniformLocation(_program, "txAtlas");
_borderWidthLocation = GL.GetUniformLocation(_program, "fBorderWidth");
_borderColorLocation = GL.GetUniformLocation(_program, "v4BorderColor");
_fillColorLocation = GL.GetUniformLocation(_program, "v4FillColor");
_recorder.Initialize();
}
public void SetContextExecutor(IContextExecutor executor)
{
Executor = executor;
}
public void BeginFrame()
{
}
public void BeginDraw()
{
_recorder.Clear();
}
public void EndDraw()
{
GL.UseProgram(_program);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
_recorder.Execute();
GL.Disable(EnableCap.Blend);
}
public void EndFrame()
{
}
public void ProcessCommand(ICommandFrame frame)
{
switch (frame.Command.Name)
{
case "Text":
DrawText(frame);
break;
}
}
private void DrawText(ICommandFrame frame)
{
TextCommandArgs args = frame.GetParameter<TextCommandArgs>();
DbBlurgFont font = Engine.InternFont(args.Font);
BlurgColor color;
switch (args.TextBrush)
{
case SolidBrush solid:
color = new BlurgColor()
{
R = solid.Color.R,
G = solid.Color.G,
B = solid.Color.B,
A = solid.Color.A,
};
break;
default:
color = new BlurgColor() { R = 255, G = 0, B = 255, A = 255 };
break;
}
BlurgResult? result = Engine.Blurg.BuildString(font.Font, font.Size, color, args.Text);
if (result == null)
return;
Vector3 position = new Vector3(args.Position.X, args.Position.Y, args.Position.Z);
ExecuteBlurgResult(result, position);
result.Dispose();
}
private void ExecuteBlurgResult(BlurgResult result, Vector3 position)
{
Matrix4 transforms = Executor.TransformStack.Top;
for (int i = 0; i < result.Count; i++)
{
BlurgRect rect = result[i];
int texture = (int)rect.UserData;
Vector4 color = new Vector4(rect.Color.R / 255f, rect.Color.G / 255f, rect.Color.B / 255f,
rect.Color.A / 255f);
if (i == 0)
{
_recorder.Begin(PrimitiveType.Triangles, new Call()
{
Texture = texture,
FillColor = color,
Transforms = transforms,
});
}
else if (
_recorder.CurrentCall.Texture != texture ||
_recorder.CurrentCall.FillColor != color)
{
_recorder.End();
Call call = new Call()
{
Texture = texture,
FillColor = color,
Transforms = transforms,
};
_recorder.Begin(PrimitiveType.Triangles, call);
}
Vector3 p00 = new Vector3(rect.X, rect.Y, 0) + position;
Vector3 p10 = p00 + new Vector3(rect.Width, 0, 0);
Vector3 p11 = p00 + new Vector3(rect.Width, rect.Height, 0);
Vector3 p01 = p00 + new Vector3(0, rect.Height, 0);
Vector2 uv00 = new Vector2(rect.U0, rect.V0);
Vector2 uv10 = new Vector2(rect.U1, rect.V0);
Vector2 uv11 = new Vector2(rect.U1, rect.V1);
Vector2 uv01 = new Vector2(rect.U0, rect.V1);
_recorder.Vertex(p00, uv00);
_recorder.Vertex(p10, uv10);
_recorder.Vertex(p11, uv11);
_recorder.Vertex(p00, uv00);
_recorder.Vertex(p11, uv11);
_recorder.Vertex(p01, uv01);
}
_recorder.End();
}
private struct Call
{
public Matrix4 Transforms = Matrix4.Identity;
public int Texture = 0;
public float BorderWidth = 0f;
public Vector4 FillColor = Vector4.One;
public Vector4 BorderColor = new Vector4(0,0,0,1);
public Call()
{
}
}
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
private struct Vertex
{
[FieldOffset(0)]
public Vector3 Position;
[FieldOffset(4 * sizeof(float))]
public Vector2 TexCoords;
}
private class DrawCallRecorder : DrawCallRecorder<Call, Vertex>
{
private TextCommandExecutor Executor { get; }
public DrawCallRecorder(TextCommandExecutor executor)
{
Executor = executor;
}
public void Vertex(Vector3 position, Vector2 texCoords)
{
Vertex(new Vertex(){Position = position, TexCoords = texCoords});
}
protected override void PrepareCall(in Call call)
{
Matrix4 transforms = call.Transforms;
GL.UniformMatrix4f(Executor._transformsLocation, 1, true, ref transforms);
GL.Uniform1f(Executor._borderWidthLocation, call.BorderWidth);
GL.Uniform4f(Executor._borderColorLocation, 1, in call.BorderColor);
GL.Uniform4f(Executor._fillColorLocation, 1, in call.FillColor);
GL.Uniform1i(Executor._atlasLocation, 0);
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2d, call.Texture);
}
protected override void SetVertexFormat()
{
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, VertexSize, 0);
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, VertexSize, 4*sizeof(float));
GL.EnableVertexAttribArray(0);
GL.EnableVertexAttribArray(1);
}
}
}
}
@@ -1,55 +0,0 @@
#ifndef _GRADIENT_GLSL_
#define _GRADIENT_GLSL_
#define DB_GRADIENT_MAX 16
struct Gradient_t {
float fPosition;
float pad0;
float pad1;
float pad2;
vec4 v4Color;
};
uniform GradientBlock
{
Gradient_t vstGradientStops[DB_GRADIENT_MAX];
};
vec4 getGradientColor(float position, int index, int count)
{
position = clamp(position, 0, 1);
int i0 = 0;
float p0 = vstGradientStops[index + i0].fPosition;
int i1 = count - 1;
float p1 = vstGradientStops[index + i1].fPosition;
for (int i = 0; i < count; i++)
{
float px = vstGradientStops[index + i].fPosition;
if (px > p0 && px <= position)
{
p0 = px;
i0 = i;
}
if (px < p1 && px >= position)
{
p1 = px;
i1 = i;
}
}
vec4 c0 = vstGradientStops[index + i0].v4Color;
vec4 c1 = vstGradientStops[index + i1].v4Color;
float l = p1 - p0;
float w = (l > 0) ? (position - p0) / (p1 - p0) : 0;
return mix(c0, c1, w);
}
#endif
@@ -1,224 +0,0 @@
#version 140
#define DB_GRADIENT_MAX 16
#define DB_COMMAND_MAX 64
#define CMD_POINT 1
#define CMD_LINE 2
#define CMD_RECT 3
#define STRIKE_CENTER 0
#define STRIKE_OUTSET 1
#define STRIKE_INSET 2
in vec3 v_v3Position;
in vec2 v_v2TexCoords;
flat in int v_iCmdIndex;
out vec4 f_Color;
uniform sampler2D txForeground;
uniform sampler2D txBackground;
struct Gradient_t {
float fPosition;
float pad0;
float pad1;
float pad2;
vec4 v4Color;
};
uniform GradientBlock
{
Gradient_t vstGradientStops[DB_GRADIENT_MAX];
};
vec4 getGradientColor(float position, int index, int count)
{
position = clamp(position, 0, 1);
int i0 = 0;
float p0 = vstGradientStops[index + i0].fPosition;
int i1 = count - 1;
float p1 = vstGradientStops[index + i1].fPosition;
for (int i = 0; i < count; i++)
{
float px = vstGradientStops[index + i].fPosition;
if (px > p0 && px <= position)
{
p0 = px;
i0 = i;
}
if (px < p1 && px >= position)
{
p1 = px;
i1 = i;
}
}
vec4 c0 = vstGradientStops[index + i0].v4Color;
vec4 c1 = vstGradientStops[index + i1].v4Color;
float l = p1 - p0;
float w = (l > 0) ? (position - p0) / (p1 - p0) : 0;
return mix(c0, c1, w);
}
struct CommandInfo_t {
int iCommand;
int iFlags;
float fArg0;
float fArg1;
int iFgGradientIndex;
int iFgGradientCount;
int iBgGradientIndex;
int iBgGradientCount;
vec4 v4FgColor;
vec4 v4BgColor;
};
uniform CommandBlock
{
CommandInfo_t vstCommandInfo[DB_COMMAND_MAX];
};
CommandInfo_t getCommandInfo()
{
return vstCommandInfo[v_iCmdIndex];
}
vec4 fgColor()
{
return getCommandInfo().v4FgColor;
}
vec4 bgColor()
{
return getCommandInfo().v4BgColor;
}
void Point(void)
{
vec4 fg = fgColor();
if (dot(v_v2TexCoords, v_v2TexCoords) <= 0.25)
f_Color = fg;
else
discard;
}
#define LINE_NORMALIZED_RADIUS(cmd) cmd.fArg0
void Line(void)
{
vec4 fg = fgColor();
CommandInfo_t cmd = getCommandInfo();
float t = clamp(v_v2TexCoords.x, 0, 1);
vec2 dv = v_v2TexCoords - vec2(t, 0);
float d = dot(dv, dv);
float lim = LINE_NORMALIZED_RADIUS(cmd);
lim *= lim;
if (d <= lim)
f_Color = fg;
else
discard;
}
#define RECT_ASPECT_RATIO(cmd) (cmd.fArg0)
#define RECT_BORDER_WIDTH(cmd) (cmd.fArg1)
#define RECT_FILL(cmd) ((cmd.iFlags & (1 << 0)) != 0)
#define RECT_BORDER(cmd) ((cmd.iFlags & (1 << 1)) != 0)
#define RECT_STRIKE_MASK 3
#define RECT_STRIKE_SHIFT 2
#define RECT_STRIKE_KIND(cmd) ((cmd.iFlags & RECT_STRIKE_MASK) >> RECT_STRIKE_SHIFT)
void Rect(void)
{
vec4 fg = fgColor();
vec4 bg = bgColor();
CommandInfo_t cmd = getCommandInfo();
float aspect = RECT_ASPECT_RATIO(cmd);
float border = RECT_BORDER_WIDTH(cmd);
int strikeKind = RECT_STRIKE_KIND(cmd);
vec2 p = abs(2*v_v2TexCoords - vec2(1));
p.x = p.x/aspect;
float m0;
float m1;
if (!RECT_BORDER(cmd))
{
m0 = 1;
m1 = 1;
}
else if (strikeKind == STRIKE_OUTSET)
{
m0 = 1;
m1 = border;
}
else if (strikeKind == STRIKE_INSET)
{
m0 = 1-border;
m1 = 1;
}
else // strikeKind == STRIKE_CENTER
{
float h = 0.5 * border;
m0 = 1-border;
m1 = 1+border;
}
if (p.x > m1*aspect || p.y > m1)
{
discard;
}
if (RECT_FILL(cmd))
{
if (p.x <= 1 && p.y <= 1)
{
f_Color = fg;
}
}
if (RECT_BORDER(cmd))
{
float x = clamp(p.x, aspect*m0, aspect*m1);
float y = clamp(p.y, m0, m1);
if (p.x == x || p.y == y)
{
f_Color = bg;
}
}
}
void main(void)
{
switch (getCommandInfo().iCommand)
{
case CMD_POINT:
Point();
break;
case CMD_LINE:
Line();
break;
case CMD_RECT:
Rect();
break;
default:
// Unimplemented value.
f_Color = vec4(1, 0, 1, 1);
break;
}
}
@@ -1,21 +0,0 @@
#version 140
in vec3 a_v3Position;
in vec2 a_v2TexCoords;
in int a_iCmdIndex;
out vec3 v_v3Position;
out vec2 v_v2TexCoords;
flat out int v_iCmdIndex;
uniform mat4 m4Transforms;
void main(void)
{
vec4 position = vec4(a_v3Position, 1) * m4Transforms;
gl_Position = position;
v_v3Position = position.xyz/position.w;
v_v2TexCoords = a_v2TexCoords;
v_iCmdIndex = a_iCmdIndex;
}
@@ -1,21 +0,0 @@
#version 140
in vec3 v_v3Position;
in vec2 v_v2TexCoords;
out vec4 f_Color;
uniform sampler2D txAtlas;
uniform float fBorderWidth;
uniform vec4 v4BorderColor;
uniform vec4 v4FillColor;
void main() {
// For now just honor the fill color
vec4 color = texture(txAtlas, v_v2TexCoords) * v4FillColor;
if (color.a <= 0.1)
discard;
f_Color = color;
}
@@ -1,18 +0,0 @@
#version 140
in vec3 a_v3Position;
in vec2 a_v2TexCoords;
out vec3 v_v3Position;
out vec2 v_v2TexCoords;
uniform mat4 m4Transforms;
void main(void)
{
vec4 position = vec4(a_v3Position, 1) * m4Transforms;
gl_Position = position;
v_v3Position = position.xyz/position.w;
v_v2TexCoords = a_v2TexCoords;
}
-39
View File
@@ -1,39 +0,0 @@
using Dashboard.Drawing.OpenGL.Text;
using OpenTK;
using OpenTK.Graphics;
namespace Dashboard.Drawing.OpenGL
{
public class GLEngine
{
private readonly Dictionary<IGLContext, ContextExecutor> _executors = new Dictionary<IGLContext, ContextExecutor>();
public bool IsInitialized { get; private set; } = false;
public ContextResourcePoolManager ResourcePoolManager { get; private set; } = new ContextResourcePoolManager();
public void Initialize(IBindingsContext? bindingsContext = null)
{
if (IsInitialized)
return;
IsInitialized = true;
if (bindingsContext != null)
GLLoader.LoadBindings(bindingsContext);
Typesetter.Backend = BlurgEngine.Global;
}
public ContextExecutor GetExecutor(IGLContext glContext)
{
if (!_executors.TryGetValue(glContext, out ContextExecutor? executor))
{
executor = new ContextExecutor(this, glContext);
executor.Initialize();
_executors.Add(glContext, executor);
}
return executor;
}
}
}
@@ -1,89 +0,0 @@
using System.Runtime.InteropServices;
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL
{
public class GradientUniformBuffer : IInitializer, IGLDisposable, IResourceManager
{
private bool _isDisposed;
private int _top = 0;
private readonly MappableBuffer<GradientUniformStruct> _buffer = new MappableBuffer<GradientUniformStruct>();
private readonly Dictionary<Gradient, Entry> _entries = new Dictionary<Gradient, Entry>();
public bool IsInitialized { get; private set; } = false;
public void Initialize()
{
if (IsInitialized)
return;
IsInitialized = true;
_buffer.Initialize();
}
public Entry InternGradient(Gradient gradient)
{
if (_entries.TryGetValue(gradient, out Entry entry))
return entry;
int count = gradient.Count;
int offset = _top;
_top += count;
_buffer.EnsureCapacity(_top);
_buffer.Map();
Span<GradientUniformStruct> span = _buffer.AsSpan()[offset.._top];
for (int i = 0; i < count; i++)
{
GradientStop stop = gradient[i];
span[i] = new GradientUniformStruct()
{
Position = stop.Position,
Color = new Vector4(
stop.Color.R / 255f,
stop.Color.G / 255f,
stop.Color.B / 255f,
stop.Color.A / 255f),
};
}
entry = new Entry(offset, count);
_entries.Add(gradient, entry);
return entry;
}
public void Clear()
{
_entries.Clear();
_top = 0;
}
public record struct Entry(int Offset, int Count);
public void Dispose() => Dispose(true);
public void Dispose(bool safeExit)
{
if (_isDisposed)
return;
_isDisposed = true;
_buffer.Dispose(safeExit);
}
string IResourceManager.Name { get; } = nameof(GradientUniformBuffer);
}
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
public struct GradientUniformStruct
{
[FieldOffset(0 * sizeof(float))]
public float Position;
[FieldOffset(4 * sizeof(float))]
public Vector4 Color;
}
}
-24
View File
@@ -1,24 +0,0 @@
namespace Dashboard.Drawing.OpenGL
{
/// <summary>
/// Atomic reference counter.
/// </summary>
public interface IArc : IDisposable
{
/// <summary>
/// The number of references to this.
/// </summary>
int References { get; }
/// <summary>
/// Increment the number of references.
/// </summary>
void IncrementReference();
/// <summary>
/// Decrement the number of references.
/// </summary>
/// <returns>True if this was the last reference.</returns>
bool DecrementReference();
}
}
-42
View File
@@ -1,42 +0,0 @@
using System.Drawing;
namespace Dashboard.Drawing.OpenGL
{
/// <summary>
/// Interface for GL context operations
/// </summary>
public interface IGLContext
{
/// <summary>
/// The associated group for context sharing.
/// </summary>
/// <remarks>-1 assigns no group.</remarks>
public int ContextGroup { get; }
/// <summary>
/// The size of the framebuffer in pixels.
/// </summary>
public Size FramebufferSize { get; }
/// <summary>
/// Called when the context is disposed.
/// </summary>
event Action Disposed;
}
/// <summary>
/// Extension interface for GL contexts in a DPI-aware environment.
/// </summary>
public interface IDpiAwareGLContext : IGLContext
{
/// <summary>
/// Dpi for current context.
/// </summary>
public float Dpi { get; }
/// <summary>
/// Scale for the current context. This will be used to scale drawn geometry.
/// </summary>
public float Scale { get; }
}
}
-16
View File
@@ -1,16 +0,0 @@
namespace Dashboard.Drawing.OpenGL
{
/// <summary>
/// Interface much like <see cref="IDisposable"/> except GL resources are dropped.
/// </summary>
/// <remarks>
/// The main reason this interface exists is that you need a way to differentiate
/// when a context is deleted, versus when the context is to remain present.
/// </remarks>
public interface IGLDisposable : IDisposable
{
/// <inheritdoc cref="IDisposable.Dispose"/>
/// <param name="safeExit">Set to true to spend the time clearing out GL objects.</param>
void Dispose(bool safeExit);
}
}
-9
View File
@@ -1,9 +0,0 @@
namespace Dashboard.Drawing.OpenGL
{
public interface IInitializer
{
bool IsInitialized { get; }
void Initialize();
}
}
@@ -1,7 +0,0 @@
namespace Dashboard.Drawing.OpenGL
{
public interface IResourceManager
{
public string Name { get; }
}
}
-167
View File
@@ -1,167 +0,0 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL
{
public class MappableBuffer<T> : IInitializer, IGLDisposable where T : struct
{
public int Handle { get; private set; } = 0;
public int Capacity { get; set; } = BASE_CAPACITY;
public IntPtr Pointer { get; private set; } = IntPtr.Zero;
public bool IsInitialized => Handle != 0;
private bool _isDisposed = false;
private const int BASE_CAPACITY = 4 << 10; // 4 KiB
private const int MAX_INCREMENT = 4 << 20; // 4 MiB
~MappableBuffer()
{
Dispose(true, false);
}
public void Initialize()
{
if (IsInitialized)
return;
Handle = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
GL.BufferData(BufferTarget.ArrayBuffer, Capacity, IntPtr.Zero, BufferUsage.DynamicDraw);
}
public void EnsureCapacity(int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (Capacity > count)
return;
SetSize(count, false);
}
public void SetSize(int count, bool clear = false)
{
AssertInitialized();
Unmap();
int sz = Unsafe.SizeOf<T>();
int oldsize = Capacity * sz;
int request = count * sz;
int newsize;
if (request < BASE_CAPACITY)
request = BASE_CAPACITY;
if (request > MAX_INCREMENT)
{
newsize = ((request + MAX_INCREMENT - 1) / MAX_INCREMENT) * MAX_INCREMENT;
}
else
{
newsize = checked((int)BitOperations.RoundUpToPowerOf2((ulong)request));
}
int dest = GL.GenBuffer();
if (clear)
{
GL.BindBuffer(BufferTarget.ArrayBuffer, dest);
GL.BufferData(BufferTarget.ArrayBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
}
else
{
GL.BindBuffer(BufferTarget.CopyWriteBuffer, dest);
GL.BindBuffer(BufferTarget.CopyReadBuffer, Handle);
GL.BufferData(BufferTarget.CopyWriteBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
GL.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer, 0, 0, Math.Min(newsize, oldsize));
}
GL.DeleteBuffer(Handle);
Handle = dest;
Capacity = newsize / Unsafe.SizeOf<T>();
}
public unsafe void Map()
{
if (Pointer != IntPtr.Zero)
return;
AssertInitialized();
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
Pointer = (IntPtr)GL.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);
}
public void Unmap()
{
if (Pointer == IntPtr.Zero)
return;
AssertInitialized();
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
GL.UnmapBuffer(BufferTarget.ArrayBuffer);
Pointer = IntPtr.Zero;
}
public unsafe Span<T> AsSpan()
{
if (Pointer == IntPtr.Zero)
throw new InvalidOperationException("The buffer is not currently mapped.");
AssertInitialized();
return new Span<T>(Pointer.ToPointer(), Capacity);
}
private void AssertInitialized()
{
if (Handle == 0)
throw new InvalidOperationException("The buffer is not initialized.");
}
private void Dispose(bool safeExit, bool disposing)
{
if (_isDisposed)
return;
_isDisposed = true;
if (disposing)
GC.SuppressFinalize(this);
if (safeExit)
ContextCollector.Global.DeleteBufffer(Handle);
}
public void Dispose() => Dispose(true, true);
public void Dispose(bool safeExit) => Dispose(safeExit, true);
}
public class MappableBumpAllocator<T> : MappableBuffer<T> where T : struct
{
private int _top = 0;
private int _previousTop = 0;
public ref T Take(out int index)
{
index = _top;
EnsureCapacity(++_top);
Map();
return ref AsSpan()[index];
}
public ref T Take() => ref Take(out _);
public void Clear()
{
SetSize(0, true);
_previousTop = _top;
_top = 0;
}
}
}
-60
View File
@@ -1,60 +0,0 @@
using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL
{
public static class ShaderUtil
{
public static int CompileShader(ShaderType type, string source)
{
int shader = GL.CreateShader(type);
GL.ShaderSource(shader, source);
GL.CompileShader(shader);
int compileStatus = 0;
GL.GetShaderi(shader, ShaderParameterName.CompileStatus, out compileStatus);
if (compileStatus == 0)
{
GL.GetShaderInfoLog(shader, out string log);
GL.DeleteShader(shader);
throw new Exception($"{type} Shader compilation failed: " + log);
}
return shader;
}
public static int CompileShader(ShaderType type, Stream stream)
{
using StreamReader reader = new StreamReader(stream, leaveOpen: true);
return CompileShader(type, reader.ReadToEnd());
}
public static int LinkProgram(int s1, int s2, IReadOnlyList<string>? attribLocations = null)
{
int program = GL.CreateProgram();
GL.AttachShader(program, s1);
GL.AttachShader(program, s2);
for (int i = 0; i < attribLocations?.Count; i++)
{
GL.BindAttribLocation(program, (uint)i, attribLocations[i]);
}
GL.LinkProgram(program);
int linkStatus = 0;
GL.GetProgrami(program, ProgramProperty.LinkStatus, out linkStatus);
if (linkStatus == 0)
{
GL.GetProgramInfoLog(program, out string log);
GL.DeleteProgram(program);
throw new Exception("Shader program linking failed: " + log);
}
return program;
}
}
}
@@ -1,188 +0,0 @@
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using BlurgText;
using OpenTK.Graphics.OpenGL;
using OPENGL = OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL.Text
{
public class BlurgEngine : IResourceManager, IGLDisposable, ITypeSetter
{
public string Name { get; } = "BlurgEngine";
public Blurg Blurg { get; }
public bool SystemFontsEnabled { get; }
private readonly List<int> _textures = new List<int>();
public BlurgEngine() : this(false)
{
}
private BlurgEngine(bool global)
{
if (global)
Blurg = new Blurg(AllocateTextureGlobal, UpdateTextureGlobal);
else
Blurg = new Blurg(AllocateTexture, UpdateTexture);
SystemFontsEnabled = Blurg.EnableSystemFonts();
}
~BlurgEngine()
{
Dispose(false, true);
}
public SizeF MeasureString(IFont font, string value)
{
return MeasureStringInternal(InternFont(font), value);
}
private SizeF MeasureStringInternal(DbBlurgFont font, string value)
{
Vector2 v = Blurg.MeasureString(font.Font, font.Size, value);
return new SizeF(v.X, v.Y);
}
public IFont LoadFont(Stream stream)
{
string path;
Stream dest;
for (int i = 0;; i++)
{
path = Path.GetTempFileName();
try
{
dest = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.None);
}
catch (IOException ex)
{
if (i < 3)
continue;
else
throw new Exception("Could not open a temporary file for writing the font.", ex);
}
break;
}
stream.CopyTo(dest);
dest.Dispose();
DbBlurgFont font = (DbBlurgFont)LoadFont(path);
File.Delete(path);
return font;
}
public IFont LoadFont(string path)
{
BlurgFont? font = Blurg.AddFontFile(path) ?? throw new Exception("Failed to load the font file.");
return new DbBlurgFont(Blurg, font, 12f);
}
public IFont LoadFont(NamedFont font)
{
// Ignore the stretch argument.
bool italic = font.Slant != FontSlant.Normal;
BlurgFont? loaded = Blurg.QueryFont(font.Family, new BlurgText.FontWeight((int)font.Weight), italic);
if (loaded != null)
return new DbBlurgFont(Blurg, loaded, 12f);
else
throw new Exception("Font not found.");
}
public DbBlurgFont InternFont(IFont font)
{
if (font is NamedFont named)
{
return (DbBlurgFont)LoadFont(named);
}
else if (font is DbBlurgFont dblurg)
{
if (dblurg.Owner != Blurg)
{
throw new Exception();
}
else
{
return dblurg;
}
}
else
{
throw new Exception("Unsupported font resource.");
}
}
private void UpdateTexture(IntPtr texture, IntPtr buffer, int x, int y, int width, int height)
{
GL.BindTexture(TextureTarget.Texture2d, (int)texture);
GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, buffer);
// GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Red, PixelType.Byte, buffer);
}
private IntPtr AllocateTexture(int width, int height)
{
int texture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2d, texture);
GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.Rgba, width, height, 0, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
// GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.R8, width, height, 0, OPENGL.PixelFormat.Red, PixelType.Byte, IntPtr.Zero);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.One);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.One);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.One);
// GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.Red);
_textures.Add(texture);
return texture;
}
private bool _isDisposed = false;
private void Dispose(bool disposing, bool safeExit)
{
if (_isDisposed)
return;
_isDisposed = true;
if (disposing)
{
Blurg.Dispose();
GC.SuppressFinalize(this);
}
if (safeExit)
{
foreach (int texture in _textures)
ContextCollector.Global.DeleteTexture(texture);
}
}
public void Dispose() => Dispose(true, true);
public void Dispose(bool safeExit) => Dispose(true, safeExit);
/// <summary>
/// The global Blurg engine implements the needed methods for command queues to work.
/// </summary>
public static BlurgEngine Global { get; } = new BlurgEngine(true);
private static void UpdateTextureGlobal(IntPtr userdata, IntPtr buffer, int x, int y, int width, int height)
{
// Report the user error.
Debug.WriteLine("Attempt to create or update a texture from the global BlurgEngine.", "Dashboard/BlurgEngine");
}
private static IntPtr AllocateTextureGlobal(int width, int height)
{
Debug.WriteLine("Attempt to create or update a texture from the global BlurgEngine.", "Dashboard/BlurgEngine");
return IntPtr.Zero;
}
}
}
@@ -1,13 +0,0 @@
namespace Dashboard.Drawing.OpenGL.Text
{
public class BlurgFontExtension : IDrawExtension
{
public string Name { get; } = "BLURG_Font";
public IReadOnlyList<IDrawExtension> Requires { get; } = new [] { FontExtension.Instance };
public IReadOnlyList<IDrawCommand> Commands { get; } = new IDrawCommand[] { };
private BlurgFontExtension() {}
public static readonly BlurgFontExtension Instance = new BlurgFontExtension();
}
}
@@ -1,28 +0,0 @@
using BlurgText;
namespace Dashboard.Drawing.OpenGL.Text
{
public class DbBlurgFont : IFont
{
public IDrawExtension Kind { get; } = BlurgFontExtension.Instance;
public Blurg Owner { get; }
public BlurgFont Font { get; }
public float Size { get; }
public string Family => Font.FamilyName;
public FontWeight Weight => (FontWeight)Font.Weight.Value;
public FontSlant Slant => Font.Italic ? FontSlant.Italic : FontSlant.Normal;
public FontStretch Stretch => FontStretch.Normal;
public DbBlurgFont(Blurg owner, BlurgFont font, float size)
{
Owner = owner;
Font = font;
Size = size;
}
public DbBlurgFont WithSize(float size)
{
return new DbBlurgFont(Owner, Font, size);
}
}
}
@@ -1,51 +0,0 @@
using OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL
{
/// <summary>
/// The current stack of transformations.
/// </summary>
public class TransformStack
{
private Matrix4 _top = Matrix4.Identity;
private readonly Stack<Matrix4> _stack = new Stack<Matrix4>();
/// <summary>
/// The top-most transform matrix.
/// </summary>
public ref readonly Matrix4 Top => ref _top;
/// <summary>
/// The number of matrices in the stack.
/// </summary>
public int Count => _stack.Count;
/// <summary>
/// Push a transform.
/// </summary>
/// <param name="transform">The transform to push.</param>
public void Push(in Matrix4 transform)
{
_stack.Push(_top);
_top = transform * _top;
}
/// <summary>
/// Pop a transform.
/// </summary>
public void Pop()
{
if (!_stack.TryPop(out _top))
_top = Matrix4.Identity;
}
/// <summary>
/// Clear the stack of transformations.
/// </summary>
public void Clear()
{
_stack.Clear();
_top = Matrix4.Identity;
}
}
}
-52
View File
@@ -1,52 +0,0 @@
using System;
using System.Drawing;
namespace Dashboard.Drawing
{
public class BrushExtension : DrawExtension
{
private BrushExtension() : base("DB_Brush") { }
public static readonly BrushExtension Instance = new BrushExtension();
}
public interface IBrush : IDrawResource
{
}
public readonly struct SolidBrush(Color color) : IBrush
{
public IDrawExtension Kind { get; } = SolidBrushExtension.Instance;
public Color Color { get; } = color;
public override int GetHashCode()
{
return HashCode.Combine(Kind, Color);
}
}
public readonly struct GradientBrush(Gradient gradient) : IBrush
{
public IDrawExtension Kind { get; } = GradientBrushExtension.Instance;
public Gradient Gradient { get; } = gradient;
public override int GetHashCode()
{
return HashCode.Combine(Kind, Gradient);
}
}
public class SolidBrushExtension : DrawExtension
{
private SolidBrushExtension() : base("DB_Brush_solid", new[] { BrushExtension.Instance }) { }
public static readonly SolidBrushExtension Instance = new SolidBrushExtension();
}
public class GradientBrushExtension : DrawExtension
{
private GradientBrushExtension() : base("DB_Brush_gradient", new[] { BrushExtension.Instance }) { }
public static readonly GradientBrushExtension Instance = new GradientBrushExtension();
}
}
-292
View File
@@ -1,292 +0,0 @@
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing
{
public class DbBaseCommands : DrawExtension
{
public DrawCommand<PointCommandArgs> DrawPoint { get; }
public DrawCommand<LineCommandArgs> DrawLine { get; }
public RectCommand DrawRectF { get; }
public RectCommand DrawRectS { get; }
public RectCommand DrawRectFS { get; }
private DbBaseCommands() : base("DB_base",
new[]
{
BrushExtension.Instance,
})
{
AddCommand(DrawPoint = new DrawCommand<PointCommandArgs>("Point", this, PointCommandArgs.CommandSize));
AddCommand(DrawLine = new DrawCommand<LineCommandArgs>("Line", this, LineCommandArgs.CommandSize));
AddCommand(DrawRectF = new RectCommand(this, RectCommand.Mode.Fill));
AddCommand(DrawRectS = new RectCommand(this, RectCommand.Mode.Strike));
AddCommand(DrawRectFS = new RectCommand(this, RectCommand.Mode.FillStrike));
}
public static readonly DbBaseCommands Instance = new DbBaseCommands();
}
public struct PointCommandArgs : IParameterSerializer<PointCommandArgs>
{
public Vector2 Position { get; private set; }
public float Depth { get; private set; }
public float Size { get; private set; }
public IBrush? Brush { get; private set; }
public PointCommandArgs(Vector2 position, float depth, float size, IBrush brush)
{
Position = position;
Depth = depth;
Brush = brush;
Size = size;
}
public int Serialize(DrawQueue queue, Span<byte> bytes)
{
if (bytes.Length < CommandSize)
return CommandSize;
Span<Value> value = stackalloc Value[]
{
new Value(Position, Depth, Size, queue.RequireResource(Brush!))
};
MemoryMarshal.AsBytes(value).CopyTo(bytes);
return CommandSize;
}
[MemberNotNull(nameof(Brush))]
public void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes)
{
if (bytes.Length < CommandSize)
throw new Exception("Not enough bytes");
Value value = MemoryMarshal.AsRef<Value>(bytes);
Position = value.Position;
Depth = value.Depth;
Size = value.Size;
Brush = (IBrush)queue.Resources[value.BrushIndex];
}
private record struct Value(Vector2 Position, float Depth, float Size, int BrushIndex);
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
}
public struct LineCommandArgs : IParameterSerializer<LineCommandArgs>
{
public Vector2 Start { get; private set; }
public Vector2 End { get; private set; }
public float Depth { get; private set; }
public float Size { get; private set; }
public IBrush? Brush { get; private set; }
public LineCommandArgs(Vector2 start, Vector2 end, float depth, float size, IBrush brush)
{
Start = start;
End = end;
Depth = depth;
Size = size;
Brush = brush;
}
public int Serialize(DrawQueue queue, Span<byte> bytes)
{
if (bytes.Length < CommandSize)
return CommandSize;
Span<Value> value = stackalloc Value[]
{
new Value(Start, End, Depth, Size, queue.RequireResource(Brush!))
};
MemoryMarshal.AsBytes(value).CopyTo(bytes);
return CommandSize;
}
public void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes)
{
if (bytes.Length < CommandSize)
throw new Exception("Not enough bytes");
Value value = MemoryMarshal.AsRef<Value>(bytes);
Start = value.Start;
End = value.End;
Depth = value.Depth;
Size = value.Size;
Brush = (IBrush)queue.Resources[value.BrushIndex];
}
private record struct Value(Vector2 Start, Vector2 End, float Depth, float Size, int BrushIndex);
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
}
public class RectCommand : IDrawCommand<RectCommandArgs>
{
private readonly Mode _mode;
public string Name { get; }
public IDrawExtension Extension { get; }
public int Length { get; }
public RectCommand(IDrawExtension extension, Mode mode)
{
Extension = extension;
_mode = mode;
switch (mode)
{
case Mode.Fill:
Name = "RectF";
Length = Unsafe.SizeOf<RectF>();
break;
case Mode.Strike:
Name = "RectS";
Length = Unsafe.SizeOf<RectS>();
break;
default:
Name = "RectFS";
Length = Unsafe.SizeOf<RectFS>();
break;
}
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);
}
public RectCommandArgs GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
if (param.Length < Length)
throw new Exception("Not enough bytes");
RectCommandArgs args;
switch (_mode)
{
case Mode.Fill:
ref readonly RectF f = ref MemoryMarshal.AsRef<RectF>(param);
args = new RectCommandArgs(f.Start, f.End, f.Depth, (IBrush)queue.Resources[f.FillBrushIndex]);
break;
case Mode.Strike:
ref readonly RectS s = ref MemoryMarshal.AsRef<RectS>(param);
args = new RectCommandArgs(s.Start, s.End, s.Depth, (IBrush)queue.Resources[s.StrikeBrushIndex], s.StrikeSize, s.BorderKind);
break;
default:
ref readonly RectFS fs = ref MemoryMarshal.AsRef<RectFS>(param);
args = new RectCommandArgs(fs.Start, fs.End, fs.Depth, (IBrush)queue.Resources[fs.FillBrushIndex],
(IBrush)queue.Resources[fs.StrikeBrushIndex], fs.StrikeSize, fs.BorderKind);
break;
}
return args;
}
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return WriteParams(queue, (RectCommandArgs)obj, param);
}
public int WriteParams(DrawQueue queue, RectCommandArgs obj, Span<byte> param)
{
if (param.Length < Length)
return Length;
switch (_mode)
{
case Mode.Fill:
ref RectF f = ref MemoryMarshal.AsRef<RectF>(param);
f.Start = obj.Start;
f.End = obj.End;
f.Depth = obj.Depth;
f.FillBrushIndex = queue.RequireResource(obj.FillBrush!);
break;
case Mode.Strike:
ref RectS s = ref MemoryMarshal.AsRef<RectS>(param);
s.Start = obj.Start;
s.End = obj.End;
s.Depth = obj.Depth;
s.StrikeBrushIndex = queue.RequireResource(obj.StrikeBrush!);
s.StrikeSize = obj.StrikeSize;
s.BorderKind = obj.BorderKind;
break;
default:
ref RectFS fs = ref MemoryMarshal.AsRef<RectFS>(param);
fs.Start = obj.Start;
fs.End = obj.End;
fs.Depth = obj.Depth;
fs.FillBrushIndex = queue.RequireResource(obj.FillBrush!);
fs.StrikeBrushIndex = queue.RequireResource(obj.StrikeBrush!);
fs.StrikeSize = obj.StrikeSize;
fs.BorderKind = obj.BorderKind;
break;
}
return Length;
}
[Flags]
public enum Mode
{
Fill = 1,
Strike = 2,
FillStrike = Fill | Strike,
}
private record struct RectF(Vector2 Start, Vector2 End, float Depth, int FillBrushIndex);
private record struct RectS(Vector2 Start, Vector2 End, float Depth, int StrikeBrushIndex, float StrikeSize, BorderKind BorderKind);
private record struct RectFS(Vector2 Start, Vector2 End, float Depth, int FillBrushIndex, int StrikeBrushIndex, float StrikeSize, BorderKind BorderKind);
}
public struct RectCommandArgs
{
public Vector2 Start { get; private set; }
public Vector2 End { get; private set; }
public float Depth { get; private set; }
public float StrikeSize { get; private set; } = 0f;
public BorderKind BorderKind { get; private set; } = BorderKind.Center;
public IBrush? FillBrush { get; private set; } = null;
public IBrush? StrikeBrush { get; private set; } = null;
public bool IsStruck => StrikeSize != 0;
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush fillBrush)
{
Start = start;
End = end;
Depth = depth;
FillBrush = fillBrush;
}
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush strikeBrush, float strikeSize, BorderKind borderKind)
{
Start = start;
End = end;
Depth = depth;
StrikeBrush = strikeBrush;
StrikeSize = strikeSize;
BorderKind = borderKind;
}
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush fillBrush, IBrush strikeBrush, float strikeSize,
BorderKind borderKind)
{
Start = start;
End = end;
Depth = depth;
FillBrush = fillBrush;
StrikeBrush = strikeBrush;
StrikeSize = strikeSize;
BorderKind = borderKind;
}
}
}
-107
View File
@@ -1,107 +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);
int WriteParams(DrawQueue queue, object? obj, Span<byte> param);
}
public interface IDrawCommand<T> : IDrawCommand
{
/// <summary>
/// Get the parameters object for this command.
/// </summary>
/// <param name="param">The parameter array.</param>
/// <returns>The parameters object.</returns>
new T? GetParams(DrawQueue queue, ReadOnlySpan<byte> param);
new int WriteParams(DrawQueue queue, T? obj, Span<byte> param);
}
public sealed class DrawCommand : IDrawCommand
{
public string Name { get; }
public IDrawExtension Extension { get; }
public int Length { get; } = 0;
public DrawCommand(string name, IDrawExtension extension)
{
Name = name;
Extension = extension;
}
public object? GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return null;
}
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return 0;
}
}
public sealed class DrawCommand<T> : IDrawCommand<T>
where T : IParameterSerializer<T>, new()
{
public string Name { get; }
public IDrawExtension Extension { get; }
public int Length { get; }
public DrawCommand(string name, IDrawExtension extension, int length)
{
Name = name;
Extension = extension;
Length = length;
}
public T? GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
T t = new T();
t.Deserialize(queue, param);
return t;
}
public int WriteParams(DrawQueue queue, T? obj, Span<byte> param)
{
return obj!.Serialize(queue, param);
}
int IDrawCommand.WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return WriteParams(queue, (T?)obj, param);
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);
}
}
}
-141
View File
@@ -1,141 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing
{
/// <summary>
/// Interface for all drawing extensions.
/// </summary>
public interface IDrawExtension
{
/// <summary>
/// Name of this extension.
/// </summary>
public string Name { get; }
public IReadOnlyList<IDrawExtension> Requires { get; }
/// <summary>
/// The list of commands this extension defines, if any.
/// </summary>
public IReadOnlyList<IDrawCommand> Commands { get; }
}
/// <summary>
/// A simple draw extension.
/// </summary>
public class DrawExtension : IDrawExtension
{
private readonly List<IDrawCommand> _drawCommands = new List<IDrawCommand>();
public string Name { get; }
public IReadOnlyList<IDrawCommand> Commands { get; }
public IReadOnlyList<IDrawExtension> Requires { get; }
public DrawExtension(string name, IEnumerable<IDrawExtension>? requires = null)
{
Name = name;
Commands = _drawCommands.AsReadOnly();
Requires = (requires ?? Enumerable.Empty<IDrawExtension>()).ToImmutableList();
}
protected void AddCommand(IDrawCommand command)
{
_drawCommands.Add(command);
}
}
public static class DrawExtensionClass
{
/// <summary>
/// Get the draw controller for the given queue.
/// </summary>
/// <param name="extension">The extension instance.</param>
/// <param name="queue">The draw queue.</param>
/// <returns>The draw controller for this queue.</returns>
public static IDrawController GetController(this IDrawExtension extension, DrawQueue queue)
{
return queue.GetController(extension);
}
public static void Point(this DrawQueue queue, Vector2 position, float depth, float size, IBrush brush)
{
Vector2 radius = new Vector2(0.5f * size);
Box2d bounds = new Box2d(position - radius, position + radius);
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
controller.EnsureBounds(bounds, depth);
controller.Write(DbBaseCommands.Instance.DrawPoint, new PointCommandArgs(position, depth, size, brush));
}
public static void Line(this DrawQueue queue, Vector2 start, Vector2 end, float depth, float size, IBrush brush)
{
Vector2 radius = new Vector2(size / 2f);
Vector2 min = Vector2.Min(start, end) - radius;
Vector2 max = Vector2.Max(start, end) + radius;
Box2d bounds = new Box2d(min, max);
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
controller.EnsureBounds(bounds, depth);
controller.Write(DbBaseCommands.Instance.DrawLine, new LineCommandArgs(start, end, depth, size, brush));
}
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush fillBrush)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
Vector2 min = Vector2.Min(start, end);
Vector2 max = Vector2.Max(start, end);
controller.EnsureBounds(new Box2d(min, max), depth);
controller.Write(DbBaseCommands.Instance.DrawRectF, new RectCommandArgs(start, end, depth, fillBrush));
}
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush strikeBrush, float strikeSize,
BorderKind kind = BorderKind.Center)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
Vector2 min = Vector2.Min(start, end);
Vector2 max = Vector2.Max(start, end);
controller.EnsureBounds(new Box2d(min, max), depth);
controller.Write(DbBaseCommands.Instance.DrawRectS, new RectCommandArgs(start, end, depth, strikeBrush, strikeSize, kind));
}
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush fillBrush, IBrush strikeBrush,
float strikeSize, BorderKind kind = BorderKind.Center)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
Vector2 min = Vector2.Min(start, end);
Vector2 max = Vector2.Max(start, end);
controller.EnsureBounds(new Box2d(min, max), depth);
controller.Write(DbBaseCommands.Instance.DrawRectFS, new RectCommandArgs(start, end, depth, fillBrush, strikeBrush, strikeSize, kind));
}
public static void Text(this DrawQueue queue, Vector3 position, IBrush brush, string text, IFont font,
Anchor anchor = Anchor.Left)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
SizeF size = Typesetter.MeasureString(font, text);
controller.EnsureBounds(new Box2d(position.X, position.Y, position.X + size.Width, position.Y + size.Height), position.Z);
controller.Write(TextExtension.Instance.TextCommand, new TextCommandArgs(font, brush, anchor, position, text));
}
public static void Text(this DrawQueue queue, Vector3 position, IBrush textBrush, IBrush borderBrush,
float borderRadius, string text, IFont font, Anchor anchor = Anchor.Left, BorderKind borderKind = BorderKind.Outset)
{
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
SizeF size = Typesetter.MeasureString(font, text);
controller.EnsureBounds(new Box2d(position.X, position.Y, position.X + size.Width, position.Y + size.Height), position.Z);
controller.Write(TextExtension.Instance.TextCommand, new TextCommandArgs(font, textBrush, anchor, position, text)
{
BorderBrush = borderBrush,
BorderRadius = borderRadius,
BorderKind = borderKind,
});
}
}
}
-370
View File
@@ -1,370 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Runtime.CompilerServices;
namespace Dashboard.Drawing
{
public class DrawQueue : IEnumerable<ICommandFrame>, IDisposable
{
private readonly HashList<IDrawExtension> _extensions = new HashList<IDrawExtension>();
private readonly HashList<IDrawCommand> _commands = new HashList<IDrawCommand>();
private readonly HashList<IDrawResource> _resources = new HashList<IDrawResource>();
private readonly DrawController _controller;
private readonly MemoryStream _commandStream = new MemoryStream();
/// <summary>
/// The absolute boundary of all graphics objects.
/// </summary>
public Box3d Bounds { get; set; }
/// <summary>
/// The extensions required to draw the image.
/// </summary>
public IReadOnlyList<IDrawExtension> Extensions => _extensions;
/// <summary>
/// The resources used by this draw queue.
/// </summary>
public IReadOnlyList<IDrawResource> Resources => _resources;
/// <summary>
/// The list of commands used by the extension.
/// </summary>
public IReadOnlyList<IDrawCommand> Command => _commands;
public DrawQueue()
{
_controller = new DrawController(this);
}
/// <summary>
/// Clear the queue.
/// </summary>
public void Clear()
{
_resources.Clear();
_commands.Clear();
_extensions.Clear();
_commandStream.SetLength(0);
}
public int RequireExtension(IDrawExtension extension)
{
foreach (IDrawExtension super in extension.Requires)
RequireExtension(super);
return _extensions.Intern(extension);
}
public int RequireResource(IDrawResource resource)
{
RequireExtension(resource.Kind);
return _resources.Intern(resource);
}
internal IDrawController GetController(IDrawExtension extension)
{
_extensions.Intern(extension);
return _controller;
}
private void Write(IDrawCommand command)
{
if (command.Length > 0)
throw new InvalidOperationException("This command has a finite length argument.");
int cmdIndex = _commands.Intern(command);
Span<byte> cmd = stackalloc byte[6];
int sz;
if (command.Length == 0)
{
// Write a fixed command.
sz = ToVlq(cmdIndex, cmd);
}
else
{
// Write a variadic with zero length.
sz = ToVlq(cmdIndex, cmd);
cmd[sz++] = 0;
}
_commandStream.Write(cmd[..sz]);
}
private void Write(IDrawCommand command, ReadOnlySpan<byte> param)
{
if (command.Length < 0)
{
Span<byte> cmd = stackalloc byte[10];
int cmdIndex = _commands.Intern(command);
int sz = ToVlq(cmdIndex, cmd);
sz += ToVlq(param.Length, cmd[sz..]);
_commandStream.Write(cmd[..sz]);
_commandStream.Write(param);
}
else
{
if (command.Length != param.Length)
throw new ArgumentOutOfRangeException(nameof(param.Length), "Length of the parameter does not match the command.");
Span<byte> cmd = stackalloc byte[5];
int cmdIndex = _commands.Intern(command);
int sz = ToVlq(cmdIndex, cmd);
_commandStream.Write(cmd[..sz]);
_commandStream.Write(param);
}
}
public Enumerator GetEnumerator() => new Enumerator(this);
IEnumerator<ICommandFrame> IEnumerable<ICommandFrame>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private static int ToVlq(int value, Span<byte> bytes)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Must be a positive integer.");
else if (bytes.Length < 5)
throw new ArgumentOutOfRangeException(nameof(bytes), "Must at least be five bytes long.");
if (value == 0)
{
bytes[0] = 0;
return 1;
}
int i;
for (i = 0; i < 5 && value != 0; i++, value >>= 7)
{
if (i > 0)
bytes[i - 1] |= 1 << 7;
bytes[i] = (byte)(value & 0x7F);
}
return i;
}
private static int FromVlq(ReadOnlySpan<byte> bytes, out int value)
{
value = 0;
int i;
for (i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
value = (value << 7) | b;
if ((b & (1 << 7)) == 0)
{
i++;
break;
}
}
return i;
}
public void Dispose()
{
throw new NotImplementedException();
}
private class DrawController(DrawQueue Queue) : IDrawController
{
public void EnsureBounds(Box2d bounds, float depth)
{
Queue.Bounds = Box3d.Union(Queue.Bounds, bounds, depth);
}
public void Write(IDrawCommand command)
{
Queue.Write(command);
}
public void Write(IDrawCommand command, ReadOnlySpan<byte> bytes)
{
Queue.Write(command, bytes);
}
public void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>
{
int length = param.Serialize(Queue, Span<byte>.Empty);
Span<byte> bytes = stackalloc byte[length];
param.Serialize(Queue, bytes);
Write(command, bytes);
}
public void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>
{
int length = command.WriteParams(Queue, param, Span<byte>.Empty);
Span<byte> bytes = stackalloc byte[length];
command.WriteParams(Queue, param, bytes);
Write(command, bytes);
}
}
public class Enumerator : ICommandFrame, IEnumerator<ICommandFrame>
{
private readonly DrawQueue _queue;
private readonly byte[] _stream;
private int _length;
private int _index = -1;
private int _paramsIndex = -1;
private int _paramLength = 0;
private IDrawCommand? _current = null;
public ICommandFrame Current => this;
object? IEnumerator.Current => Current;
public IDrawCommand Command => _current ?? throw new InvalidOperationException();
public bool HasParameters { get; private set; }
public Enumerator(DrawQueue queue)
{
_queue = queue;
_stream = queue._commandStream.GetBuffer();
_length = (int)queue._commandStream.Length;
}
public bool MoveNext()
{
if (_index == -1)
_index = 0;
if (_index >= _length)
return false;
_index += FromVlq(_stream[_index .. (_index + 5)], out int command);
_current = _queue.Command[command];
HasParameters = _current.Length != 0;
if (!HasParameters)
{
_paramsIndex = -1;
return true;
}
int length;
if (_current.Length < 0)
{
_index += FromVlq(_stream[_index .. (_index + 5)], out length);
}
else
{
length = _current.Length;
}
_paramsIndex = _index;
_paramLength = length;
_index += length;
return true;
}
public void Reset()
{
_index = -1;
_current = null;
}
public object? GetParameter()
{
return _current?.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength));
}
public T GetParameter<T>()
{
if (_current is IDrawCommand<T> command)
{
return command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!;
}
else
{
throw new InvalidOperationException();
}
}
public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter)
{
if (_current is IDrawCommand<T> command)
{
parameter = command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!;
return true;
}
else
{
parameter = default;
return false;
}
}
public void Dispose()
{
}
}
}
public interface ICommandFrame
{
public IDrawCommand Command { get; }
public bool HasParameters { get; }
public object? GetParameter();
public T GetParameter<T>();
public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter);
}
public interface IDrawController
{
/// <summary>
/// Ensures that the canvas is at least a certain size.
/// </summary>
/// <param name="bounds">The bounding box.</param>
void EnsureBounds(Box2d bounds, float depth);
/// <summary>
/// Write into the command stream.
/// </summary>
/// <param name="command">The command to write.</param>
void Write(IDrawCommand command);
/// <summary>
/// Write into the command stream.
/// </summary>
/// <param name="command">The command to write.</param>
/// <param name="param">Any data associated with the command.</param>
void Write(IDrawCommand command, ReadOnlySpan<byte> param);
/// <summary>
/// Write into the command stream.
/// </summary>
/// <param name="command">The command to write.</param>
/// <param name="param">Any data associated with the command.</param>
void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>;
/// <summary>
/// Write into the command stream.
/// </summary>
/// <param name="command">The command to write.</param>
/// <param name="param">Any data associated with the command.</param>
void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>;
}
}
-52
View File
@@ -1,52 +0,0 @@
using System.Linq;
namespace Dashboard.Drawing
{
public class FontExtension : DrawExtension
{
private FontExtension() : base("DB_Font", Enumerable.Empty<DrawExtension>())
{
}
public static readonly IDrawExtension Instance = new FontExtension();
}
public interface IFont : IDrawResource
{
public string Family { get; }
public float Size { get; }
public FontWeight Weight { get; }
public FontSlant Slant { get; }
public FontStretch Stretch { get; }
}
public struct NamedFont : IFont
{
public IDrawExtension Kind { get; } = Instance;
public string Family { get; }
public float Size { get; }
public FontWeight Weight { get; }
public FontSlant Slant { get; }
public FontStretch Stretch { get; }
public NamedFont(string family, float size, FontWeight weight = FontWeight.Normal,
FontSlant slant = FontSlant.Normal, FontStretch stretch = FontStretch.Normal)
{
Family = family;
Size = size;
Weight = weight;
Slant = slant;
Stretch = Stretch;
}
private static readonly IDrawExtension Instance = new Extension();
private class Extension : DrawExtension
{
public Extension() : base("DB_Font_Named", [FontExtension.Instance])
{
}
}
}
}
-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);
}
}
-140
View File
@@ -1,140 +0,0 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dashboard.Drawing
{
public class TextExtension : DrawExtension
{
public TextCommand TextCommand { get; }
private TextExtension() : base("DB_Text", new [] { FontExtension.Instance, BrushExtension.Instance })
{
TextCommand = new TextCommand(this);
}
public static readonly TextExtension Instance = new TextExtension();
}
public class TextCommand : IDrawCommand<TextCommandArgs>
{
public string Name { get; } = "Text";
public IDrawExtension Extension { get; }
public int Length { get; } = -1;
public TextCommand(TextExtension ext)
{
Extension = ext;
}
public int WriteParams(DrawQueue queue, TextCommandArgs obj, Span<byte> param)
{
int size = Unsafe.SizeOf<Header>() + obj.Text.Length * sizeof(char) + sizeof(char);
if (param.Length < size)
return size;
ref Header header = ref MemoryMarshal.Cast<byte, Header>(param[0..Unsafe.SizeOf<Header>()])[0];
Span<char> text = MemoryMarshal.Cast<byte, char>(param[Unsafe.SizeOf<Header>()..]);
header = new Header()
{
Font = queue.RequireResource(obj.Font),
TextBrush = queue.RequireResource(obj.TextBrush),
BorderBrush = (obj.BorderBrush != null) ? queue.RequireResource(obj.BorderBrush) : -1,
BorderRadius = (obj.BorderBrush != null) ? obj.BorderRadius : 0f,
Anchor = obj.Anchor,
Position = obj.Position,
BorderKind = obj.BorderKind,
};
obj.Text.CopyTo(text);
return size;
}
public TextCommandArgs GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
Header header = MemoryMarshal.Cast<byte, Header>(param[0..Unsafe.SizeOf<Header>()])[0];
ReadOnlySpan<char> text = MemoryMarshal.Cast<byte, char>(param[Unsafe.SizeOf<Header>()..]);
if (header.BorderBrush != -1 && header.BorderRadius != 0)
{
return new TextCommandArgs(
(IFont)queue.Resources[header.Font],
(IBrush)queue.Resources[header.TextBrush],
header.Anchor,
header.Position,
text.ToString())
{
BorderBrush = (IBrush)queue.Resources[header.BorderBrush],
BorderRadius = header.BorderRadius,
BorderKind = header.BorderKind,
};
}
else
{
return new TextCommandArgs(
(IFont)queue.Resources[header.Font],
(IBrush)queue.Resources[header.TextBrush],
header.Anchor,
header.Position,
text.ToString());
}
}
int IDrawCommand.WriteParams(DrawQueue queue, object? obj, Span<byte> param)
{
return WriteParams(queue, (TextCommandArgs)obj!, param);
}
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
{
return GetParams(queue, param);
}
private struct Header
{
private int _flags;
public int Font;
public int TextBrush;
public int BorderBrush;
public Vector3 Position;
public float BorderRadius;
public Anchor Anchor
{
get => (Anchor)(_flags & 0xF);
set => _flags = (_flags & ~0xF) | (int)value;
}
public BorderKind BorderKind
{
get => (_flags & INSET) switch
{
OUTSET => BorderKind.Outset,
INSET => BorderKind.Inset,
_ => BorderKind.Center,
};
set => _flags = value switch
{
BorderKind.Outset => (_flags & ~INSET) | OUTSET,
BorderKind.Inset => (_flags & ~INSET) | INSET,
_ => (_flags & ~INSET) | CENTER,
};
}
private const int INSET = 0x30;
private const int CENTER = 0x00;
private const int OUTSET = 0x10;
}
}
public record struct TextCommandArgs(IFont Font, IBrush TextBrush, Anchor Anchor, Vector3 Position, string Text)
{
public IBrush? BorderBrush { get; init; } = null;
public float BorderRadius { get; init; } = 0;
public BorderKind BorderKind { get; init; } = BorderKind.Center;
}
}
-104
View File
@@ -1,104 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Reflection.PortableExecutable;
namespace Dashboard.Drawing
{
/// <summary>
/// Interface for registered typesetters.
/// </summary>
public interface ITypeSetter
{
/// <summary>
/// Name of the typesetter.
/// </summary>
string Name { get; }
SizeF MeasureString(IFont font, string value);
IFont LoadFont(Stream stream);
IFont LoadFont(string path);
IFont LoadFont(NamedFont font);
}
/// <summary>
/// Class for typesetting related functions.
/// </summary>
public static class Typesetter
{
/// <summary>
/// The typesetting backend for this instance.
/// </summary>
public static ITypeSetter Backend { get; set; } = new UndefinedTypeSetter();
public static string Name => Backend.Name;
public static SizeF MeasureString(IFont font, string value)
{
return Backend.MeasureString(font, value);
}
public static IFont LoadFont(Stream stream)
{
return Backend.LoadFont(stream);
}
public static IFont LoadFont(string path)
{
return Backend.LoadFont(path);
}
public static IFont LoadFont(FileInfo file)
{
return Backend.LoadFont(file.FullName);
}
public static IFont LoadFont(NamedFont font)
{
return Backend.LoadFont(font);
}
public static IFont LoadFont(string family, float size, FontWeight weight = FontWeight.Normal,
FontSlant slant = FontSlant.Normal, FontStretch stretch = FontStretch.Normal)
{
return LoadFont(new NamedFont(family, size, weight, slant, stretch));
}
private class UndefinedTypeSetter : ITypeSetter
{
public string Name { get; } = "Undefined";
[DoesNotReturn]
private void Except()
{
throw new InvalidOperationException("No typesetting backend is loaded.");
}
public SizeF MeasureString(IFont font, string value)
{
Except();
return default;
}
public IFont LoadFont(Stream stream)
{
Except();
return default;
}
public IFont LoadFont(string path)
{
Except();
return default;
}
public IFont LoadFont(NamedFont font)
{
Except();
return default;
}
}
}
}
@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Drawing\Dashboard.Drawing.csproj" />
</ItemGroup>
</Project>
-235
View File
@@ -1,235 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Net.Http;
using System.Numerics;
using System.Text;
using Dashboard.Drawing;
namespace Dashboard.ImmediateUI
{
public class DimUIConfig
{
public Vector2 Margin = new Vector2(8, 4);
public Vector2 Padding = new Vector2(4);
public required IFont Font { get; init; }
public IBrush TextBrush = new SolidBrush(Color.Black);
public IBrush DisabledText = new SolidBrush(Color.Gray);
public float ButtonBorderSize = 2f;
public IBrush ButtonBorderBrush = new SolidBrush(Color.SteelBlue);
public IBrush ButtonFillBrush = new SolidBrush(Color.SlateGray);
public IBrush? ButtonShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
public float ButtonShadowOffset = 2f;
public float InputBorderSize = 2f;
public IBrush InputPlaceholderTextBrush = new SolidBrush(Color.SteelBlue);
public IBrush InputBorderBrush = new SolidBrush(Color.SlateGray);
public IBrush InputFillBrush = new SolidBrush(Color.LightGray);
public IBrush? InputShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
public float InputShadowOffset = -2f;
public float MenuBorderSize = 2f;
public IBrush MenuBorderBrush = new SolidBrush(Color.SteelBlue);
public IBrush MenuFillBrush = new SolidBrush(Color.SlateGray);
public IBrush? MenuShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
public float MenuShadowOffset = 2f;
}
public class DimUI
{
private readonly DimUIConfig _config;
private Vector2 _pen;
private Box2d _bounds;
private bool _firstLine = false;
private bool _sameLine = false;
private float _z = -1;
private float _lineHeight;
private DrawQueue _queue;
public DimUI(DimUIConfig config)
{
_config = config;
}
[MemberNotNull(nameof(_queue))]
public void Begin(Box2d bounds, DrawQueue queue)
{
_bounds = bounds;
_pen = _bounds.Min;
_queue = queue;
_firstLine = true;
_lineHeight = 0;
_z = -1;
}
public void SameLine()
{
_sameLine = true;
}
private void Line()
{
if (!_firstLine && !_sameLine)
{
_pen = new Vector2(_bounds.Left, _pen.Y + _lineHeight);
}
else
{
_firstLine = false;
_sameLine = false;
}
_pen.X += _config.Margin.X;
_lineHeight = 0;
}
private float Z()
{
return _z += 0.001f;
}
public void Text(string text)
{
Line();
SizeF sz = Typesetter.MeasureString(_config.Font, text);
float z = Z();
float h = _config.Margin.Y * 2 + sz.Height;
_queue.Text(new Vector3(_pen + new Vector2(0, _config.Margin.X), z), _config.TextBrush, text, _config.Font);
_lineHeight = Math.Max(_lineHeight, h);
_pen.X += sz.Width;
}
public void DrawBox(
Vector2 position,
Vector2 size,
IBrush fill,
IBrush border, float borderWidth,
IBrush? shadow, float offset)
{
float z = Z();
if (shadow != null)
{
if (offset >= 0)
{
_queue.Rect(position + new Vector2(offset), position + size + new Vector2(offset + borderWidth), z, shadow);
}
else
{
// Inset shadows are draw a bit weirdly.
_queue.Rect(position, position + new Vector2(offset, size.Y), z, shadow);
_queue.Rect(position + new Vector2(offset, 0), position + new Vector2(size.X - offset, offset), z, shadow);
}
}
_queue.Rect(position, position + size, z, fill, border, borderWidth, BorderKind.Outset);
}
public bool Button(string label)
{
Line();
SizeF sz = Typesetter.MeasureString(_config.Font, label);
float h = _config.Margin.Y * 2 + _config.Padding.Y * 2 + sz.Height;
DrawBox(
_pen + new Vector2(0, _config.Margin.Y),
new Vector2(sz.Width + 2 * _config.Padding.X, sz.Height + 2 * _config.Padding.Y),
_config.ButtonFillBrush,
_config.ButtonBorderBrush,
_config.ButtonBorderSize,
_config.ButtonShadowBrush,
_config.ButtonShadowOffset);
float z = Z();
_queue.Text(new Vector3(_pen + new Vector2(_config.Padding.X, _config.Margin.Y + _config.Padding.Y), z),
_config.TextBrush, label, _config.Font);
_lineHeight = Math.Max(_lineHeight, h);
_pen.X += sz.Width + 2 * _config.Padding.X;
return false;
}
public bool Input(string placeholder, StringBuilder value)
{
Line();
IBrush textBrush;
string str;
if (value.Length == 0)
{
textBrush = _config.DisabledText;
str = placeholder;
}
else
{
textBrush = _config.TextBrush;
str = value.ToString();
}
SizeF sz = Typesetter.MeasureString(_config.Font, str);
float h = _config.Margin.Y * 2 + _config.Padding.Y * 2 + sz.Height;
DrawBox(
_pen + new Vector2(0, _config.Margin.Y),
new Vector2(sz.Width + 2 * _config.Padding.X, sz.Height + 2 * _config.Padding.Y),
_config.InputFillBrush,
_config.InputBorderBrush,
_config.InputBorderSize,
_config.InputShadowBrush,
_config.InputShadowOffset);
float z = Z();
_queue.Text(new Vector3(_pen + new Vector2(_config.Padding.X, _config.Margin.Y + _config.Padding.Y), z),
textBrush, str, _config.Font);
_lineHeight = Math.Max(_lineHeight, h);
_pen.X += sz.Width + 2 * _config.Padding.X;
return false;
}
public void BeginMenu()
{
}
public bool MenuItem(string name)
{
return false;
}
public void EndMenu()
{
}
public int Id(ReadOnlySpan<char> str)
{
// Uses the FVN-1A algorithm in 32-bit mode.
const int PRIME = 0x01000193;
const int BASIS = unchecked((int)0x811c9dc5);
int hash = BASIS;
for (int i = 0; i < str.Length; i++)
{
hash ^= str[i] & 0xFF;
hash *= PRIME;
hash ^= str[i] >> 8;
hash *= PRIME;
}
return hash;
}
public void Finish()
{
// TODO:
}
}
}
@@ -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);
}
}
+29 -39
View File
@@ -3,59 +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.Common", "Dashboard.Common\Dashboard.Common.csproj", "{C77CDD2B-2482-45F9-B330-47A52F5F13C0}"
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.Drawing.OpenGL", "Dashboard.Drawing.OpenGL\Dashboard.Drawing.OpenGL.csproj", "{454198BA-CB95-41C5-A934-B1C8FDA35A6B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.ImmediateUI", "Dashboard.ImmediateUI\Dashboard.ImmediateUI.csproj", "{3F33197F-0B7B-4CD8-98BD-05D6D5EC76B2}"
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
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Release|Any CPU.Build.0 = Release|Any CPU
{454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Release|Any CPU.Build.0 = Release|Any CPU
{3F33197F-0B7B-4CD8-98BD-05D6D5EC76B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F33197F-0B7B-4CD8-98BD-05D6D5EC76B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F33197F-0B7B-4CD8-98BD-05D6D5EC76B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F33197F-0B7B-4CD8-98BD-05D6D5EC76B2}.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
+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
{
}
}
+16 -6
View File
@@ -1,9 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<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
}
}

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