diff --git a/.scripts/Setup.ps1 b/.scripts/Setup.ps1
index ee5587a..999152a 100644
--- a/.scripts/Setup.ps1
+++ b/.scripts/Setup.ps1
@@ -71,6 +71,22 @@ $projectsList = @(
)
PromptIgnore = $false
Depends = @("Arcane")
+ },
+ @{
+ Module = "Crystal"
+ Projects = @(
+ @{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Core" }
+ )
+ PromptIgnore = $false
+ Depends = @("Core")
+ },
+ @{
+ Module = "Crystal.Glfw3"
+ Projects = @(
+ @{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Glfw3" }
+ )
+ PromptIgnore = $false
+ Depends = @("Crystal")
}
)
diff --git a/CatalystUI/CatalystUI.sln b/CatalystUI/CatalystUI.sln
index 0b50ed1..6a5b756 100644
--- a/CatalystUI/CatalystUI.sln
+++ b/CatalystUI/CatalystUI.sln
@@ -36,6 +36,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Arcane.I
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Arcane.Core", "Modules\Arcane\CatalystUI.Modules.Arcane.Core\CatalystUI.Modules.Arcane.Core.csproj", "{047744B0-87DC-4808-99C4-5AC1F8A1EB4D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.Core", "Modules\Crystal\CatalystUI.Modules.Crystal.Core\CatalystUI.Modules.Crystal.Core.csproj", "{4DE1A1EB-103B-4E07-859A-354908ACE0E2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.Glfw3", "Modules\Crystal\CatalystUI.Modules.Crystal.Glfw3\CatalystUI.Modules.Crystal.Glfw3.csproj", "{BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -90,6 +94,14 @@ Global
{047744B0-87DC-4808-99C4-5AC1F8A1EB4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{047744B0-87DC-4808-99C4-5AC1F8A1EB4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{047744B0-87DC-4808-99C4-5AC1F8A1EB4D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DE1A1EB-103B-4E07-859A-354908ACE0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4DE1A1EB-103B-4E07-859A-354908ACE0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DE1A1EB-103B-4E07-859A-354908ACE0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4DE1A1EB-103B-4E07-859A-354908ACE0E2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{68F496AC-9438-40F1-9DF8-97363033D661} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D}
@@ -107,5 +119,7 @@ Global
{0E1F2B64-37D9-4C24-9CED-9A44D7CDBB8C} = {9C3F6A00-82F5-4900-9D6C-07ACBBAAE823}
{C02600D7-087B-4190-9B47-F15184C19B2D} = {C8B02B42-826B-4EDE-B72F-F4F97C1A088D}
{047744B0-87DC-4808-99C4-5AC1F8A1EB4D} = {C8B02B42-826B-4EDE-B72F-F4F97C1A088D}
+ {4DE1A1EB-103B-4E07-859A-354908ACE0E2} = {41BEF490-7005-4D10-9958-22D636F9DE38}
+ {BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18} = {41BEF490-7005-4D10-9958-22D636F9DE38}
EndGlobalSection
EndGlobal
diff --git a/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs b/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
index c58b100..ae5d624 100644
--- a/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
+++ b/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
@@ -9,6 +9,7 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
+using Catalyst.Builders;
using Catalyst.Debugging;
using Catalyst.Threading;
using System;
diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs
index da20dbc..c1c1a5c 100644
--- a/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs
+++ b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs
@@ -17,8 +17,15 @@ namespace Catalyst.Connectors {
///
/// Represents the data connector in the CatalystUI model.
///
+ ///
+ /// The and
+ /// are the only two connectors that contain a layer without an associated layer restriction. For the native
+ /// connector, this is typically the higher layer, since various model layers may interact with the native
+ /// level of the system. Similarly, the data connector generally has a lower layer without restrictions, as
+ /// various model layers may need to interact with raw data sources.
+ ///
///
- public interface IDataConnector : IConnector where TLayerLow : ISemanticsLayer where TLayerHigh : IDataLayer {
+ public interface IDataConnector : IConnector where TLayerLow : ILayer where TLayerHigh : IDataLayer {
// ...
diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs
index b16e6ed..ade122f 100644
--- a/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs
+++ b/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs
@@ -17,8 +17,15 @@ namespace Catalyst.Connectors {
///
/// Represents the native connector in the CatalystUI model.
///
+ ///
+ /// The and
+ /// are the only two connectors that contain a layer without an associated layer restriction. For the native
+ /// connector, this is typically the higher layer, since various model layers may interact with the native
+ /// level of the system. Similarly, the data connector generally has a lower layer without restrictions, as
+ /// various model layers may need to interact with raw data sources.
+ ///
///
- public interface INativeConnector : IConnector where TLayerLow : ISystemLayer where TLayerHigh : IWindowLayer {
+ public interface INativeConnector : IConnector where TLayerLow : ISystemLayer where TLayerHigh : ILayer {
// ...
diff --git a/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs b/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs
index e0d6606..a629216 100644
--- a/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs
+++ b/CatalystUI/Core/CatalystUI.Core/Debugging/DebugContext.cs
@@ -84,8 +84,8 @@ public void LogFatal(string message, params object[] args) {
/// Logs an error with the specified message and optional arguments.
///
///
- /// If an exception is provided as an argument, the exception's will be included in the log output.
- /// If the message is null or empty, it will be replaced with the exception's
+ /// If an exception is provided as an argument, the exception's will be included in the log output.
+ /// If the message is null or empty, it will be replaced with the exception's
///
///
[Conditional("DEBUG")]
diff --git a/CatalystUI/Core/CatalystUI.Core/IOptions.cs b/CatalystUI/Core/CatalystUI.Core/IOptions.cs
new file mode 100644
index 0000000..4e16dc6
--- /dev/null
+++ b/CatalystUI/Core/CatalystUI.Core/IOptions.cs
@@ -0,0 +1,23 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+namespace Catalyst {
+
+ ///
+ /// Represents some form of constructor or configuration options.
+ ///
+ public interface IOptions {
+
+ // ...
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Core/CatalystUI.Core/ModelRegistry.cs b/CatalystUI/Core/CatalystUI.Core/ModelRegistry.cs
index e1f987b..c7e3297 100644
--- a/CatalystUI/Core/CatalystUI.Core/ModelRegistry.cs
+++ b/CatalystUI/Core/CatalystUI.Core/ModelRegistry.cs
@@ -15,9 +15,8 @@
using Catalyst.Layers;
using System;
using System.Collections.Generic;
-using System.Threading;
using System.Linq;
-
+using System.Threading;
using LayerSet = System.Collections.Generic.HashSet>;
using LayerMap = System.Collections.Generic.Dictionary>>;
using DomainMap = System.Collections.Generic.Dictionary>>>;
diff --git a/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs b/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs
index 6ad2be3..852cbff 100644
--- a/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs
+++ b/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs
@@ -9,6 +9,7 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
+using Catalyst.Threading;
using System;
namespace Catalyst.Native {
@@ -29,8 +30,30 @@ public interface INativeApi : IDisposable where TSelf : INa
///
/// Requests an instance of the API.
///
+ ///
+ ///
+ /// When requesting with a custom dispatcher,
+ /// upon first initialization, the API will
+ /// be associated with the provided dispatcher.
+ /// Subsequent requests with a different dispatcher
+ /// will be ignored, and the originally associated
+ /// dispatcher will be used.
+ ///
+ ///
+ ///
+ /// If an API instance is initialized on the main thread,
+ /// and then requested from a background thread with a custom dispatcher,
+ /// the main thread dispatcher will be used for all operations.
+ /// As a result, some APIs may respond poorly to cross-thread
+ /// communication if they are not designed for it (such as OpenGL,
+ /// which requires a context to be current on the thread it is used on).
+ /// Always ensure to request the API instance on the thread it was
+ /// originally initialized on, or use a custom dispatcher
+ /// appropriate to the API's threading model.
+ ///
+ /// A thread dispatcher to associate with the API instance, or to use a captured main thread dispatcher.
/// The instance of the API.
- static abstract TSelf GetInstance();
+ static abstract TSelf GetInstance(ThreadDelegateDispatcher? dispatcher = null);
}
diff --git a/CatalystUI/Core/CatalystUI.Debug/CatalystSerilogDebug.cs b/CatalystUI/Core/CatalystUI.Debug/CatalystSerilogDebug.cs
index 3dc2b16..67dc4f2 100644
--- a/CatalystUI/Core/CatalystUI.Debug/CatalystSerilogDebug.cs
+++ b/CatalystUI/Core/CatalystUI.Debug/CatalystSerilogDebug.cs
@@ -71,7 +71,7 @@ private static void ConfigureLogger() {
// Add an async sink
config.WriteTo.Async(writeTo => {
// Create the logging template
- string levelmap = @"
+ const string levelMap = """
if @l = 'Verbose' then 'VERBOSE'
else if @l = 'Debug' then 'DEBUG'
else if @l = 'Information' then 'INFO'
@@ -79,14 +79,14 @@ private static void ConfigureLogger() {
else if @l = 'Error' then 'ERROR'
else if @l = 'Fatal' then 'CRITICAL'
else 'UNKNOWN'
- ";
- string thread = $@"
+ """;
+ string thread = $"""
if ThreadName is not null then ThreadName
else if ThreadId = {Environment.CurrentManagedThreadId} then 'MainThread'
else Concat('Thread ', ToString(ThreadId))
- ";
+ """;
string threadTemplate = CatalystDebug.DebugOptions.ShowsThread ? "<{" + thread + "}> " : string.Empty;
- string template = threadTemplate + "[{SourceContext}] [{@t:HH:mm:ss:fff}] [{" + levelmap + "}] {@m}{if @x is not null then '" + Environment.NewLine + "' + @x else ''}";
+ string template = threadTemplate + "[{SourceContext}] [{@t:HH:mm:ss:fff}] [{" + levelMap + "}] {@m}{if @x is not null then '" + Environment.NewLine + "' + @x else ''}";
ExpressionTemplate formatter = new(template + Environment.NewLine);
// Add a debug sink
diff --git a/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs b/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs
index 875c2ef..a89f97e 100644
--- a/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs
+++ b/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs
@@ -57,8 +57,8 @@ public override void Log(LogLevel level, string message, string? prefix = null,
bool needsComma = false;
if (CatalystDebug.DebugOptions.ShowsFileName) {
string? file = frame.GetFileName();
- string? typeName = method.Name;
- string? assemblyName = method.DeclaringAssemblyName;
+ string typeName = method.Name;
+ string assemblyName = method.DeclaringAssemblyName;
sb.Append('<');
if (!string.IsNullOrEmpty(file)) {
sb.Append(Path.GetFileName(file));
@@ -73,25 +73,18 @@ public override void Log(LogLevel level, string message, string? prefix = null,
needsComma = true;
}
if (CatalystDebug.DebugOptions.ShowsMethodName) {
- if (needsComma) {
- sb.Append('#');
- } else {
- sb.Append('<');
- }
+ sb.Append(needsComma ? '#' : '<');
sb.Append(method.Name);
needsContinuation = true;
needsComma = true;
}
if (CatalystDebug.DebugOptions.ShowsLineNumber) {
- if (needsComma) {
- sb.Append('(');
- } else {
- sb.Append('<');
- }
+ sb.Append(needsComma ? '(' : '<');
sb.Append(frame.GetFileLineNumber());
if (needsComma) sb.Append(')');
needsContinuation = true;
- needsComma = true;
+ // ReSharper disable once RedundantAssignment
+ needsComma = true; // in case more options are added later
}
if (needsContinuation) {
sb.Append('>').Append(' ');
@@ -101,6 +94,9 @@ public override void Log(LogLevel level, string message, string? prefix = null,
} catch {
// not supported probably
}
+ if (!string.IsNullOrWhiteSpace(prefix)) {
+ sb.Append('[').Append(prefix).Append(']').Append(' '); // prefix
+ }
sb.Append(message).Append(' ');
if (args.Length > 0) {
if (args[0] is Exception e) {
@@ -113,7 +109,7 @@ public override void Log(LogLevel level, string message, string? prefix = null,
}
} else {
for (int i = 0; i < args.Length; i++) {
- sb.Append(args[i]?.ToString());
+ sb.Append(args[i]);
if (i < args.Length - 1) {
sb.Append(' ');
}
diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs
new file mode 100644
index 0000000..16a86af
--- /dev/null
+++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs
@@ -0,0 +1,221 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+using System.Globalization;
+
+namespace Catalyst.Mathematics.Color {
+
+ ///
+ /// Represents an RGBA color with a 32-bit color depth.
+ ///
+ public readonly record struct Color128 : IColor {
+
+ ///
+ /// Represents a fully opaque white color (1.0f, 1.0f, 1.0f, 1.0f).
+ ///
+ public static readonly Color128 WHITE = new(1.0f);
+
+ ///
+ /// Represents a fully opaque red color (1.0f, 0.0f, 0.0f, 1.0f).
+ ///
+ public static readonly Color128 RED = new(1.0f, 0.0f, 0.0f);
+
+ ///
+ /// Represents a fully opaque green color (0.0f, 1.0f, 0.0f, 1.0f).
+ ///
+ public static readonly Color128 GREEN = new(0.0f, 1.0f, 0.0f);
+
+ ///
+ /// Represents a fully opaque blue color (0.0f, 0.0f, 1.0f, 1.0f).
+ ///
+ public static readonly Color128 BLUE = new(0.0f, 0.0f, 1.0f);
+
+ ///
+ /// Represents a fully opaque black color (0.0f, 0.0f, 0.0f, 1.0f).
+ ///
+ public static readonly Color128 BLACK = new(0.0f);
+
+ ///
+ /// Represents a fully transparent color (0.0f, 0.0f, 0.0f, 0.0f).
+ ///
+ public static readonly Color128 TRANSPARENT = new(0.0f, 0.0f);
+
+ ///
+ public float R { get; init; }
+
+ ///
+ public float G { get; init; }
+
+ ///
+ public float B { get; init; }
+
+ ///
+ public float A { get; init; }
+
+ ///
+ /// Constructs a new from a hexadecimal color string.
+ ///
+ ///
+ /// The hexadecimal value should follow the #RRGGBBAA format,
+ /// where the AA field is optional and defaults to FF.
+ ///
+ /// The hexadecimal color string.
+ /// Thrown if the hexadecimal value was invalid.
+ /// Thrown if the parsed hexadecimal value gave an invalid range.
+ public Color128(string hex) {
+ if (string.IsNullOrWhiteSpace(hex)) throw new ArgumentException("Hexadecimal value cannot be null or empty.", nameof(hex));
+ if (hex[0] == '#') hex = hex[1..]; // remove the leading '#'
+ if (hex.Length != 6 && hex.Length != 8) throw new ArgumentException("The hexadecimal value must be in the format #RRGGBB or #RRGGBBAA!", nameof(hex));
+ try {
+ R = byte.Parse(hex[0..2], NumberStyles.HexNumber) / 255.0f;
+ G = byte.Parse(hex[2..4], NumberStyles.HexNumber) / 255.0f;
+ B = byte.Parse(hex[4..6], NumberStyles.HexNumber) / 255.0f;
+ A = hex.Length == 8 ? (byte.Parse(hex[6..8], NumberStyles.HexNumber) / 255.0f) : 1.0f;
+ } catch (FormatException) {
+ throw new ArgumentException("The hexadecimal value contains invalid characters.", nameof(hex));
+ } catch (OverflowException) {
+ throw new ArgumentException("The hexadecimal value contains numbers out of range.", nameof(hex));
+ } catch {
+ throw new ArgumentException("The hexadecimal value must be in the format #RRGGBB or #RRGGBBAA!", nameof(hex));
+ }
+ }
+
+ ///
+ /// Constructs a new from a hexadecimal color value.
+ ///
+ ///
+ /// The hexadecimal value should follow the 0xRRGGBB or 0xRRGGBBAA format,
+ /// depending on whether the parameter is set to .
+ ///
+ /// The hexadecimal color value.
+ /// Whether the hexadecimal value includes an alpha channel.
+ public Color128(uint hex, bool alpha = false) {
+ if (alpha) {
+ R = ((hex >> 24) & 0xFF) / 255.0f;
+ G = ((hex >> 16) & 0xFF) / 255.0f;
+ B = ((hex >> 8) & 0xFF) / 255.0f;
+ A = (hex & 0xFF) / 255.0f;
+ } else {
+ R = ((hex >> 16) & 0xFF) / 255.0f;
+ G = ((hex >> 8) & 0xFF) / 255.0f;
+ B = (hex & 0xFF) / 255.0f;
+ A = 1.0f;
+ }
+ }
+
+ ///
+ /// Constructs a new with the
+ /// specified red, green, blue, and alpha values.
+ ///
+ /// The red value.
+ /// The green value.
+ /// The blue value.
+ /// The alpha value.
+ public Color128(float r, float g, float b, float a) {
+ R = Math.Clamp(r, 0.0f, 1.0f);
+ G = Math.Clamp(g, 0.0f, 1.0f);
+ B = Math.Clamp(b, 0.0f, 1.0f);
+ A = Math.Clamp(a, 0.0f, 1.0f);
+ }
+
+ ///
+ /// Constructs a new with the
+ /// specified red, green, and blue values and an
+ /// alpha value of 1.0f.
+ ///
+ /// The red value.
+ /// The green value.
+ /// The blue value.
+ public Color128(float r, float g, float b) {
+ R = Math.Clamp(r, 0.0f, 1.0f);
+ G = Math.Clamp(g, 0.0f, 1.0f);
+ B = Math.Clamp(b, 0.0f, 1.0f);
+ A = 1.0f;
+ }
+
+ ///
+ /// Constructs a new with the specified
+ /// value for all color channels and the specified alpha value.
+ ///
+ /// The value to set for all color channels.
+ /// The alpha value.
+ public Color128(float value, float alpha) {
+ R = Math.Clamp(value, 0.0f, 1.0f);
+ G = Math.Clamp(value, 0.0f, 1.0f);
+ B = Math.Clamp(value, 0.0f, 1.0f);
+ A = Math.Clamp(alpha, 0.0f, 1.0f);
+ }
+
+ ///
+ /// Constructs a new with the specified
+ /// value for all color channels and an alpha value of 1.0f.
+ ///
+ ///
+ public Color128(float value) {
+ R = Math.Clamp(value, 0.0f, 1.0f);
+ G = Math.Clamp(value, 0.0f, 1.0f);
+ B = Math.Clamp(value, 0.0f, 1.0f);
+ A = 1.0f;
+ }
+
+ ///
+ /// Constructs a new as the color black.
+ ///
+ public Color128() {
+ R = 0;
+ G = 0;
+ B = 0;
+ A = 1.0f;
+ }
+
+ ///
+ public Vector4 ToVector4() {
+ return new(R, G, B, A);
+ }
+
+ ///
+ public static TSelf FromVector4(Vector4 vector) where TSelf : IColor {
+ return (TSelf) (IColor) new Color128(vector.X, vector.Y, vector.Z, vector.W);
+ }
+
+ ///
+ public static Color128 FromVector4(Vector4 vector) {
+ return FromVector4(vector);
+ }
+
+ ///
+ public static TSelf Lerp(TSelf c1, TSelf c2, float t) where TSelf : IColor {
+ t = Math.Clamp(t, 0.0f, 1.0f);
+ return (TSelf) (IColor) new Color128(
+ c1.R + (c2.R - c1.R) * t,
+ c1.G + (c2.G - c1.G) * t,
+ c1.B + (c2.B - c1.B) * t,
+ c1.A + (c2.A - c1.A) * t
+ );
+ }
+
+ ///
+ public static Color128 Lerp(Color128 c1, Color128 c2, float t) {
+ return Lerp(c1, c2, t);
+ }
+
+ ///
+ /// Explicitly converts a to a .
+ ///
+ /// The to convert.
+ public static explicit operator Color32(Color128 color) {
+ return new Color32((byte) Math.Round(color.R * 255.0f), (byte) Math.Round(color.G * 255.0f), (byte) Math.Round(color.B * 255.0f), (byte) Math.Round(color.A * 255.0f));
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs
new file mode 100644
index 0000000..72e946a
--- /dev/null
+++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs
@@ -0,0 +1,221 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+using System.Globalization;
+
+namespace Catalyst.Mathematics.Color {
+
+ ///
+ /// Represents an RGBA color with an 8-bit color depth.
+ ///
+ public readonly record struct Color32 : IColor {
+
+ ///
+ /// Represents a fully opaque white color (255, 255, 255, 255).
+ ///
+ public static readonly Color32 WHITE = new(255);
+
+ ///
+ /// Represents a fully opaque red color (255, 0, 0, 255).
+ ///
+ public static readonly Color32 RED = new(255, 0, 0);
+
+ ///
+ /// Represents a fully opaque green color (0, 255, 0, 255).
+ ///
+ public static readonly Color32 GREEN = new(0, 255, 0);
+
+ ///
+ /// Represents a fully opaque blue color (0, 0, 255, 255).
+ ///
+ public static readonly Color32 BLUE = new(0, 0, 255);
+
+ ///
+ /// Represents a fully opaque black color (0, 0, 0, 255).
+ ///
+ public static readonly Color32 BLACK = new(0);
+
+ ///
+ /// Represents a fully transparent color (0, 0, 0, 0).
+ ///
+ public static readonly Color32 TRANSPARENT = new(0, 0);
+
+ ///
+ public byte R { get; init; }
+
+ ///
+ public byte G { get; init; }
+
+ ///
+ public byte B { get; init; }
+
+ ///
+ public byte A { get; init; }
+
+ ///
+ /// Constructs a new from a hexadecimal color string.
+ ///
+ ///
+ /// The hexadecimal value should follow the #RRGGBBAA format,
+ /// where the AA field is optional and defaults to FF.
+ ///
+ /// The hexadecimal color string.
+ /// Thrown if the hexadecimal value was invalid.
+ /// Thrown if the parsed hexadecimal value gave an invalid range.
+ public Color32(string hex) {
+ if (string.IsNullOrWhiteSpace(hex)) throw new ArgumentException("Hexadecimal value cannot be null or empty.", nameof(hex));
+ if (hex[0] == '#') hex = hex[1..]; // remove the leading '#'
+ if (hex.Length != 6 && hex.Length != 8) throw new ArgumentException("The hexadecimal value must be in the format #RRGGBB or #RRGGBBAA!", nameof(hex));
+ try {
+ R = byte.Parse(hex[0..2], NumberStyles.HexNumber);
+ G = byte.Parse(hex[2..4], NumberStyles.HexNumber);
+ B = byte.Parse(hex[4..6], NumberStyles.HexNumber);
+ A = hex.Length == 8 ? byte.Parse(hex[6..8], NumberStyles.HexNumber) : (byte) 0xFF;
+ } catch (FormatException) {
+ throw new ArgumentException("The hexadecimal value contains invalid characters.", nameof(hex));
+ } catch (OverflowException) {
+ throw new ArgumentException("The hexadecimal value contains numbers out of range.", nameof(hex));
+ } catch {
+ throw new ArgumentException("The hexadecimal value must be in the format #RRGGBB or #RRGGBBAA!", nameof(hex));
+ }
+ }
+
+ ///
+ /// Constructs a new from a hexadecimal color value.
+ ///
+ ///
+ /// The hexadecimal value should follow the 0xRRGGBB or 0xRRGGBBAA format,
+ /// depending on whether the parameter is set to .
+ ///
+ /// The hexadecimal color value.
+ /// Whether the hexadecimal value includes an alpha channel.
+ public Color32(uint hex, bool alpha = false) {
+ if (alpha) {
+ R = (byte) ((hex >> 24) & 0xFF);
+ G = (byte) ((hex >> 16) & 0xFF);
+ B = (byte) ((hex >> 8) & 0xFF);
+ A = (byte) (hex & 0xFF);
+ } else {
+ R = (byte) ((hex >> 16) & 0xFF);
+ G = (byte) ((hex >> 8) & 0xFF);
+ B = (byte) (hex & 0xFF);
+ A = 0xFF;
+ }
+ }
+
+ ///
+ /// Constructs a new with the
+ /// specified red, green, blue, and alpha values.
+ ///
+ /// The red value.
+ /// The green value.
+ /// The blue value.
+ /// The alpha value.
+ public Color32(byte r, byte g, byte b, byte a) {
+ R = r;
+ G = g;
+ B = b;
+ A = a;
+ }
+
+ ///
+ /// Constructs a new with the
+ /// specified red, green, and blue values and an
+ /// alpha value of 255.
+ ///
+ /// The red value.
+ /// The green value.
+ /// The blue value.
+ public Color32(byte r, byte g, byte b) {
+ R = r;
+ G = g;
+ B = b;
+ A = 255;
+ }
+
+ ///
+ /// Constructs a new with the specified
+ /// value for all color channels and the specified alpha value.
+ ///
+ /// The value to set for all color channels.
+ /// The alpha value.
+ public Color32(byte value, byte alpha) {
+ R = value;
+ G = value;
+ B = value;
+ A = alpha;
+ }
+
+ ///
+ /// Constructs a new with the specified
+ /// value for all color channels and an alpha value of 255.
+ ///
+ ///
+ public Color32(byte value) {
+ R = value;
+ G = value;
+ B = value;
+ A = 255;
+ }
+
+ ///
+ /// Constructs a new as the color black.
+ ///
+ public Color32() {
+ R = 0;
+ G = 0;
+ B = 0;
+ A = 255;
+ }
+
+ ///
+ public Vector4 ToVector4() {
+ return new(R, G, B, A);
+ }
+
+ ///
+ public static TSelf FromVector4(Vector4 vector) where TSelf : IColor {
+ return (TSelf) (IColor) new Color32(vector.X, vector.Y, vector.Z, vector.W);
+ }
+
+ ///
+ public static Color32 FromVector4(Vector4 vector) {
+ return FromVector4(vector);
+ }
+
+ ///
+ public static TSelf Lerp(TSelf c1, TSelf c2, float t) where TSelf : IColor {
+ t = Math.Clamp(t, 0.0f, 1.0f);
+ return (TSelf) (IColor) new Color32(
+ (byte) (c1.R + (c2.R - c1.R) * t),
+ (byte) (c1.G + (c2.G - c1.G) * t),
+ (byte) (c1.B + (c2.B - c1.B) * t),
+ (byte) (c1.A + (c2.A - c1.A) * t)
+ );
+ }
+
+ ///
+ public static Color32 Lerp(Color32 c1, Color32 c2, float t) {
+ return Lerp(c1, c2, t);
+ }
+
+ ///
+ /// Explicitly converts a to a .
+ ///
+ /// The to convert.
+ public static explicit operator Color128(Color32 color) {
+ return new Color128(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, color.A / 255.0f);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Color/IColor.cs b/CatalystUI/Core/CatalystUI.Mathematics/Color/IColor.cs
new file mode 100644
index 0000000..fe30f07
--- /dev/null
+++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/IColor.cs
@@ -0,0 +1,65 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System.Numerics;
+
+namespace Catalyst.Mathematics.Color {
+
+ ///
+ /// Represents a contract for representing an RGBA color.
+ ///
+ public interface IColor where T : struct, INumber {
+
+ ///
+ /// Gets the red value of the color.
+ ///
+ T R { get; }
+
+ ///
+ /// Gets the green value of the color.
+ ///
+ T G { get; }
+
+ ///
+ /// Gets the blue value of the color.
+ ///
+ T B { get; }
+
+ ///
+ /// Gets the alpha value of the color.
+ ///
+ T A { get; }
+
+ ///
+ /// Converts the color to a representation.
+ ///
+ /// A representation of the color.
+ Vector4 ToVector4();
+
+ ///
+ /// Creates a new color from a representation.
+ ///
+ /// The representation of the color.
+ /// A new color created from the .
+ static abstract TSelf FromVector4(Vector4 vector) where TSelf : IColor;
+
+ ///
+ /// Linearly interpolates between two colors.
+ ///
+ /// The first color.
+ /// The second color.
+ /// The position of the interpolation.
+ /// The interpolated color.
+ static abstract TSelf Lerp(TSelf c1, TSelf c2, float t) where TSelf : IColor;
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Geometry/Angle.cs b/CatalystUI/Core/CatalystUI.Mathematics/Geometry/Angle.cs
index 0f7d5c1..cc3af3f 100644
--- a/CatalystUI/Core/CatalystUI.Mathematics/Geometry/Angle.cs
+++ b/CatalystUI/Core/CatalystUI.Mathematics/Geometry/Angle.cs
@@ -21,35 +21,30 @@ namespace Catalyst.Mathematics.Geometry {
[StructLayout(LayoutKind.Sequential)]
public readonly record struct Angle {
- ///
- /// The underlying angle in radians.
- ///
- private readonly double _radians;
-
///
/// Gets the angle in degrees.
///
/// The angle in degrees.
- public double Degrees => RadiansToDegrees(_radians);
+ public double Degrees => RadiansToDegrees(Radians);
///
/// Gets the angle in radians.
///
/// The angle in radians.
- public double Radians => _radians;
+ public double Radians { get; }
///
/// Gets the angle in gradians (also known as gon).
///
/// The angle is gradians (gon).
- public double Gradians => RadiansToGradians(_radians);
+ public double Gradians => RadiansToGradians(Radians);
///
/// Constructs a new .
///
/// The angle in radians.
private Angle(double radians) {
- _radians = radians;
+ Radians = radians;
}
///
@@ -85,7 +80,7 @@ public static Angle FromGradians(double gradians) {
/// The normalized angle.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Angle Normalize() {
- return new((_radians % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI));
+ return new((Radians % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI));
}
///
@@ -94,7 +89,7 @@ public Angle Normalize() {
/// The quadrant of the angle.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Quadrant ToQuadrant() {
- return (Quadrant) ((int) (Normalize()._radians / (Math.PI / 2)) + 1);
+ return (Quadrant) ((int) (Normalize().Radians / (Math.PI / 2)) + 1);
}
///
@@ -172,7 +167,7 @@ public static double GradiansToDegrees(double gradians) {
/// The angle to negate.
/// The negated angle instance.
public static Angle operator -(Angle angle) {
- return new(-angle._radians);
+ return new(-angle.Radians);
}
///
@@ -181,7 +176,7 @@ public static double GradiansToDegrees(double gradians) {
/// The angle to increment.
/// A new angle instance incremented by 1 degree.
public static Angle operator ++(Angle angle) {
- return new(angle._radians + DegreesToRadians(1));
+ return new(angle.Radians + DegreesToRadians(1));
}
///
@@ -190,7 +185,7 @@ public static double GradiansToDegrees(double gradians) {
/// The angle to decrement.
/// A new angle instance decremented by 1 degree.
public static Angle operator --(Angle angle) {
- return new(angle._radians - DegreesToRadians(1));
+ return new(angle.Radians - DegreesToRadians(1));
}
///
@@ -200,7 +195,7 @@ public static double GradiansToDegrees(double gradians) {
/// The second angle to add.
/// A new angle that is the sum of the two angles.
public static Angle operator +(Angle left, Angle right) {
- return new(left._radians + right._radians);
+ return new(left.Radians + right.Radians);
}
///
@@ -210,7 +205,7 @@ public static double GradiansToDegrees(double gradians) {
/// The angle to subtract.
/// A new angle that is the difference of the two angles.
public static Angle operator -(Angle left, Angle right) {
- return new(left._radians - right._radians);
+ return new(left.Radians - right.Radians);
}
///
@@ -220,7 +215,7 @@ public static double GradiansToDegrees(double gradians) {
/// The scalar value to multiply the angle by.
/// A new angle that is the product of the angle and the scalar.
public static Angle operator *(Angle angle, double scalar) {
- return new(angle._radians * scalar);
+ return new(angle.Radians * scalar);
}
///
@@ -230,7 +225,7 @@ public static double GradiansToDegrees(double gradians) {
/// The scalar value to divide the angle by.
/// A new angle that is the quotient of the angle and the scalar.
public static Angle operator /(Angle angle, double scalar) {
- return new(angle._radians / scalar);
+ return new(angle.Radians / scalar);
}
///
@@ -240,7 +235,7 @@ public static double GradiansToDegrees(double gradians) {
/// The scalar value to apply the modulo operation with.
/// A new angle that is the result of the modulo operation.
public static Angle operator %(Angle angle, double scalar) {
- return new(angle._radians % scalar);
+ return new(angle.Radians % scalar);
}
///
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs
index 00672da..cfb7e43 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs
@@ -9,12 +9,10 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
-
-
-using System.Threading;
-// ReSharper disable once CheckNamespace
using System;
+using System.Threading;
+// ReSharper disable once CheckNamespace
namespace Catalyst.Supplementary {
///
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/FileDataConnectorBase.cs b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/FileDataConnectorBase.cs
index f12fe37..edd141f 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/FileDataConnectorBase.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/FileDataConnectorBase.cs
@@ -12,9 +12,9 @@
using Catalyst.Domains;
using Catalyst.Layers;
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
// ReSharper disable once CheckNamespace
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/IFileDataConnector.cs b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/IFileDataConnector.cs
index b2e2c42..886df0e 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/IFileDataConnector.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/IFileDataConnector.cs
@@ -9,9 +9,9 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
-using Catalyst.Layers;
using Catalyst.Connectors;
using Catalyst.Domains;
+using Catalyst.Layers;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Common/IDigitalInputData.cs b/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Common/IDigitalInputData.cs
index 88f910e..41b354f 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Common/IDigitalInputData.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Common/IDigitalInputData.cs
@@ -9,7 +9,7 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
-using Catalyst.Interactions;
+
// ReSharper disable once CheckNamespace
namespace Catalyst.Interactions {
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Devices/Mouse/MouseMovedInputData.cs b/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Devices/Mouse/MouseMovedInputData.cs
index 36a56ef..897d8a2 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Devices/Mouse/MouseMovedInputData.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Interactions/Devices/Mouse/MouseMovedInputData.cs
@@ -11,8 +11,8 @@
-using System.Diagnostics.CodeAnalysis;
using Catalyst.Mathematics;
+using System.Diagnostics.CodeAnalysis;
// ReSharper disable once CheckNamespace
namespace Catalyst.Interactions.Devices {
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/ILinuxSystemLayer.cs b/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/ILinuxSystemLayer.cs
index dada472..e4c892b 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/ILinuxSystemLayer.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/ILinuxSystemLayer.cs
@@ -9,8 +9,8 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
-using Catalyst.Layers;
using Catalyst.Domains;
+using Catalyst.Layers;
// ReSharper disable once CheckNamespace
namespace Catalyst.Supplementary {
diff --git a/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/IMacSystemLayer.cs b/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/IMacSystemLayer.cs
index d7e7d49..9caadeb 100644
--- a/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/IMacSystemLayer.cs
+++ b/CatalystUI/Core/CatalystUI.Supplementary/Layers/SystemLayer/IMacSystemLayer.cs
@@ -9,8 +9,8 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
-using Catalyst.Layers;
using Catalyst.Domains;
+using Catalyst.Layers;
// ReSharper disable once CheckNamespace
namespace Catalyst.Supplementary {
diff --git a/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs b/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
index f448e02..f7ba7b0 100644
--- a/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
+++ b/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
@@ -65,6 +65,12 @@ public sealed class DelegateQueue : IDisposable {
///
private readonly StaticArrayQueue _queue;
+ ///
+ /// Gets a read-only collection for the underlying queue of enqueued delegates.
+ ///
+ /// The underlying queue of enqueued delegates.
+ public IReadOnlyCollection Queue => _queue;
+
///
/// A wait handle that can be used to block until the queue is not full.
///
@@ -85,12 +91,6 @@ public sealed class DelegateQueue : IDisposable {
///
private readonly Lock _lock;
- ///
- /// Gets a read-only collection for the underlying queue of enqueued delegates.
- ///
- /// The underlying queue of enqueued delegates.
- public IReadOnlyCollection Queue => _queue;
-
///
/// Constructs a new .
///
diff --git a/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs b/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs
index 66a3e0f..22925bc 100644
--- a/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs
+++ b/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs
@@ -98,14 +98,21 @@ public sealed class ThreadDelegateDispatcher : IDisposable {
public static bool IsMainThreadCaptured => MainThreadDispatcher != null;
///
- /// The dispatcher's worker thread, or if it is associated with the main thread.
+ /// Gets the worker thread of the dispatcher.
///
- private readonly Thread? _thread;
+ /// The dispatcher's worker thread, or if it is associated with the main thread.
+ public Thread? Thread { get; }
///
- /// The ID of the dispatcher's worker thread.
+ /// Gets the managed thread ID of dispatcher's worker thread.
///
- private readonly int _threadId;
+ /// The dispatcher's worker thread managed thread ID.
+ public int ThreadId { get; }
+
+ ///
+ /// Gets the number of delegates currently enqueued in the dispatcher.
+ ///
+ public int Enqueued => _queue.Queue.Count;
///
/// The queue of delegates to be executed on the thread.
@@ -122,23 +129,6 @@ public sealed class ThreadDelegateDispatcher : IDisposable {
///
private readonly Lock _lock;
- ///
- /// Gets the worker thread of the dispatcher.
- ///
- /// The dispatcher's worker thread, or if it is associated with the main thread.
- public Thread? Thread => _thread;
-
- ///
- /// Gets the managed thread ID of dispatcher's worker thread.
- ///
- /// The dispatcher's worker thread managed thread ID.
- public int ThreadId => _threadId;
-
- ///
- /// Gets the number of delegates currently enqueued in the dispatcher.
- ///
- public int Enqueued => _queue.Queue.Count;
-
///
/// Constructs a new .
///
@@ -147,14 +137,16 @@ public sealed class ThreadDelegateDispatcher : IDisposable {
/// The size of the delegate queue, or to use the default value of .
private ThreadDelegateDispatcher(Thread thread, int threadId, int? queueSize = null) {
// Fields
- _thread = thread;
- _threadId = threadId;
_queue = new(queueSize);
_queue.DelegateEnqueued += HandleDelegateEnqueued;
_queue.DelegateDequeued += HandleDelegateDequeued;
_queue.DelegateExecuted += HandleDelegateExecuted;
_disposed = false;
_lock = new();
+
+ // Properties
+ Thread = thread;
+ ThreadId = threadId;
}
///
@@ -252,7 +244,7 @@ public bool Execute(in Action @deleg
ActionCache.Lock.Enter();
bool shouldUnlock = true;
try {
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@delegate(caller, parameters);
return true;
} else {
@@ -295,7 +287,7 @@ public bool Execute(in Action @delegate, TCaller caller, bool
ActionCache.Lock.Enter();
bool shouldUnlock = true;
try {
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@delegate(caller);
return true;
@@ -335,7 +327,7 @@ public bool Execute(in Action @delegate, TCaller caller, bool
public bool Execute(in Action @delegate, nint caller, nint parameters, bool wait = false, int timeout = -2) {
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout == -2) timeout = LockoutTimeout;
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@delegate(caller, parameters);
return true;
} else {
@@ -347,7 +339,7 @@ public bool Execute(in Action @delegate, nint caller, nint parameter
public bool Execute(in Action @delegate, nint caller, bool wait = false, int timeout = -2) {
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout == -2) timeout = LockoutTimeout;
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@delegate(caller);
return true;
} else {
@@ -359,7 +351,7 @@ public bool Execute(in Action @delegate, nint caller, bool wait = false, i
public bool Execute(in Action @delegate, bool wait = false, int timeout = -2) {
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout == -2) timeout = LockoutTimeout;
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@delegate();
return true;
} else {
@@ -374,7 +366,7 @@ public bool Execute(in Func.Lock.Enter();
bool shouldUnlock = true;
try {
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@return = @delegate(caller, parameters);
return true;
} else {
@@ -417,7 +409,7 @@ public bool Execute(in Func @delegate, TCall
FunctionCache.Lock.Enter();
bool shouldUnlock = true;
try {
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@return = @delegate(caller);
return true;
@@ -460,7 +452,7 @@ public bool Execute(in Func @delegate, out TReturn @return, in
FunctionCache.Lock.Enter();
bool shouldUnlock = true;
try {
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@return = @delegate();
return true;
} else {
@@ -498,7 +490,7 @@ public bool Execute(in Func @delegate, out TReturn @return, in
public bool Execute(in Func @delegate, nint caller, nint parameters, out nint @return, int timeout = -2) {
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout == -2) timeout = LockoutTimeout;
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@return = @delegate(caller, parameters);
return true;
} else {
@@ -510,7 +502,7 @@ public bool Execute(in Func @delegate, nint caller, nint param
public bool Execute(in Func @delegate, nint caller, out nint @return, int timeout = -2) {
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout == -2) timeout = LockoutTimeout;
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@return = @delegate(caller);
return true;
} else {
@@ -522,7 +514,7 @@ public bool Execute(in Func @delegate, nint caller, out nint @return
public bool Execute(in Func @delegate, out nint @return, int timeout = -2) {
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout == -2) timeout = LockoutTimeout;
- if (Environment.CurrentManagedThreadId == _threadId) {
+ if (Environment.CurrentManagedThreadId == ThreadId) {
@return = @delegate();
return true;
} else {
diff --git a/CatalystUI/Modules/Arcane/CatalystUI.Modules.Arcane.Ini/Components/IniComponent.cs b/CatalystUI/Modules/Arcane/CatalystUI.Modules.Arcane.Ini/Components/IniComponent.cs
index f9bf257..e9f7841 100644
--- a/CatalystUI/Modules/Arcane/CatalystUI.Modules.Arcane.Ini/Components/IniComponent.cs
+++ b/CatalystUI/Modules/Arcane/CatalystUI.Modules.Arcane.Ini/Components/IniComponent.cs
@@ -77,7 +77,7 @@ public IniComponent(IReadOnlyDictionary? entries = null, IReadO
}
///
- public void SetValue(string key, string? value) {
+ public virtual void SetValue(string key, string? value) {
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key), "The key cannot be null or whitespace.");
_lock.Enter();
try {
@@ -88,7 +88,7 @@ public void SetValue(string key, string? value) {
}
///
- public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) {
+ public virtual bool TryGetValue(string key, [NotNullWhen(true)] out string? value) {
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key), "The key cannot be null or whitespace.");
_lock.Enter();
try {
@@ -99,7 +99,7 @@ public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) {
}
///
- public bool TryRemoveValue(string key, [NotNullWhen(true)] out string? removed) {
+ public virtual bool TryRemoveValue(string key, [NotNullWhen(true)] out string? removed) {
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key), "The key cannot be null or whitespace.");
_lock.Enter();
try {
@@ -110,7 +110,7 @@ public bool TryRemoveValue(string key, [NotNullWhen(true)] out string? removed)
}
///
- public void SetSection(IIniSectionComponent section) {
+ public virtual void SetSection(IIniSectionComponent section) {
if (section is null) throw new ArgumentNullException(nameof(section), "The section cannot be null.");
_lock.Enter();
try {
@@ -126,7 +126,7 @@ public void SetSection(IIniSectionComponent section) {
}
///
- public bool TryGetSection(string name, [NotNullWhen(true)] out IIniSectionComponent? section) {
+ public virtual bool TryGetSection(string name, [NotNullWhen(true)] out IIniSectionComponent? section) {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "The section name cannot be null or whitespace.");
_lock.Enter();
try {
@@ -138,7 +138,7 @@ public bool TryGetSection(string name, [NotNullWhen(true)] out IIniSectionCompon
}
///
- public bool TryRemoveSection(string name, [NotNullWhen(true)] out IIniSectionComponent? removed) {
+ public virtual bool TryRemoveSection(string name, [NotNullWhen(true)] out IIniSectionComponent? removed) {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "The section name cannot be null or whitespace.");
_lock.Enter();
try {
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/CatalystUI.Modules.Crystal.Core.csproj b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/CatalystUI.Modules.Crystal.Core.csproj
new file mode 100644
index 0000000..482064b
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/CatalystUI.Modules.Crystal.Core.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Catalyst.Modules.Crystal
+ Catalyst.Modules.Crystal
+
+
+ CatalystUI Crystal Core
+ 1.0.0
+ alpha.1
+ CatalystUI LLC
+ Core API for the Crystal subset of modules provided by the CatalystUI library.
+ CatalystUI,Crystal,core,ui,gui,graphics,visual
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs
new file mode 100644
index 0000000..5c37d90
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs
@@ -0,0 +1,26 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Domains;
+using Catalyst.Layers;
+
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents the renderer layer for visual or graphical rendering.
+ ///
+ public interface IRendererLayer : IRendererLayer {
+
+ // ...
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs
new file mode 100644
index 0000000..ff4a09f
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs
@@ -0,0 +1,47 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Domains;
+using Catalyst.Layers;
+using Catalyst.Threading;
+using System.Collections.Generic;
+
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents the window layer for visual or graphical windows.
+ ///
+ public interface IWindowLayer : IWindowLayer {
+
+ ///
+ /// Queries the window for connected displays.
+ ///
+ /// The dispatcher to use for thread-affine operations, or to use the main thread dispatcher.
+ /// An enumerable collection of connected displays.
+ IEnumerable GetDisplays(ThreadDelegateDispatcher? dispatcher = null);
+
+ ///
+ /// Queries the window for the primary display.
+ ///
+ /// The dispatcher to use for thread-affine operations, or to use the main thread dispatcher.
+ /// The primary display, or if one could not be determined.
+ IDisplay? GetPrimaryDisplay(ThreadDelegateDispatcher? dispatcher = null);
+
+ ///
+ /// Constructs a new window with the specified options.
+ ///
+ /// The options to use when creating the window, or to use defaults.
+ /// The created window.
+ IWindow CreateWindow(WindowOptions? options = null);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs
new file mode 100644
index 0000000..e0edf99
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs
@@ -0,0 +1,68 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents a module or extension that will
+ /// be given an opportunity to render content.
+ ///
+ ///
+ /// Render layers allow the rendering systems to be
+ /// more modular by providing discrete layers,
+ /// each of which has an opportunity to render content. The
+ /// renderer passes through each layer in sequence,
+ /// providing complete control over the rendering process.
+ /// A layer may choose to render nothing, or may
+ /// render complex scenes as needed.
+ ///
+ ///
+ /// One layer may perform complex 3D rendering of a scene,
+ /// while another layer may render a user interface overlay
+ /// on top of the 3D content.
+ ///
+ public interface IRenderLayer {
+
+ ///
+ /// Invoked when the layer encounters an error during rendering.
+ ///
+ event IRenderer.RendererErroredEventHandler? Errored;
+
+ ///
+ /// Performs rendering operations using the provided renderer.
+ ///
+ /// The renderer to use for rendering operations.
+ void Render(IRenderer renderer);
+
+ ///
+ /// Occurs when the layer is registered with a renderer.
+ ///
+ ///
+ /// Provides an opportunity for the layer to allocate
+ /// the resources it needs for rendering.
+ ///
+ /// The renderer the layer is being registered with.
+ void OnRegistered(IRenderer renderer);
+
+ ///
+ /// Occurs when the layer is unregistered from a renderer.
+ ///
+ ///
+ /// Provides an opportunity for the layer to clean up
+ /// the resources allocated during registration.
+ ///
+ /// The renderer the layer is being unregistered from.
+ void OnUnregistered(IRenderer renderer);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs
new file mode 100644
index 0000000..6cd6f0d
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs
@@ -0,0 +1,137 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents a target for rendering operations.
+ ///
+ ///
+ ///
+ /// A render target is composed to two primary bounds: a surface and a target area.
+ /// A target area is the portion of the surface that is actively being rendered to,
+ /// whereas the surface is the overall area that contains the render target. It
+ /// may or may not be the same size as the target area.
+ ///
+ ///
+ public interface IRenderTarget {
+
+ ///
+ /// Delegate for that doesn't pass any additional arguments.
+ ///
+ delegate void RenderTargetEventHandler(IRenderTarget renderTarget);
+
+ ///
+ /// Invoked when the render target has been modified
+ /// and requires a new frame to be rendered.
+ ///
+ event RenderTargetEventHandler? Updated;
+
+ ///
+ /// Invoked when the render target is about to be destroyed,
+ /// but has not yet been disposed.
+ ///
+ ///
+ /// It is important the surface continues to accept calls
+ /// during the destruction phase, as some renderers
+ /// may need to finalize operations before the target is disposed.
+ ///
+ event RenderTargetEventHandler? Destroying;
+
+ ///
+ /// Gets the system's native handle(s) for the render target.
+ ///
+ ///
+ ///
+ /// Handle format and quantity may vary between platforms.
+ /// For example, on Windows it may return a single HWND handle,
+ /// while on Linux with X11 it may return the display server
+ /// or graphics context handle along with the window ID.
+ ///
+ ///
+ /// The 0th index is always expected to contain the primary window handle.
+ ///
+ ///
+ /// The native handle(s) for the render target.
+ nint[] NativeHandle { get; }
+
+ ///
+ /// Gets the horizontal position of the render
+ /// target relative to its surface.
+ ///
+ /// The horizontal position of the render target.
+ double X { get; }
+
+ ///
+ /// Gets the vertical position of the render
+ /// target relative to its surface.
+ ///
+ /// The vertical position of the render target.
+ double Y { get; }
+
+ ///
+ /// Gets the width of the render target in pixels.
+ ///
+ /// The width of the render target in pixels.
+ uint Width { get; }
+
+ ///
+ /// Gets the height of the render target in pixels.
+ ///
+ /// The height of the render target in pixels.
+ uint Height { get; }
+
+ ///
+ /// Gets the horizontal position of the surface in pixels.
+ ///
+ /// The horizontal position of the surface in pixels.
+ double SurfaceX { get; }
+
+ ///
+ /// Gets the vertical position of the surface in pixels.
+ ///
+ /// The vertical position of the surface in pixels.
+ double SurfaceY { get; }
+
+ ///
+ /// Gets the width of the surface in pixels.
+ ///
+ /// The width of the surface in pixels.
+ double SurfaceWidth { get; }
+
+ ///
+ /// Gets the height of the surface in pixels.
+ ///
+ /// The height of the surface in pixels.
+ double SurfaceHeight { get; }
+
+ ///
+ /// Gets the pixel density of the surface.
+ ///
+ /// The pixel density of the surface.
+ double SurfacePixelDensity { get; }
+
+ ///
+ /// Gets the DPI (dots per inch) of the surface.
+ ///
+ /// The DPI of the surface.
+ double SurfaceDpi { get; }
+
+ ///
+ /// Gets a value indicating whether the render target is currently enabled.
+ ///
+ /// if the render target is enabled; otherwise, .
+ bool IsEnabled { get; }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs
new file mode 100644
index 0000000..5ad0e6b
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs
@@ -0,0 +1,170 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Mathematics.Color;
+using System;
+using System.Collections.Generic;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents a visual renderer for a graphical window.
+ ///
+ public interface IRenderer : IDisposable {
+
+ ///
+ /// Delegate for that doesn't pass any additional arguments.
+ ///
+ delegate void RendererEventHandler(IRenderer renderer);
+
+ ///
+ /// Delegate for that passes a .
+ ///
+ delegate void RendererErroredEventHandler(IRenderer renderer, RendererException exception);
+
+ ///
+ /// Invoked when the renderer encounters an error.
+ ///
+ event RendererErroredEventHandler? Errored;
+
+ ///
+ /// Invoked when the renderer has been created.
+ ///
+ ///
+ /// Raised after the renderer has been prepared
+ /// and is ready to begin rendering.
+ ///
+ event RendererEventHandler? Created;
+
+ ///
+ /// Invoked when the renderer is about to start a new frame.
+ ///
+ event RendererEventHandler? FrameStart;
+
+ ///
+ /// Invoked when the renderer has finished rendering a frame.
+ ///
+ event RendererEventHandler? FrameEnd;
+
+ ///
+ /// Gets or sets the render target for the renderer.
+ ///
+ /// The render target, or if none is set.
+ IRenderTarget? Target { get; set; }
+
+ ///
+ /// Gets the collection of render layers used by the renderer.
+ ///
+ /// A read-only list of render layers.
+ IReadOnlyList Layers { get; }
+
+ ///
+ /// Gets or sets a value indicating whether vertical synchronization (VSync) is enabled.
+ ///
+ /// if VSync is enabled; otherwise, .
+ bool UseVSync { get; set; }
+
+ ///
+ /// Gets or sets the target frame rate for the renderer.
+ ///
+ ///
+ ///
+ /// A renderer's frame rate can be considered either active or passive.
+ /// Passive rendering uses reactive event handling, whereas
+ /// active polling continuously requests new frames at the
+ /// specified rate in frames per second (FPS).
+ ///
+ ///
+ /// Some systems may require rendering operations to be performed
+ /// on the main thread of the application. In such cases,
+ /// setting a high frame rate may lead to increased CPU usage
+ /// as the main thread is frequently interrupted to handle
+ /// render requests. Conversely, a very low frame rate may result
+ /// in a less responsive user interface.
+ ///
+ ///
+ /// It is generally recommended to prefer passive rendering
+ /// where possible, and to use active rendering for real-time
+ /// applications such as video games or simulations.
+ ///
+ ///
+ ///
+ ///
+ /// - 0 represents passive rendering.
+ /// - >=1 represents the rate at which the renderer will request new frames in frames per second (FPS).
+ /// - represents an unlimited rendering rate.
+ ///
+ ///
+ ushort TargetFrameRate { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to measure and report the frame rate.
+ ///
+ /// to measure frame rate; otherwise, .
+ ///
+ ///
+ bool ShouldMeasureFrameRate { get; set; }
+
+ ///
+ /// Gets the average frame rate of the renderer.
+ ///
+ ///
+ /// Only available if is enabled.
+ ///
+ /// The average frame rate in frames per second (FPS).
+ ///
+ double AvgFrameRate { get; }
+
+ ///
+ /// Gets the average delta time (in seconds) between frames.
+ ///
+ ///
+ /// Only available if is enabled.
+ ///
+ /// The average delta time in seconds.
+ ///
+ double AvgDeltaTime { get; }
+
+ ///
+ /// Sets the render target for the renderer.
+ ///
+ /// The render target to set.
+ void SetTarget(IRenderTarget target);
+
+ ///
+ /// Attempts to register a new render layer with the renderer.
+ ///
+ /// The render layer to register.
+ /// if the layer was successfully registered; otherwise, .
+ bool TryRegisterLayer(IRenderLayer layer);
+
+ ///
+ /// Unregisters a render layer from the renderer.
+ ///
+ /// The render layer to unregister.
+ void UnregisterLayer(IRenderLayer layer);
+
+ ///
+ /// Requests all registered layers to render their content,
+ /// then presents the final output to the render target.
+ ///
+ void Render();
+
+ ///
+ /// Clears the rendering surface with the specified color.
+ ///
+ /// The color to clear the surface with.
+ void Clear(Color128 color);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs
new file mode 100644
index 0000000..813dcbb
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs
@@ -0,0 +1,49 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents an error that occurs during rendering operations.
+ ///
+ ///
+ public class RendererException : Exception {
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RendererException() : base() {
+ // ...
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message.
+ ///
+ public RendererException(string message) : base(message) {
+ // ...
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message and a reference to the inner exception
+ /// that is the cause of this exception.
+ ///
+ public RendererException(string message, Exception innerException) : base(message, innerException) {
+ // ...
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/DisplayOrientation.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/DisplayOrientation.cs
new file mode 100644
index 0000000..b16da91
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/DisplayOrientation.cs
@@ -0,0 +1,77 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Mathematics.Geometry;
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// A list of possible orientations for a display.
+ ///
+ ///
+ /// The value of each orientation corresponds to the clockwise rotation angle of the display.
+ /// For example, is 90 degrees, and is 180 degrees.
+ ///
+ public enum DisplayOrientation {
+
+ ///
+ /// The display is oriented in landscape mode.
+ ///
+ /// The clockwise orientation of the display in degrees.
+ Landscape = 0,
+
+ ///
+ /// The display is oriented in portrait mode.
+ ///
+ /// The clockwise orientation of the display in degrees.
+ Portrait = 90,
+
+ ///
+ /// The display is flipped and oriented in landscape mode.
+ ///
+ /// The clockwise orientation of the display in degrees.
+ LandscapeFlipped = 180,
+
+ ///
+ /// The display is flipped and oriented in portrait mode.
+ ///
+ /// The clockwise orientation of the display in degrees.
+ PortraitFlipped = 270
+
+ }
+
+ ///
+ /// Extension methods for enum.
+ ///
+ public static class DisplayOrientationExtensions {
+
+ ///
+ /// Converts a rotational angle to a .
+ ///
+ /// The angle to convert, in degrees.
+ /// The corresponding .
+ public static DisplayOrientation ToOrientation(this Angle rotation) {
+ Angle shifted = Angle.FromRadians((rotation.Normalize().Radians + Math.PI / 4) % (2 * Math.PI));
+ Quadrant quadrant = shifted.ToQuadrant();
+ return quadrant switch {
+ Quadrant.First => DisplayOrientation.Landscape,
+ Quadrant.Second => DisplayOrientation.Portrait,
+ Quadrant.Third => DisplayOrientation.LandscapeFlipped,
+ Quadrant.Fourth => DisplayOrientation.PortraitFlipped,
+ _ => throw new ArgumentOutOfRangeException(nameof(rotation), "Invalid rotation angle for display orientation.")
+ };
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs
new file mode 100644
index 0000000..2159863
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs
@@ -0,0 +1,177 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Mathematics.Geometry;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents a visual or graphical display device.
+ ///
+ public interface IDisplay {
+
+ ///
+ /// Gets the device descriptor of the display.
+ ///
+ /// The display's device descriptor.
+ string Descriptor { get; }
+
+ ///
+ /// Gets the manufacturer of the display.
+ ///
+ /// The display's manufacturer, or if one could not be determined.
+ string? Manufacturer { get; }
+
+ ///
+ /// Gets the refresh rate of the display in hertz (Hz).
+ ///
+ /// The display's refresh rate.
+ double RefreshRate { get; }
+
+ ///
+ /// Gets the display's horizontal position relative to the
+ /// primary display in pixels.
+ ///
+ ///
+ /// The horizontal position will always be reported as
+ /// the distance from the primary display's left edge.
+ /// Negative values indicate the display is to the left
+ /// of the primary display, while positive values indicate
+ /// the display is to the right of the primary display.
+ ///
+ /// The display's horizontal position in pixels.
+ double X { get; }
+
+ ///
+ /// Gets the display's vertical position relative to the
+ /// primary display in pixels.
+ ///
+ ///
+ /// The vertical position will always be reported as
+ /// the distance from the primary display's top edge.
+ /// Negative values indicate the display is above the
+ /// primary display, while positive values indicate
+ /// the display is below the primary display.
+ ///
+ /// The display's vertical position in pixels.
+ double Y { get; }
+
+ ///
+ /// Gets the physical width of the display in pixels.
+ ///
+ /// The display's width in pixels.
+ uint Width { get; }
+
+ ///
+ /// Gets the physical height of the display in pixels.
+ ///
+ /// The display's height in pixels.
+ uint Height { get; }
+
+ ///
+ /// Gets the display's rotation in degrees.
+ ///
+ ///
+ ///
+ /// A display's rotation is a more precise value for
+ /// describing the display's orientation. If the
+ /// rotation cannot be determined, then the display's
+ /// rotation is reported as the 's
+ /// underlying rotational value.
+ ///
+ ///
+ /// Positive values rotate the display clockwise,
+ /// whereas negative values rotate the display counter-clockwise.
+ ///
+ ///
+ /// The display's rotation in degrees.
+ Angle Rotation { get; }
+
+ ///
+ /// Gets the orientation of the display.
+ ///
+ /// The display's orientation.
+ DisplayOrientation Orientation { get; }
+
+ ///
+ /// Gets the number of pixels per inch of the display.
+ ///
+ ///
+ ///
+ /// Also known as dots per inch (DPI), the default
+ /// value is 96 PPI (pixels per inch). For
+ /// one pixel, this would be known as 3/4
+ /// of a point in typography, which is defined
+ /// as 1/72 of an inch.
+ ///
+ ///
+ /// The PPI will always be reported as the
+ /// physical pixel density of the display,
+ /// and scaling should be performed
+ /// by using the
+ /// property instead.
+ ///
+ ///
+ /// In the context of the CatalystUI framework,
+ /// the PPI and DPI are considered equivalent
+ /// values, with both referring to the same
+ /// context of pixel density for a display.
+ ///
+ ///
+ /// For more information on the differences between
+ /// PPI and DPI, see the following resources:
+ ///
+ /// Wikipedia - Pixel Density
+ ///
+ /// Wikipedia - Dots Per Inch
+ ///
+ ///
+ /// The display's pixel density in pixels per inch.
+ double PixelsPerInch { get; }
+
+ ///
+ /// Gets the scaling factor of the display.
+ ///
+ ///
+ ///
+ /// A scaling factor is used to determine the
+ /// display's logical size in pixels, and
+ /// is represented as a decimal percentage.
+ /// The value of this percentage
+ /// is set by the user and reported by
+ /// the user's system.
+ ///
+ ///
+ /// Larger values indicate a smaller logical
+ /// display size, which results in multiple
+ /// physical pixels being used to represent
+ /// one logical pixel. As a consequence,
+ /// the perceived size of the display
+ /// is increased.
+ ///
+ ///
+ ///
+ /// A scaling factor of 1.25
+ /// indicates the display's physical
+ /// size should be scaled by 125%.
+ /// If the reported physical width of the display
+ /// is 1920 pixels, then to calculate
+ /// the logical width, you would
+ /// perform 1920 / 1.25 to get
+ /// the value of 1536.
+ ///
+ /// The display's scaling factor as a decimal percentage.
+ double ScalingFactor { get; }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs
new file mode 100644
index 0000000..28f0cc6
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs
@@ -0,0 +1,511 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Interactions;
+using Catalyst.Threading;
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents the logical, interactive, visual or graphical window into the system.
+ ///
+ public interface IWindow : IDisposable {
+
+ ///
+ /// The default value for .
+ ///
+ const uint DEFAULT_WIDTH = 800;
+
+ ///
+ /// The default value for .
+ ///
+ const uint DEFAULT_HEIGHT = 450;
+
+ ///
+ /// The default value for .
+ ///
+ const string DEFAULT_TITLE = "Catalyst Window";
+
+ ///
+ /// Delegate for that doesn't pass any additional arguments.
+ ///
+ delegate void WindowEventHandler(IWindow window);
+
+ ///
+ /// Delegate for that passes a .
+ ///
+ delegate void WindowErroredEventHandler(IWindow window, WindowException exception);
+
+ ///
+ /// Delegate for that passes an .
+ ///
+ delegate void WindowInteractedEventHandler(IWindow window, IInteraction interaction);
+
+ ///
+ /// Delegate for that allows cancellation of the close operation.
+ ///
+ /// to cancel the close operation; otherwise, .
+ delegate bool WindowClosingEventHandler(IWindow window);
+
+ ///
+ /// Invoked when the window encounters an error.
+ ///
+ event WindowErroredEventHandler? Errored;
+
+ ///
+ /// Invoked when the window is created.
+ ///
+ ///
+ /// Raised after the window has been created,
+ /// but prior to the first event being processed.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Created;
+
+ ///
+ /// Invoked when the window's position on the display has changed.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Repositioned;
+
+ ///
+ /// Invoked when the window's size has changed.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Resized;
+
+ ///
+ /// Invoked when the window's contents may need to be partially redrawn.
+ ///
+ ///
+ event WindowEventHandler? Refresh;
+
+ ///
+ /// Invoked when the window's contents needs to be entirely redrawn.
+ ///
+ ///
+ event WindowEventHandler? Redraw;
+
+ ///
+ /// Invoked when the window gains input focus.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Focused;
+
+ ///
+ /// Invoked when the window loses input focus.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Unfocused;
+
+ ///
+ /// Invoked when the window is minimized to the taskbar, dock, or another similar system feature.
+ ///
+ ///
+ event WindowEventHandler? Minimized;
+
+ ///
+ /// Invoked when the window is maximized to fill the available display area.
+ ///
+ ///
+ event WindowEventHandler? Maximized;
+
+ ///
+ /// Invoked when the window is restored from a minimized or maximized state.
+ ///
+ event WindowEventHandler? Restored;
+
+ ///
+ /// Invoked when a window becomes visible, typically after being hidden.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Shown;
+
+ ///
+ /// Invoked when a window becomes hidden.
+ ///
+ ///
+ /// Raised when the window is no longer visible to the user,
+ /// which is different from Minimization.
+ ///
+ ///
+ ///
+ event WindowEventHandler? Hidden;
+
+ ///
+ /// Invoked when the window is about to close.
+ ///
+ ///
+ /// Raised prior to the window closing. Handlers can
+ /// request to cancel the close operation by returning .
+ /// Not all systems support cancelling the close operation,
+ /// or the request may be ignored under certain circumstances.
+ ///
+ event WindowClosingEventHandler? Closing;
+
+ ///
+ /// Invoked when the window has closed and resources have been released.
+ ///
+ ///
+ event WindowEventHandler? Closed;
+
+ ///
+ /// Invoked when the window has received user interaction.
+ ///
+ event WindowInteractedEventHandler? Interacted;
+
+ ///
+ /// Gets the threading dispatcher associated with the window.
+ ///
+ ///
+ /// On macOS, iOS, and certain other platforms,
+ /// all windowing operations must be performed
+ /// on the main thread of the application.
+ /// On these platforms, the dispatcher must be set
+ /// to a captured
+ /// for windows to function correctly.
+ ///
+ /// The window's thread dispatcher.
+ ThreadDelegateDispatcher Dispatcher { get; }
+
+ ///
+ /// Gets the system's native handle(s) for the window.
+ ///
+ ///
+ ///
+ /// Handle format and quantity may vary between platforms.
+ /// For example, on Windows, it may return a single HWND handle,
+ /// while on Linux with X11, it may return the display server
+ /// or graphics context handle along with the window ID.
+ ///
+ ///
+ /// The 0th index is always expected to contain the primary window handle.
+ ///
+ ///
+ /// The native handle(s) for the window.
+ nint[] NativeHandle { get; }
+
+ ///
+ /// Gets or sets the polling rate for the window's event loop.
+ ///
+ ///
+ ///
+ /// A window's polling rate can be considered either active or passive.
+ /// Passive polling uses reactive event handling, whereas
+ /// active polling continuously checks for events at the
+ /// specified rate in milliseconds (ms).
+ ///
+ ///
+ /// Many systems require windowing operations to be performed
+ /// on the main thread of the application. In such cases,
+ /// setting a high poll rate may lead to increased CPU usage
+ /// as the main thread is frequently interrupted to handle
+ /// window events. Conversely, a very low poll rate may result
+ /// in sluggish responsiveness to user interactions.
+ ///
+ ///
+ /// It is generally recommended to prefer passive event handling
+ /// where possible, and to use active polling for real-time
+ /// applications such as video games or simulations.
+ ///
+ ///
+ ///
+ ///
+ /// - 0 represents passive polling.
+ /// - >=1 represents the rate at which the window will be polled in milliseconds (ms).
+ /// - represents an unlimited polling rate.
+ ///
+ ///
+ ushort PollRate { get; set; }
+
+ ///
+ /// Gets the display that the window is currently associated with.
+ ///
+ /// The display or if the window is not associated with any display.
+ ///
+ IDisplay? Display { get; }
+
+ ///
+ /// Gets or sets the text displayed in the title bar of the window.
+ ///
+ ///
+ /// The title typically appears in a window's title bar,
+ /// but its visibility and location may vary depending
+ /// on the system. Some systems or window decorations
+ /// may not support or display the title at all.
+ ///
+ string Title { get; set; }
+
+ ///
+ /// Gets or sets the horizontal position of the window
+ /// relative to the top-left corner of the primary display
+ /// in pixels.
+ ///
+ /// The window's horizontal position in pixels.
+ ///
+ double X { get; set; }
+
+ ///
+ /// Gets or sets the vertical position of the window
+ /// relative to the top-left corner of the primary display
+ /// in pixels.
+ ///
+ /// The window's vertical position in pixels.
+ ///
+ double Y { get; set; }
+
+ ///
+ /// Gets or sets the minimum allowed width of the window in pixels.
+ ///
+ /// The window's minimum width in pixels.
+ ///
+ uint MinimumWidth { get; set; }
+
+ ///
+ /// Gets or sets the current width of the window in pixels.
+ ///
+ /// The window's width in pixels.
+ ///
+ uint Width { get; set; }
+
+ ///
+ /// Gets or sets the maximum allowed width of the window in pixels.
+ ///
+ /// The window's maximum width in pixels.
+ ///
+ uint MaximumWidth { get; set; }
+
+ ///
+ /// Gets or sets the minimum allowed height of the window in pixels.
+ ///
+ /// The window's minimum height in pixels.
+ ///
+ uint MinimumHeight { get; set; }
+
+ ///
+ /// Gets or sets the current height of the window in pixels.
+ ///
+ /// The window's height in pixels.
+ ///
+ uint Height { get; set; }
+
+ ///
+ /// Gets or sets the maximum allowed height of the window in pixels.
+ ///
+ /// The window's maximum height in pixels.
+ ///
+ uint MaximumHeight { get; set; }
+
+ ///
+ /// Gets or sets the current fullscreen mode of the window.
+ ///
+ ///
+ /// The fullscreen mode determines how the window
+ /// appears on the display.
+ ///
+ /// The window's fullscreen mode.
+ ///
+ WindowFullscreenMode FullscreenMode { get; set; }
+
+ ///
+ /// Gets a value indicating whether the window can be resized by the user.
+ ///
+ /// if the window is resizable; otherwise, .
+ bool IsResizable { get; }
+
+ ///
+ /// Gets a value indicating whether the window has decorations such as borders and title bar.
+ ///
+ /// if the window is decorated; otherwise, .
+ bool IsDecorated { get; }
+
+ ///
+ /// Gets a value indicating whether the window currently has input focus.
+ ///
+ /// if the window is focused; otherwise, .
+ ///
+ ///
+ bool IsFocused { get; }
+
+ ///
+ /// Gets a value indicating whether the window currently does not have input focus.
+ ///
+ /// if the window is unfocused; otherwise, .
+ ///
+ bool IsUnfocused { get; }
+
+ ///
+ /// Gets a value indicating whether the window is currently minimized.
+ ///
+ /// if the window is minimized; otherwise, .
+ ///
+ ///
+ bool IsMinimized { get; }
+
+ ///
+ /// Gets a value indicating whether the window is currently maximized.
+ ///
+ /// if the window is maximized; otherwise, .
+ ///
+ ///
+ bool IsMaximized { get; }
+
+ ///
+ /// Gets a value indicating whether the window is currently visible to the user.
+ ///
+ /// if the window is visible; otherwise, .
+ ///
+ ///
+ bool IsVisible { get; }
+
+ ///
+ /// Gets a value indicating whether the window is currently hidden from the user.
+ ///
+ /// if the window is hidden; otherwise, .
+ ///
+ ///
+ bool IsHidden { get; }
+
+ ///
+ /// Gets a value indicating whether the window has been closed.
+ ///
+ ///
+ /// A closed window has released its resources and has been disposed of.
+ ///
+ /// if the window is closed; otherwise, .
+ ///
+ ///
+ bool IsClosed { get; }
+
+ ///
+ /// Sets the window's current position relative to the
+ /// top-left corner of the primary display in pixels.
+ ///
+ /// The desired horizontal position in pixels.
+ /// The desired vertical position in pixels.
+ void SetPosition(double x, double y);
+
+ ///
+ /// Sets the window's current size in pixels.
+ ///
+ /// The desired width in pixels.
+ /// The desired height in pixels.
+ void SetSize(uint width, uint height);
+
+ ///
+ /// Sets the window's size limits in pixels.
+ ///
+ /// The desired minimum width in pixels.
+ /// The desired minimum height in pixels.
+ /// The desired maximum width in pixels.
+ /// The desired maximum height in pixels.
+ void SetSizeLimits(uint minWidth, uint minHeight, uint maxWidth, uint maxHeight);
+
+ ///
+ /// Sets the window's fullscreen mode.
+ ///
+ /// The fullscreen mode.
+ /// The display to use in fullscreen mode, or to use the current display.
+ /// The override width in pixels when using windowed fullscreen mode, or 0 to use the display's width.
+ /// The override height in pixels when using windowed fullscreen mode, or 0 to use the display's height.
+ void SetFullscreen(WindowFullscreenMode mode, IDisplay? display = null, uint width = 0, uint height = 0);
+
+ ///
+ /// Sets the available icons for the window.
+ ///
+ ///
+ /// If the provided pixel data does not match the specified
+ /// width and height, an exception will be thrown.
+ ///
+ /// An array of objects representing the icons to set.
+ void SetIcons(params WindowIcon[] icons);
+
+ ///
+ /// Requests the window to be the active target for user input.
+ ///
+ void RequestFocus();
+
+ ///
+ /// Requests the user's attention to the window (e.g., flashing the taskbar icon).
+ ///
+ void RequestAttention();
+
+ ///
+ /// Minimizes the window to the taskbar, dock, or another similar system feature.
+ ///
+ ///
+ ///
+ void Minimize();
+
+ ///
+ /// Maximizes the window to fill the available display area.
+ ///
+ ///
+ ///
+ void Maximize();
+
+ ///
+ /// Restores the window from a minimized or maximized state.
+ ///
+ ///
+ void Restore();
+
+ ///
+ /// Shows the window if it is currently hidden.
+ ///
+ ///
+ ///
+ void Show();
+
+ ///
+ /// Hides the window if it is currently visible.
+ ///
+ ///
+ ///
+ void Hide();
+
+ ///
+ /// Closes the window and releases its resources.
+ ///
+ ///
+ /// Closing may be canceled by event handlers attached to the event.
+ ///
+ ///
+ ///
+ void Close();
+
+ ///
+ /// Forcefully exits the window's event loop and closes it immediately,
+ /// bypassing the event.
+ ///
+ ///
+ ///
+ void Exit();
+
+ ///
+ /// Waits for the window to finish processing its event loop.
+ ///
+ ///
+ /// Blocks the calling thread until the window has been closed.
+ ///
+ void Wait();
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowException.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowException.cs
new file mode 100644
index 0000000..4073036
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowException.cs
@@ -0,0 +1,49 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents an error that occurs during windowing operations.
+ ///
+ ///
+ public class WindowException : Exception {
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WindowException() : base() {
+ // ...
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message.
+ ///
+ public WindowException(string message) : base(message) {
+ // ...
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message and a reference to the inner exception
+ /// that is the cause of this exception.
+ ///
+ public WindowException(string message, Exception innerException) : base(message, innerException) {
+ // ...
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowFullscreenMode.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowFullscreenMode.cs
new file mode 100644
index 0000000..976f6c8
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowFullscreenMode.cs
@@ -0,0 +1,37 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// The supported fullscreen modes for a window.
+ ///
+ public enum WindowFullscreenMode {
+
+ ///
+ /// The window is displayed in normal mode within the bounds of the system environment.
+ ///
+ Windowed,
+
+ ///
+ /// The window fills the entire screen without entering exclusive fullscreen mode.
+ ///
+ Borderless,
+
+ ///
+ /// The window enters exclusive fullscreen mode which may change the display resolution.
+ ///
+ Fullscreen
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs
new file mode 100644
index 0000000..23fec98
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs
@@ -0,0 +1,106 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Mathematics;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// An icon for the window.
+ ///
+ public readonly record struct WindowIcon {
+
+ ///
+ /// Gets the width of the icon in pixels.
+ ///
+ /// The width of the icon.
+ public required uint Width { get; init; }
+
+ ///
+ /// Gets the height of the icon in pixels.
+ ///
+ /// The height of the icon.
+ public required uint Height { get; init; }
+
+ ///
+ /// Gets a read-only collection of pixel data for the icon.
+ ///
+ ///
+ /// The pixel data is expected to be a list of
+ /// values where each
+ /// vector represents a pixel's RGBA color.
+ ///
+ /// A collection of pixel data represented as values.
+ public required IReadOnlyCollection> Pixels { get; init; }
+
+ ///
+ /// Constructs a new .
+ ///
+ /// The width of the icon in pixels.
+ /// The height of the icon in pixels.
+ /// A read-only collection of pixel data for the icon.
+ [SetsRequiredMembers]
+ public WindowIcon(uint width, uint height, IReadOnlyCollection> pixels) {
+ Width = width;
+ Height = height;
+ Pixels = pixels ?? throw new ArgumentNullException(nameof(pixels), "Pixel data cannot be null.");
+ }
+
+ ///
+ /// Creates an array of icon instances from the application manifest resources.
+ ///
+ ///
+ ///
+ /// The resulting assembly path will be in the format: {location}.{filename},
+ /// and as such, the provided filename must contain at least one % placeholder
+ /// for the size of the icon and include the associated file extension (e.g., ".bmp", ".png").
+ ///
+ ///
+ /// For example, if icons were being searched for in the Catalyst.Examples.BasicWindow
+ /// project, the provided location might be Catalyst.Examples.BasicWindow.Resources.Icons,
+ /// and the provided filename might be icon_%x%.bmp. This would result in the
+ /// method searching for a resource named Catalyst.Examples.BasicWindow.Resources.Icons.icon_16x16.bmp.
+ ///
+ ///
+ /// The provided icon function will be called for each icon resource found,
+ /// and the associated resources' stream will be passed to it. This
+ /// provides flexibility in how the icon data is processed, such as
+ /// utilizing the Catalyst Arcane library to decode BMP files,
+ /// or using other image processing libraries to decode special
+ /// file types such as JPEG or WEBP icons.
+ ///
+ ///
+ /// The location of the application manifest.
+ /// The filename template for the icon resources, which must contain a "%" placeholder.
+ /// An array of sizes for the icons to be created.
+ /// A function that takes a and returns a containing the icon data.
+ /// An array of instances created from the specified resources.
+ /// The type of the assembly containing the application manifest resources.
+ public static WindowIcon[] FromApplicationManifest(string location, string filename, int[] sizes, Func iconFunc) {
+ string template = $"{location}.{filename}";
+ WindowIcon[] icons = new WindowIcon[sizes.Length];
+ for (int i = 0; i < sizes.Length; i++) {
+ int size = sizes[i];
+ string resourceName = template.Replace("%", size.ToString());
+ Stream? stream = typeof(T).Assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException($"Resource not found: {resourceName}");
+ icons[i] = iconFunc(stream);
+ }
+ return icons;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs
new file mode 100644
index 0000000..f0634dc
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs
@@ -0,0 +1,110 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Threading;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents options for constructing/configuring a window.
+ ///
+ public readonly record struct WindowOptions : IOptions {
+
+ /// The window's thread dispatcher, or to use a captured .
+ ///
+ public ThreadDelegateDispatcher? Dispatcher { get; init; } = null;
+
+ ///
+ public uint Width { get; init; } = IWindow.DEFAULT_WIDTH;
+
+ ///
+ public uint Height { get; init; } = IWindow.DEFAULT_HEIGHT;
+
+ ///
+ public string Title { get; init; } = IWindow.DEFAULT_TITLE;
+
+ ///
+ public bool Hidden { get; init; } = false;
+
+ ///
+ public bool Resizable { get; init; } = true;
+
+ ///
+ public bool Decorated { get; init; } = true;
+
+ ///
+ public WindowFullscreenMode FullscreenMode { get; init; } = WindowFullscreenMode.Windowed;
+
+ ///
+ public ushort PollRate { get; init; } = 0;
+
+ ///
+ public WindowIcon[]? Icons { get; init; } = null;
+
+ ///
+ /// Invoked after the window is initialized and
+ /// prepared to be created, but prior to the
+ /// actual construction of the window.
+ ///
+ public IWindow.WindowEventHandler? InitializedHandler { get; init; } = null;
+
+ ///
+ /// Constructs a new .
+ ///
+ /// The window's thread dispatcher.
+ /// The initial width of the window.
+ /// The initial height of the window.
+ /// The initial title of the window.
+ /// Whether the window is initially hidden.
+ /// Whether the window is resizable.
+ /// Whether the window is decorated.
+ /// The initial fullscreen mode of the window.
+ /// The poll rate of the window.
+ /// A set of icons to use for the window.
+ /// An optional handler invoked after window initialization.
+ public WindowOptions(
+ ThreadDelegateDispatcher? dispatcher = null,
+ uint width = IWindow.DEFAULT_WIDTH,
+ uint height = IWindow.DEFAULT_HEIGHT,
+ string title = IWindow.DEFAULT_TITLE,
+ bool hidden = false,
+ bool resizable = true,
+ bool decorated = true,
+ WindowFullscreenMode fullscreenMode = WindowFullscreenMode.Windowed,
+ ushort pollRate = 0,
+ WindowIcon[]? icons = null,
+ IWindow.WindowEventHandler? initializedHandler = null
+ ) {
+ Dispatcher = dispatcher;
+ Width = width;
+ Height = height;
+ Title = title;
+ Hidden = hidden;
+ Resizable = resizable;
+ Decorated = decorated;
+ FullscreenMode = fullscreenMode;
+ PollRate = pollRate;
+ Icons = icons;
+ InitializedHandler = initializedHandler;
+ }
+
+ ///
+ /// Constructs a new
+ /// with default values.
+ ///
+ public WindowOptions() {
+ // ...
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/CatalystUI.Modules.Crystal.Glfw3.csproj b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/CatalystUI.Modules.Crystal.Glfw3.csproj
new file mode 100644
index 0000000..b5486e0
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/CatalystUI.Modules.Crystal.Glfw3.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Catalyst.Modules.Crystal.Glfw3
+ Catalyst.Modules.Crystal.Glfw3
+ true
+
+
+ CatalystUI Crystal – Glfw3
+ 1.0.0
+ alpha.1
+ CatalystUI LLC
+ Glfw3 API for the Crystal subset of modules provided by the CatalystUI library.
+ CatalystUI,Crystal,glfw3,glfw,ui,gui,graphics,visual,window
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs
new file mode 100644
index 0000000..33a1a74
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs
@@ -0,0 +1,59 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Modules.Crystal.Glfw3;
+using Catalyst.Supplementary;
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Builders.Extensions {
+
+ ///
+ /// Builder extensions for the .
+ ///
+ public static class CatalystAppBuilderExtensions {
+
+ ///
+ /// Adds the Crystal-based Glfw3 windowing module to the .
+ ///
+ ///
+ ///
+ /// The Crystal-based Glfw3 windowing module adds the following to your CatalystUI application:
+ ///
+ ///
+ ///
+ ///
+ /// Click on any of the above links to learn more about each component.
+ ///
+ ///
+ /// The to add the module to.
+ /// The with the Glfw3 windowing module added.
+ public static CatalystAppBuilder AddCrystalGlfw3Module(this CatalystAppBuilder builder) {
+ Glfw3WindowLayer glfw3WindowingLayer = new();
+ ModelRegistry.RegisterLayer(glfw3WindowingLayer);
+ if (SystemDetector.IsSystem()) {
+ Glfw3WindowsNativeHandler windowsHandler = new();
+ ModelRegistry.RegisterConnector(windowsHandler);
+ } else if (SystemDetector.IsSystem()) {
+ Glfw3MacNativeHandler macHandler = new();
+ ModelRegistry.RegisterConnector(macHandler);
+ } else if (SystemDetector.IsSystem()) {
+ throw new NotImplementedException();
+ } else {
+ // Native functionality for Glfw3 is OPTIONAL.
+ // Do not throw an error here unless it becomes REQUIRED.
+ }
+ return builder;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs
new file mode 100644
index 0000000..580adb0
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs
@@ -0,0 +1,200 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Attributes.Threading;
+using Catalyst.Debugging;
+using Catalyst.Native;
+using Catalyst.Threading;
+using Silk.NET.GLFW;
+using System;
+using System.Threading;
+
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// Native API wrapper for the Glfw3 library.
+ ///
+ public sealed partial class Glfw3 : INativeApi {
+
+ ///
+ /// The underlying wrapped API instance.
+ ///
+ private static Glfw? _api;
+
+ ///
+ /// The dispatcher which was used to initialize the API.
+ ///
+ private static ThreadDelegateDispatcher? _initDispatcher;
+
+ ///
+ /// The total number of 'requests' or instantiations of the API.
+ ///
+ private static ushort _referenceCount;
+
+ ///
+ /// A static lock used to ensure thread-safe access to the static members.
+ ///
+ private static readonly Lock _staticLock;
+
+ ///
+ /// Gets the Glfw3 debug context.
+ ///
+ /// The debug context for Glfw3.
+ public static DebugContext DebugContext { get; }
+
+ ///
+ /// Gets the wrapped API instance.
+ ///
+ /// The wrapped Glfw3 API instance.
+ public Glfw Api {
+ get {
+ _staticLock.Enter();
+ try {
+ return _api!; // Non-nullable because it is initialized in the static constructor
+ } finally {
+ _staticLock.Exit();
+ }
+ }
+ }
+
+ ///
+ /// A flag indicating whether the object has been disposed of.
+ ///
+ private bool _disposed;
+
+ ///
+ /// A lock used to ensure thread-safe access to the object.
+ ///
+ private readonly Lock _lock;
+
+ ///
+ /// Static constructor for .
+ ///
+ static Glfw3() {
+ // Fields
+ _referenceCount = 0;
+ _staticLock = new();
+
+ // Properties
+ DebugContext = CatalystDebug.ForContext("Glfw3");
+ }
+
+ ///
+ /// Constructs a new .
+ ///
+ private Glfw3() {
+ // Fields
+ _disposed = false;
+ _lock = new();
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ ~Glfw3() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: false);
+ }
+
+ ///
+ public static Glfw3 GetInstance(ThreadDelegateDispatcher? dispatcher = null) {
+ _staticLock.Enter();
+ try {
+ if (_referenceCount == 0) Initialize();
+ _referenceCount++;
+ return new();
+ } finally {
+ _staticLock.Exit();
+ }
+ }
+
+ ///
+ /// Initializes the Glfw3 API.
+ ///
+ /// A thread dispatcher to associate with the API instance, or to use a captured main thread dispatcher.
+ private static void Initialize(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3), nameof(GetInstance));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) {
+ throw new TypeInitializationException(nameof(Glfw3), new InvalidOperationException("Failed to initialize Glfw3 API on the main thread."));
+ }
+ _initDispatcher = dispatcher;
+ }
+
+ [CachedDelegate]
+ private static void InitializeUnsafe() {
+ if (_api != null) throw new InvalidOperationException("The Glfw3 API is already initialized.");
+ _api = Glfw.GetApi();
+ if (!_api.Init()) throw new WindowException("Failed to initialize the Glfw3 API.");
+ }
+
+ ///
+ /// Terminates the Glfw3 API.
+ ///
+ private static void Terminate(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3), nameof(Terminate));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedActionTerminateUnsafe, wait: true)) {
+ throw new InvalidOperationException("Failed to terminate Glfw3 API on the main thread.");
+ }
+ }
+
+ [CachedDelegate]
+ private static void TerminateUnsafe() {
+ _api?.Terminate();
+ _api?.Dispose();
+ _api = null;
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ public void Dispose() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// if disposal is being performed by the garbage collector, otherwise
+ ///
+ private void Dispose(bool disposing) {
+ _lock.Enter();
+ try {
+ if (_disposed) return;
+
+ // Dispose managed state (managed objects)
+ if (disposing) {
+ // ...
+ }
+
+ // Dispose unmanaged state (unmanaged objects)
+ _staticLock.Enter();
+ try {
+ _referenceCount--;
+ if (_referenceCount == 0) Terminate(_initDispatcher);
+ } finally {
+ _staticLock.Exit();
+ }
+
+ // Indicate disposal completion
+ _disposed = true;
+ } finally {
+ _lock.Exit();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs
new file mode 100644
index 0000000..16c4383
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs
@@ -0,0 +1,77 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Attributes.Threading;
+using Catalyst.Threading;
+using System.Collections.Generic;
+using System.Linq;
+using Monitor = Silk.NET.GLFW.Monitor;
+
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// The Crystal implementation of the CatalystUI type
+ /// using the Glfw3 windowing API.
+ ///
+ public sealed unsafe partial class Glfw3WindowLayer : IWindowLayer {
+
+ ///
+ public IEnumerable GetDisplays(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3WindowLayer), nameof(GetDisplays));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedFunctionGetDisplaysUnsafe, dispatcher, out GlfwDisplay[] displays)) {
+ throw new WindowException("Failed to get displays from Glfw3 on the main thread.");
+ }
+ return displays.OfType();
+ }
+
+ [CachedDelegate]
+ internal static GlfwDisplay[] GetDisplaysUnsafe(ThreadDelegateDispatcher dispatcher) {
+ using Glfw3 glfw = Glfw3.GetInstance(dispatcher);
+ Monitor** pMonitors = glfw.Api.GetMonitors(out int count);
+ if (pMonitors == null || count <= 0) return [];
+ GlfwDisplay[] displays = new GlfwDisplay[count];
+ for (int i = 0; i < count; i++) {
+ Monitor* pMonitor = pMonitors[i];
+ displays[i] = GlfwDisplay.FromMonitor(glfw, pMonitor);
+ }
+ return displays;
+ }
+
+ ///
+ public IDisplay? GetPrimaryDisplay(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3WindowLayer), nameof(GetPrimaryDisplay));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedFunctionGetPrimaryDisplayUnsafe, dispatcher, out GlfwDisplay? display)) {
+ throw new WindowException("Failed to get primary display from Glfw3 on the main thread.");
+ }
+ return display;
+ }
+
+ [CachedDelegate]
+ internal static unsafe GlfwDisplay? GetPrimaryDisplayUnsafe(ThreadDelegateDispatcher dispatcher) {
+ using Glfw3 glfw = Glfw3.GetInstance(dispatcher);
+ Monitor* pPrimaryMonitor = glfw.Api.GetPrimaryMonitor();
+ return pPrimaryMonitor != null ? GlfwDisplay.FromMonitor(glfw, pPrimaryMonitor) : null;
+ }
+
+ ///
+ public IWindow CreateWindow(WindowOptions? options = null) {
+ return new GlfwWindow(options ?? new());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/IGlfw3NativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/IGlfw3NativeConnector.cs
new file mode 100644
index 0000000..cd36964
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/IGlfw3NativeConnector.cs
@@ -0,0 +1,59 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Connectors;
+using Catalyst.Domains;
+using Catalyst.Layers;
+using Silk.NET.GLFW;
+
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// The Crystal interface of a native connector for the Glfw3 windowing API.
+ ///
+ ///
+ public unsafe interface IGlfw3NativeConnector : INativeConnector where TLayerLow : ISystemLayer {
+
+ ///
+ /// Gets the platform-specific native window handle of the specified Glfw3 window.
+ ///
+ /// The Glfw3 instance to use.
+ /// The Glfw3 window handle.
+ /// A platform-specific native window handle.
+ nint GetNativeHandle(Glfw3 glfw, WindowHandle* pWindow);
+
+ ///
+ /// Gets the rotation of the specified display in degrees.
+ ///
+ /// The GLFW instance.
+ /// The GLFW monitor to get the rotation for.
+ /// The rotation of the display in degrees.
+ double GetDisplayRotation(Glfw3 glfw, Monitor* pMonitor);
+
+ ///
+ /// Gets the display descriptor from the EDID for the specified monitor.
+ ///
+ /// The GLFW instance.
+ /// The GLFW monitor to get the descriptor for.
+ /// The descriptor string.
+ string GetDisplayDescriptor(Glfw3 glfw, Monitor* pMonitor);
+
+ ///
+ /// Gets the display manufacturer from the EDID for the specified monitor.
+ ///
+ /// The GLFW instance.
+ /// The GLFW monitor to get the manufacturer for.
+ /// The manufacturer string.
+ string GetDisplayManufacturer(Glfw3 glfw, Monitor* pMonitor);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs
new file mode 100644
index 0000000..c063ec1
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs
@@ -0,0 +1,71 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System.Text;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ // TODO: Maybe make this an Arcane thing? Seems reasonable enough.
+ ///
+ /// EDID utilities for working with Extended Display Identification Data (EDID).
+ ///
+ public static class EdidHelper {
+
+ ///
+ /// Gets the monitor descriptor from an EDID byte array.
+ ///
+ /// The EDID byte array.
+ /// The monitor descriptor string, or an empty string if not found.
+ public static string GetMonitorDescriptorFromEdid(ref byte[] edid) {
+ // Descriptor blocks from offset 0x36 to 0x7F
+ for (int i = 0x36; i <= 0x6C; i += 18) {
+ // Check for the monitor name tag (0xFC)
+ if (edid[i] == 0x00 &&
+ edid[i + 1] == 0x00 &&
+ edid[i + 2] == 0x00 &&
+ (edid[i + 3] == 0xFC || (edid[i + 3] == 0x00 && edid[i + 4] == 0xFC))) {
+ // Fetch the name data (stored in i+5 to i+17, or 13 bytes).
+ byte[] nameBytes = edid[(i + 5)..(i + 18)];
+ string name = Encoding.ASCII.GetString(nameBytes).Trim();
+ return name;
+ }
+ }
+ return string.Empty;
+ }
+
+ ///
+ /// Gets the 3-character manufacturer EISA ID from the EDID byte array.
+ ///
+ /// The EDID byte array.
+ /// The manufacturer code (e.g., "CFL"), or an empty string if one could not be determined.
+ public static string GetMonitorManufacturerCodeFromEdid(ref byte[] edid) {
+ // Must be at least 10 bytes long to contain the manufacturer ID
+ if (edid.Length < 0x0A) return string.Empty;
+
+ // The manufacturer ID is packed into bytes 0x08 and 0x09
+ ushort raw = (ushort) ((edid[0x08] << 8) | edid[0x09]);
+
+ // Extract 3 5-bit character codes
+ char c1 = (char) (((raw >> 10) & 0x1F) + 'A' - 1);
+ char c2 = (char) (((raw >> 5) & 0x1F) + 'A' - 1);
+ char c3 = (char) ((raw & 0x1F) + 'A' - 1);
+
+ // Sanity check: all must be in 'A'..'Z'
+ if (c1 is < 'A' or > 'Z' || c2 is < 'A' or > 'Z' || c3 is < 'A' or > 'Z')
+ return string.Empty;
+
+ return new string([ c1, c2, c3 ]);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs
new file mode 100644
index 0000000..1e430f1
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs
@@ -0,0 +1,293 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Native;
+using Catalyst.Supplementary;
+using Silk.NET.GLFW;
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// An Apple Mac-based implementation of
+ ///
+ public sealed unsafe class Glfw3MacNativeHandler : IGlfw3NativeConnector {
+
+ ///
+ public nint GetNativeHandle(Glfw3 glfw, WindowHandle* pWindow) {
+ return MacGlfwImports.glfwGetCocoaWindow(glfw, pWindow);
+ }
+
+ ///
+ public double GetDisplayRotation(Glfw3 glfw, Monitor* pMonitor) {
+ int displayId = MacGlfwImports.glfwGetCocoaMonitor(glfw, pMonitor);
+ return MacImports.GetDisplayRotation(displayId);
+ }
+
+ ///
+ public string GetDisplayDescriptor(Glfw3 glfw, Monitor* pMonitor) {
+ int displayId = MacGlfwImports.glfwGetCocoaMonitor(glfw, pMonitor);
+ byte[] edid = MacImports.GetDisplayEDID(displayId);
+ string descriptor = EdidHelper.GetMonitorDescriptorFromEdid(ref edid);
+ if (string.IsNullOrEmpty(descriptor)) throw new NativeException("Failed to retrieve monitor descriptor from EDID.");
+ return descriptor;
+ }
+
+ ///
+ public string GetDisplayManufacturer(Glfw3 glfw, Monitor* pMonitor) {
+ int displayId = MacGlfwImports.glfwGetCocoaMonitor(glfw, pMonitor);
+ byte[] edid = MacImports.GetDisplayEDID(displayId);
+ string manufacturer = EdidHelper.GetMonitorManufacturerCodeFromEdid(ref edid);
+ if (string.IsNullOrEmpty(manufacturer)) throw new NativeException("Failed to retrieve monitor manufacturer from EDID.");
+ return manufacturer;
+ }
+
+ }
+
+ // ReSharper disable InconsistentNaming
+ internal static unsafe class MacGlfwImports {
+
+ ///
+ /// Returns the MacOS displayID for the specified monitor.
+ ///
+ /// The monitor to get the screen for.
+ /// The display ID of the monitor.
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate int GetCocoaMonitor(Monitor* pMonitor);
+
+ private static GetCocoaMonitor? _glfwGetCocoaMonitor;
+
+ ///
+ /// Returns the NSWindow of the specified window.
+ ///
+ /// The GLFW handle for the window.
+ /// The NSWindow of the window.
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate nint GetCocoaWindow(WindowHandle* handle);
+
+ private static GetCocoaWindow? _glfwGetCocoaWindow;
+
+ public static int glfwGetCocoaMonitor(Glfw3 glfw, Monitor* pMonitor) {
+ if (_glfwGetCocoaMonitor == null) {
+ if (glfw.Api.Context.TryGetProcAddress("glfwGetCocoaMonitor", out nint procAddress)) {
+ _glfwGetCocoaMonitor = Marshal.GetDelegateForFunctionPointer(procAddress);
+ } else {
+ throw new NativeException("glfwGetCocoaMonitor is not supported on this platform.");
+ }
+ }
+ return _glfwGetCocoaMonitor(pMonitor);
+ }
+
+ public static nint glfwGetCocoaWindow(Glfw3 glfw, WindowHandle* handle) {
+ if (_glfwGetCocoaWindow == null) {
+ if (glfw.Api.Context.TryGetProcAddress("glfwGetCocoaWindow", out nint proc)) {
+ _glfwGetCocoaWindow = Marshal.GetDelegateForFunctionPointer(proc);
+ } else {
+ return 0;
+ }
+ }
+ return _glfwGetCocoaWindow(handle);
+ }
+
+ }
+ // ReSharper restore InconsistentNaming
+
+ // ReSharper disable InconsistentNaming
+ internal static unsafe class MacImports {
+
+ private static readonly nint _CoreGraphics;
+ private static readonly nint _IOKit;
+ private static readonly nint _CoreFoundation;
+
+ private static readonly delegate* unmanaged[Cdecl] _CGDisplayRotation;
+ private static readonly delegate* unmanaged[Cdecl] _CGDisplayVendorNumber;
+ private static readonly delegate* unmanaged[Cdecl] _CGDisplayModelNumber;
+
+ private static readonly delegate* unmanaged[Cdecl] _IOServiceGetMatchingServices;
+ private static readonly delegate* unmanaged[Cdecl] _IOServiceMatching;
+ private static readonly delegate* unmanaged[Cdecl] _IOIteratorNext;
+ private static readonly delegate* unmanaged[Cdecl] _IOObjectRelease;
+ private static readonly delegate* unmanaged[Cdecl] _IORegistryEntryCreateCFProperty;
+
+ private static readonly delegate* unmanaged[Cdecl] _CFStringCreateWithCString;
+ private static readonly delegate* unmanaged[Cdecl] _CFDataGetLength;
+ private static readonly delegate* unmanaged[Cdecl] _CFDataGetBytes;
+ private static readonly delegate* unmanaged[Cdecl] _CFNumberGetValue;
+ private static readonly delegate* unmanaged[Cdecl] _CFRelease;
+
+
+ static MacImports() {
+ _CoreGraphics = NativeLibrary.Load("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics");
+ _IOKit = NativeLibrary.Load("/System/Library/Frameworks/IOKit.framework/IOKit");
+ _CoreFoundation= NativeLibrary.Load("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation");
+
+ _CGDisplayRotation = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreGraphics, "CGDisplayRotation");
+ _CGDisplayVendorNumber = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreGraphics, "CGDisplayVendorNumber");
+ _CGDisplayModelNumber = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreGraphics, "CGDisplayModelNumber");
+
+ _IOServiceGetMatchingServices = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_IOKit, "IOServiceGetMatchingServices");
+ _IOServiceMatching = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_IOKit, "IOServiceMatching");
+ _IOIteratorNext = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_IOKit, "IOIteratorNext");
+ _IOObjectRelease = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_IOKit, "IOObjectRelease");
+ _IORegistryEntryCreateCFProperty = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_IOKit, "IORegistryEntryCreateCFProperty");
+
+ _CFStringCreateWithCString = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreFoundation, "CFStringCreateWithCString");
+ _CFDataGetLength = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreFoundation, "CFDataGetLength");
+ _CFDataGetBytes = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreFoundation, "CFDataGetBytes");
+ _CFNumberGetValue = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreFoundation, "CFNumberGetValue");
+ _CFRelease = (delegate* unmanaged[Cdecl])
+ NativeLibrary.GetExport(_CoreFoundation, "CFRelease");
+ }
+
+ ///
+ /// Gets the rotation of the display with the specified ID.
+ ///
+ /// The ID of the display.
+ /// The rotation of the display in degrees.
+ public static double GetDisplayRotation(int displayId) {
+ return _CGDisplayRotation(displayId);
+ }
+
+ ///
+ /// Gets the EDID (Extended Display Identification Data) for the specified display.
+ ///
+ /// The ID of the display.
+ /// The EDID data as a byte array.
+ public static byte[] GetDisplayEDID(int displayId) {
+ int vendorId = _CGDisplayVendorNumber(displayId);
+ int productId = _CGDisplayModelNumber(displayId);
+ nint matchingDictionary = IOServiceMatching("IODisplayConnect");
+ if (matchingDictionary == 0) throw new NativeException($"Failed to create matching dictionary for display {displayId}.");
+ nint result = IOServiceGetMatchingServices(0, matchingDictionary, out nint iterator);
+ if (result != 0) throw new NativeException($"Failed to get matching services for display {displayId}.");
+ if (iterator == 0) throw new NativeException($"No matching services found for display {displayId}.");
+ try {
+ nint service;
+ while ((service = _IOIteratorNext(iterator)) != 0) {
+ try {
+ int foundVendorId = 0;
+ int foundProductId = 0;
+ nint vendorKey = CFString("IODisplayVendorID");
+ nint productKey = CFString("IODisplayProductID");
+ nint edidKey = CFString("IODisplayEDID");
+ try {
+ nint cfVendor = _IORegistryEntryCreateCFProperty(service, vendorKey, 0, 0);
+ nint cfProduct = _IORegistryEntryCreateCFProperty(service, productKey, 0, 0);
+ nint cfEdid = _IORegistryEntryCreateCFProperty(service, edidKey, 0, 0);
+ try {
+ if (cfVendor != 0 && cfProduct != 0 && cfEdid != 0) {
+ CFNumberGetValue(cfVendor, 9 /* kCFNumberSInt32Type */, out foundVendorId);
+ CFNumberGetValue(cfProduct, 9 /* kCFNumberSInt32Type */, out foundProductId);
+ if (foundVendorId == vendorId && foundProductId == productId) {
+ int length = _CFDataGetLength(cfEdid);
+ byte[] buffer = new byte[length];
+ CFDataGetBytes(cfEdid, new() {
+ Location = 0,
+ Length = length
+ }, buffer);
+ return buffer;
+ }
+ }
+ } finally {
+ if (cfVendor != 0) _CFRelease(cfVendor);
+ if (cfProduct != 0) _CFRelease(cfProduct);
+ if (cfEdid != 0) _CFRelease(cfEdid);
+ }
+ } finally {
+ if (vendorKey != 0) _CFRelease(vendorKey);
+ if (productKey != 0) _CFRelease(productKey);
+ if (edidKey != 0) _CFRelease(edidKey);
+ }
+ } finally {
+ _IOObjectRelease(service);
+ }
+ }
+ throw new NativeException($"EDID not found for display {displayId} with vendor ID {vendorId} and product ID {productId}.");
+ } finally {
+ if (iterator != 0) _IOObjectRelease(iterator);
+ }
+ }
+
+ internal static nint IOServiceGetMatchingServices(nint masterPort, nint matching, out nint iterator) {
+ nint it;
+ nint result = _IOServiceGetMatchingServices(masterPort, matching, &it);
+ iterator = it;
+ return result;
+ }
+
+ internal static nint IOServiceMatching(string name) {
+ // UTF-8 + null
+ byte[] bytes = Encoding.UTF8.GetBytes(name);
+ fixed (byte* p = bytes) {
+ // stack-allocate a null-terminated buffer
+ int len = bytes.Length;
+ byte* tmp = stackalloc byte[len + 1];
+ Buffer.MemoryCopy(p, tmp, len + 1, len);
+ tmp[len] = 0;
+ return _IOServiceMatching((sbyte*)tmp);
+ }
+ }
+
+ // kCFStringEncodingUTF8 = 0x08000100
+ internal static nint CFStringCreateWithCString(nint alloc, string str, uint encoding) {
+ // Ensure null-terminated UTF-8
+ byte[] utf8 = Encoding.UTF8.GetBytes(str);
+ fixed (byte* pUtf8 = utf8) {
+ int len = utf8.Length;
+ byte* tmp = stackalloc byte[len + 1];
+ Buffer.MemoryCopy(pUtf8, tmp, len + 1, len);
+ tmp[len] = 0;
+ return _CFStringCreateWithCString(alloc, (sbyte*)tmp, encoding);
+ }
+ }
+
+ internal static nint CFString(string str) => CFStringCreateWithCString(0, str, 0x08000100u);
+
+ internal static void CFDataGetBytes(nint cfData, CFRange range, byte[] buffer) {
+ if (buffer is null) throw new ArgumentNullException(nameof(buffer));
+ fixed (byte* p = buffer) {
+ _CFDataGetBytes(cfData, range, p);
+ }
+ }
+
+ internal static int CFNumberGetValue(nint number, int theType, out int value) {
+ fixed (int* p = &value) {
+ return _CFNumberGetValue(number, theType, p);
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct CFRange {
+
+ public int Location;
+ public int Length;
+
+ }
+
+ }
+ // ReSharper restore InconsistentNaming
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs
new file mode 100644
index 0000000..fdfb9fe
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs
@@ -0,0 +1,355 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Native;
+using Catalyst.Supplementary;
+using Microsoft.Win32;
+using Silk.NET.GLFW;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// A Microsoft Windows-based implementation of .
+ ///
+ public sealed unsafe class Glfw3WindowsNativeHandler : IGlfw3NativeConnector {
+
+ ///
+ public nint GetNativeHandle(Glfw3 glfw, WindowHandle* pWindow) {
+ return WindowsGlfwImports.glfwGetWin32Window(glfw, pWindow);
+ }
+
+ ///
+ public double GetDisplayRotation(Glfw3 glfw, Monitor* pMonitor) {
+ nint deviceName = WindowsGlfwImports.glfwGetWin32Monitor(glfw, pMonitor);
+ string? str = Marshal.PtrToStringUTF8(deviceName);
+ if (str == null) throw new NativeException("Failed to retrieve device name from monitor.");
+ return WindowsImports.GetDisplayRotation(str);
+ }
+
+ ///
+ public string GetDisplayDescriptor(Glfw3 glfw, Monitor* pMonitor) {
+ nint deviceName = WindowsGlfwImports.glfwGetWin32Monitor(glfw, pMonitor);
+ string? str = Marshal.PtrToStringUTF8(deviceName);
+ if (str == null) throw new NativeException("Failed to retrieve device name from monitor.");
+ List edids = WindowsImports.GetDisplayEdid(str);
+ if (edids.Count == 0) throw new NativeException("No EDID data found for monitor.");
+ string descriptor = string.Empty;
+ for (int i = 0; i < edids.Count; i++) {
+ byte[] edid = edids[i];
+ descriptor = EdidHelper.GetMonitorDescriptorFromEdid(ref edid);
+ if (!string.IsNullOrEmpty(descriptor)) break;
+ }
+ if (string.IsNullOrEmpty(descriptor)) throw new NativeException("No monitor descriptor found in EDID data.");
+ return descriptor;
+ }
+
+ ///
+ public string GetDisplayManufacturer(Glfw3 glfw, Monitor* pMonitor) {
+ nint deviceName = WindowsGlfwImports.glfwGetWin32Monitor(glfw, pMonitor);
+ string? str = Marshal.PtrToStringUTF8(deviceName);
+ if (str == null) throw new NativeException("Failed to retrieve device name from monitor.");
+ List edids = WindowsImports.GetDisplayEdid(str);
+ if (edids.Count == 0) throw new NativeException("No EDID data found for monitor.");
+ string manufacturer = string.Empty;
+ for (int i = 0; i < edids.Count; i++) {
+ byte[] edid = edids[i];
+ manufacturer = EdidHelper.GetMonitorManufacturerCodeFromEdid(ref edid);
+ if (!string.IsNullOrEmpty(manufacturer)) break;
+ }
+ if (string.IsNullOrEmpty(manufacturer)) throw new NativeException("No monitor manufacturer found in EDID data.");
+ return manufacturer;
+ }
+
+ }
+
+ // ReSharper disable InconsistentNaming
+ internal static unsafe class WindowsGlfwImports {
+
+ ///
+ /// Returns the Win32 device name for the specified monitor.
+ ///
+ /// The monitor to get the device name for.
+ /// The device name of the monitor.
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate nint GetWin32Monitor(Monitor* pMonitor);
+
+ private static GetWin32Monitor? _glfwGetWin32Monitor;
+
+ ///
+ /// Returns the HWND of the specified window.
+ ///
+ /// The GLFW handle for the window.
+ /// The HWND of the window.
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate nint GetWin32Window(WindowHandle* handle);
+
+ private static GetWin32Window? _glfwGetWin32Window;
+
+ public static nint glfwGetWin32Monitor(Glfw3 glfw, Monitor* pMonitor) {
+ if (_glfwGetWin32Monitor == null) {
+ if (glfw.Api.Context.TryGetProcAddress("glfwGetWin32Monitor", out nint procAddress)) {
+ _glfwGetWin32Monitor = Marshal.GetDelegateForFunctionPointer(procAddress);
+ } else {
+ throw new PlatformNotSupportedException("glfwGetWin32Monitor is not supported on this platform.");
+ }
+ }
+ return _glfwGetWin32Monitor(pMonitor);
+ }
+
+ public static nint glfwGetWin32Window(Glfw3 glfw, WindowHandle* handle) {
+ if (_glfwGetWin32Window == null) {
+ if (glfw.Api.Context.TryGetProcAddress("glfwGetWin32Window", out nint proc)) {
+ _glfwGetWin32Window = Marshal.GetDelegateForFunctionPointer(proc);
+ } else {
+ return 0;
+ }
+ }
+ return _glfwGetWin32Window(handle);
+ }
+
+ }
+ // ReSharper restore InconsistentNaming
+
+ // ReSharper disable InconsistentNaming
+ internal static unsafe class WindowsImports {
+
+ private static readonly nint _user32;
+ private static readonly nint _gdi32;
+
+ private static readonly delegate* unmanaged[Stdcall] _GetMonitorInfoW;
+ private static readonly delegate* unmanaged[Stdcall] _EnumDisplayMonitors;
+ private static readonly delegate* unmanaged[Stdcall] _EnumDisplaySettingsW;
+ private static readonly delegate* unmanaged[Stdcall] _EnumDisplayDevicesW;
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal unsafe delegate bool EnumMonitorsProc(nint hMonitor, nint hdc, RECT* rect, nint data);
+
+ static WindowsImports() {
+ _user32 = NativeLibrary.Load("user32.dll");
+ _gdi32 = NativeLibrary.Load("gdi32.dll");
+
+ _GetMonitorInfoW = (delegate* unmanaged[Stdcall])
+ NativeLibrary.GetExport(_user32, "GetMonitorInfoW");
+ _EnumDisplayMonitors = (delegate* unmanaged[Stdcall])
+ NativeLibrary.GetExport(_user32, "EnumDisplayMonitors");
+ _EnumDisplaySettingsW = (delegate* unmanaged[Stdcall])
+ NativeLibrary.GetExport(_user32, "EnumDisplaySettingsW");
+ _EnumDisplayDevicesW = (delegate* unmanaged[Stdcall])
+ NativeLibrary.GetExport(_user32, "EnumDisplayDevicesW");
+ }
+
+ ///
+ /// Gets the rotation of the display for the specified device name.
+ ///
+ /// The name of the device.
+ /// The rotation of the display in degrees.
+ public static double GetDisplayRotation(string deviceName) {
+ DEVMODEW devmode = GetDEVMODEW(deviceName);
+ const uint DM_DISPLAYORIENTATION = 0x80;
+ if ((devmode.dmFields & DM_DISPLAYORIENTATION) != 0) {
+ return devmode.dmDisplayOrientation switch {
+ 0 => 0, // DMDO_DEFAULT
+ 1 => 90, // DMDO_90
+ 2 => 180, // DMDO_180
+ 3 => 270, // DMDO_270
+ _ => throw new NativeException($"Unknown display orientation: {devmode.dmDisplayOrientation}")
+ };
+ } else {
+ throw new NativeException($"Display orientation not set in DEVMODEW for device: {deviceName}");
+ }
+ }
+
+ ///
+ /// Gets the EDID (Extended Display Identification Data) for the specified display device.
+ ///
+ /// The name of the display device.
+ /// A list of byte arrays representing the EDID data for the display.
+ public static List GetDisplayEdid(string deviceName) {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) throw new PlatformNotSupportedException("EDID registry access is Windows-only.");
+ DISPLAY_DEVICEW device = GetDISPLAY_DEVICEW(deviceName);
+ string deviceID = device.DeviceID.ToString().TrimEnd('\0');
+ string hwid = deviceID.Split('\\')[1]; // Extract the hardware ID part after the first backslash
+ List found = [ ];
+ using RegistryKey? displayKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\DISPLAY");
+ if (displayKey == null) throw new NativeException("Failed to open display registry key.");
+ string[] displaySubKeyNames = displayKey.GetSubKeyNames();
+ for (int i = 0; i < displaySubKeyNames.Length; i++) {
+ // Check against the hwid
+ string hwidKeyName = displaySubKeyNames[i];
+ if (string.IsNullOrEmpty(hwidKeyName)) continue;
+ if (!hwidKeyName.Contains(hwid, StringComparison.OrdinalIgnoreCase)) continue;
+
+ // If it matches, pull the EDID data
+ using RegistryKey? hwidKey = displayKey.OpenSubKey(hwidKeyName);
+ if (hwidKey == null) continue;
+ string[] hwidSubKeyNames = hwidKey.GetSubKeyNames();
+ for (int j = 0; j < hwidSubKeyNames.Length; j++) {
+ using RegistryKey? instanceKey = hwidKey.OpenSubKey(hwidSubKeyNames[j]);
+ using RegistryKey? deviceParamsKey = instanceKey?.OpenSubKey("Device Parameters");
+ if (deviceParamsKey?.GetValue("EDID") is byte[] { Length: >= 128 } edid) {
+ found.Add(edid);
+ }
+ }
+ }
+ return found;
+ }
+
+ internal static MONITORINFOEXW GetMonitorInfo(IntPtr hMonitor) {
+ MONITORINFOEXW info = new();
+ info.monitorInfo.cbSize = (uint) Marshal.SizeOf();
+ if (!_GetMonitorInfoW(hMonitor, (MONITORINFO*)&info)) {
+ throw new NativeException($"Failed to retrieve monitor info for HMONITOR: {hMonitor}");
+ }
+ return info;
+ }
+
+ internal static nint GetHMONITOR(string deviceName) {
+ nint? result = null;
+ int backslash = deviceName.IndexOf('\\', @"\\.\DISPLAY".Length);
+ string trimmed = backslash >= 0 ? deviceName[..backslash] : deviceName;
+ EnumMonitorsProc proc = (hMonitor, hdcMonitor, lprcMonitor, dwData) => {
+ try {
+ MONITORINFOEXW info = GetMonitorInfo(hMonitor);
+ string currentDevice = new string(info.szDevice).TrimEnd('\0');
+ if (string.Equals(currentDevice, trimmed, StringComparison.OrdinalIgnoreCase)) {
+ result = hMonitor;
+ return false; // stop enumeration
+ }
+ } catch {
+ // ...
+ }
+ return true; // continue enumeration
+ };
+ GCHandle handle = GCHandle.Alloc(proc);
+ try {
+ nint pProc = Marshal.GetFunctionPointerForDelegate(proc);
+ _EnumDisplayMonitors(nint.Zero, null, pProc, nint.Zero);
+ } finally {
+ handle.Free();
+ }
+ if (result == null) throw new NativeException("Failed to retrieve HMONITOR for device: " + new string(deviceName));
+ return result.Value;
+ }
+
+ internal static DEVMODEW GetDEVMODEW(string deviceName) {
+ DEVMODEW devmode = new() {
+ dmSize = (ushort)Marshal.SizeOf()
+ };
+ int backslash = deviceName.IndexOf('\\', @"\\.\DISPLAY".Length);
+ string trimmed = backslash >= 0 ? deviceName[..backslash] : deviceName;
+ nint lpDeviceName = Marshal.StringToHGlobalUni(trimmed);
+ try {
+ if (!_EnumDisplaySettingsW(lpDeviceName, unchecked((uint) -1), ref devmode)) {
+ throw new NativeException("Failed to retrieve DEVMODEW for device: " + deviceName);
+ }
+ } finally {
+ if (lpDeviceName != nint.Zero) Marshal.FreeHGlobal(lpDeviceName);
+ }
+ return devmode;
+ }
+
+ internal static DISPLAY_DEVICEW GetDISPLAY_DEVICEW(string deviceName) {
+ nint hMonitor = GetHMONITOR(deviceName);
+ MONITORINFOEXW info = GetMonitorInfo(hMonitor);
+ DISPLAY_DEVICEW device = new() {
+ cb = (uint)Marshal.SizeOf()
+ };
+ nint lpAdapterName = Marshal.StringToHGlobalUni(new string(info.szDevice));
+ try {
+ if (!_EnumDisplayDevicesW(lpAdapterName, 0, ref device, 0)) {
+ throw new NativeException("Failed to retrieve DISPLAY_DEVICEW for device: " + deviceName);
+ }
+ } finally {
+ if (lpAdapterName != nint.Zero) Marshal.FreeHGlobal(lpAdapterName);
+ }
+ return device;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct RECT {
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct MONITORINFO {
+ public uint cbSize;
+ public RECT rcMonitor;
+ public RECT rcWork;
+ public uint dwFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal unsafe struct MONITORINFOEXW {
+ public MONITORINFO monitorInfo;
+ public fixed char szDevice[32];
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct DEVMODEW {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string dmDeviceName;
+ public ushort dmSpecVersion;
+ public ushort dmDriverVersion;
+ public ushort dmSize;
+ public ushort dmDriverExtra;
+ public uint dmFields;
+ public int dmPositionX;
+ public int dmPositionY;
+ public uint dmDisplayOrientation;
+ public uint dmDisplayFixedOutput;
+ public short dmColor;
+ public short dmDuplex;
+ public short dmYResolution;
+ public short dmTTOption;
+ public short dmCollate;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string dmFormName;
+ public ushort dmLogPixels;
+ public uint dmBitsPerPel;
+ public uint dmPelsWidth;
+ public uint dmPelsHeight;
+ public uint dmDisplayFlags;
+ public uint dmDisplayFrequency;
+ public uint dmICMMethod;
+ public uint dmICMIntent;
+ public uint dmMediaType;
+ public uint dmDitherType;
+ public uint dmReserved1;
+ public uint dmReserved2;
+ public uint dmPanningWidth;
+ public uint dmPanningHeight;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct DISPLAY_DEVICEW {
+ public uint cb;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string DeviceName;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string DeviceString;
+ public uint StateFlags;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string DeviceID;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string DeviceKey;
+ }
+
+ }
+ // ReSharper enable InconsistentNaming
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs
new file mode 100644
index 0000000..5aad99d
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs
@@ -0,0 +1,168 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Debugging;
+using Catalyst.Domains;
+using Catalyst.Layers;
+using Catalyst.Mathematics.Geometry;
+using Silk.NET.GLFW;
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// An implementation of using the Glfw3 windowing API.
+ ///
+ public readonly record struct GlfwDisplay : IDisplay {
+
+ ///
+ /// Gets the Glfw3 monitor handle for this display.
+ ///
+ /// The display's Glfw3 monitor handle.
+ public required nint Monitor { get; init; }
+
+ ///
+ public required string Descriptor { get; init; }
+
+ ///
+ public required string? Manufacturer { get; init; }
+
+ ///
+ public required double RefreshRate { get; init; }
+
+ ///
+ public required double X { get; init; }
+
+ ///
+ public required double Y { get; init; }
+
+ ///
+ public required uint Width { get; init; }
+
+ ///
+ public required uint Height { get; init; }
+
+ ///
+ public required Angle Rotation { get; init; }
+
+ ///
+ public required DisplayOrientation Orientation { get; init; }
+
+ ///
+ public required double PixelsPerInch { get; init; }
+
+ ///
+ public required double ScalingFactor { get; init; }
+
+ ///
+ /// Constructs a from a Glfw pointer.
+ ///
+ /// The Glfw3 API instance.
+ /// The pointer to the Glfw monitor.
+ /// A new instance of .
+ public static unsafe GlfwDisplay FromMonitor(Glfw3 glfw, in Monitor* pMonitor) {
+ // Parse video mode
+ VideoMode* pVideoMode = glfw.Api.GetVideoMode(pMonitor);
+ if (pVideoMode == null) throw new WindowException("Failed to get the video mode on the monitor!");
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed video mode: {pVideoMode->Width}x{pVideoMode->Height} @ {pVideoMode->RefreshRate}Hz");
+
+ // Get position and scaling
+ glfw.Api.GetMonitorPos(pMonitor, out int x, out int y);
+ glfw.Api.GetMonitorContentScale(pMonitor, out float xScale, out float yScale);
+ double scale = (xScale + yScale) / 2.0; // Average scaling factor
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed monitor position: ({x}, {y}), Scaling factor: {scale}");
+
+ // Get the PPI (Pixels Per Inch)
+ glfw.Api.GetMonitorPhysicalSize(pMonitor, out int physicalWidth, out int physicalHeight);
+ double physicalWidthInInches = physicalWidth / 25.4; // Convert mm to inches
+ double physicalHeightInInches = physicalHeight / 25.4; // Convert mm to inches
+ double ppiX = physicalWidthInInches > 0 ? pVideoMode->Width / physicalWidthInInches : 0;
+ double ppiY = physicalHeightInInches > 0 ? pVideoMode->Height / physicalHeightInInches : 0;
+ double ppi = (ppiX + ppiY) / 2.0; // Average PPI
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed monitor physical size: {physicalWidth}mm x {physicalHeight}mm, PPI: {ppi}");
+
+ // Check if we have a native connector for this system
+ IGlfw3NativeConnector>? nativeConnector;
+ try {
+ nativeConnector = ModelRegistry.RequestConnector>>();
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native connector: {nativeConnector.GetType().Name}");
+ } catch {
+ nativeConnector = null;
+ Glfw3.DebugContext.Log(LogLevel.Warning, "No native connector found for Glfw3. Some QOL features may not be available.");
+ }
+
+ // Get native details
+ Angle rotation;
+ DisplayOrientation orientation;
+ try {
+ if (nativeConnector != null) {
+ rotation = Angle.FromDegrees(nativeConnector.GetDisplayRotation(glfw, pMonitor));
+ orientation = rotation.ToOrientation();
+ } else {
+ throw new PlatformNotSupportedException();
+ }
+ } catch {
+ // Use physical size to determine orientation if rotation is not available
+ if (physicalWidth >= physicalHeight) {
+ rotation = Angle.FromDegrees(0);
+ orientation = DisplayOrientation.Landscape;
+ } else {
+ rotation = Angle.FromDegrees(90);
+ orientation = DisplayOrientation.Portrait;
+ }
+ }
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed monitor rotation: {rotation}, Orientation: {orientation}");
+
+ // Get EDID descriptor and manufacturer
+ string descriptor;
+ try {
+ if (nativeConnector != null) {
+ descriptor = nativeConnector.GetDisplayDescriptor(glfw, pMonitor);
+ } else {
+ throw new PlatformNotSupportedException();
+ }
+ } catch {
+ descriptor = glfw.Api.GetMonitorName(pMonitor); // Fallback to GLFW monitor name if EDID is not available
+ }
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed monitor descriptor: {descriptor}");
+ string? manufacturer;
+ try {
+ if (nativeConnector != null) {
+ manufacturer = nativeConnector.GetDisplayManufacturer(glfw, pMonitor);
+ } else {
+ throw new PlatformNotSupportedException();
+ }
+ } catch {
+ manufacturer = null; // Fallback to null if manufacturer is not available
+ }
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed monitor manufacturer: {manufacturer ?? "Unknown"}");
+
+ // Construct and return
+ return new() {
+ Monitor = (nint) pMonitor,
+ Descriptor = descriptor,
+ Manufacturer = manufacturer,
+ RefreshRate = (uint) pVideoMode->RefreshRate,
+ X = x,
+ Y = y,
+ Width = (uint) pVideoMode->Width,
+ Height = (uint) pVideoMode->Height,
+ Rotation = rotation,
+ Orientation = orientation,
+ PixelsPerInch = ppi,
+ ScalingFactor = scale
+ };
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs
new file mode 100644
index 0000000..1fce8e0
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs
@@ -0,0 +1,1156 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Debugging;
+using Catalyst.Domains;
+using Catalyst.Layers;
+using Catalyst.Mathematics;
+using Catalyst.Threading;
+using Silk.NET.GLFW;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Monitor = Silk.NET.GLFW.Monitor;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Glfw3 {
+
+ ///
+ /// An implementation of using
+ /// the Glfw3 windowing API.
+ ///
+ public unsafe class GlfwWindow : IWindow {
+
+ ///
+ /// The minimum polling rate for the window in milliseconds.
+ ///
+ ///
+ /// Prevents lockups of the main thread when the
+ /// window may not be responding.
+ ///
+ public const ushort MINIMUM_POLL_RATE = 3000;
+
+ ///
+ public event IWindow.WindowErroredEventHandler? Errored;
+
+ ///
+ public event IWindow.WindowEventHandler? Created;
+
+ ///
+ public event IWindow.WindowEventHandler? Repositioned;
+
+ ///
+ public event IWindow.WindowEventHandler? Resized;
+
+ ///
+ public event IWindow.WindowEventHandler? Refresh;
+
+ ///
+ public event IWindow.WindowEventHandler? Redraw;
+
+ ///
+ public event IWindow.WindowEventHandler? Focused;
+
+ ///
+ public event IWindow.WindowEventHandler? Unfocused;
+
+ ///
+ public event IWindow.WindowEventHandler? Minimized;
+
+ ///
+ public event IWindow.WindowEventHandler? Maximized;
+
+ ///
+ public event IWindow.WindowEventHandler? Restored;
+
+ ///
+ public event IWindow.WindowEventHandler? Shown;
+
+ ///
+ public event IWindow.WindowEventHandler? Hidden;
+
+ ///
+ public event IWindow.WindowClosingEventHandler? Closing;
+
+ ///
+ public event IWindow.WindowEventHandler? Closed;
+
+ ///
+ public event IWindow.WindowInteractedEventHandler? Interacted;
+
+ ///
+ /// Gets a generated log identifier for the window.
+ ///
+ /// The window class name and native handle in hexadecimal format.
+ protected string LogId => $"{nameof(GlfwWindow)} {(NativeHandle.Length > 0 ? $"0x{NativeHandle[0]:X}" : "(No Native Handle)")}";
+
+ ///
+ /// Gets or sets the Glfw3 instance associated with the window.
+ ///
+ /// The Glfw3 instance.
+ public Glfw3 Glfw { get; private set; }
+
+ ///
+ /// Gets or sets the Glfw3 window handle.
+ ///
+ /// The Glfw3 window handle.
+ public nint GlfwHandle { get; private set; }
+
+ ///
+ public ThreadDelegateDispatcher Dispatcher { get; init; }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected nint[] _nativeHandle;
+
+ ///
+ public nint[] NativeHandle => _nativeHandle;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile ushort _pollRate;
+
+ ///
+ public virtual ushort PollRate {
+ get => _pollRate;
+ set {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _pollRate = value; // set outside dispatcher to avoid deadlock
+ _ = Dispatcher.Execute(() => {
+ // simply enqueing a no-op will cause the poll rate to
+ // be re-evaluated due to the polling loop
+ }, wait: true);
+ }
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile IDisplay? _display;
+
+ ///
+ public virtual IDisplay? Display => _display;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile string _title;
+
+ ///
+ public virtual string Title {
+ get => _title;
+ set {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Title));
+ _ = ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => {
+ Glfw.Api.SetWindowTitle((WindowHandle*) GlfwHandle, value);
+ _title = value;
+ Glfw3.DebugContext.Log(LogLevel.Debug, $"Window title set to \"{value}\".", prefix: LogId);
+ }, wait: true);
+ }
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected double _x;
+
+ ///
+ public virtual double X {
+ get => Volatile.Read(ref _x);
+ set => SetPosition(value, Y);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected double _y;
+
+ ///
+ public virtual double Y {
+ get => Volatile.Read(ref _y);
+ set => SetPosition(X, value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _minimumWidth;
+
+ ///
+ public virtual uint MinimumWidth {
+ get => _minimumWidth;
+ set => SetSizeLimits(value, MinimumHeight, MaximumWidth, MaximumHeight);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _width;
+
+ ///
+ public virtual uint Width {
+ get => _width;
+ set => SetSize(value, Height);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _maximumWidth;
+
+ ///
+ public virtual uint MaximumWidth {
+ get => _maximumWidth;
+ set => SetSizeLimits(MinimumWidth, MinimumHeight, value, MaximumHeight);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _minimumHeight;
+
+ ///
+ public virtual uint MinimumHeight {
+ get => _minimumHeight;
+ set => SetSizeLimits(MinimumWidth, value, MaximumWidth, MaximumHeight);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _height;
+
+ ///
+ public virtual uint Height {
+ get => _height;
+ set => SetSize(Width, value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _maximumHeight;
+
+ ///
+ public virtual uint MaximumHeight {
+ get => _maximumHeight;
+ set => SetSizeLimits(MinimumWidth, MinimumHeight, MaximumWidth, value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile WindowFullscreenMode _fullscreenMode;
+
+ ///
+ public virtual WindowFullscreenMode FullscreenMode {
+ get => _fullscreenMode;
+ set => SetFullscreen(value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isResizable;
+
+ ///
+ public virtual bool IsResizable => _isResizable;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isDecorated;
+
+ ///
+ public virtual bool IsDecorated => _isDecorated;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isFocused;
+
+ ///
+ public virtual bool IsFocused => _isFocused;
+
+ ///
+ public virtual bool IsUnfocused => !_isFocused;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isMinimized;
+
+ ///
+ public virtual bool IsMinimized => _isMinimized;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isMaximized;
+
+ ///
+ public virtual bool IsMaximized => _isMaximized;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isVisible;
+
+ ///
+ public virtual bool IsVisible => _isVisible;
+
+ ///
+ public virtual bool IsHidden => !_isVisible;
+
+ ///
+ public virtual bool IsClosed => _disposed;
+
+ ///
+ /// Used to cache the list of known displays.
+ ///
+ protected GlfwDisplay[] _cachedDisplays;
+
+ ///
+ /// Used to cache the primary display.
+ ///
+ protected GlfwDisplay? _cachedPrimaryDisplay;
+
+ ///
+ /// Used to signal polling resets.
+ ///
+ protected readonly ManualResetEvent _resetPollEventHandle;
+
+ ///
+ /// Used to track the current state of the poll event handle.
+ ///
+ protected bool _resetPollEventHandleState;
+
+ ///
+ /// Used to store the previous fullscreen mode for the window.
+ ///
+ protected WindowFullscreenMode _previousFullscreenMode;
+
+ ///
+ /// Used to store the previous window position for restoring.
+ ///
+ protected (double, double) _restorePos;
+
+ ///
+ /// Used to store the previous window size for restoring.
+ ///
+ protected (uint, uint) _restoreSize;
+
+ ///
+ /// Used to store the previous window decorated state for restoring.
+ ///
+ protected bool _restoreDecorated;
+
+ ///
+ /// Used to prevent window refreshing from corrupting window resizing events.
+ ///
+ protected volatile bool _pendingResize;
+
+ ///
+ /// The amount of time left waiting for pending resize.
+ ///
+ protected volatile int _resizeTimeout;
+
+ // Cached delegates for callbacks for performance
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ protected readonly ThreadDelegateDispatcher.DispatcherEventHandler _handlePreExecute;
+ protected readonly ThreadDelegateDispatcher.DispatcherQueueEventHandler _handleDelegateEnqueued;
+ protected readonly GlfwCallbacks.ErrorCallback _handleError;
+ protected readonly GlfwCallbacks.WindowCloseCallback _handleWindowClose;
+ protected readonly GlfwCallbacks.WindowRefreshCallback _handleWindowRefresh;
+ protected readonly GlfwCallbacks.FramebufferSizeCallback _handleFramebufferSize;
+ protected readonly GlfwCallbacks.WindowSizeCallback _handleWindowSize;
+ protected readonly GlfwCallbacks.WindowPosCallback _handleWindowPos;
+ protected readonly GlfwCallbacks.WindowMaximizeCallback _handleWindowMaximize;
+ protected readonly GlfwCallbacks.WindowIconifyCallback _handleWindowIconify;
+ protected readonly GlfwCallbacks.WindowFocusCallback _handleWindowFocus;
+ protected readonly GlfwCallbacks.MonitorCallback _handleMonitor;
+ protected GlfwCallbacks.MonitorCallback? _previousMonitorCallback;
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+
+ ///
+ /// A flag indicating whether the object has been disposed of.
+ ///
+ private bool _disposed;
+
+ ///
+ /// A lock used to ensure thread-safe access to the object.
+ ///
+ protected readonly ReaderWriterLockSlim _lock;
+
+ ///
+ /// Constructs a new with
+ /// the specified options.
+ ///
+ /// The options to use when creating the window.
+ public GlfwWindow(WindowOptions options) {
+ // Fields
+ _nativeHandle = [];
+ _pollRate = options.PollRate;
+ _display = null;
+ _title = options.Title;
+ _x = 0;
+ _y = 0;
+ _minimumWidth = uint.MinValue;
+ _width = options.Width;
+ _maximumWidth = uint.MaxValue;
+ _minimumHeight = uint.MinValue;
+ _height = options.Height;
+ _maximumHeight = uint.MaxValue;
+ _fullscreenMode = options.FullscreenMode;
+ _isResizable = options.Resizable;
+ _isDecorated = options.Decorated;
+ _isFocused = false;
+ _isMinimized = false;
+ _isMaximized = false;
+ _isVisible = !options.Hidden;
+ _cachedDisplays = [];
+ _cachedPrimaryDisplay = null;
+ _resetPollEventHandle = new(false);
+ _resetPollEventHandleState = false;
+ _previousFullscreenMode = options.FullscreenMode;
+ _restorePos = (0, 0);
+ _restoreSize = (0, 0);
+ _restoreDecorated = options.Decorated;
+ _pendingResize = false;
+ _resizeTimeout = 0;
+ _handlePreExecute = HandlePreExecute;
+ _handleDelegateEnqueued = HandleDelegateEnqueued;
+ _handleError = HandleError;
+ _handleWindowClose = HandleWindowClose;
+ _handleWindowRefresh = HandleWindowRefresh;
+ _handleFramebufferSize = HandleFramebufferSize;
+ _handleWindowSize = HandleWindowSize;
+ _handleWindowPos = HandleWindowPos;
+ _handleWindowMaximize = HandleWindowMaximize;
+ _handleWindowIconify = HandleWindowIconify;
+ _handleWindowFocus = HandleWindowFocus;
+ _handleMonitor = HandleMonitor;
+ _previousMonitorCallback = null;
+ _disposed = false;
+ _lock = new(LockRecursionPolicy.SupportsRecursion);
+
+ // Properties
+ Glfw = null!;
+ GlfwHandle = nint.Zero;
+ if (options.Dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) {
+ Glfw3.DebugContext.Log(LogLevel.Error, $"A main-thread dispatcher is required to create a {nameof(GlfwWindow)} when no dispatcher is provided.", prefix: LogId);
+ throw new RequiresMainThreadException(nameof(GlfwWindow), "constructor");
+ }
+ Dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ } else {
+ Dispatcher = options.Dispatcher;
+ }
+
+ // Wait for other processes if they are initializing
+ using Mutex mutex = new(true, "Global\\CatalystUI_Glfw3_Lock", out bool newMutex);
+ if (!newMutex) {
+ if (!mutex.WaitOne(ThreadDelegateDispatcher.LockoutTimeout)) {
+ throw new WindowException("Failed to acquire mutex lock for Glfw3 window initialization.");
+ }
+ }
+ try {
+ // Perform window initialization
+ if (!Dispatcher.Execute(() => {
+ // Request a Glfw3 API instance
+ Glfw3.DebugContext.Log(LogLevel.Verbose, "Requesting Glfw3 instance...");
+ Glfw = Glfw3.GetInstance(Dispatcher);
+ Glfw glfw = Glfw.Api;
+ glfw.SetErrorCallback(_handleError);
+ Glfw3.DebugContext.Log(LogLevel.Verbose, "Done.");
+
+ // Assign cached displays to avoid multiple creations/destruction of the API
+ _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher);
+
+ // Specify no client API
+ glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi);
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowHintClientApi.ClientApi} window hint to {ClientApi.NoApi}");
+
+ // Specify initial visibility
+ // TODO: Wayland (Linux) check.
+ glfw.WindowHint(WindowHintBool.Visible, !options.Hidden);
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowHintBool.Visible} window hint to {!options.Hidden}");
+
+ // Specify resizability
+ glfw.WindowHint(WindowHintBool.Resizable, options.Resizable);
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowHintBool.Resizable} window hint to {options.Resizable}");
+
+ // Specify decorations
+ glfw.WindowHint(WindowHintBool.Decorated, options.Decorated);
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowHintBool.Decorated} window hint to {options.Decorated}");
+
+ // Fire the initialization handler
+ options.InitializedHandler?.Invoke(this);
+
+ // Create the window
+ Glfw3.DebugContext.Log(LogLevel.Verbose, "Creating the window...");
+ WindowHandle* handle = glfw.CreateWindow((int) options.Width, (int) options.Height, options.Title, null, null);
+ GlfwHandle = (nint) handle;
+ if (handle == null || GlfwHandle == 0) throw new WindowException("Glfw3 failed to create the window.");
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Glfw3 window created with {nameof(GlfwHandle)} 0x{GlfwHandle:X}.");
+
+ // Get the native handle(s)
+ IGlfw3NativeConnector>? nativeConnector;
+ try {
+ nativeConnector = ModelRegistry.RequestConnector>>();
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native connector: {nativeConnector.GetType().Name}");
+ } catch {
+ nativeConnector = null;
+ Glfw3.DebugContext.Log(LogLevel.Warning, "No native connector found for Glfw3. Native windowing functionality will be limited, and renderer compatibility may be null.", prefix: LogId);
+ }
+ if (nativeConnector != null) {
+ _nativeHandle = [
+ nativeConnector.GetNativeHandle(Glfw, handle)
+ ];
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Retrieved native handle(s): {string.Join(", ", _nativeHandle.Select(h => $"0x{h:X}"))}");
+ }
+
+ // TODO: Wayland (Linux) check.
+ // Wait for wayland...
+
+ // Attach events
+ glfw.SetWindowCloseCallback(handle, _handleWindowClose);
+ glfw.SetWindowRefreshCallback(handle, _handleWindowRefresh);
+ glfw.SetFramebufferSizeCallback(handle, _handleFramebufferSize);
+ glfw.SetWindowSizeCallback(handle, _handleWindowSize);
+ glfw.SetWindowPosCallback(handle, _handleWindowPos);
+ glfw.SetWindowMaximizeCallback(handle, _handleWindowMaximize);
+ glfw.SetWindowIconifyCallback(handle, _handleWindowIconify);
+ glfw.SetWindowFocusCallback(handle, _handleWindowFocus);
+ _previousMonitorCallback = glfw.SetMonitorCallback(_handleMonitor);
+
+ // Set initial window size limits
+ SetSizeLimits(_minimumWidth, _minimumHeight, _maximumWidth, _maximumHeight);
+
+ // Set initial fullscreen mode
+ SetFullscreen(_fullscreenMode);
+
+ // Set window icons
+ if (options.Icons is { Length: > 0 }) SetIcons(options.Icons);
+
+ // Refresh properties
+ RefreshProperties();
+
+ // Fire created
+ OnCreated();
+
+ // Attach dispatcher event loop
+ Glfw3.DebugContext.Log(LogLevel.Verbose, "Starting event loop...", prefix: LogId);
+ Dispatcher.PreExecute += _handlePreExecute;
+ Dispatcher.DelegateEnqueued += _handleDelegateEnqueued;
+ }, wait: true, timeout: Timeout.Infinite)) {
+ throw new WindowException("Failed to initialize the window.");
+ }
+ } finally {
+ // Release the mutex
+ mutex.ReleaseMutex();
+ }
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ ~GlfwWindow() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: false);
+ }
+
+ ///
+ public virtual void SetPosition(double x, double y) {
+ // TODO: Wayland (Linux) check.
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.SetWindowPos((WindowHandle*) GlfwHandle, (int) x, (int) y);
+ RefreshProperties();
+ Glfw3.DebugContext.Log(LogLevel.Debug, $"Window position set to ({x}, {y}).", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetSize(uint width, uint height) {
+ // TODO: Wayland (Linux) check.
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (width == 0) width = IWindow.DEFAULT_WIDTH;
+ if (height == 0) height = IWindow.DEFAULT_HEIGHT;
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.SetWindowSize((WindowHandle*) GlfwHandle, (int) width, (int) height);
+ RefreshProperties();
+ Glfw3.DebugContext.Log(LogLevel.Debug, $"Window size set to {width}x{height}.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetSizeLimits(uint minWidth, uint minHeight, uint maxWidth, uint maxHeight) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (minWidth == 0) minWidth = uint.MinValue;
+ if (minHeight == 0) minHeight = uint.MinValue;
+ if (maxWidth == 0) maxWidth = uint.MaxValue;
+ if (maxHeight == 0) maxHeight = uint.MaxValue;
+ _ = Dispatcher.Execute(() => {
+ _minimumWidth = minWidth;
+ _minimumHeight = minHeight;
+ _maximumWidth = maxWidth;
+ _maximumHeight = maxHeight;
+ Glfw.Api.SetWindowSizeLimits((WindowHandle*) GlfwHandle,
+ _minimumWidth == uint.MinValue ? Silk.NET.GLFW.Glfw.DontCare : (int) minWidth,
+ _minimumHeight == uint.MinValue ? Silk.NET.GLFW.Glfw.DontCare : (int) minHeight,
+ _maximumWidth == uint.MaxValue ? Silk.NET.GLFW.Glfw.DontCare : (int) maxWidth,
+ _maximumHeight == uint.MaxValue ? Silk.NET.GLFW.Glfw.DontCare : (int) maxHeight
+ );
+ RefreshProperties();
+ Glfw3.DebugContext.Log(LogLevel.Debug, $"Window size limits set to {(_minimumWidth == uint.MinValue ? "Unlimited" : _minimumWidth)}x{(_minimumHeight == uint.MinValue ? "Unlimited" : _minimumHeight)} - {(_maximumWidth == uint.MaxValue ? "Unlimited" : _maximumWidth)}x{(_maximumHeight == uint.MaxValue ? "Unlimited" : _maximumHeight)}.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetFullscreen(WindowFullscreenMode mode, IDisplay? display = null, uint width = 0, uint height = 0) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (_fullscreenMode == mode) return;
+ _ = Dispatcher.Execute(() => {
+ if (display == null) {
+ if (_cachedPrimaryDisplay == null) throw new WindowException("Failed to fetch the primary display!");
+ display = _cachedPrimaryDisplay;
+ }
+
+ // Store the windowed position and size
+ if (mode != WindowFullscreenMode.Windowed && _previousFullscreenMode == WindowFullscreenMode.Windowed) {
+ _restorePos = (X, Y);
+ _restoreSize = (_width, _height);
+ _restoreDecorated = Glfw.Api.GetWindowAttrib((WindowHandle*) GlfwHandle, WindowAttributeGetter.Decorated);
+ }
+
+ // Update the mode
+ switch (mode) {
+ case WindowFullscreenMode.Windowed:
+ Glfw.Api.SetWindowMonitor((WindowHandle*) GlfwHandle, null, (int) _restorePos.Item1, (int) _restorePos.Item2, (int) _restoreSize.Item1, (int) _restoreSize.Item2, 0);
+ Glfw.Api.WindowHint(WindowHintBool.Decorated, _restoreDecorated);
+ SetPosition(_restorePos.Item1, _restorePos.Item2);
+ SetSize(width == 0 ? _restoreSize.Item1 : width, height == 0 ? _restoreSize.Item2 : height);
+ break;
+ case WindowFullscreenMode.Borderless:
+ // If we match existing monitor specs, GLFW will use borderless.
+ Glfw.Api.WindowHint(WindowHintBool.Decorated, false);
+ Glfw.Api.SetWindowMonitor((WindowHandle*) GlfwHandle, (Monitor*) ((GlfwDisplay) display).Monitor, 0, 0, (int) display.Width, (int) display.Height, (int) Math.Round(display.RefreshRate));
+ break;
+ case WindowFullscreenMode.Fullscreen:
+ // TODO: Handling custom resolutions? See https://github.com/glfw/glfw/issues/1904
+ Glfw.Api.SetWindowMonitor((WindowHandle*) GlfwHandle, (Monitor*) ((GlfwDisplay) display).Monitor, 0, 0, (int) display.Width, (int) display.Height, (int) Math.Round(display.RefreshRate));
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
+ }
+
+ // Request focus and update the fullscreen mode
+ RequestFocus();
+ _previousFullscreenMode = _fullscreenMode;
+ _fullscreenMode = mode;
+
+ Glfw3.DebugContext.Log(LogLevel.Debug, $"Window fullscreen mode set to {mode} on display {display} with resolution {width}x{height}.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetIcons(params WindowIcon[] icons) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ if (icons.Length == 0) {
+ Glfw.Api.SetWindowIcon((WindowHandle*) GlfwHandle, 0, null);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window icons cleared.", prefix: LogId);
+ } else {
+ // Allocate handles and pointers for the icons
+ List iconHandles = new(icons.Length);
+ List iconPointers = new(icons.Length);
+ for (int i = 0; i < icons.Length; i++) {
+ WindowIcon icon = icons[i];
+
+ // Convert the icon to a byte array
+ Vector4[] pixelData = [..icon.Pixels];
+ if (pixelData.Length != (icon.Width * icon.Height)) throw new ArgumentException($"The number of pixels does not match the specified width and height for icon index {i}.");
+
+ // Pin the pixel data in memory
+ GCHandle handle = GCHandle.Alloc(pixelData, GCHandleType.Pinned);
+ iconHandles.Add(handle);
+ nint pointer = handle.AddrOfPinnedObject();
+ iconPointers.Add(pointer);
+ }
+
+ // Convert the icon to an array of Silk.NET.GLFW.Image
+ Image[] images = new Image[icons.Length];
+ for (int i = 0; i < icons.Length; i++) {
+ WindowIcon icon = icons[i];
+ images[i] = new() {
+ Width = (int) icon.Width,
+ Height = (int) icon.Height,
+ Pixels = (byte*) iconPointers[i]
+ };
+ }
+
+ // Upload the icons to GLFW
+ fixed (Image* imagesPtr = images) {
+ Glfw.Api.SetWindowIcon((WindowHandle*) GlfwHandle, icons.Length, imagesPtr);
+ }
+
+ // Release the pinned pixel data for each icon
+ for (int i = 0; i < iconHandles.Count; i++) {
+ iconHandles[i].Free();
+ }
+
+ Glfw3.DebugContext.Log(LogLevel.Debug, $"Window icons set ({icons.Length} icons).", prefix: LogId);
+ }
+ }, wait: true);
+ }
+
+ ///
+ public virtual void RequestFocus() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.FocusWindow((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window focus requested.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void RequestAttention() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.RequestWindowAttention((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window attention requested.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Minimize() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.IconifyWindow((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window minimized.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Maximize() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.MaximizeWindow((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window maximized.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Restore() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.RestoreWindow((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window restored.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Show() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.ShowWindow((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window shown.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Hide() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.HideWindow((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window hidden.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Close() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Glfw.Api.SetWindowShouldClose((WindowHandle*) GlfwHandle, true);
+ HandleWindowClose((WindowHandle*) GlfwHandle);
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window close requested.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Exit() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Dispose();
+ OnClosed();
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window exited.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Wait() {
+ if (_disposed) return;
+ ManualResetEvent reset = new(false);
+ try {
+ Closed += _ => {
+ // ReSharper disable once AccessToDisposedClosure
+ reset.Set();
+ };
+ reset.WaitOne();
+ } finally {
+ reset.Dispose();
+ }
+ }
+
+ ///
+ /// Refreshes the backing fields for the window properties.
+ ///
+ ///
+ /// Since Glfw3 does not provide events for all window property changes,
+ /// the following method is provided to manually refresh all properties
+ /// from the underlying Glfw3 window state. It should be called whenever
+ /// a property change is suspected that may not have triggered an event.
+ ///
+ protected virtual void RefreshProperties() {
+ // TODO: Wayland (Linux) check.
+ // Pull position
+ Glfw.Api.GetWindowPos((WindowHandle*) GlfwHandle, out int x, out int y);
+ _x = x;
+ _y = y;
+
+ // Pull size
+ Glfw.Api.GetWindowSize((WindowHandle*) GlfwHandle, out int width, out int height);
+ _width = (uint) width;
+ _height = (uint) height;
+
+ // Pull state
+ _isFocused = Glfw.Api.GetWindowAttrib((WindowHandle*) GlfwHandle, WindowAttributeGetter.Focused);
+ _isMinimized = Glfw.Api.GetWindowAttrib((WindowHandle*) GlfwHandle, WindowAttributeGetter.Iconified);
+ _isMaximized = Glfw.Api.GetWindowAttrib((WindowHandle*) GlfwHandle, WindowAttributeGetter.Maximized);
+ _isVisible = Glfw.Api.GetWindowAttrib((WindowHandle*) GlfwHandle, WindowAttributeGetter.Visible);
+
+ // Refresh the display
+ RefreshDisplay();
+
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Refreshed on-request properties: Pos({_x}, {_y}), Size({_width}x{_height}), Focused({_isFocused}), Minimized({_isMinimized}), Maximized({_isMaximized}), Visible({_isVisible}), Display({_display})", prefix: LogId);
+ // The rest of internal variables are state-driven and requested on-demand.
+
+ // Fire refresh
+ OnRefresh();
+ }
+
+ ///
+ /// Refreshes the property by
+ /// determining which display the window is currently on.
+ ///
+ protected virtual void RefreshDisplay() {
+ if (_cachedDisplays.Length == 0) return;
+
+ // Determine which monitor has the most overlap
+ double bestOverlap = 0.0f;
+ GlfwDisplay? bestDisplay = null;
+ for (int i = 0; i < _cachedDisplays.Length; i++) {
+ double wx = _x;
+ double wy = _y;
+ uint ww = _width;
+ uint wh = _height;
+ double dx = _cachedDisplays[i].X;
+ double dy = _cachedDisplays[i].Y;
+ uint dw = _cachedDisplays[i].Width;
+ uint dh = _cachedDisplays[i].Height;
+
+ // Determine overlap
+ double overlap =
+ (uint) Math.Max(0, Math.Min(wx + ww, dx + (int) dw) - Math.Max(wx, dx)) *
+ (double) Math.Max(0, Math.Min(wy + wh, dy + (int) dh) - Math.Max(wy, dy));
+
+ // Determine best overlap
+ if (overlap > bestOverlap) {
+ bestOverlap = overlap;
+ bestDisplay = _cachedDisplays[i];
+ }
+ }
+
+ // Assign the best display
+ _display = bestDisplay;
+
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Refreshed display. Determined best display: {_display}", prefix: LogId);
+ }
+
+ ///
+ protected virtual void HandlePreExecute(ThreadDelegateDispatcher dispatcher) {
+ // Nullability check due to race conditions during disposal
+ // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
+ if (Glfw?.Api == null) return;
+ if (PollRate == 0) {
+ // Passive polling (only when events are enqueued)
+ while (!_disposed && dispatcher.Enqueued == 0) {
+ Glfw.Api.PollEvents(); // double poll for responsivity
+ Glfw.Api.WaitEventsTimeout(MINIMUM_POLL_RATE);
+ }
+ } else if (PollRate != ushort.MaxValue) {
+ // Active polling (on X intervals of time)
+ Glfw.Api.PollEvents();
+ _resetPollEventHandle.WaitOne(PollRate);
+ if (_resetPollEventHandleState) {
+ _resetPollEventHandle.Reset();
+ _resetPollEventHandleState = false;
+ }
+ } else {
+ // Fastest polling (on every loop of the main thread)
+ Glfw.Api.PollEvents();
+ }
+ }
+
+ ///
+ protected virtual void HandleDelegateEnqueued(ThreadDelegateDispatcher dispatcher, Delegate @delegate) {
+ Glfw.Api.PostEmptyEvent(); // un-block the main thread if waiting
+ _resetPollEventHandle.Set();
+ _resetPollEventHandleState = true;
+ }
+
+ ///
+ protected virtual void HandleError(ErrorCode error, string description) {
+ OnErrored(new($"{error}: {description}"));
+ }
+
+ ///
+ protected virtual void HandleWindowClose(WindowHandle* handle) {
+ if (GlfwHandle != (nint) handle) return;
+ bool shouldCancel = false;
+ _ = Dispatcher.Execute(() => {
+ shouldCancel = OnClosing();
+ }, wait: true);
+ if (shouldCancel) {
+ Glfw.Api.SetWindowShouldClose(handle, false);
+ } else {
+ Dispose();
+ OnClosed();
+ }
+ }
+
+ ///
+ protected virtual void HandleWindowRefresh(WindowHandle* handle) {
+ if (_pendingResize) return;
+ RefreshProperties();
+ }
+
+ ///
+ protected virtual void HandleFramebufferSize(WindowHandle* handle, int width, int height) {
+ _resizeTimeout = 500;
+ if (!_pendingResize) {
+ _pendingResize = true;
+ FramebufferResizeStabilizer();
+ }
+ RefreshProperties();
+ OnRedraw();
+ }
+
+ ///
+ /// Delays the frame-buffer size callback to allow for the window to stabilize.
+ ///
+ protected virtual void FramebufferResizeStabilizer() {
+ // Delay resetting the value to allow for the window to stabilize
+ // Thanks GLFW for the confusing results of refreshing and resizing :)
+ new Thread(() => {
+ while (_resizeTimeout > 0) {
+ Thread.Sleep(50);
+ int newTimeout = _resizeTimeout - 50;
+ Interlocked.Exchange(ref _resizeTimeout, newTimeout);
+ }
+ _pendingResize = false;
+ }).Start();
+ }
+
+ ///
+ protected virtual void HandleWindowSize(WindowHandle* handle, int width, int height) {
+ OnResized();
+ }
+
+ ///
+ protected virtual void HandleWindowPos(WindowHandle* handle, int x, int y) {
+ RefreshProperties();
+ OnRepositioned();
+ }
+
+ ///
+ protected virtual void HandleWindowMaximize(WindowHandle* handle, bool maximized) {
+ RefreshProperties();
+ if (maximized) OnMaximized();
+ }
+
+ ///
+ protected virtual void HandleWindowIconify(WindowHandle* handle, bool iconified) {
+ RefreshProperties();
+ if (iconified) OnMinimized();
+ }
+
+ ///
+ protected virtual void HandleWindowFocus(WindowHandle* handle, bool focused) {
+ RefreshProperties();
+ if (focused) OnFocused();
+ else OnUnfocused();
+ }
+
+ ///
+ protected virtual void HandleMonitor(Monitor* monitor, ConnectedState state) {
+ // Propagate the event
+ _previousMonitorCallback?.Invoke(monitor, state);
+
+ // Update the cached displays
+ _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher);
+ _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(Dispatcher);
+ }
+
+ ///
+ protected virtual void OnErrored(WindowException exception) {
+ Glfw3.DebugContext.Log(LogLevel.Error, "Window exception occurred!", prefix: LogId, args: exception);
+ Errored?.Invoke(this, exception);
+ }
+
+ ///
+ protected virtual void OnCreated() {
+ Glfw3.DebugContext.Log(LogLevel.Info, $"Window created.", prefix: LogId);
+ Created?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRepositioned() {
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Window repositioned to ({X}, {Y}).", prefix: LogId);
+ Repositioned?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnResized() {
+ Glfw3.DebugContext.Log(LogLevel.Verbose, $"Window resized to {Width}x{Height}.", prefix: LogId);
+ Resized?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRefresh() {
+ Refresh?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRedraw() {
+ Redraw?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnFocused() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window focused.", prefix: LogId);
+ Focused?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnUnfocused() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window unfocused.", prefix: LogId);
+ Unfocused?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnMinimized() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window minimized.", prefix: LogId);
+ Minimized?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnMaximized() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window maximized.", prefix: LogId);
+ Maximized?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRestored() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window restored.", prefix: LogId);
+ Restored?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnShown() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window shown.", prefix: LogId);
+ Shown?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnHidden() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window hidden.", prefix: LogId);
+ Hidden?.Invoke(this);
+ }
+
+ ///
+ protected virtual bool OnClosing() {
+ Glfw3.DebugContext.Log(LogLevel.Debug, "Window closing...", prefix: LogId);
+ Delegate[] delegates = Closing?.GetInvocationList() ?? [];
+ bool result = delegates.Cast().Any(handler => handler(this));
+ if (result) Glfw3.DebugContext.Log(LogLevel.Debug, "Window closure cancelled by event handler.", prefix: LogId);
+ return result;
+ }
+
+ ///
+ protected virtual void OnClosed() {
+ Glfw3.DebugContext.Log(LogLevel.Info, "Window closed.", prefix: LogId);
+ Closed?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnInteracted() {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ public virtual void Dispose() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// if disposal is being performed by the garbage collector, otherwise
+ ///
+ private void Dispose(bool disposing) {
+ _lock.EnterWriteLock();
+ try {
+ if (_disposed) return;
+
+ // Dispose managed state (managed objects)
+ if (disposing) {
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (Dispatcher != null) {
+ Dispatcher.PreExecute -= HandlePreExecute;
+ Dispatcher.DelegateEnqueued -= HandleDelegateEnqueued;
+ }
+
+ // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
+ Glfw?.Api.PostEmptyEvent();
+ Glfw?.Dispose();
+ }
+
+ // Dispose unmanaged state (unmanaged objects)
+ // ...
+
+ // Indicate disposal completion
+ _disposed = true;
+ } finally {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs b/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs
index a0680a5..ab26fb2 100644
--- a/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs
+++ b/CatalystUI/Tooling/CatalystUI.Analyzers/Threading/CachedDelegateGenerator.cs
@@ -124,6 +124,8 @@ private static void GenerateCacheDelegateSource(SourceProductionContext context,
$$"""
#nullable enable
+ using System;
+
//
namespace {{ns}} {