10 Commits

91 changed files with 4321 additions and 913 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,7 +82,7 @@ namespace Dashboard.Collections
public void Clear() => _objects.Clear(); public void Clear() => _objects.Clear();
public IEnumerator<T> GetEnumerator() => _objects.Values.GetEnumerator(); public IEnumerator<T> GetEnumerator() => _objects.Values.Distinct().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using System.Drawing; using System.Drawing;
using Dashboard.Pal;
namespace Dashboard.Pal namespace Dashboard.Drawing
{ {
public interface ITextureExtension : IDeviceContextExtension public interface ITextureExtension : IDeviceContextExtension
{ {

View File

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

View File

@@ -1,20 +0,0 @@
namespace Dashboard.Events
{
public class AnimationTickEventArgs : UiEventArgs
{
/// <summary>
/// Animation delta time in seconds.
/// </summary>
public float Delta { get; }
public AnimationTickEventArgs(float delta) : base(UiEventType.AnimationTick)
{
Delta = delta;
}
}
public interface IAnimationTickEvent
{
event EventHandler<AnimationTickEventArgs> AnimationTimerEvent;
}
}

View File

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

View File

@@ -20,50 +20,24 @@ namespace Dashboard.Events
Middle = M3, Middle = M3,
} }
public sealed class MouseMoveEventArgs : UiEventArgs public sealed class MouseMoveEventArgs(Vector2 clientPosition, Vector2 delta) : UiEventArgs(UiEventType.MouseMove)
{ {
public Vector2 ClientPosition { get; } public Vector2 ClientPosition { get; } = clientPosition;
public Vector2 Delta { get; } public Vector2 Delta { get; } = delta;
public MouseMoveEventArgs(Vector2 clientPosition, Vector2 delta)
: base(UiEventType.MouseMove)
{
ClientPosition = clientPosition;
Delta = delta;
}
} }
public sealed class MouseButtonEventArgs : UiEventArgs public sealed class MouseButtonEventArgs(Vector2 clientPosition, MouseButtons buttons, ModifierKeys modifierKeys, bool up)
: UiEventArgs(up ? UiEventType.MouseButtonUp : UiEventType.MouseButtonDown)
{ {
public Vector2 ClientPosition { get; } public ModifierKeys ModifierKeys { get; } = modifierKeys;
public MouseButtons Buttons { get; } public Vector2 ClientPosition { get; } = clientPosition;
public MouseButtons Buttons { get; } = buttons;
public MouseButtonEventArgs(Vector2 clientPosition, MouseButtons buttons, bool up)
: base(up ? UiEventType.MouseButtonUp : UiEventType.MouseButtonDown)
{
ClientPosition = clientPosition;
Buttons = buttons;
}
} }
public sealed class MouseScrollEventArgs : UiEventArgs public sealed class MouseScrollEventArgs(Vector2 clientPosition, Vector2 scrollDelta)
: UiEventArgs(UiEventType.MouseScroll)
{ {
public Vector2 ClientPosition { get; } public Vector2 ClientPosition { get; } = clientPosition;
public Vector2 ScrollDelta { get; } public Vector2 ScrollDelta { get; } = scrollDelta;
public MouseScrollEventArgs(Vector2 clientPosition, Vector2 scrollDelta)
: base(UiEventType.MouseScroll)
{
ClientPosition = clientPosition;
ScrollDelta = scrollDelta;
}
}
public interface IMouseEvents
{
event EventHandler<MouseMoveEventArgs> MouseMoved;
event EventHandler<MouseButtonEventArgs> MouseButtonDown;
event EventHandler<MouseButtonEventArgs> MouseButtonUp;
event EventHandler<MouseScrollEventArgs> MouseScroll;
} }
} }

View File

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

View File

@@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using Dashboard.Pal;
namespace Dashboard.Events namespace Dashboard.Events
{ {
@@ -6,6 +7,7 @@ namespace Dashboard.Events
{ {
None, None,
AnimationTick, // Generic timer event. AnimationTick, // Generic timer event.
Paint, // Generic paint event.
// Text input related events. // Text input related events.
KeyDown, // Keyboard key down. KeyDown, // Keyboard key down.
@@ -57,6 +59,11 @@ namespace Dashboard.Events
public static readonly UiEventArgs None = new UiEventArgs(UiEventType.None); public static readonly UiEventArgs None = new UiEventArgs(UiEventType.None);
} }
public class PaintEventArgs(DeviceContext dc) : UiEventArgs(UiEventType.Paint)
{
public DeviceContext DeviceContext { get; } = dc;
}
public class ControlMovedEventArgs : UiEventArgs public class ControlMovedEventArgs : UiEventArgs
{ {
public Vector2 OldPosition { get; } public Vector2 OldPosition { get; }
@@ -69,8 +76,7 @@ namespace Dashboard.Events
} }
} }
public class ControlResizedEventArgs public class ResizeEventArgs() : UiEventArgs(UiEventType.ControlResized)
{ {
} }
} }

View File

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

View File

@@ -8,6 +8,7 @@ namespace Dashboard
_400 = 400, _400 = 400,
_500 = 500, _500 = 500,
_600 = 600, _600 = 600,
_700 = 700,
_800 = 800, _800 = 800,
_900 = 900, _900 = 900,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,48 +1,108 @@
using System.Numerics;
namespace Dashboard namespace Dashboard
{ {
public enum MeasurementUnit public record struct LayoutUnits(LayoutUnit All)
{ {
/// <summary> public LayoutUnit XUnit
/// The default unit. A size of a single picture element. {
/// </summary> get => (LayoutUnit)(((int)All & 0xF) >> 0);
Pixel, set => All = (LayoutUnit)(((int)All & ~(0xF << 0)) | ((int)value << 0));
/// <summary>
/// 1/72th of an inch traditional in graphics design.
/// </summary>
Point,
/// <summary>
/// The universal length unit for small distances.
/// </summary>
Millimeter,
/// <summary>
/// An inverse proportional unit with respect to the container size.
/// </summary>
Star,
/// <summary>
/// A directly proportional unit with respect to the container size.
/// </summary>
Percent,
} }
public record struct AdvancedMetric(MeasurementUnit Unit, float Value) public LayoutUnit YUnit
{ {
public AdvancedMetric Convert(MeasurementUnit target, float dpi, float rel, int stars) get => (LayoutUnit)(((int)All & 0xF) >> 4);
set => All = (LayoutUnit)(((int)All & ~(0xF << 4)) | ((int)value << 4));
}
public LayoutUnit ZUnit
{
get => (LayoutUnit)(((int)All & 0xF) >> 8);
set => All = (LayoutUnit)(((int)All & ~(0xF << 8)) | ((int)value << 8));
}
public LayoutUnit WUnit
{
get => (LayoutUnit)(((int)All & 0xF) >> 12);
set => All = (LayoutUnit)(((int)All & ~(0xF << 12)) | ((int)value << 12));
}
}
public record struct Metric(LayoutUnit Units, float Value)
{
public float Compute(float dpi, float rel, float star)
{
switch (Units)
{
case LayoutUnit.Auto:
return -1;
case LayoutUnit.Millimeter:
float mm2Px = dpi / 25.4f;
return Value * mm2Px;
case LayoutUnit.Pixel:
return Value;
case LayoutUnit.Point:
float pt2Px = 72 / dpi;
return Value * pt2Px;
case LayoutUnit.Percent:
return rel * Value;
case LayoutUnit.Star:
return star * Value;
default:
throw new Exception("Unrecognized unit.");
}
}
}
public record struct Metric2D(LayoutUnits Units, Vector2 Value)
{
public float X
{
get => Value.X;
set => Value = new Vector2(value, Value.Y);
}
public LayoutUnit XUnits
{
get => Units.XUnit;
set => Units = Units with { XUnit = value };
}
public float Y
{
get => Value.Y;
set => Value = new Vector2(Value.X, value);
}
public LayoutUnit YUnits
{
get => Units.YUnit;
set => Units = Units with { YUnit = value };
}
}
public record struct BoxMetric(LayoutUnit Units, Box2d Value);
public record struct AdvancedMetric(LayoutUnit Unit, float Value)
{
public AdvancedMetric Convert(LayoutUnit target, float dpi, float rel, int stars)
{ {
if (Unit == target) if (Unit == target)
return this; return this;
float pixels = Unit switch { float pixels = Unit switch {
MeasurementUnit.Pixel => Value, LayoutUnit.Pixel => Value,
MeasurementUnit.Point => Value * (72f / dpi), LayoutUnit.Point => Value * (72f / dpi),
MeasurementUnit.Millimeter => Value * (28.3464566929f / dpi), LayoutUnit.Millimeter => Value * (28.3464566929f / dpi),
MeasurementUnit.Star => Value * rel / stars, LayoutUnit.Star => Value * rel / stars,
MeasurementUnit.Percent => Value * rel / 100, LayoutUnit.Percent => Value * rel / 100,
_ => throw new Exception(), _ => throw new Exception(),
}; };
float value = target switch { float value = target switch {
MeasurementUnit.Pixel => pixels, LayoutUnit.Pixel => pixels,
MeasurementUnit.Point => Value * (dpi / 72f), LayoutUnit.Point => Value * (dpi / 72f),
// MeasurementUnit.Millimeter => // MeasurementUnit.Millimeter =>
}; };
@@ -65,22 +125,4 @@ namespace Dashboard
? metric ? metric
: throw new Exception($"Could not parse the value '{str}'."); : throw new Exception($"Could not parse the value '{str}'.");
} }
public static class MeasurementExtensions
{
public static bool IsRelative(this MeasurementUnit unit) => unit switch {
MeasurementUnit.Star or MeasurementUnit.Percent => true,
_ => false,
};
public static bool IsAbsolute(this MeasurementUnit unit) => !IsRelative(unit);
public static string ToShortString(this MeasurementUnit unit) => unit switch {
MeasurementUnit.Pixel => "px",
MeasurementUnit.Point => "pt",
MeasurementUnit.Millimeter => "mm",
MeasurementUnit.Star => "*",
MeasurementUnit.Percent => "%",
_ => throw new Exception("Unknown unit."),
};
}
} }

View File

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

View File

@@ -1,9 +1,10 @@
using Dashboard.Collections; using Dashboard.Collections;
using Dashboard.Windowing; using Dashboard.Windowing;
using BindingFlags = System.Reflection.BindingFlags;
namespace Dashboard.Pal namespace Dashboard.Pal
{ {
public abstract class AppContext : IContextBase<AppContext, IAppContextExtension> public abstract class Application : IContextBase<Application, IApplicationExtension>
{ {
public abstract string DriverName { get; } public abstract string DriverName { get; }
public abstract string DriverVendor { get; } public abstract string DriverVendor { get; }
@@ -15,12 +16,22 @@ namespace Dashboard.Pal
public bool IsDisposed { get; private set; } = false; public bool IsDisposed { get; private set; } = false;
public IContextDebugger? Debugger { get; set; } public IContextDebugger? Debugger { get; set; }
private readonly TypeDictionary<IAppContextExtension> _extensions = protected CancellationToken? CancellationToken { get; private set; }
new TypeDictionary<IAppContextExtension>(true); protected bool Quit { get; set; } = false;
private readonly TypeDictionary<IAppContextExtension, Func<IAppContextExtension>> _preloadedExtensions =
new TypeDictionary<IAppContextExtension, Func<IAppContextExtension>>(true);
~AppContext() private readonly TypeDictionary<IApplicationExtension> _extensions =
new TypeDictionary<IApplicationExtension>(true);
private readonly TypeDictionary<IApplicationExtension, Func<IApplicationExtension>> _preloadedExtensions =
new TypeDictionary<IApplicationExtension, Func<IApplicationExtension>>(true);
public event EventHandler<DeviceContext>? DeviceContextCreated;
public Application()
{
Current = this;
}
~Application()
{ {
InvokeDispose(false); InvokeDispose(false);
} }
@@ -39,21 +50,29 @@ namespace Dashboard.Pal
{ {
} }
protected internal virtual void OnDeviceContextCreated(DeviceContext dc)
{
DeviceContextCreated?.Invoke(this, dc);
}
public virtual void RunEvents(bool wait) public virtual void RunEvents(bool wait)
{ {
if (!IsInitialized) if (!IsInitialized)
Initialize(); Initialize();
} }
public void Run() => Run(true, CancellationToken.None); public void Run() => Run(true, System.Threading.CancellationToken.None);
public void Run(bool wait) => Run(wait, CancellationToken.None); public void Run(bool wait) => Run(wait, System.Threading.CancellationToken.None);
public void Run(bool waitForEvents, CancellationToken token) public void Run(bool waitForEvents, CancellationToken token)
{ {
CancellationToken = token;
CancellationToken.Value.Register(() => Quit = true);
InitializeInternal(); InitializeInternal();
while (!token.IsCancellationRequested) while (!Quit && !token.IsCancellationRequested)
{ {
RunEvents(waitForEvents); RunEvents(waitForEvents);
} }
@@ -82,19 +101,37 @@ namespace Dashboard.Pal
window.WindowManager = wm; window.WindowManager = wm;
return window; return window;
} }
public IWindow CreateDialogWindow(IWindow? parent = null)
{
if (parent is IVirtualWindow virtualWindow)
{
IWindow? window = virtualWindow.WindowManager?.CreateWindow();
if (window != null)
return window;
}
return CreatePhysicalWindow();
}
#endregion #endregion
public bool IsExtensionAvailable<T>() where T : IAppContextExtension public bool IsExtensionAvailable<T>() where T : IApplicationExtension
{ {
return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>(); return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>();
} }
public bool ExtensionPreload<T>() where T : IAppContextExtension, new() public bool ExtensionPreload<T>(Func<IApplicationExtension> loader) where T : IApplicationExtension
{
return _preloadedExtensions.Add<T>(loader);
}
public bool ExtensionPreload<T>() where T : IApplicationExtension, new()
{ {
return _preloadedExtensions.Add<T>(() => new T()); return _preloadedExtensions.Add<T>(() => new T());
} }
public T ExtensionRequire<T>() where T : IAppContextExtension, new() public T ExtensionRequire<T>() where T : IApplicationExtension
{ {
T? extension = default; T? extension = default;
@@ -106,13 +143,13 @@ namespace Dashboard.Pal
if (_extensions.TryGet(out extension)) if (_extensions.TryGet(out extension))
return extension; return extension;
if (_preloadedExtensions.Remove<T>(out Func<IAppContextExtension>? loader)) if (_preloadedExtensions.Remove<T>(out Func<IApplicationExtension>? loader))
{ {
extension = (T)loader!(); extension = (T)loader!();
} }
else else
{ {
extension = new T(); extension = Activator.CreateInstance<T>();
} }
_extensions.Add(extension); _extensions.Add(extension);
@@ -122,11 +159,22 @@ namespace Dashboard.Pal
return extension; return extension;
} }
public bool ExtensionLoad<T>(T instance) where T : IApplicationExtension
{
if (_extensions.Contains(instance))
return false;
_extensions.Add(instance);
instance.Require(this);
return true;
}
protected virtual void Dispose(bool isDisposing) protected virtual void Dispose(bool isDisposing)
{ {
if (!isDisposing) return; if (!isDisposing) return;
foreach (IAppContextExtension extension in _extensions) Quit = true;
foreach (IApplicationExtension extension in _extensions)
{ {
extension.Dispose(); extension.Dispose();
} }
@@ -143,5 +191,12 @@ namespace Dashboard.Pal
} }
public void Dispose() => InvokeDispose(true); public void Dispose() => InvokeDispose(true);
[ThreadStatic] private static Application _current;
public static Application Current
{
get => _current ?? throw new InvalidOperationException("There is currently no current application.");
set => _current = value;
}
} }
} }

View File

@@ -1,4 +1,6 @@
using Dashboard.Collections; using Dashboard.Collections;
using Dashboard.Windowing;
using BindingFlags = System.Reflection.BindingFlags;
namespace Dashboard.Pal namespace Dashboard.Pal
{ {
@@ -9,6 +11,11 @@ namespace Dashboard.Pal
private readonly TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>> _preloadedExtensions = private readonly TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>> _preloadedExtensions =
new TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>>(true); new TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>>(true);
private readonly Dictionary<string, object> _attributes = new Dictionary<string, object>();
public Application Application { get; }
public IWindow? Window { get; }
public abstract string DriverName { get; } public abstract string DriverName { get; }
public abstract string DriverVendor { get; } public abstract string DriverVendor { get; }
public abstract Version DriverVersion { get; } public abstract Version DriverVersion { get; }
@@ -20,6 +27,13 @@ namespace Dashboard.Pal
/// </summary> /// </summary>
public IContextDebugger? Debugger { get; set; } public IContextDebugger? Debugger { get; set; }
protected DeviceContext(Application app, IWindow? window)
{
Application = app;
Window = window;
app.OnDeviceContextCreated(this);
}
~DeviceContext() ~DeviceContext()
{ {
Dispose(false); Dispose(false);
@@ -36,12 +50,17 @@ namespace Dashboard.Pal
return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>(); return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>();
} }
public bool ExtensionPreload<T>(Func<IDeviceContextExtension> loader) where T : IDeviceContextExtension
{
return _preloadedExtensions.Add<T>(loader);
}
public bool ExtensionPreload<T>() where T : IDeviceContextExtension, new() public bool ExtensionPreload<T>() where T : IDeviceContextExtension, new()
{ {
return _preloadedExtensions.Add<T>(() => new T()); return _preloadedExtensions.Add<T>(() => new T());
} }
public T ExtensionRequire<T>() where T : IDeviceContextExtension, new() public T ExtensionRequire<T>() where T : IDeviceContextExtension
{ {
T? extension = default; T? extension = default;
@@ -59,7 +78,7 @@ namespace Dashboard.Pal
} }
else else
{ {
extension = new T(); extension = Activator.CreateInstance<T>();
} }
_extensions.Add(extension); _extensions.Add(extension);
@@ -69,6 +88,38 @@ namespace Dashboard.Pal
return extension; return extension;
} }
public bool ExtensionLoad<T>(T instance) where T : IDeviceContextExtension
{
if (_extensions.Contains(instance))
return false;
_extensions.Add(instance);
return true;
}
public void SetAttribute(string name, object? v)
{
if (v != null)
_attributes[name] = v;
else
_attributes.Remove(name);
}
public void SetAttribute<T>(string name, T v) => SetAttribute(name, (object?)v);
public object? GetAttribute(string name)
{
return _attributes.GetValueOrDefault(name);
}
public T? GetAttribute<T>(string name)
{
object? o = GetAttribute(name);
if (o != null)
return (T?)o;
return default;
}
/// <summary> /// <summary>
/// Implement your dispose in this function. /// Implement your dispose in this function.
/// </summary> /// </summary>

View File

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

View File

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

View File

@@ -58,12 +58,31 @@ namespace Dashboard.Pal
/// </returns> /// </returns>
bool ExtensionPreload<T>() where T : TExtension, new(); bool ExtensionPreload<T>() where T : TExtension, new();
/// <summary>
/// Preload extensions, to be lazy loaded when required.
/// </summary>
/// <param name="loader">The loader delegate.</param>
/// <typeparam name="T">The extension to preload.</typeparam>
/// <returns>
/// True if the extension was added to the preload set. Otherwise, already loaded or another extension
/// exists which provides this.
/// </returns>
bool ExtensionPreload<T>(Func<TExtension> loader) where T : TExtension;
/// <summary> /// <summary>
/// Require an extension. /// Require an extension.
/// </summary> /// </summary>
/// <typeparam name="T">The extension to require.</typeparam> /// <typeparam name="T">The extension to require.</typeparam>
/// <returns>The extension instance.</returns> /// <returns>The extension instance.</returns>
T ExtensionRequire<T>() where T : TExtension, new(); T ExtensionRequire<T>() where T : TExtension;
/// <summary>
/// Load an extension.
/// </summary>
/// <param name="instance">The extension instance.</param>
/// <typeparam name="T">The extension to require.</typeparam>
/// <returns>True if the extension was loaded, false if there was already one.</returns>
bool ExtensionLoad<T>(T instance) where T : TExtension;
} }
/// <summary> /// <summary>

View File

@@ -11,7 +11,7 @@ namespace Dashboard.Windowing
/// <summary> /// <summary>
/// Interface for classes that implement a window manager. /// Interface for classes that implement a window manager.
/// </summary> /// </summary>
public interface IWindowManager : IEnumerable<IVirtualWindow>, IEventListener, IPaintable, IDisposable public interface IWindowManager : IEnumerable<IVirtualWindow>, IEventListener
{ {
/// <summary> /// <summary>
/// The physical window that this window manager is associated with. /// The physical window that this window manager is associated with.

View File

@@ -1,4 +1,5 @@
using System.Drawing; using System.Drawing;
using System.Numerics;
namespace Dashboard.Windowing namespace Dashboard.Windowing
{ {
@@ -15,6 +16,6 @@ namespace Dashboard.Windowing
/// <summary> /// <summary>
/// The size of the window framebuffer in pixels. /// The size of the window framebuffer in pixels.
/// </summary> /// </summary>
Size FramebufferSize { get; } Vector2 FramebufferSize { get; }
} }
} }

View File

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

View File

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

View File

@@ -1,17 +1,18 @@
using System.Drawing; using System.Drawing;
using Dashboard.Events; using Dashboard.Pal;
namespace Dashboard.Windowing namespace Dashboard.Windowing
{ {
/// <summary> /// <summary>
/// Base class of all Dashboard windows. /// Base class of all Dashboard windows.
/// </summary> /// </summary>
public interface IWindow : public interface IWindow : IDisposable
IPaintable,
IDisposable,
IAnimationTickEvent,
IMouseEvents
{ {
/// <summary>
/// The application for this window.
/// </summary>
Application Application { get; }
/// <summary> /// <summary>
/// Name of the window. /// Name of the window.
/// </summary> /// </summary>
@@ -26,6 +27,23 @@ namespace Dashboard.Windowing
/// The size of the window that excludes the window extents. /// The size of the window that excludes the window extents.
/// </summary> /// </summary>
SizeF ClientSize { get; set; } SizeF ClientSize { get; set; }
IForm? Form { get; set; }
public event EventHandler? EventRaised;
/// <summary>
/// Subscribe to events from this window.
/// </summary>
/// <param name="listener">The event listener instance.</param>
/// <returns>An unsubscription token.</returns>
public void SubcribeEvent(IEventListener listener);
/// <summary>
/// Unsubscribe from events in from this window.
/// </summary>
/// <param name="listener">The event listener to unsubscribe.</param>
public void UnsubscribeEvent(IEventListener listener);
} }
/// <summary> /// <summary>
@@ -49,6 +67,7 @@ namespace Dashboard.Windowing
/// </summary> /// </summary>
public interface IVirtualWindow : IWindow, IEventListener public interface IVirtualWindow : IWindow, IEventListener
{ {
IWindowManager? WindowManager { get; set; }
} }
/// <summary> /// <summary>
@@ -59,7 +78,7 @@ namespace Dashboard.Windowing
/// <summary> /// <summary>
/// The device context for this window. /// The device context for this window.
/// </summary> /// </summary>
IDeviceContext DeviceContext { get; } DeviceContext DeviceContext { get; }
/// <summary> /// <summary>
/// True if the window is double buffered. /// True if the window is double buffered.

View File

@@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" /> <PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
<PackageReference Include="OpenTK.Graphics" Version="[5.0.0-pre.*,5.1)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -24,4 +23,8 @@
<EmbeddedResource Include="Executors\text.frag" /> <EmbeddedResource Include="Executors\text.frag" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Text\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,7 @@
using System.Drawing; using System.Drawing;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using System.Numerics; using System.Numerics;
using Dashboard.OpenGL;
using OTK = OpenTK.Mathematics; using OTK = OpenTK.Mathematics;
namespace Dashboard.Drawing.OpenGL.Executors namespace Dashboard.Drawing.OpenGL.Executors

View File

@@ -1,7 +1,7 @@
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using BlurgText; using BlurgText;
using Dashboard.Drawing.OpenGL.Text; using Dashboard.OpenGL;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics; using OpenTK.Mathematics;
@@ -11,7 +11,7 @@ namespace Dashboard.Drawing.OpenGL.Executors
{ {
public IEnumerable<string> Extensions { get; } = new[] { "DB_Text" }; public IEnumerable<string> Extensions { get; } = new[] { "DB_Text" };
public IContextExecutor Executor { get; private set; } public IContextExecutor Executor { get; private set; }
private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager<BlurgEngine>(); // private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager<BlurgEngine>();
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
private DrawCallRecorder _recorder; private DrawCallRecorder _recorder;
@@ -97,7 +97,7 @@ namespace Dashboard.Drawing.OpenGL.Executors
private void DrawText(ICommandFrame frame) private void DrawText(ICommandFrame frame)
{ {
TextCommandArgs args = frame.GetParameter<TextCommandArgs>(); TextCommandArgs args = frame.GetParameter<TextCommandArgs>();
DbBlurgFont font = Engine.InternFont(args.Font); // DbBlurgFont font = Engine.InternFont(args.Font);
BlurgColor color; BlurgColor color;
switch (args.TextBrush) switch (args.TextBrush)
@@ -116,15 +116,15 @@ namespace Dashboard.Drawing.OpenGL.Executors
break; break;
} }
BlurgResult? result = Engine.Blurg.BuildString(font.Font, font.Size, color, args.Text); //BlurgResult? result = Engine.Blurg.BuildString(font.Font, font.Size, color, args.Text);
if (result == null) // if (result == null)
return; // return;
//
Vector3 position = new Vector3(args.Position.X, args.Position.Y, args.Position.Z); // Vector3 position = new Vector3(args.Position.X, args.Position.Y, args.Position.Z);
ExecuteBlurgResult(result, position); // ExecuteBlurgResult(result, position);
//
result.Dispose(); // result.Dispose();
} }
private void ExecuteBlurgResult(BlurgResult result, Vector3 position) private void ExecuteBlurgResult(BlurgResult result, Vector3 position)

View File

@@ -1,4 +1,4 @@
using Dashboard.Drawing.OpenGL.Text; // using Dashboard.Drawing.OpenGL.Text;
using Dashboard.OpenGL; using Dashboard.OpenGL;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
@@ -21,7 +21,7 @@ namespace Dashboard.Drawing.OpenGL
if (bindingsContext != null) if (bindingsContext != null)
GLLoader.LoadBindings(bindingsContext); GLLoader.LoadBindings(bindingsContext);
Typesetter.Backend = BlurgEngine.Global; // Typesetter.Backend = BlurgEngine.Global;
} }
public ContextExecutor GetExecutor(IGLContext glContext) public ContextExecutor GetExecutor(IGLContext glContext)

View File

@@ -1,193 +0,0 @@
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using BlurgText;
using Dashboard.OpenGL;
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.OpenOrCreate, FileAccess.Write, FileShare.None);
}
catch (IOException ex)
{
if (i < 3)
continue;
else
throw new Exception("Could not open a temporary file for writing the font.", ex);
}
break;
}
stream.CopyTo(dest);
dest.Dispose();
DbBlurgFont font = (DbBlurgFont)LoadFont(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) { Path = path };
}
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)
{
if (dblurg.Path == null)
return (DbBlurgFont)LoadFont(new NamedFont(dblurg.Family, dblurg.Size, dblurg.Weight,
dblurg.Slant,
dblurg.Stretch));
else
return (DbBlurgFont)LoadFont(dblurg.Path);
}
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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -1,30 +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;
internal string? Path { get; init; }
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);
}
}
}

View File

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

View File

@@ -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])
{
}
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Dashboard.Drawing
{ {
public TextCommand TextCommand { get; } public TextCommand TextCommand { get; }
private TextExtension() : base("DB_Text", new [] { FontExtension.Instance, BrushExtension.Instance }) private TextExtension() : base("DB_Text", new [] { BrushExtension.Instance })
{ {
TextCommand = new TextCommand(this); TextCommand = new TextCommand(this);
} }
@@ -40,7 +40,7 @@ namespace Dashboard.Drawing
header = new Header() header = new Header()
{ {
Font = queue.RequireResource(obj.Font), // Font = queue.RequireResource(obj.Font),
TextBrush = queue.RequireResource(obj.TextBrush), TextBrush = queue.RequireResource(obj.TextBrush),
BorderBrush = (obj.BorderBrush != null) ? queue.RequireResource(obj.BorderBrush) : -1, BorderBrush = (obj.BorderBrush != null) ? queue.RequireResource(obj.BorderBrush) : -1,
BorderRadius = (obj.BorderBrush != null) ? obj.BorderRadius : 0f, BorderRadius = (obj.BorderBrush != null) ? obj.BorderRadius : 0f,

View File

@@ -20,7 +20,7 @@ namespace Dashboard.Drawing
IFont LoadFont(Stream stream); IFont LoadFont(Stream stream);
IFont LoadFont(string path); IFont LoadFont(string path);
IFont LoadFont(NamedFont font); // IFont LoadFont(NamedFont font);
} }
/// <summary> /// <summary>
@@ -55,15 +55,16 @@ namespace Dashboard.Drawing
return Backend.LoadFont(file.FullName); return Backend.LoadFont(file.FullName);
} }
public static IFont LoadFont(NamedFont font) // public static IFont LoadFont(NamedFont font)
{ // {
return Backend.LoadFont(font); // return Backend.LoadFont(font);
} // }
public static IFont LoadFont(string family, float size, FontWeight weight = FontWeight.Normal, public static IFont LoadFont(string family, float size, FontWeight weight = FontWeight.Normal,
FontSlant slant = FontSlant.Normal, FontStretch stretch = FontStretch.Normal) FontSlant slant = FontSlant.Normal, FontStretch stretch = FontStretch.Normal)
{ {
return LoadFont(new NamedFont(family, size, weight, slant, stretch)); // return LoadFont(new NamedFont(family, size, weight, slant, stretch));
throw new Exception();
} }
private class UndefinedTypeSetter : ITypeSetter private class UndefinedTypeSetter : ITypeSetter
@@ -94,11 +95,11 @@ namespace Dashboard.Drawing
return default; return default;
} }
public IFont LoadFont(NamedFont font) // public IFont LoadFont(NamedFont font)
{ // {
Except(); // Except();
return default; // return default;
} // }
} }
} }
} }

View File

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

View File

@@ -1,7 +1,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL namespace Dashboard.OpenGL
{ {
public class ContextCollector : IDisposable public class ContextCollector : IDisposable
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,14 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using Dashboard.OpenGL; using Dashboard.Drawing;
using Dashboard.OpenGL.Drawing;
using Dashboard.Pal; using Dashboard.Pal;
using Dashboard.Windowing;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL.Pal namespace Dashboard.OpenGL
{ {
internal class GLContextBindingsContext(IGLContext context) : IBindingsContext internal class GLContextBindingsContext(IGLContext context) : IBindingsContext
{ {
@@ -20,6 +22,8 @@ namespace Dashboard.Drawing.OpenGL.Pal
{ {
public IGLContext GLContext { get; } public IGLContext GLContext { get; }
public ContextCollector Collector { get; } = new ContextCollector();
public override string DriverName => "Dashboard OpenGL Device Context"; public override string DriverName => "Dashboard OpenGL Device Context";
public override string DriverVendor => "Dashboard"; public override string DriverVendor => "Dashboard";
public override Version DriverVersion => new Version(0, 1, 0); public override Version DriverVersion => new Version(0, 1, 0);
@@ -35,7 +39,7 @@ namespace Dashboard.Drawing.OpenGL.Pal
private readonly ConcurrentQueue<Task> _beforeDrawActions = new ConcurrentQueue<Task>(); private readonly ConcurrentQueue<Task> _beforeDrawActions = new ConcurrentQueue<Task>();
private readonly ConcurrentQueue<Task> _afterDrawActions = new ConcurrentQueue<Task>(); private readonly ConcurrentQueue<Task> _afterDrawActions = new ConcurrentQueue<Task>();
public GLDeviceContext(IGLContext context) public GLDeviceContext(Application app, IWindow? window, IGLContext context) : base(app, window)
{ {
GLContext = context; GLContext = context;
context.MakeCurrent(); context.MakeCurrent();
@@ -61,7 +65,9 @@ namespace Dashboard.Drawing.OpenGL.Pal
Extensions = extensions.ToImmutableHashSet(); Extensions = extensions.ToImmutableHashSet();
ExtensionPreload<DeviceContextBase>();
ExtensionPreload<GLTextureExtension>(); ExtensionPreload<GLTextureExtension>();
ExtensionPreload<ImmediateMode>();
} }
public bool IsGLExtensionAvailable(string name) public bool IsGLExtensionAvailable(string name)
@@ -130,6 +136,9 @@ namespace Dashboard.Drawing.OpenGL.Pal
base.Begin(); base.Begin();
GLContext.MakeCurrent(); GLContext.MakeCurrent();
IDeviceContextBase dc = ExtensionRequire<IDeviceContextBase>();
dc.ResetClip();
dc.ResetTransforms();
while (_beforeDrawActions.TryDequeue(out Task? action)) while (_beforeDrawActions.TryDequeue(out Task? action))
{ {

View File

@@ -1,9 +1,10 @@
using System.Drawing; using System.Drawing;
using Dashboard.Drawing;
using Dashboard.Pal; using Dashboard.Pal;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using OGL = OpenTK.Graphics.OpenGL; using OGL = OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL.Pal namespace Dashboard.OpenGL
{ {
public class GLTextureExtension : ITextureExtension, IContextExtensionBase<GLDeviceContext> public class GLTextureExtension : ITextureExtension, IContextExtensionBase<GLDeviceContext>
{ {
@@ -21,7 +22,6 @@ namespace Dashboard.Drawing.OpenGL.Pal
public void Dispose() public void Dispose()
{ {
throw new NotImplementedException();
} }
@@ -90,6 +90,11 @@ namespace Dashboard.Drawing.OpenGL.Pal
private GLTextureExtension Extension { get; } = extension; private GLTextureExtension Extension { get; } = extension;
private GLDeviceContext Context => Extension.Context; private GLDeviceContext Context => Extension.Context;
~GLTexture()
{
Dispose(false);
}
public void SetStorage(PixelFormat format, int width, int height, int depth, int levels) public void SetStorage(PixelFormat format, int width, int height, int depth, int levels)
{ {
if (!Context.IsRenderThread) if (!Context.IsRenderThread)
@@ -98,6 +103,11 @@ namespace Dashboard.Drawing.OpenGL.Pal
return; return;
} }
if (levels == 0)
{
levels = Math.Max(Math.ILogB(width), Math.ILogB(height));
}
Bind(); Bind();
SizedInternalFormat glFormat = GetFormat(format); SizedInternalFormat glFormat = GetFormat(format);
if (Extension.SupportsArbTextureStorage) if (Extension.SupportsArbTextureStorage)
@@ -122,18 +132,23 @@ namespace Dashboard.Drawing.OpenGL.Pal
switch (Type) switch (Type)
{ {
case TextureType.Texture1D: case TextureType.Texture1D:
GL.TexImage1D(Target, 0, (InternalFormat)glFormat, width, 0, (OGL.PixelFormat)glFormat, PixelType.Byte, IntPtr.Zero); GL.TexImage1D(Target, 0, (InternalFormat)glFormat, width, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
break; break;
case TextureType.Texture2D: case TextureType.Texture2D:
GL.TexImage2D(Target, 0, (InternalFormat)glFormat, width, height, 0, (OGL.PixelFormat)glFormat, PixelType.Byte, IntPtr.Zero); GL.TexImage2D(Target, 0, (InternalFormat)glFormat, width, height, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
break; break;
case TextureType.Texture3D: case TextureType.Texture3D:
case TextureType.Texture2DArray: case TextureType.Texture2DArray:
case TextureType.Texture2DCube: case TextureType.Texture2DCube:
GL.TexImage3D(Target, 0, (InternalFormat)glFormat, width, height, depth, 0, (OGL.PixelFormat)glFormat, PixelType.Byte, IntPtr.Zero); GL.TexImage3D(Target, 0, (InternalFormat)glFormat, width, height, depth, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
break; break;
} }
} }
Width = width;
Height = height;
Depth = depth;
Levels = levels;
} }
public void Read<T>(Span<T> buffer, int level = 0, int align = 0) where T : unmanaged public void Read<T>(Span<T> buffer, int level = 0, int align = 0) where T : unmanaged
@@ -141,11 +156,19 @@ namespace Dashboard.Drawing.OpenGL.Pal
throw new NotImplementedException(); throw new NotImplementedException();
} }
public unsafe void Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level = 0, int align = 4) where T : unmanaged void ITexture.Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level, int align) => Write(format, buffer, level, align, null);
public unsafe void Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level = 0, int align = 4, TimeSpan? timeout = null) where T : unmanaged
{ {
if (!Context.IsRenderThread) if (!Context.IsRenderThread)
{ {
throw new NotImplementedException(); T[] bufferArray = buffer.ToArray();
Task task = Context.InvokeBeforeDraw(() => Write<T>(format, bufferArray, level, align));
if (timeout.HasValue)
task.Wait(timeout.Value);
else
task.Wait();
} }
Bind(); Bind();
@@ -160,7 +183,7 @@ namespace Dashboard.Drawing.OpenGL.Pal
PixelType glType = format switch PixelType glType = format switch
{ {
PixelFormat.R8I or PixelFormat.Rg8I or PixelFormat.Rgb8I or PixelFormat.Rgba8I => PixelType.Byte, PixelFormat.R8I or PixelFormat.Rg8I or PixelFormat.Rgb8I or PixelFormat.Rgba8I => PixelType.UnsignedByte,
PixelFormat.R16F or PixelFormat.Rg16F or PixelFormat.Rgb16F or PixelFormat.Rgba16F => PixelType.HalfFloat, PixelFormat.R16F or PixelFormat.Rg16F or PixelFormat.Rgb16F or PixelFormat.Rgba16F => PixelType.HalfFloat,
_ => throw new NotSupportedException() _ => throw new NotSupportedException()
}; };
@@ -207,10 +230,36 @@ namespace Dashboard.Drawing.OpenGL.Pal
GL.GenerateMipmap(Target); GL.GenerateMipmap(Target);
} }
public void Dispose() private void Dispose(bool disposing)
{ {
throw new NotImplementedException(); if (IsDisposed)
return;
IsDisposed = true;
if (disposing)
{
if (Thread.CurrentThread != Context.RendererThread)
{
Context.Collector.DeleteTexture(Handle);
} }
else
{
GL.DeleteTexture(Handle);
}
Handle = 0;
GC.SuppressFinalize(this);
}
else
{
Context.Collector.DeleteTexture(Handle);
}
}
public bool IsDisposed { get; private set; }
public void Dispose() => Dispose(false);
private void Bind() private void Bind()
{ {

View File

@@ -1,4 +1,4 @@
using System.Drawing; using System.Numerics;
using Dashboard.Windowing; using Dashboard.Windowing;
namespace Dashboard.OpenGL namespace Dashboard.OpenGL
@@ -17,7 +17,7 @@ namespace Dashboard.OpenGL
/// <summary> /// <summary>
/// The size of the framebuffer in pixels. /// The size of the framebuffer in pixels.
/// </summary> /// </summary>
public Size FramebufferSize { get; } public Vector2 FramebufferSize { get; }
/// <summary> /// <summary>
/// Called when the context is disposed. /// Called when the context is disposed.

View File

@@ -1,6 +1,6 @@
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
namespace Dashboard.Drawing.OpenGL namespace Dashboard.OpenGL
{ {
public static class ShaderUtil public static class ShaderUtil
{ {

View File

@@ -1,18 +0,0 @@
using Dashboard.Events;
using OpenTK.Platform;
namespace Dashboard.OpenTK.PAL2
{
public static class EventConverter
{
public static UiEventArgs? Convert(PlatformEventType type, EventArgs ea)
{
switch (type)
{
case PlatformEventType.KeyDown:
default:
return null;
}
}
}
}

View File

@@ -1,7 +0,0 @@
namespace Dashboard.OpenTK.PAL2
{
public static class OpenTKEventExtensions
{
// public static EventArgs ToDashboardEvent(this EventArgs)
}
}

View File

@@ -1,56 +0,0 @@
using Dashboard.Windowing;
using OpenTK.Graphics;
using OpenTK.Platform;
using AppContext = Dashboard.Pal.AppContext;
using TK = OpenTK.Platform.Toolkit;
namespace Dashboard.OpenTK.PAL2
{
public class Pal2AppContext : AppContext
{
public override string DriverName => "Dashboard OpenTK PAL2.0 Driver";
public override string DriverVendor => "Dashboard";
public override Version DriverVersion => new Version(0, 1);
public GraphicsApiHints GraphicsApiHints { get; set; } = new OpenGLGraphicsApiHints();
public bool OpenGLBindingsInitialized { get; private set; } = false;
private readonly List<PhysicalWindow> _windows = new List<PhysicalWindow>();
public override IPhysicalWindow CreatePhysicalWindow()
{
PhysicalWindow window = new PhysicalWindow(GraphicsApiHints);
_windows.Add(window);
if (!OpenGLBindingsInitialized)
{
OpenGLBindingsInitialized = true;
GLLoader.LoadBindings(
new Pal2BindingsContext(TK.OpenGL,
((OpenGLDeviceContext)window.DeviceContext).ContextHandle));
}
return window;
}
public override IWindow CreateWindow()
{
return CreatePhysicalWindow();
}
public override void RunEvents(bool wait)
{
TK.Window.ProcessEvents(wait);
for (int i = 0; i < _windows.Count; i++)
{
if (_windows[i].IsDisposed)
{
_windows.RemoveAt(i);
continue;
}
_windows[i].Paint();
}
}
}
}

View File

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

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Drawing; using System.Drawing;
using Dashboard.OpenGL; using Dashboard.OpenGL;
using Dashboard.Pal;
using Dashboard.Windowing; using Dashboard.Windowing;
using OpenTK.Mathematics; using OpenTK.Mathematics;
using OpenTK.Platform; using OpenTK.Platform;
@@ -8,31 +9,32 @@ using TK = OpenTK.Platform.Toolkit;
namespace Dashboard.OpenTK.PAL2 namespace Dashboard.OpenTK.PAL2
{ {
public class OpenGLDeviceContext : IGLContext, IGLDisposable public class Pal2GLContext : IGLContext, IDisposable
{ {
public OpenGLContextHandle ContextHandle { get; } public OpenGLContextHandle ContextHandle { get; }
public WindowHandle WindowHandle { get; } public WindowHandle WindowHandle { get; }
public ISwapGroup SwapGroup { get; } public ISwapGroup SwapGroup { get; }
public int ContextGroup { get; } public int ContextGroup { get; }
public Size FramebufferSize public System.Numerics.Vector2 FramebufferSize
{ {
get get
{ {
TK.Window.GetFramebufferSize(WindowHandle, out Vector2i size); TK.Window.GetFramebufferSize(WindowHandle, out Vector2i size);
return new Size(size.X, size.Y); return new System.Numerics.Vector2(size.X, size.Y);
} }
} }
public event Action? Disposed; public event Action? Disposed;
public OpenGLDeviceContext(WindowHandle window, OpenGLContextHandle context, ISwapGroup? group = null) public Pal2GLContext(WindowHandle window, OpenGLContextHandle context)
{ {
WindowHandle = window; WindowHandle = window;
ContextHandle = context; ContextHandle = context;
SwapGroup = group ?? new DummySwapGroup(context); SwapGroup = new DummySwapGroup(context);
ContextGroup = GetContextGroup(context); ContextGroup = GetContextGroup(ContextHandle);
} }
public void MakeCurrent() public void MakeCurrent()
@@ -45,18 +47,13 @@ namespace Dashboard.OpenTK.PAL2
return TK.OpenGL.GetProcedureAddress(ContextHandle, procName); return TK.OpenGL.GetProcedureAddress(ContextHandle, procName);
} }
private bool _isDisposed = false;
public void Dispose() => Dispose(true); public void Dispose() => Dispose(true);
public void Dispose(bool safeExit) protected void Dispose(bool isDisposing)
{ {
if (_isDisposed) return;
_isDisposed = true;
if (SwapGroup is IGLDisposable glDisposable) if (SwapGroup is IGLDisposable glDisposable)
{ {
glDisposable.Dispose(safeExit); glDisposable.Dispose(isDisposing);
} }
else if (SwapGroup is IDisposable disposable) else if (SwapGroup is IDisposable disposable)
{ {
@@ -68,6 +65,7 @@ namespace Dashboard.OpenTK.PAL2
private static int _contextGroupId = 0; private static int _contextGroupId = 0;
private static ConcurrentDictionary<OpenGLContextHandle, int> _contextGroupRootContexts = new ConcurrentDictionary<OpenGLContextHandle, int>(); private static ConcurrentDictionary<OpenGLContextHandle, int> _contextGroupRootContexts = new ConcurrentDictionary<OpenGLContextHandle, int>();
private Size _framebufferSize;
private static int GetContextGroup(OpenGLContextHandle handle) private static int GetContextGroup(OpenGLContextHandle handle)
{ {

View File

@@ -1,7 +1,10 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Drawing; using System.Drawing;
using Dashboard.Drawing; using System.Net;
using System.Runtime.CompilerServices;
using Dashboard.Events; using Dashboard.Events;
using Dashboard.OpenGL;
using Dashboard.Pal;
using Dashboard.Windowing; using Dashboard.Windowing;
using OpenTK.Mathematics; using OpenTK.Mathematics;
using OpenTK.Platform; using OpenTK.Platform;
@@ -10,12 +13,15 @@ using TK = OpenTK.Platform.Toolkit;
namespace Dashboard.OpenTK.PAL2 namespace Dashboard.OpenTK.PAL2
{ {
public class PhysicalWindow : IPhysicalWindow, IDrawQueuePaintable, IEventListener, IDpiAwareWindow public class PhysicalWindow : IPhysicalWindow, IEventListener, IDpiAwareWindow
{ {
public DrawQueue DrawQueue { get; } = new DrawQueue(); private readonly List<IEventListener> _listeners = new List<IEventListener>();
public Application Application { get; }
public WindowHandle WindowHandle { get; } public WindowHandle WindowHandle { get; }
public IDeviceContext DeviceContext { get; } public DeviceContext DeviceContext { get; }
public bool DoubleBuffered => true; // Always true for OpenTK windows. public bool DoubleBuffered => true; // Always true for OpenTK windows.
public IForm? Form { get; set; } = null;
public IWindowManager? WindowManager { get; set; } public IWindowManager? WindowManager { get; set; }
@@ -45,42 +51,37 @@ namespace Dashboard.OpenTK.PAL2
set => TK.Window.SetClientSize(WindowHandle, new Vector2i((int)value.Width, (int)value.Height)); set => TK.Window.SetClientSize(WindowHandle, new Vector2i((int)value.Width, (int)value.Height));
} }
public event EventHandler? Painting; public event EventHandler? EventRaised;
public event EventHandler<AnimationTickEventArgs>? AnimationTimerEvent;
public event EventHandler<MouseMoveEventArgs>? MouseMoved;
public event EventHandler<MouseButtonEventArgs>? MouseButtonDown;
public event EventHandler<MouseButtonEventArgs>? MouseButtonUp;
public event EventHandler<MouseScrollEventArgs>? MouseScroll;
public PhysicalWindow(WindowHandle window, IDeviceContext dc) public PhysicalWindow(Application app, WindowHandle window)
{ {
Application = app;
WindowHandle = window; WindowHandle = window;
DeviceContext = dc; DeviceContext = CreateDeviceContext(app, this, new OpenGLGraphicsApiHints());
AddWindow(this);
} }
public PhysicalWindow(WindowHandle window, OpenGLContextHandle context, ISwapGroup? swapGroup = null) public PhysicalWindow(Application app, WindowHandle window, OpenGLContextHandle context)
{ {
Application = app;
WindowHandle = window; WindowHandle = window;
DeviceContext = new OpenGLDeviceContext(window, context, swapGroup); DeviceContext = new GLDeviceContext(app, this, new Pal2GLContext(window, context));
AddWindow(this);
} }
public PhysicalWindow(GraphicsApiHints hints) public PhysicalWindow(Application app, GraphicsApiHints hints)
{ {
Application = app;
WindowHandle = TK.Window.Create(hints); WindowHandle = TK.Window.Create(hints);
DeviceContext = CreateDeviceContext(WindowHandle, hints); DeviceContext = CreateDeviceContext(app, this, hints);
AddWindow(this);
} }
private static IDeviceContext CreateDeviceContext(WindowHandle window, GraphicsApiHints hints) private static DeviceContext CreateDeviceContext(Application app, PhysicalWindow window, GraphicsApiHints hints)
{ {
WindowHandle handle = window.WindowHandle;
switch (hints.Api) switch (hints.Api)
{ {
case GraphicsApi.OpenGL: case GraphicsApi.OpenGL:
case GraphicsApi.OpenGLES: case GraphicsApi.OpenGLES:
OpenGLContextHandle context = TK.OpenGL.CreateFromWindow(window); return new GLDeviceContext(app, window, new Pal2GLContext(handle, TK.OpenGL.CreateFromWindow(handle)));
return new OpenGLDeviceContext(window, context);
default: default:
throw new Exception($"Unknown graphics API {hints.Api}."); throw new Exception($"Unknown graphics API {hints.Api}.");
} }
@@ -92,72 +93,46 @@ namespace Dashboard.OpenTK.PAL2
if (IsDisposed) return; if (IsDisposed) return;
IsDisposed = true; IsDisposed = true;
RemoveWindow(this);
(DeviceContext as IDisposable)?.Dispose(); (DeviceContext as IDisposable)?.Dispose();
((Pal2Application)Application).RemoveWindow(this);
TK.Window.Destroy(WindowHandle); TK.Window.Destroy(WindowHandle);
} }
protected virtual void OnPaint() public virtual void SendEvent(object? sender, EventArgs args)
{ {
WindowManager?.Paint(); args = TransformEvent(sender, args);
Painting?.Invoke(this, EventArgs.Empty);
Form?.SendEvent(this, args);
EventRaised?.Invoke(this, args);
lock (_listeners)
{
foreach (IEventListener listener in _listeners)
listener.SendEvent(this, args);
}
} }
public void Paint() private EventArgs TransformEvent(object? sender, EventArgs args)
{ {
DrawQueue.Clear(); // TODO: future
OnPaint(); return args;
} }
protected virtual void OnAnimationTimerEvent(AnimationTickEventArgs ea) => public void SubcribeEvent(IEventListener listener)
AnimationTimerEvent?.Invoke(this, ea);
protected virtual void OnMouseMoved(MouseMoveEventArgs ea) => MouseMoved?.Invoke(this, ea);
protected virtual void OnMouseButtonDown(MouseButtonEventArgs ea) => MouseButtonDown?.Invoke(this, ea);
protected virtual void OnMouseButtonUp(MouseButtonEventArgs ea) => MouseButtonUp?.Invoke(this, ea);
protected virtual void OnMouseScroll(MouseScrollEventArgs ea) => MouseScroll?.Invoke(this, ea);
public void SendEvent(EventArgs args)
{ {
lock (_listeners)
{
_listeners.Add(listener);
}
} }
private static readonly ConcurrentDictionary<WindowHandle, PhysicalWindow> _windows = public void UnsubscribeEvent(IEventListener listener)
new ConcurrentDictionary<WindowHandle, PhysicalWindow>();
static PhysicalWindow()
{ {
EventQueue.EventRaised += EventQueueOnEventRaised; lock (_listeners)
}
private static void AddWindow(PhysicalWindow window)
{ {
_windows.TryAdd(window.WindowHandle, window); _listeners.Remove(listener);
}
private static void RemoveWindow(PhysicalWindow window)
{
_windows.TryRemove(window.WindowHandle, out _);
}
private static void EventQueueOnEventRaised(PalHandle? handle, PlatformEventType type, EventArgs args)
{
if (handle is not WindowHandle windowHandle)
return;
if (!_windows.TryGetValue(windowHandle, out PhysicalWindow? window))
return;
switch (type)
{
case PlatformEventType.MouseMove:
case PlatformEventType.Scroll:
case PlatformEventType.MouseUp:
case PlatformEventType.MouseDown:
break;
} }
} }

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReFuel.StbImage" Version="2.1.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
using Dashboard.Drawing;
using Dashboard.Pal;
using ReFuel.Stb;
namespace Dashboard.StbImage
{
public class StbImageLoader : IImageLoader
{
public string DriverName { get; } = "Dashboard Stb Image Loader";
public string DriverVendor { get; } = "Dashboard";
public Version DriverVersion { get; } = new Version(1, 0);
public void Dispose()
{
}
IContextBase IContextExtensionBase.Context => Context;
public void Require(Application context)
{
Context = context;
}
public ImageData LoadImageData(Stream stream)
{
using ReFuel.Stb.StbImage image = ReFuel.Stb.StbImage.Load(stream, StbiImageFormat.Rgba);
ReadOnlySpan<byte> data = image.AsSpan<byte>();
return new ImageData(TextureType.Texture2D, image.Format switch
{
StbiImageFormat.GreyAlpha => PixelFormat.Rg8I,
StbiImageFormat.Rgb => PixelFormat.Rgb8I,
StbiImageFormat.Rgba => PixelFormat.Rgba8I,
_ => PixelFormat.R8I,
},
image.Width,
image.Height,
data.ToArray());
}
public Application Context { get; private set; } = null!;
void IContextExtensionBase.Require(IContextBase context)
{
Require((Application)context);
}
}
}

View File

@@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard", "Dashboard\Dashboard.csproj", "{49A62F46-AC1C-4240-8615-020D4FBBF964}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard", "Dashboard\Dashboard.csproj", "{49A62F46-AC1C-4240-8615-020D4FBBF964}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.Drawing", "Dashboard.Drawing\Dashboard.Drawing.csproj", "{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.TestApplication", "tests\Dashboard.TestApplication\Dashboard.TestApplication.csproj", "{7C90B90B-DF31-439B-9080-CD805383B014}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.TestApplication", "tests\Dashboard.TestApplication\Dashboard.TestApplication.csproj", "{7C90B90B-DF31-439B-9080-CD805383B014}"
@@ -17,16 +15,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.TestApplication",
EndProject 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.Common", "Dashboard.Common\Dashboard.Common.csproj", "{C77CDD2B-2482-45F9-B330-47A52F5F13C0}"
EndProject 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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Frameworks", "Frameworks", "{9B62A92D-ABF5-4704-B831-FD075515A82F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Frameworks", "Frameworks", "{9B62A92D-ABF5-4704-B831-FD075515A82F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.OpenTK", "Dashboard.OpenTK\Dashboard.OpenTK.csproj", "{7B064228-2629-486E-95C6-BDDD4B4602C4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.OpenTK", "Dashboard.OpenTK\Dashboard.OpenTK.csproj", "{7B064228-2629-486E-95C6-BDDD4B4602C4}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.OpenGL", "Dashboard.OpenGL\Dashboard.OpenGL.csproj", "{33EB657C-B53A-41B4-BC3C-F38C09ABA577}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.OpenGL", "Dashboard.OpenGL\Dashboard.OpenGL.csproj", "{33EB657C-B53A-41B4-BC3C-F38C09ABA577}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.StbImage", "Dashboard.StbImage\Dashboard.StbImage.csproj", "{85BCEB9E-DEC2-4A53-B2DA-6BFC6F3EE4E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.BlurgText.OpenGL", "Dashboard.BlurgText.OpenGL\Dashboard.BlurgText.OpenGL.csproj", "{14616F42-663B-4673-8561-5637FAD1B22F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.BlurgText", "Dashboard.BlurgText\Dashboard.BlurgText.csproj", "{8C68EFB6-B477-48EC-9AAA-31E89883482B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -37,10 +37,6 @@ Global
{49A62F46-AC1C-4240-8615-020D4FBBF964}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{49A62F46-AC1C-4240-8615-020D4FBBF964}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{7C90B90B-DF31-439B-9080-CD805383B014}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
@@ -49,14 +45,6 @@ Global
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Release|Any CPU.Build.0 = 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
{7B064228-2629-486E-95C6-BDDD4B4602C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B064228-2629-486E-95C6-BDDD4B4602C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B064228-2629-486E-95C6-BDDD4B4602C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B064228-2629-486E-95C6-BDDD4B4602C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B064228-2629-486E-95C6-BDDD4B4602C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B064228-2629-486E-95C6-BDDD4B4602C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -65,6 +53,18 @@ Global
{33EB657C-B53A-41B4-BC3C-F38C09ABA577}.Debug|Any CPU.Build.0 = Debug|Any CPU {33EB657C-B53A-41B4-BC3C-F38C09ABA577}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33EB657C-B53A-41B4-BC3C-F38C09ABA577}.Release|Any CPU.ActiveCfg = Release|Any CPU {33EB657C-B53A-41B4-BC3C-F38C09ABA577}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33EB657C-B53A-41B4-BC3C-F38C09ABA577}.Release|Any CPU.Build.0 = Release|Any CPU {33EB657C-B53A-41B4-BC3C-F38C09ABA577}.Release|Any CPU.Build.0 = Release|Any CPU
{85BCEB9E-DEC2-4A53-B2DA-6BFC6F3EE4E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85BCEB9E-DEC2-4A53-B2DA-6BFC6F3EE4E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85BCEB9E-DEC2-4A53-B2DA-6BFC6F3EE4E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85BCEB9E-DEC2-4A53-B2DA-6BFC6F3EE4E7}.Release|Any CPU.Build.0 = Release|Any CPU
{14616F42-663B-4673-8561-5637FAD1B22F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14616F42-663B-4673-8561-5637FAD1B22F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14616F42-663B-4673-8561-5637FAD1B22F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14616F42-663B-4673-8561-5637FAD1B22F}.Release|Any CPU.Build.0 = Release|Any CPU
{8C68EFB6-B477-48EC-9AAA-31E89883482B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C68EFB6-B477-48EC-9AAA-31E89883482B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C68EFB6-B477-48EC-9AAA-31E89883482B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C68EFB6-B477-48EC-9AAA-31E89883482B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -72,5 +72,8 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{7C90B90B-DF31-439B-9080-CD805383B014} = {9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40} {7C90B90B-DF31-439B-9080-CD805383B014} = {9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}
{7B064228-2629-486E-95C6-BDDD4B4602C4} = {9B62A92D-ABF5-4704-B831-FD075515A82F} {7B064228-2629-486E-95C6-BDDD4B4602C4} = {9B62A92D-ABF5-4704-B831-FD075515A82F}
{85BCEB9E-DEC2-4A53-B2DA-6BFC6F3EE4E7} = {9B62A92D-ABF5-4704-B831-FD075515A82F}
{14616F42-663B-4673-8561-5637FAD1B22F} = {9B62A92D-ABF5-4704-B831-FD075515A82F}
{8C68EFB6-B477-48EC-9AAA-31E89883482B} = {9B62A92D-ABF5-4704-B831-FD075515A82F}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -0,0 +1,69 @@
using System;
using System.Drawing;
using System.Numerics;
using Dashboard.Drawing;
using Dashboard.Layout;
using Dashboard.Pal;
namespace Dashboard.Controls
{
public class Button : Control
{
private Vector2 _intrinsicSize = Vector2.Zero;
public bool AutoSize { get; set; } = true;
public string Text { get; set; } = "Click!";
public Font Font { get; set; } = Font.Create(new FontInfo("Rec Mono Linear"));
public float TextSize { get; set; } = 12f;
public Brush TextBrush { get; set; } = new SolidColorBrush(Color.Black);
public Brush ButtonBrush { get; set; } = new SolidColorBrush(Color.DarkSlateGray);
public Vector2 Padding { get; set; } = new Vector2(4, 4);
public event EventHandler? Clicked;
public override Vector2 CalculateIntrinsicSize()
{
return _intrinsicSize + 2 * Padding;
}
protected void CalculateSize(DeviceContext dc)
{
Box2d box = dc.ExtensionRequire<ITextRenderer>().MeasureText(Font.Base, TextSize, Text);
_intrinsicSize = box.Size;
// Layout.Size = box.Size;
// ClientArea = new Box2d(ClientArea.Min, ClientArea.Min + Layout.Size);
}
public override void OnPaint(DeviceContext dc)
{
base.OnPaint(dc);
if (AutoSize)
CalculateSize(dc);
bool hidden = Layout.OverflowMode == OverflowMode.Hidden;
var dcb = dc.ExtensionRequire<IDeviceContextBase>();
if (hidden)
dcb.PushScissor(ClientArea);
dcb.PushTransforms(Matrix4x4.CreateTranslation(ClientArea.Left, ClientArea.Top, 0));
var imm = dc.ExtensionRequire<IImmediateMode>();
Color color = (ButtonBrush as SolidColorBrush)?.Color ?? Color.Black;
Vector4 colorVector = new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
imm.Rectangle(ClientArea, 0, colorVector);
var text = dc.ExtensionRequire<ITextRenderer>();
color = (TextBrush as SolidColorBrush)?.Color ?? Color.Black;
colorVector = new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
text.DrawText(Vector2.Zero, colorVector, TextSize, Font.Base, Text);
if (hidden)
dcb.PopScissor();
dcb.PopTransforms();
}
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Dashboard.Controls
{
public enum ClassChangeType
{
Added,
Removed,
}
public record struct ClassChanged(object? Owner, string ClassName, ClassChangeType Type);
public class ClassSet : ICollection<string>
{
public int Count => _classes.Count;
public bool IsReadOnly => false;
public object? Owner { get; }
public event EventHandler<ClassChanged>? ClassChanged;
private readonly HashSet<string> _classes = new HashSet<string>();
public ClassSet(object? owner)
{
Owner = owner;
}
public IEnumerator<string> GetEnumerator() => _classes.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(string item)
{
if (_classes.Add(item))
{
OnClassAdded(item);
}
}
public void Clear()
{
foreach (string @class in _classes)
OnClassRemoved(@class);
_classes.Clear();
}
public bool Contains(string item) => _classes.Contains(item);
public void CopyTo(string[] array, int arrayIndex) => _classes.CopyTo(array, arrayIndex);
public bool Remove(string item)
{
if (_classes.Remove(item))
{
OnClassRemoved(item);
return true;
}
return false;
}
private void OnClassAdded(string @class)
{
ClassChanged?.Invoke(this, new ClassChanged(Owner, @class, ClassChangeType.Added));
}
private void OnClassRemoved(string @class)
{
ClassChanged?.Invoke(this, new ClassChanged(Owner, @class, ClassChangeType.Removed));
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Dashboard.Drawing;
using Dashboard.Events;
using Dashboard.Layout;
using Dashboard.Pal;
namespace Dashboard.Controls
{
public class Container : Control, IList<Control>, ILayoutContainer
{
private readonly List<Control> _controls = new List<Control>();
public int Count => _controls.Count;
public bool IsReadOnly => false;
public ContainerLayoutInfo ContainerLayout { get; } = new ContainerLayoutInfo();
public event EventHandler<ContainerChildAddedEventArgs>? ChildAdded;
public event EventHandler<ContainerChildRemovedEventArgs>? ChildRemoved;
public Control this[int index]
{
get => _controls[index];
set => _controls[index] = value;
}
protected override void ValidateLayout()
{
if (!IsLayoutEnabled || IsLayoutValid)
return;
// LayoutSolution solution = LayoutSolution.CalculateLayout(this, ClientArea.Size);
base.ValidateLayout();
}
public override void OnPaint(DeviceContext dc)
{
base.OnPaint(dc);
var dcb = dc.ExtensionRequire<IDeviceContextBase>();
dcb.PushClip(ClientArea);
ValidateLayout();
foreach (Control child in _controls)
{
if (child.Layout.DisplayMode == DisplayMode.None)
continue;
child.SendEvent(this, new PaintEventArgs(dc));
}
dcb.PopClip();
}
IEnumerator<ILayoutItem> IEnumerable<ILayoutItem>.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<Control> GetEnumerator()
{
return _controls.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_controls).GetEnumerator();
}
public void Add(Control item)
{
SetParent(this, item);
_controls.Add(item);
ChildAdded?.Invoke(this, new ContainerChildAddedEventArgs(this, item));
}
public void Clear()
{
foreach (Control control in this)
{
ChildRemoved?.Invoke(this, new ContainerChildRemovedEventArgs(this, control));
}
_controls.Clear();
}
public bool Contains(Control item)
{
return _controls.Contains(item);
}
public void CopyTo(Control[] array, int arrayIndex)
{
_controls.CopyTo(array, arrayIndex);
}
public bool Remove(Control item)
{
if (!_controls.Remove(item))
return false;
ChildRemoved?.Invoke(this, new ContainerChildRemovedEventArgs(this, item));
return true;
}
public int IndexOf(Control item)
{
return _controls.IndexOf(item);
}
public void Insert(int index, Control item)
{
SetParent(this, item);
_controls.Insert(index, item);
ChildAdded?.Invoke(this, new ContainerChildAddedEventArgs(this, item));
}
public void RemoveAt(int index)
{
Control child = _controls[index];
_controls.RemoveAt(index);
ChildRemoved?.Invoke(this, new ContainerChildRemovedEventArgs(this, child));
}
}
public class ContainerChildAddedEventArgs(Container parent, Control child) : EventArgs
{
public Container Parent { get; } = parent;
public Control Child { get; } = child;
}
public class ContainerChildRemovedEventArgs(Container parent, Control child) : EventArgs
{
public Container Parent { get; } = parent;
public Control Child { get; } = child;
}
}

View File

@@ -1,38 +1,72 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using Dashboard.Drawing; using Dashboard.Drawing;
using Dashboard.Events;
using Dashboard.Layout;
using Dashboard.Pal;
using Dashboard.Windowing; using Dashboard.Windowing;
namespace Dashboard.Controls namespace Dashboard.Controls
{ {
public class Control : IEventListener, IDrawQueuePaintable, IDisposable public class Control : IEventListener, ILayoutItem, IDisposable
{ {
private Form? _owner = null;
public string? Id { get; set; } public string? Id { get; set; }
public ClassSet Classes { get; }
public Form? Owner { get; protected set; } = null; public Form Owner
{
get => _owner ?? throw NoOwnerException;
protected set
{
_owner = value;
OnOwnerChanged(value);
}
}
public Control? Parent { get; private set; } = null; public Control? Parent { get; private set; } = null;
public bool Disposed { get; private set; } public bool Disposed { get; private set; }
public virtual DrawQueue DrawQueue => Owner?.DrawQueue ?? throw NoOwnerException;
public virtual Box2d ClientArea { get; set; } public virtual Box2d ClientArea { get; set; }
public bool IsFocused => _owner?.FocusedControl == this;
public event EventHandler? Painting; public Brush Background { get; set; } = new SolidColorBrush(Color.Transparent);
public Brush BorderBrush { get; set; } = new SolidColorBrush(Color.Black);
public LayoutInfo Layout { get; } = new LayoutInfo();
public bool IsLayoutEnabled { get; private set; } = true;
protected bool IsLayoutValid { get; set; } = false;
public event EventHandler<DeviceContext>? Painting;
public event EventHandler<TickEventArgs>? AnimationTick;
public event EventHandler? OwnerChanged; public event EventHandler? OwnerChanged;
public event EventHandler? ParentChanged; public event EventHandler? ParentChanged;
public event EventHandler? FocusGained;
public event EventHandler? FocusLost;
public event EventHandler? Disposing; public event EventHandler? Disposing;
public event EventHandler? Resized; public event EventHandler? Resized;
public Control() public virtual Vector2 CalculateIntrinsicSize()
{ {
Classes = new ClassSet(this); return Vector2.Zero;
// return Vector2.Max(Vector2.Zero, Vector2.Max(Layout.Size, Layout.MinimumSize));
} }
public virtual void OnPaint() public Vector2 CalculateSize(Vector2 limits)
{ {
Painting?.Invoke(this, EventArgs.Empty); return CalculateIntrinsicSize();
} }
public void Paint() public virtual void OnPaint(DeviceContext dc)
{ {
OnPaint(); Painting?.Invoke(this, dc);
}
public virtual void OnAnimationTick(TickEventArgs tick)
{
AnimationTick?.Invoke(this, tick);
} }
protected void InvokeDispose(bool disposing) protected void InvokeDispose(bool disposing)
@@ -49,20 +83,99 @@ namespace Dashboard.Controls
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing) Disposing?.Invoke(this, EventArgs.Empty);
} }
public void Dispose() => InvokeDispose(true); public void Dispose() => InvokeDispose(true);
public void SendEvent(EventArgs args) public event EventHandler? EventRaised;
protected virtual void OnEventRaised(object? sender, EventArgs args)
{ {
throw new NotImplementedException(); switch (args)
{
case PaintEventArgs paint:
OnPaint(paint.DeviceContext);
break;
} }
internal static void SetParent(Control parent, Control child) EventRaised?.Invoke(this, TransformEvent(sender, args));
}
protected virtual EventArgs TransformEvent(object? sender, EventArgs args)
{
return args;
}
public void SendEvent(object? sender, EventArgs args)
{
OnEventRaised(sender, args);
}
internal static void SetParent(Container parent, Control child)
{ {
child.Parent = parent; child.Parent = parent;
child.ParentChanged?.Invoke(child, EventArgs.Empty);
}
public virtual void Focus()
{
(Owner ?? throw NoOwnerException).Focus(this);
}
protected virtual void OnFocusGained(object sender)
{
FocusGained?.Invoke(sender, EventArgs.Empty);
}
protected virtual void OnFocusLost(object sender)
{
FocusLost?.Invoke(sender, EventArgs.Empty);
}
internal static void InvokeFocusGained(Form form, Control control)
{
control.OnFocusGained(form);
}
internal static void InvokeFocusLost(Form form, Control control)
{
control.OnFocusLost(form);
}
protected virtual void OnResize()
{
Resized?.Invoke(this, EventArgs.Empty);
InvalidateLayout();
}
private void OnOwnerChanged(Form value)
{
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
public void InvalidateLayout()
{
IsLayoutValid = false;
}
protected virtual void ValidateLayout()
{
IsLayoutValid = true;
}
public void ResumeLayout()
{
IsLayoutEnabled = true;
}
public void SuspendLayout()
{
IsLayoutEnabled = false;
IsLayoutValid = false;
} }
protected static Exception NoOwnerException => new Exception("No form owns this control"); protected static Exception NoOwnerException => new Exception("No form owns this control");
public event PropertyChangedEventHandler? PropertyChanged;
} }
} }

View File

@@ -1,12 +1,30 @@
using System;
using System.Drawing;
using Dashboard.Drawing; using Dashboard.Drawing;
using Dashboard.Events;
using Dashboard.Pal;
using Dashboard.Windowing; using Dashboard.Windowing;
namespace Dashboard.Controls namespace Dashboard.Controls
{ {
public class Form : Control public class Form : Container, IForm
{ {
private string? _title = "Untitled Form";
public IWindow Window { get; } public IWindow Window { get; }
public override DrawQueue DrawQueue { get; }
public Image? WindowIcon { get; set; }
public string? Title
{
get => _title;
set
{
_title = value;
Window.Title = _title ?? "";
}
}
public Brush Background { get; set; } = new SolidColorBrush(Color.SlateGray);
public Control? FocusedControl { get; private set; } = null;
public override Box2d ClientArea public override Box2d ClientArea
{ {
@@ -14,10 +32,64 @@ namespace Dashboard.Controls
set { } set { }
} }
public event EventHandler<WindowCloseEvent>? Closing;
public Form(IWindow window) public Form(IWindow window)
{ {
Window = window; Window = window;
DrawQueue = (window as IDrawQueuePaintable)?.DrawQueue ?? new DrawQueue(); window.Form = this;
Window.Title = _title;
}
public void Focus(Control control)
{
if (FocusedControl != null)
InvokeFocusLost(this, FocusedControl);
FocusedControl = control;
InvokeFocusGained(this, control);
}
public override void OnPaint(DeviceContext dc)
{
dc.Begin();
var dcb = dc.ExtensionRequire<IDeviceContextBase>();
dcb.ResetClip();
dcb.ResetScissor();
dcb.ResetTransforms();
if (Background is SolidColorBrush solidColorBrush)
dcb.ClearColor(solidColorBrush.Color);
foreach (Control child in this)
child.SendEvent(this, new PaintEventArgs(dc));
dc.End();
}
protected virtual void OnClosing(WindowCloseEvent ea)
{
Closing?.Invoke(this, ea);
if (ea.Cancel)
return;
Dispose();
Window.Dispose();
}
protected override void OnEventRaised(object? sender, EventArgs args)
{
base.OnEventRaised(sender, args);
switch (args)
{
case WindowCloseEvent close:
OnClosing(close);
break;
}
} }
} }
} }

View File

@@ -0,0 +1,27 @@
using System.Numerics;
using Dashboard.Drawing;
using Dashboard.Pal;
namespace Dashboard.Controls
{
public class ImageBox : Control
{
public Image? Image { get; set; }
public override Vector2 CalculateIntrinsicSize()
{
return new Vector2(Image?.Width ?? 0, Image?.Height ?? 0);
}
public override void OnPaint(DeviceContext dc)
{
if (Image == null)
return;
// Layout.Size = CalculateIntrinsicSize();
// dc.ExtensionRequire<IImmediateMode>().Image(new Box2d(ClientArea.Min, ClientArea.Min + Layout.Size), new Box2d(0, 0, 1, 1), 0, Image.InternTexture(dc));
base.OnPaint(dc);
}
}
}

View File

@@ -2,36 +2,61 @@ using System;
using System.Drawing; using System.Drawing;
using System.Numerics; using System.Numerics;
using Dashboard.Drawing; using Dashboard.Drawing;
using Dashboard.Layout;
using Dashboard.Pal;
namespace Dashboard.Controls namespace Dashboard.Controls
{ {
public class Label : Control public class Label : Control
{ {
private Vector2 _intrinsicSize = Vector2.Zero;
public bool AutoSize { get; set; } = true; public bool AutoSize { get; set; } = true;
public string Text { get; set; } = ""; public string Text { get; set; } = "";
public event EventHandler? TextChanged; public event EventHandler? TextChanged;
protected IBrush TextBrush => throw new NotImplementedException(); // protected IBrush TextBrush => throw new NotImplementedException();
protected IFont Font => throw new NotImplementedException(); public Font Font { get; set; } = Drawing.Font.Create(new FontInfo("Rec Mono Linear"));
public float TextSize { get; set; } = 12f;
public Brush TextBrush { get; set; } = new SolidColorBrush(Color.Black);
protected virtual void OnTextChanged(string oldValue, string newValue) public override Vector2 CalculateIntrinsicSize()
{ {
return _intrinsicSize;
}
protected void CalculateSize(DeviceContext dc)
{
Box2d box = dc.ExtensionRequire<ITextRenderer>().MeasureText(Font.Base, TextSize, Text);
// _intrinsicSize = box.Size;
// Layout.Size = box.Size;
// ClientArea = new Box2d(ClientArea.Min, ClientArea.Min + Layout.Size);
}
public override void OnPaint(DeviceContext dc)
{
base.OnPaint(dc);
if (AutoSize) if (AutoSize)
CalculateSize(); CalculateSize(dc);
}
protected void CalculateSize() bool hidden = Layout.OverflowMode == OverflowMode.Hidden;
{ var dcb = dc.ExtensionRequire<IDeviceContextBase>();
SizeF sz = Typesetter.MeasureString(Font, Text); if (hidden)
ClientArea = new Box2d(ClientArea.Min, ClientArea.Min + (Vector2)sz); dcb.PushScissor(ClientArea);
}
dcb.PushTransforms(Matrix4x4.CreateTranslation(ClientArea.Left, ClientArea.Top, 0));
public override void OnPaint() var text = dc.ExtensionRequire<ITextRenderer>();
{ Color color = (TextBrush as SolidColorBrush)?.Color ?? Color.Black;
base.OnPaint(); Vector4 colorVector = new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
DrawQueue.Text(new Vector3(ClientArea.Min, 0), TextBrush, Text, Font); text.DrawText(Vector2.Zero, colorVector, TextSize, Font.Base, Text);
if (hidden)
dcb.PopScissor();
dcb.PopTransforms();
} }
} }
} }

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Reflection;
using Dashboard.Drawing;
using Dashboard.Windowing;
namespace Dashboard.Controls
{
public enum MessageBoxIcon
{
Info,
Question,
Warning,
Error,
Custom,
}
public enum MessageBoxButtons
{
AbortRetryIgnore,
CancelRetryContinue,
Ok,
OkCancel,
RetryCancel,
YesNo,
YesNoCancel,
Custom,
}
/// <summary>
/// A simple message box dialog.
/// </summary>
public class MessageBox : Form
{
private MessageBoxIcon _icon;
private MessageBoxButtons _buttons;
private ImageBox _iconBox = new ImageBox();
private Label _label = new Label();
private Container _main = new Container();
private Container _buttonsContainer = new Container();
private Image? IconImage
{
get => _iconBox.Image;
set => _iconBox.Image = value;
}
public MessageBoxIcon Icon
{
get => _icon;
set
{
IconImage = value switch
{
MessageBoxIcon.Question => s_questionIcon,
MessageBoxIcon.Info => s_infoIcon,
MessageBoxIcon.Warning => s_warningIcon,
MessageBoxIcon.Error => s_errorIcon,
_ => null,
};
_icon = value;
}
}
public Image? CustomImage
{
get => Icon == MessageBoxIcon.Custom ? IconImage : null;
set
{
if (IconImage == null)
return;
Icon = MessageBoxIcon.Custom;
IconImage = value;
}
}
public string? Message
{
get => _label.Text;
set => _label.Text = value ?? String.Empty;
}
public MessageBoxButtons Buttons
{
get => _buttons;
set
{
_buttons = value;
UpdateButtons();
}
}
public ObservableCollection<string> CustomButtons { get; } = new ObservableCollection<string>();
public int Result { get; private set; } = -1;
public MessageBox(IWindow window) : base(window)
{
// Layout.Rows.Clear();
// Layout.Rows.Add(-1);
// Layout.Rows.Add(48);
//
// Add(_main);
// _main.Layout.Columns.Clear();
// _main.Layout.Columns.Add(48);
// _main.Layout.Columns.Add(-1);
_main.Add(_iconBox);
_main.Add(_label);
_label.Layout.Column = 1;
Add(_buttonsContainer);
_buttonsContainer.Layout.Row = 1;
CustomButtons.CollectionChanged += (sender, ea) => UpdateButtons();
UpdateButtons();
}
private void UpdateButtons()
{
foreach (Control button in _buttonsContainer)
{
Remove(button);
}
IList<string> list = Buttons switch
{
MessageBoxButtons.Custom => CustomButtons,
MessageBoxButtons.AbortRetryIgnore => s_abortRetryContinue,
MessageBoxButtons.CancelRetryContinue => s_cancelRetryContinue,
MessageBoxButtons.OkCancel => s_okCancel,
MessageBoxButtons.RetryCancel => s_retryCancel,
MessageBoxButtons.YesNo => s_yesNo,
MessageBoxButtons.YesNoCancel => s_yesNoCancel,
_ => s_ok,
};
// _buttonsContainer.Clear();
// _buttonsContainer.Layout.Columns.Clear();
// for (int i = 0; i < list.Count; i++)
// {
// _buttonsContainer.Layout.Columns.Add(-1);
// string str = list[i];
//
// Button button = new Button() { Text = str };
// button.Clicked += (sender, ea) => ButtonClicked(sender, ea, i);
// button.Layout.Column = i;
// _buttonsContainer.Add(button);
// Add(button);
// }
}
private void ButtonClicked(object? sender, EventArgs ea, int i)
{
Result = i;
Dispose();
}
public static readonly Image s_questionIcon;
public static readonly Image s_infoIcon;
public static readonly Image s_warningIcon;
public static readonly Image s_errorIcon;
private static readonly ImmutableList<string> s_abortRetryContinue = ["Abort", "Retry", "Continue"];
private static readonly ImmutableList<string> s_cancelRetryContinue = ["Cancel", "Retry", "Continue"];
private static readonly ImmutableList<string> s_ok = ["OK"];
private static readonly ImmutableList<string> s_okCancel = ["OK", "Cancel"];
private static readonly ImmutableList<string> s_retryCancel = ["Retry", "Cancel"];
private static readonly ImmutableList<string> s_yesNo = ["Yes", "No"];
private static readonly ImmutableList<string> s_yesNoCancel = ["Yes", "No", "Cancel"];
static MessageBox()
{
Assembly asm = typeof(MessageBox).Assembly;
using (Stream str = asm.GetManifestResourceStream("Dashboard.Resources.question.png")!)
s_questionIcon = Image.Load(str);
using (Stream str = asm.GetManifestResourceStream("Dashboard.Resources.info.png")!)
s_infoIcon = Image.Load(str);
using (Stream str = asm.GetManifestResourceStream("Dashboard.Resources.warning.png")!)
s_warningIcon = Image.Load(str);
using (Stream str = asm.GetManifestResourceStream("Dashboard.Resources.error.png")!)
s_errorIcon = Image.Load(str);
}
public static MessageBox Create(IWindow window, string message, string title, MessageBoxIcon icon, MessageBoxButtons buttons)
{
return new MessageBox(window)
{
Message = message,
Title = title,
Icon = icon,
Buttons = buttons,
};
}
// public static MessageBox Create(IWindow window, string message, string title, Image icon, IEnumerable<string> buttons)
// {
// throw new NotImplementedException();
// }
private static string GetDefaultTitle(MessageBoxIcon icon)
{
return icon switch
{
MessageBoxIcon.Error => "Error",
MessageBoxIcon.Info => "Info",
MessageBoxIcon.Question => "Question",
MessageBoxIcon.Warning => "Warning",
_ => "Message Box",
};
}
private static Image GetDefaultIcon(MessageBoxIcon icon)
{
return icon switch
{
MessageBoxIcon.Error => s_errorIcon,
MessageBoxIcon.Question => s_questionIcon,
MessageBoxIcon.Warning => s_warningIcon,
_ => s_infoIcon,
};
}
private static ImmutableList<string> GetDefaultButtons(MessageBoxButtons buttons)
{
return buttons switch
{
MessageBoxButtons.AbortRetryIgnore => s_abortRetryContinue,
MessageBoxButtons.CancelRetryContinue => s_cancelRetryContinue,
MessageBoxButtons.OkCancel => s_okCancel,
MessageBoxButtons.RetryCancel => s_retryCancel,
MessageBoxButtons.YesNo => s_yesNo,
MessageBoxButtons.YesNoCancel => s_yesNoCancel,
_ => s_ok,
};
}
}
}

View File

@@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" /> <ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
<ProjectReference Include="..\Dashboard.Drawing\Dashboard.Drawing.csproj" /> <EmbeddedResource Include="Resources\**\*.png"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="error.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="6.0664802"
inkscape:cx="-5.7694081"
inkscape:cy="25.962336"
inkscape:window-width="2560"
inkscape:window-height="1364"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="6.3500001,12.7"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="3.705891,12.686148"
orientation="-1,0"
id="guide2"
inkscape:label=""
inkscape:locked="false"
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#ff0000;stroke:#d60000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path1"
inkscape:flatsided="true"
sodipodi:sides="8"
sodipodi:cx="6.3499999"
sodipodi:cy="6.3499999"
sodipodi:r1="6.8657174"
sodipodi:r2="8.2966747"
sodipodi:arg1="1.9608592"
sodipodi:arg2="2.3535583"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 3.7393344,12.7 0.01385251,8.9941088 -2.0290497e-7,3.7393344 3.705891,0.01385251 8.9606654,-2.0290497e-7 12.686147,3.705891 12.7,8.9606654 8.9941088,12.686147 Z"
transform="matrix(0.92700733,0,0,0.92700733,0.46350342,0.46350342)" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.87777px;line-height:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Bold';text-align:center;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;direction:ltr;text-orientation:upright;text-anchor:middle;fill:#ffffff;stroke:none;stroke-width:0.600001"
x="6.3154373"
y="9.0071211"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Bold';fill:#ffffff;stroke-width:0.6"
x="6.3154373"
y="9.0071211">x</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="info.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="15.987983"
inkscape:cx="14.823634"
inkscape:cy="21.234699"
inkscape:window-width="2560"
inkscape:window-height="1364"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#00b200;fill-opacity:1;stroke:#009900;stroke-width:0.573001;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="12.127"
height="12.127"
x="0.28650019"
y="0.28650019"
rx="1.0105833"
ry="1.0105834" />
<text
xml:space="preserve"
style="font-style:italic;font-size:9.87777px;line-height:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Italic';text-align:center;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;direction:ltr;text-orientation:upright;text-anchor:middle;fill:#f9f9f9;stroke:none;stroke-width:0.600001"
x="6.3351822"
y="10.172698"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Bold Italic';fill:#f9f9f9;stroke-width:0.6"
x="6.3351822"
y="10.172698">i</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="question.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="6.4560796"
inkscape:cx="12.701206"
inkscape:cy="14.172688"
inkscape:window-width="2560"
inkscape:window-height="1364"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true"><sodipodi:guide
position="6.3500001,12.7"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" /><sodipodi:guide
position="3.705891,12.686148"
orientation="-1,0"
id="guide2"
inkscape:label=""
inkscape:locked="false"
inkscape:color="rgb(0,134,229)" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><circle
style="fill:#0000ff;stroke:#0000cc;stroke-width:0.9271;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path2"
cx="6.3500004"
cy="6.3500004"
r="5.8864961" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.87777px;line-height:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Bold';text-align:center;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;direction:ltr;text-orientation:upright;text-anchor:middle;fill:#ffffff;stroke:none;stroke-width:0.600001"
x="6.2907381"
y="9.8615408"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Bold';fill:#ffffff;stroke-width:0.6"
x="6.2907381"
y="9.8615408">?</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="warning.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="1"
inkscape:cx="23"
inkscape:cy="24.5"
inkscape:window-width="2560"
inkscape:window-height="1364"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="6.3500001,12.7"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#ffcc00;stroke:#ffbb00;stroke-width:0.9271;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 0.46350557,12.236497 6.3500001,0.46350801 12.236494,12.236497 Z"
id="path1"
sodipodi:nodetypes="cccc" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.87777px;line-height:normal;font-family:'Rec Mono Linear';-inkscape-font-specification:'Rec Mono Linear Bold';text-align:center;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;direction:ltr;text-orientation:upright;text-anchor:middle;fill:#ffcc00;stroke:none;stroke-width:0.600001"
x="6.2858014"
y="10.821102"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="fill:#000000;stroke-width:0.6;-inkscape-font-specification:'Rec Mono Linear Bold';font-family:'Rec Mono Linear';font-weight:bold;font-style:normal;font-stretch:normal;font-variant:normal"
x="6.2858014"
y="10.821102">!</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Dashboard.Drawing.OpenGL\Dashboard.Drawing.OpenGL.csproj" /> <ProjectReference Include="..\..\Dashboard.BlurgText.OpenGL\Dashboard.BlurgText.OpenGL.csproj" />
<ProjectReference Include="..\..\Dashboard.Drawing\Dashboard.Drawing.csproj" /> <ProjectReference Include="..\..\Dashboard.BlurgText\Dashboard.BlurgText.csproj" />
<ProjectReference Include="..\..\Dashboard.ImmediateUI\Dashboard.ImmediateUI.csproj" />
<ProjectReference Include="..\..\Dashboard.OpenTK\Dashboard.OpenTK.csproj" /> <ProjectReference Include="..\..\Dashboard.OpenTK\Dashboard.OpenTK.csproj" />
<ProjectReference Include="..\..\Dashboard.StbImage\Dashboard.StbImage.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,18 +1,15 @@
using Dashboard.Drawing; using Dashboard.BlurgText;
using System.Drawing; using Dashboard.BlurgText.OpenGL;
using System.Text; using Dashboard.Controls;
using Dashboard.Drawing.OpenGL; using Dashboard.Drawing;
using Dashboard.ImmediateUI; using Dashboard.OpenGL;
using Dashboard.OpenTK.PAL2; using Dashboard.OpenTK.PAL2;
using OpenTK.Platform; using Dashboard.Pal;
using Dashboard.StbImage;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics; using OpenTK.Mathematics;
using Box2d = Dashboard.Box2d; using OpenTK.Platform;
using TK = OpenTK.Platform.Toolkit; using TK = OpenTK.Platform.Toolkit;
using Dashboard;
using Dashboard.Windowing;
using OpenTK.Windowing.GraphicsLibraryFramework;
using AppContext = Dashboard.Pal.AppContext;
TK.Init(new ToolkitOptions() TK.Init(new ToolkitOptions()
{ {
@@ -25,7 +22,7 @@ TK.Init(new ToolkitOptions()
FeatureFlags = ToolkitFlags.EnableOpenGL, FeatureFlags = ToolkitFlags.EnableOpenGL,
}); });
AppContext app = new Pal2AppContext() Application app = new Pal2Application()
{ {
GraphicsApiHints = new OpenGLGraphicsApiHints() GraphicsApiHints = new OpenGLGraphicsApiHints()
{ {
@@ -47,134 +44,43 @@ AppContext app = new Pal2AppContext()
} }
}; };
PhysicalWindow window;
SolidBrush fg = new SolidBrush(Color.FromArgb(0, 0, 0, 0));
SolidBrush bg = new SolidBrush(Color.Black);
CancellationTokenSource source = new CancellationTokenSource(); CancellationTokenSource source = new CancellationTokenSource();
GLEngine engine;
ContextExecutor executor;
DimUI dimUI;
Vector2 mousePos = Vector2.Zero;
Random r = new Random();
List<Vector3> points = new List<Vector3>();
IFont font;
StringBuilder builder = new StringBuilder();
app.Initialize(); app.Initialize();
app.ExtensionRequire<StbImageLoader>();
app.ExtensionLoad(new BlurgTextExtension(new BlurgTextExtensionFactory()));
window = (PhysicalWindow)app.CreatePhysicalWindow(); PhysicalWindow window = (PhysicalWindow)app.CreatePhysicalWindow();
MessageBox box = MessageBox.Create(window, "Are you sure you want to exit?", "Confirm Exit", MessageBoxIcon.Question,
MessageBoxButtons.YesNo);
window.Title = "DashTerm"; // window.Title = "DashTerm";
TK.Window.SetMinClientSize(window.WindowHandle, 300, 200); TK.Window.SetMinClientSize(window.WindowHandle, 300, 200);
TK.Window.SetClientSize(window.WindowHandle, new Vector2i(320, 240)); TK.Window.SetClientSize(window.WindowHandle, new Vector2i(320, 240));
TK.Window.SetBorderStyle(window.WindowHandle, WindowBorderStyle.ResizableBorder); TK.Window.SetBorderStyle(window.WindowHandle, WindowBorderStyle.ResizableBorder);
// TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f); // TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f);
OpenGLDeviceContext context = (OpenGLDeviceContext)window.DeviceContext; GLDeviceContext context = (GLDeviceContext)window.DeviceContext;
context.MakeCurrent(); context.GLContext.MakeCurrent();
context.SwapGroup.SwapInterval = 1; context.GLContext.SwapGroup.SwapInterval = 1;
engine = new GLEngine(); GL.Disable(EnableCap.DepthTest);
engine.Initialize(); GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
executor = engine.GetExecutor(context); GL.ColorMask(true, true, true, true);
dimUI = new DimUI(new DimUIConfig()
{
Font = new NamedFont("Noto Sans", 9f),
});
EventQueue.EventRaised += (handle, type, eventArgs) =>
{
if (handle != window.WindowHandle)
return;
switch (type)
{
case PlatformEventType.Close:
source.Cancel();
break;
case PlatformEventType.MouseMove:
mousePos = ((MouseMoveEventArgs)eventArgs).ClientPosition;
break;
}
};
TK.Window.SetMode(window.WindowHandle, WindowMode.Normal); TK.Window.SetMode(window.WindowHandle, WindowMode.Normal);
font = Typesetter.LoadFont("Nimbus Mono", 12f);
window.Painting += (sender, ea) => { window.DeviceContext.ExtensionRequire<IDeviceContextBase>().ScaleOverride = 1.5f;
TK.Window.GetSize(context.WindowHandle, out Vector2i size);
executor.BeginFrame();
dimUI.Begin(new Box2d(0, 0, size.X, size.Y), window.DrawQueue);
dimUI.Text("Hello World!");
dimUI.Button("Cancel"); dimUI.SameLine();
dimUI.Button("OK");
dimUI.Input("type me!", builder);
dimUI.BeginMenu();
if (dimUI.MenuItem("File"))
{
dimUI.BeginMenu();
dimUI.MenuItem("New Window");
dimUI.MenuItem("Preferences");
dimUI.MenuItem("Exit");
dimUI.EndMenu();
}
if (dimUI.MenuItem("Edit"))
{
dimUI.BeginMenu();
dimUI.MenuItem("Cut");
dimUI.MenuItem("Copy");
dimUI.MenuItem("Paste");
if (dimUI.MenuItem("Send Char"))
{
dimUI.BeginMenu();
dimUI.EndMenu();
}
dimUI.EndMenu();
}
if (dimUI.MenuItem("View"))
{
dimUI.BeginMenu();
dimUI.MenuItem("Clear");
if (dimUI.MenuItem("Set Size"))
{
dimUI.BeginMenu();
dimUI.MenuItem("24 x 40");
dimUI.MenuItem("25 x 40");
dimUI.MenuItem("24 x 80");
dimUI.MenuItem("25 x 80");
dimUI.MenuItem("25 x 120");
dimUI.EndMenu();
}
dimUI.EndMenu();
}
dimUI.Finish();
GL.Viewport(0, 0, size.X, size.Y);
GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.Disable(EnableCap.DepthTest);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.ColorMask(true, true, true, true);
executor.Draw(window.DrawQueue, new RectangleF(0, 0, size.X, size.Y), 1.5f /*(window as IDpiAwareWindow)?.Scale ?? 1*/);
executor.EndFrame();
context.SwapGroup.Swap();
};
app.Run(true, source.Token); app.Run(true, source.Token);
class BlurgTextExtensionFactory : IBlurgDcExtensionFactory
{
public BlurgDcExtension CreateExtension(BlurgTextExtension appExtension, DeviceContext dc)
{
return new BlurgGLExtension();
}
};