From 18a671f7c77038b0aab52bcc4b363e599fef088b Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Mon, 27 Oct 2025 21:32:25 -0600 Subject: [PATCH 01/15] Add the Crystal project * Added the Crystal module to the solution. * Imported varying interfaces for visual windows and displays from previous project iterations. * No additional changes were made. --- .scripts/Setup.ps1 | 8 + CatalystUI/CatalystUI.sln | 7 + .../CatalystUI.Modules.Crystal.Core.csproj | 24 + .../IWindowDomain.cs | 25 + .../IWindowLayer.cs | 36 ++ .../Window/DisplayOrientation.cs | 77 +++ .../Window/IDisplay.cs | 176 +++++++ .../Window/IWindow.cs | 494 ++++++++++++++++++ .../Window/WindowException.cs | 49 ++ .../Window/WindowFullscreenMode.cs | 37 ++ .../Window/WindowIcon.cs | 106 ++++ 11 files changed, 1039 insertions(+) create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/CatalystUI.Modules.Crystal.Core.csproj create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowDomain.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/DisplayOrientation.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowException.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowFullscreenMode.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs diff --git a/.scripts/Setup.ps1 b/.scripts/Setup.ps1 index ee5587a..2005335 100644 --- a/.scripts/Setup.ps1 +++ b/.scripts/Setup.ps1 @@ -71,6 +71,14 @@ $projectsList = @( ) PromptIgnore = $false Depends = @("Arcane") + }, + @{ + Module = "Crystal" + Projects = @( + @{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Core" } + ) + PromptIgnore = $false + Depends = @("Core") } ) diff --git a/CatalystUI/CatalystUI.sln b/CatalystUI/CatalystUI.sln index 0b50ed1..f138248 100644 --- a/CatalystUI/CatalystUI.sln +++ b/CatalystUI/CatalystUI.sln @@ -36,6 +36,8 @@ 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -90,6 +92,10 @@ 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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {68F496AC-9438-40F1-9DF8-97363033D661} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D} @@ -107,5 +113,6 @@ 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} EndGlobalSection EndGlobal 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/IWindowDomain.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowDomain.cs new file mode 100644 index 0000000..c1dd600 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowDomain.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------------------------------- +// 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; + +namespace Catalyst.Modules.Crystal { + + /// + /// Represents a domain for visual or graphical windows. + /// + public interface IWindowDomain : IMultisensoryDomain { + + // ... + + } + +} \ 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..05def4f --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Layers; +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. + /// + /// An enumerable collection of connected displays. + IEnumerable GetDisplays(); + + /// + /// Queries the window for the primary display. + /// + /// The primary display, or if one could not be determined. + IDisplay? GetPrimaryDisplay(); + + } + +} \ 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..9cd0414 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs @@ -0,0 +1,176 @@ +// ------------------------------------------------------------------------------------------------- +// 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 of + /// 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 the dots per inch (DPI) of the display, + /// the default value is 96 PPI (pixels per inch), + /// which is equivalent to 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..adaacd7 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs @@ -0,0 +1,494 @@ +// ------------------------------------------------------------------------------------------------- +// 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; + +// ReSharper disable once CheckNamespace +namespace Catalyst.Modules.Crystal { + + /// + /// Represents the logical, interactive, visual or graphical window into the system. + /// + public interface IWindow { + + /// + /// 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. + /// + 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 system's native handle(s) for this 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 this 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..678b8e8 --- /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_%dx%d.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 From bd897d4a6098c4fe38b76754b0da44450dfa5285 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Mon, 27 Oct 2025 22:03:11 -0600 Subject: [PATCH 02/15] Add basic Glfw3 display implementarion * Added the previous works of the Glfw3 implementation to the Crystal module. * Resolved missing import in Core/CatalystApp.cs * Resolved missing import in Tooling/Analyzers/CachedDelegateGenerator.cs --- .scripts/Setup.ps1 | 8 + CatalystUI/CatalystUI.sln | 7 + .../Core/CatalystUI.Core/CatalystApp.cs | 1 + .../CatalystUI.Modules.Crystal.Glfw3.csproj | 25 ++ .../CatalystAppBuilderExtensions.cs | 59 +++ .../CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs | 188 ++++++++++ .../Glfw3WindowLayer.cs | 66 ++++ .../IGlfw3NativeConnector.cs | 59 +++ .../NativeConnectors/EdidHelper.cs | 71 ++++ .../Glfw3MacNativeConnector.cs | 293 +++++++++++++++ .../Glfw3WindowsNativeConnector.cs | 355 ++++++++++++++++++ .../Window/GlfwDisplay.cs | 168 +++++++++ .../Threading/CachedDelegateGenerator.cs | 2 + 13 files changed, 1302 insertions(+) create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/CatalystUI.Modules.Crystal.Glfw3.csproj create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/IGlfw3NativeConnector.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs diff --git a/.scripts/Setup.ps1 b/.scripts/Setup.ps1 index 2005335..999152a 100644 --- a/.scripts/Setup.ps1 +++ b/.scripts/Setup.ps1 @@ -79,6 +79,14 @@ $projectsList = @( ) 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 f138248..6a5b756 100644 --- a/CatalystUI/CatalystUI.sln +++ b/CatalystUI/CatalystUI.sln @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Arcane.C 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 @@ -96,6 +98,10 @@ Global {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} @@ -114,5 +120,6 @@ Global {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/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..4037cc3 --- /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.Supplementary; +using Catalyst.Modules.Crystal.Glfw3; +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..6656f32 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs @@ -0,0 +1,188 @@ +// ------------------------------------------------------------------------------------------------- +// 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 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() { + _staticLock.Enter(); + try { + if (_referenceCount == 0) Initialize(); + _referenceCount++; + return new(); + } finally { + _staticLock.Exit(); + } + } + + /// + /// Initializes the Glfw3 API. + /// + private static void Initialize() { + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3), nameof(GetInstance)); + if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) { + throw new TypeInitializationException(nameof(Glfw3), new InvalidOperationException("Failed to initialize Glfw3 API on the main thread.")); + } + + } + + [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() { + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3), nameof(Terminate)); + if (!ThreadDelegateDispatcher.MainThreadDispatcher.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(); + } 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..721c209 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------------------------------- +// 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() { + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3WindowLayer), nameof(GetDisplays)); + if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedFunctionGetDisplaysUnsafe, out GlfwDisplay[] displays)) { + throw new WindowException("Failed to get displays from Glfw3 on the main thread."); + } + return displays.OfType(); + } + + [CachedDelegate] + private static GlfwDisplay[] GetDisplaysUnsafe() { + using Glfw3 glfw = Glfw3.GetInstance(); + 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() { + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3WindowLayer), nameof(GetPrimaryDisplay)); + if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedFunctionGetPrimaryDisplayUnsafe, out GlfwDisplay? display)) { + throw new WindowException("Failed to get primary display from Glfw3 on the main thread."); + } + return display; + } + + [CachedDelegate] + private static unsafe GlfwDisplay? GetPrimaryDisplayUnsafe() { + using Glfw3 glfw = Glfw3.GetInstance(); + Monitor* pPrimaryMonitor = glfw.Api.GetPrimaryMonitor(); + return pPrimaryMonitor != null ? GlfwDisplay.FromMonitor(glfw, pPrimaryMonitor) : null; + } + + } + +} \ 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..1873cfa --- /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 = WindowsImports.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..27f720d --- /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 handler for this system + IGlfw3NativeConnector>? nativeHandler; + try { + nativeHandler = ModelRegistry.RequestConnector>>(); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native handler: {nativeHandler}"); + } catch { + nativeHandler = null; + Glfw3.DebugContext.Log(LogLevel.Warning, "No native handler found for Glfw3. Some QOL features may not be available."); + } + + // Get native details + Angle rotation; + DisplayOrientation orientation; + try { + if (nativeHandler != null) { + rotation = Angle.FromDegrees(nativeHandler.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 (nativeHandler != null) { + descriptor = nativeHandler.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 (nativeHandler != null) { + manufacturer = nativeHandler.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/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}} { From b85dfe19a4a5359ef3e25beafcf51b4954239b61 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Wed, 29 Oct 2025 20:06:46 -0600 Subject: [PATCH 03/15] Add Glfw3 window implementation * Added the previous works of the Glfw3 implementation to the Crystal module. --- CatalystUI/Core/CatalystUI.Core/IOptions.cs | 23 + .../IWindowLayer.cs | 7 + .../Window/IWindow.cs | 3 +- .../Window/WindowOptions.cs | 101 ++ .../Glfw3WindowLayer.cs | 9 +- .../Window/GlfwWindow.cs | 1167 +++++++++++++++++ 6 files changed, 1307 insertions(+), 3 deletions(-) create mode 100644 CatalystUI/Core/CatalystUI.Core/IOptions.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs 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/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs index 05def4f..5760aa9 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs @@ -31,6 +31,13 @@ public interface IWindowLayer : IWindowLayer { /// The primary display, or if one could not be determined. IDisplay? GetPrimaryDisplay(); + /// + /// 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/Window/IWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs index adaacd7..ddc127c 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs @@ -10,6 +10,7 @@ // ------------------------------------------------------------------------------------------------- using Catalyst.Interactions; +using System; // ReSharper disable once CheckNamespace namespace Catalyst.Modules.Crystal { @@ -17,7 +18,7 @@ namespace Catalyst.Modules.Crystal { /// /// Represents the logical, interactive, visual or graphical window into the system. /// - public interface IWindow { + public interface IWindow : IDisposable { /// /// The default value for . 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..4c49af2 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------------------------------- +// 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 options for constructing/configuring a window. + /// + public readonly record struct WindowOptions : IOptions { + + /// + 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 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( + 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 + ) { + 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/Glfw3WindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs index 721c209..92de4be 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs @@ -33,7 +33,7 @@ public IEnumerable GetDisplays() { } [CachedDelegate] - private static GlfwDisplay[] GetDisplaysUnsafe() { + internal static GlfwDisplay[] GetDisplaysUnsafe() { using Glfw3 glfw = Glfw3.GetInstance(); Monitor** pMonitors = glfw.Api.GetMonitors(out int count); if (pMonitors == null || count <= 0) return []; @@ -55,12 +55,17 @@ private static GlfwDisplay[] GetDisplaysUnsafe() { } [CachedDelegate] - private static unsafe GlfwDisplay? GetPrimaryDisplayUnsafe() { + internal static unsafe GlfwDisplay? GetPrimaryDisplayUnsafe() { using Glfw3 glfw = Glfw3.GetInstance(); 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/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs new file mode 100644 index 0000000..c08b2d5 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs @@ -0,0 +1,1167 @@ +// ------------------------------------------------------------------------------------------------- +// 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; + +// ReSharper disable once CheckNamespace +namespace Catalyst.Modules.Crystal.Glfw3 { + + /// + /// An implementation of using the Glfw3 windowing API. + /// + public unsafe class GlfwWindow : IWindow { + + /// + /// The minimum allowed poll rate in milliseconds. + /// + /// + /// In passive polling mode, to prevent lockups of the + /// main thread if the window is not responding, waiting + /// for events will time out after the minimum poll rate + /// in milliseconds. + /// + public const int 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 or sets the Glfw3 instance associated with the window. + /// + /// The Glfw3 instance. + public Glfw3 Glfw { get; private set; } + + /// + /// Gets or sets the internal Glfw3 window handle. + /// + /// The Glfw3 window handle. + public nint GlfwHandle { get; private set; } + + /// + /// Gets a generated string used for logging purposes. + /// + /// The window class name followed by the native handle in hexadecimal format. + protected string LogHandle => $"{nameof(GlfwWindow)} {(NativeHandle.Length > 0 ? $"0x{NativeHandle[0]:X}" : "0x????")}"; + + /// + /// 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; + if (ThreadDelegateDispatcher.MainThreadDispatcher != null) { + // Simply enqueuing a no-op will cause the main thread dispatcher to wake up + // and re-evaluate the poll rate on the next iteration. + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + // ... + }); + } + } + } + + /// + /// 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.Verbose, $"Set title to \"{value}\""); + }); + } + } + + /// + /// 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; + + /// + /// A handle used to reset the poll wait for active polling. + /// + protected readonly ManualResetEvent _resetPollEventHandle; + + /// + /// The state of the reset poll event handle. + /// + protected bool _resetPollEventHandleState; + + /// + /// Cached delegate reference to the passive wait for events method. + /// + protected readonly ThreadDelegateDispatcher.DispatcherEventHandler _preExecuteHandler; + + /// + /// Cached delegate reference to the pre-execute handler for the main thread dispatcher. + /// + protected readonly ThreadDelegateDispatcher.DispatcherQueueEventHandler _delegateEnqueuedHandler; + + /// + /// Cached delegate reference to the error callback. + /// + protected readonly GlfwCallbacks.ErrorCallback _errorCallback; + + /// + /// Cached delegate reference to the window close callback. + /// + protected readonly GlfwCallbacks.WindowCloseCallback _windowCloseCallback; + + /// + /// Cached delegate reference to the window position callback. + /// + protected readonly GlfwCallbacks.WindowRefreshCallback _windowRefreshCallback; + + /// + /// Cached delegate reference to the framebuffer size callback. + /// + protected readonly GlfwCallbacks.FramebufferSizeCallback _framebufferSizeCallback; + + /// + /// Cached delegate reference to the window size callback. + /// + protected readonly GlfwCallbacks.WindowSizeCallback _windowSizeCallback; + + /// + /// Cached delegate reference to the window content scale callback. + /// + protected readonly GlfwCallbacks.WindowPosCallback _windowPosCallback; + + /// + /// Cached delegate reference to the window maximize callback. + /// + protected readonly GlfwCallbacks.WindowMaximizeCallback _windowMaximizeCallback; + + /// + /// Cached delegate reference to the window minimize (iconify) callback. + /// + protected readonly GlfwCallbacks.WindowIconifyCallback _windowIconifyCallback; + + /// + /// Cached delegate reference to the window focus callback. + /// + protected readonly GlfwCallbacks.WindowFocusCallback _windowFocusCallback; + + /// + /// Cached delegate reference to the monitor callback. + /// + protected readonly GlfwCallbacks.MonitorCallback _monitorCallback; + + /// + /// Used to store the previous monitor callback for GLFW, if any. + /// + protected GlfwCallbacks.MonitorCallback? _previousMonitorCallback; + + /// + /// Used to cache the current list of known displays. + /// + protected GlfwDisplay[] _cachedDisplays; + + /// + /// Used to cache the primary display. + /// + protected GlfwDisplay? _cachedPrimaryDisplay; + + /// + /// 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; + + /// + /// 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 . + /// + internal 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; + _resetPollEventHandle = new(false); + _resetPollEventHandleState = false; + _preExecuteHandler = HandlePreExecute; + _delegateEnqueuedHandler = HandleDelegateEnqueued; + _errorCallback = HandleError; + _windowCloseCallback = HandleWindowClose; + _windowRefreshCallback = HandleWindowRefresh; + _framebufferSizeCallback = HandleFramebufferSize; + _windowSizeCallback = HandleWindowSize; + _windowPosCallback = HandleWindowPos; + _windowMaximizeCallback = HandleWindowMaximize; + _windowIconifyCallback = HandleWindowIconify; + _windowFocusCallback = HandleWindowFocus; + _monitorCallback = HandleMonitorCallback; + _previousMonitorCallback = null; + _previousFullscreenMode = options.FullscreenMode; + _restorePos = (0, 0); + _restoreSize = (0, 0); + _restoreDecorated = options.Decorated; + _cachedDisplays = []; + _cachedPrimaryDisplay = null; + _pendingResize = false; + _resizeTimeout = 0; + _disposed = false; + _lock = new(LockRecursionPolicy.SupportsRecursion); + + // Properties + Glfw = null!; + GlfwHandle = 0; + + // 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 (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), "constructor"); + if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + // Get a Glfw3 API instance + Glfw3.DebugContext.Log(LogLevel.Verbose, "Requesting new Glfw3 instance for window creation..."); + Glfw = Glfw3.GetInstance(); + Glfw glfw = Glfw.Api; + glfw.SetErrorCallback(_errorCallback); + Glfw3.DebugContext.Log(LogLevel.Verbose, "Done."); + + // Now assign the cached displays to avoid creating then destroying the API a bunch + _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(); + _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(); + + // Specify no client api + glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi); + Glfw3.DebugContext.Log(LogLevel.Verbose, "Set Glfw3 ClientApi hint to NoApi."); + + // TODO: Initial visibility on Wayland compat + glfw.WindowHint(WindowHintBool.Visible, !options.Hidden); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Set Glfw3 Visible hint to {!options.Hidden}."); + + // Resizable + glfw.WindowHint(WindowHintBool.Resizable, options.Resizable); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Set Glfw3 Resizable hint to {options.Resizable}."); + + // Decorations + glfw.WindowHint(WindowHintBool.Decorated, options.Decorated); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Set Glfw3 Decorated hint to {options.Decorated}."); + + // Initialized + options.InitializedHandler?.Invoke(this); + + // TODO: Handle decorated and resizable + + // Create the window + Glfw3.DebugContext.Log(LogLevel.Verbose, "Creating Glfw3 window..."); + WindowHandle* handle = glfw.CreateWindow((int) options.Width, (int) options.Height, options.Title, null, null); + GlfwHandle = (nint) handle; + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Created Glfw3 window with handle 0x{GlfwHandle:X}"); + if (handle == null || GlfwHandle == 0) throw new WindowException("Failed to create the window!"); + + // Get the native handle(s) + IGlfw3NativeConnector>? nativeHandler; + try { + nativeHandler = ModelRegistry.RequestConnector>>(); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native handler: {nativeHandler}"); + } catch { + nativeHandler = null; + Glfw3.DebugContext.Log(LogLevel.Warning, "No native handler found for Glfw3. Native windowing functionality will be limited, and renderer compatibility may be null."); + } + if (nativeHandler != null) { + _nativeHandle = [ + nativeHandler.GetNativeHandle(Glfw, handle) + ]; + } + + // TODO: Wait for Wayland + + // Attach events + glfw.SetWindowCloseCallback(handle, _windowCloseCallback); + glfw.SetWindowRefreshCallback(handle, _windowRefreshCallback); + glfw.SetFramebufferSizeCallback(handle, _framebufferSizeCallback); + glfw.SetWindowSizeCallback(handle, _windowSizeCallback); + glfw.SetWindowPosCallback(handle, _windowPosCallback); + glfw.SetWindowMaximizeCallback(handle, _windowMaximizeCallback); + glfw.SetWindowIconifyCallback(handle, _windowIconifyCallback); + glfw.SetWindowFocusCallback(handle, _windowFocusCallback); + _previousMonitorCallback = glfw.SetMonitorCallback(_monitorCallback); + + // Update initial window size limits + SetSizeLimits(_minimumWidth, _minimumHeight, _maximumWidth, _maximumHeight); + + // Update initial fullscreen mode + SetFullscreen(_fullscreenMode); + + // Set window icons + if (options.Icons is { Length: > 0 }) SetIcons(options.Icons); + + // Initial properties refresh + RefreshProperties(); + + // Fire created + OnCreated(); + + // Attach threading events + ThreadDelegateDispatcher.MainThreadDispatcher.PreExecute += _preExecuteHandler; + ThreadDelegateDispatcher.MainThreadDispatcher.DelegateEnqueued += _delegateEnqueuedHandler; + }, wait: true, timeout: Timeout.Infinite)) { + throw new WindowException("Failed to initialize the window on the main thread."); + } + } 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: Add Wayland linux check + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetPosition)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.SetWindowPos((WindowHandle*) GlfwHandle, (int) x, (int) y); + Glfw3.DebugContext.Log(LogLevel.Debug, $"Set position to {x}, {y}"); + }); + } + + /// + public virtual void SetSize(uint width, uint height) { + // TODO: Add Wayland linux check + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetSize)); + if (width == 0) width = IWindow.DEFAULT_WIDTH; + if (height == 0) height = IWindow.DEFAULT_HEIGHT; + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.SetWindowSize((WindowHandle*) GlfwHandle, (int) width, (int) height); + Glfw3.DebugContext.Log(LogLevel.Debug, $"Set size to {width}x{height}"); + }); + } + + /// + public virtual void SetSizeLimits(uint minWidth, uint minHeight, uint maxWidth, uint maxHeight) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetSizeLimits)); + if (minWidth == 0) minWidth = uint.MinValue; + if (minHeight == 0) minHeight = uint.MinValue; + if (maxWidth == 0) maxWidth = uint.MaxValue; + if (maxHeight == 0) maxHeight = uint.MaxValue; + ThreadDelegateDispatcher.MainThreadDispatcher.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 + ); + Glfw3.DebugContext.Log(LogLevel.Debug, $"Set size limits to {(_minimumWidth == uint.MinValue ? "Unlimited" : _minimumWidth)}x{(_minimumHeight == uint.MinValue ? "Unlimited" : _minimumHeight)} - {(_maximumWidth == uint.MaxValue ? "Unlimited" : _maximumWidth)}x{(_maximumHeight == uint.MaxValue ? "Unlimited" : _maximumHeight)}"); + }); + } + + /// + public virtual void SetFullscreen(WindowFullscreenMode mode, IDisplay? display = null, uint width = 0, uint height = 0) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetFullscreen)); + if (_fullscreenMode == mode) return; + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + if (display == null) { + if (_cachedPrimaryDisplay == null) throw new WindowException("Failed to fetch the primary display!"); + display = _cachedPrimaryDisplay; + } + + // Log 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, (Silk.NET.GLFW.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, (Silk.NET.GLFW.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, $"Set fullscreen mode to {mode} on display {display} with resolution {(width == 0 ? _width : width)}x{(height == 0 ? _height : height)}"); + }); + } + + /// + public virtual void SetIcons(params WindowIcon[] icons) { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetIcons)); + if (icons is not { Length: > 0 }) throw new ArgumentException("At least one icon must be provided.", nameof(icons)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + // 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, $"Set {icons.Length} window icons"); + }); + } + + /// + public virtual void RequestFocus() { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(RequestFocus)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.FocusWindow((WindowHandle*) GlfwHandle); + Glfw3.DebugContext.Log(LogLevel.Debug, "Requested focus"); + }); + } + + /// + public virtual void RequestAttention() { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(RequestAttention)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.RequestWindowAttention((WindowHandle*) GlfwHandle); + Glfw3.DebugContext.Log(LogLevel.Debug, "Requested attention"); + }); + } + + /// + public virtual void Minimize() { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Minimize)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.IconifyWindow((WindowHandle*) GlfwHandle); + Glfw3.DebugContext.Log(LogLevel.Debug, "Minimized window"); + }); + } + + /// + public virtual void Maximize() { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Maximize)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.MaximizeWindow((WindowHandle*) GlfwHandle); + Glfw3.DebugContext.Log(LogLevel.Debug, "Maximized window"); + }); + } + + /// + public virtual void Restore() { + throw new NotImplementedException(); + } + + /// + public virtual void Show() { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Show)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.ShowWindow((WindowHandle*) GlfwHandle); + Glfw3.DebugContext.Log(LogLevel.Debug, "Shown window"); + }); + } + + /// + public virtual void Hide() { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Hide)); + ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + Glfw.Api.HideWindow((WindowHandle*) GlfwHandle); + Glfw3.DebugContext.Log(LogLevel.Debug, "Hidden window"); + }); + } + + /// + public virtual void Close() { + throw new NotImplementedException(); + } + + /// + public virtual void Exit() { + throw new NotImplementedException(); + } + + /// + public virtual void Wait() { + if (_disposed) return; + ManualResetEvent reset = new(false); + Closed += _ => { + reset.Set(); + }; + reset.WaitOne(); + } + + /// + /// Refreshes the underlying variables for the property of this window. + /// + protected void RefreshProperties() { + // TODO: Linux Wayland 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 for {LogHandle}: Pos({_x}, {_y}), Size({_width}x{_height}), Focused({_isFocused}), Minimized({_isMinimized}), Maximized({_isMaximized}), Visible({_isVisible}), Display({_display})"); + // The rest of internal variables are state-driven and requested on-demand. + + // Fire refresh + OnRefresh(); + } + + /// + /// Refreshes the current display for this window. + /// + protected 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; + } + + /// + /// Various calls to Glfw.Api? are nullable due to the speed + /// at which the method is executed, causing potential + /// nullability issues during disposal. + /// + /// + protected virtual void HandlePreExecute(ThreadDelegateDispatcher dispatcher) { + 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 + _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; + ThreadDelegateDispatcher.MainThreadDispatcher?.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 HandleMonitorCallback(Silk.NET.GLFW.Monitor* monitor, ConnectedState state) { + // Propagate the event + _previousMonitorCallback?.Invoke(monitor, state); + + // Update the cached displays + _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(); + _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(); + } + + /// + public virtual void OnErrored(WindowException exception) { + Glfw3.DebugContext.Log(LogLevel.Error, $"Exception occurred in {LogHandle}", args: exception); + Errored?.Invoke(this, exception); + } + + /// + public virtual void OnCreated() { + Glfw3.DebugContext.Log(LogLevel.Info, $"Created new {LogHandle}"); + Created?.Invoke(this); + } + + /// + public virtual void OnRepositioned() { + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Repositioned {LogHandle}"); + Repositioned?.Invoke(this); + } + + /// + public virtual void OnResized() { + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Resized {LogHandle}"); + Resized?.Invoke(this); + } + + /// + public virtual void OnRefresh() { + Refresh?.Invoke(this); + } + + /// + public virtual void OnRedraw() { + Redraw?.Invoke(this); + } + + /// + public virtual void OnFocused() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Focused {LogHandle}"); + Focused?.Invoke(this); + } + + /// + public virtual void OnUnfocused() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Unfocused {LogHandle}"); + Unfocused?.Invoke(this); + } + + /// + public virtual void OnMinimized() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Minimized {LogHandle}"); + Minimized?.Invoke(this); + } + + /// + public virtual void OnMaximized() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Maximized {LogHandle}"); + Maximized?.Invoke(this); + } + + /// + public virtual void OnRestored() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Restored {LogHandle}"); + Restored?.Invoke(this); + } + + /// + public virtual void OnShown() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Shown {LogHandle}"); + Shown?.Invoke(this); + } + + /// + public virtual void OnHidden() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Hidden {LogHandle}"); + Hidden?.Invoke(this); + } + + /// + public virtual bool OnClosing() { + Glfw3.DebugContext.Log(LogLevel.Debug, $"Closing {LogHandle}"); + Delegate[] delegates = Closing?.GetInvocationList() ?? []; + bool result = delegates.Cast().Any(handler => handler(this)); + Glfw3.DebugContext.Log(LogLevel.Debug, result ? $"Closure of {LogHandle} cancelled by event handler" : $"Closure of {LogHandle} proceeding"); + return result; + } + + /// + public virtual void OnClosed() { + Glfw3.DebugContext.Log(LogLevel.Info, $"Closed {LogHandle}"); + Closed?.Invoke(this); + } + + /// + /// 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) { + // Detach threading events + ThreadDelegateDispatcher? dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher; + if (dispatcher != null) { + dispatcher.PreExecute -= _preExecuteHandler; + dispatcher.DelegateEnqueued -= _delegateEnqueuedHandler; + } + + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + Glfw?.Api.PostEmptyEvent(); // always post to unblock the main thread + Glfw?.Dispose(); // dispose of our requested api instance + } + + // Dispose unmanaged state (unmanaged objects) + // ... + + // Indicate disposal completion + _disposed = true; + } finally { + _lock.ExitWriteLock(); + } + } + + } + +} \ No newline at end of file From 1e6f3eff6f36a86aa06a43edd3eda86c0bc21c67 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Wed, 29 Oct 2025 21:00:54 -0600 Subject: [PATCH 04/15] Add Catalyst.Mathematics.Color * Added Color128, Color32, & IColor interface to the Mathematics project --- .../CatalystUI.Mathematics/Color/Color128.cs | 220 ++++++++++++++++++ .../CatalystUI.Mathematics/Color/Color32.cs | 220 ++++++++++++++++++ .../CatalystUI.Mathematics/Color/IColor.cs | 65 ++++++ 3 files changed, 505 insertions(+) create mode 100644 CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs create mode 100644 CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs create mode 100644 CatalystUI/Core/CatalystUI.Mathematics/Color/IColor.cs diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs new file mode 100644 index 0000000..8efc53e --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs @@ -0,0 +1,220 @@ +// ------------------------------------------------------------------------------------------------- +// 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 0xRRGGBBAA format, + /// where the AA field is optional and defaults to FF. + /// + /// The hexadecimal color value. + public Color128(uint hex) { + if (hex <= 0xFFFFFF) { + R = ((hex >> 16) & 0xFF) / 255.0f; + G = ((hex >> 8) & 0xFF) / 255.0f; + B = (hex & 0xFF) / 255.0f; + A = 1.0f; + } else { + R = ((hex >> 24) & 0xFF) / 255.0f; + G = ((hex >> 16) & 0xFF) / 255.0f; + B = ((hex >> 8) & 0xFF) / 255.0f; + A = (hex & 0xFF) / 255.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..abc21f2 --- /dev/null +++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs @@ -0,0 +1,220 @@ +// ------------------------------------------------------------------------------------------------- +// 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 0xRRGGBBAA format, + /// where the AA field is optional and defaults to FF. + /// + /// The hexadecimal color value. + public Color32(uint hex) { + if (hex <= 0xFFFFFF) { + R = (byte) ((hex >> 16) & 0xFF); + G = (byte) ((hex >> 8) & 0xFF); + B = (byte) (hex & 0xFF); + A = 0xFF; + } else { + R = (byte) ((hex >> 24) & 0xFF); + G = (byte) ((hex >> 16) & 0xFF); + B = (byte) ((hex >> 8) & 0xFF); + A = (byte) (hex & 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 From 2b2697b5ecd4da7b12c7d61ea75d5b919c0c6ce9 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Wed, 29 Oct 2025 21:02:05 -0600 Subject: [PATCH 05/15] Add renderer interfaces to Catalyst.Modules.Crystal.Core * Added the new IRenderer interface from 4 month-old CatalystUI's `dev`branch * Created a new concept "IRenderLayer" and "IRenderTarget." * Removed "IWindowDomain" and swapped it to be on the "IMultisensoryDomain." --- .../{IWindowDomain.cs => IRendererLayer.cs} | 7 +- .../IWindowLayer.cs | 3 +- .../Renderer/IRenderLayer.cs | 68 ++++++++ .../Renderer/IRenderTarget.cs | 121 +++++++++++++ .../Renderer/IRenderer.cs | 164 ++++++++++++++++++ .../Renderer/RendererException.cs | 49 ++++++ .../Window/IWindow.cs | 4 +- 7 files changed, 410 insertions(+), 6 deletions(-) rename CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/{IWindowDomain.cs => IRendererLayer.cs} (79%) create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs create mode 100644 CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowDomain.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs similarity index 79% rename from CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowDomain.cs rename to CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs index c1dd600..06841f4 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowDomain.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs @@ -10,15 +10,16 @@ // ------------------------------------------------------------------------------------------------- using Catalyst.Domains; +using Catalyst.Layers; namespace Catalyst.Modules.Crystal { /// - /// Represents a domain for visual or graphical windows. + /// Represents the renderer layer for visual or graphical rendering. /// - public interface IWindowDomain : IMultisensoryDomain { + public interface IRendererLayer : IRendererLayer { + - // ... } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs index 5760aa9..9912418 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs @@ -9,6 +9,7 @@ // For full terms, see the LICENSE and NOTICE files in the project root. // ------------------------------------------------------------------------------------------------- +using Catalyst.Domains; using Catalyst.Layers; using System.Collections.Generic; @@ -17,7 +18,7 @@ namespace Catalyst.Modules.Crystal { /// /// Represents the window layer for visual or graphical windows. /// - public interface IWindowLayer : IWindowLayer { + public interface IWindowLayer : IWindowLayer { /// /// Queries the window for connected displays. 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..b256dba --- /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 which will + /// be given an opportunity to render content. + /// + /// + /// Render layers allow the rendering systems to be + /// more modular by providing discrete layers which + /// each have 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..081aca3 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs @@ -0,0 +1,121 @@ +// ------------------------------------------------------------------------------------------------- +// 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. + /// + double X { get; } + + /// + /// Gets the vertical position of the render + /// target relative to its surface. + /// + double Y { get; } + + /// + /// Gets the width of the render target in pixels. + /// + uint Width { get; } + + /// + /// Gets the height of the render target in pixels. + /// + uint Height { get; } + + /// + /// Gets the horizontal position of the surface in pixels. + /// + double SurfaceX { get; } + + /// + /// Gets the vertical position of the surface in pixels. + /// + double SurfaceY { get; } + + /// + /// Gets the width of the surface in pixels. + /// + double SurfaceWidth { get; } + + /// + /// Gets the height of the surface in pixels. + /// + double SurfaceHeight { get; } + + /// + /// Gets the pixel density of the surface. + /// + double SurfacePixelDensity { get; } + + /// + /// Gets the DPI (dots per inch) of the surface. + /// + double SurfaceDpi { 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..eb092c1 --- /dev/null +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs @@ -0,0 +1,164 @@ +// ------------------------------------------------------------------------------------------------- +// 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. + /// + IRenderTarget Target { get; set; } + + /// + /// Gets the collection of render layers used by the renderer. + /// + IReadOnlyList Layers { get; } + + /// + /// Gets or sets a value indicating whether vertical synchronization (VSync) is enabled. + /// + 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. + /// + /// + /// + bool ShouldMeasureFrameRate { get; set; } + + /// + /// Gets the average frame rate of the renderer. + /// + /// + /// Only available if is enabled. + /// + /// + double AvgFrameRate { get; } + + /// + /// Gets the average delta time (in seconds) between frames. + /// + /// + /// Only available if is enabled. + /// + /// + 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..3b023c9 --- /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/IWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs index ddc127c..bb59e8e 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs @@ -169,7 +169,7 @@ public interface IWindow : IDisposable { event WindowInteractedEventHandler? Interacted; /// - /// Gets the system's native handle(s) for this window. + /// Gets the system's native handle(s) for the window. /// /// /// @@ -182,7 +182,7 @@ public interface IWindow : IDisposable { /// The 0th index is always expected to contain the primary window handle. /// /// - /// The native handle(s) for this window. + /// The native handle(s) for the window. nint[] NativeHandle { get; } /// From 34a79c90c0f9504abe5e90d0908c7505cdb781b0 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Fri, 31 Oct 2025 16:30:44 -0600 Subject: [PATCH 06/15] Modify the IDataConnector and INativeConnector generic requirements * Updated the data and native connectors to allow for connecting to various layers, rather than being restricted to their immediate neighbors. --- .../Core/CatalystUI.Core/Connectors/IDataConnector.cs | 9 ++++++++- .../Core/CatalystUI.Core/Connectors/INativeConnector.cs | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs index da20dbc..c365d71 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 who 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 typically 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..ee1f932 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 who 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 typically 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 { // ... From a77174e46696fd435a8c5527de25e8de6cb5ca80 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sat, 1 Nov 2025 21:24:25 -0600 Subject: [PATCH 07/15] Minor comment improvements --- .../IRendererLayer.cs | 2 +- .../Renderer/IRenderTarget.cs | 16 ++++++++++++ .../Renderer/IRenderer.cs | 10 +++++-- .../CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs | 1 - .../Window/GlfwDisplay.cs | 26 +++++++++---------- .../Window/GlfwWindow.cs | 2 +- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs index 06841f4..5c37d90 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs @@ -19,7 +19,7 @@ namespace Catalyst.Modules.Crystal { /// public interface IRendererLayer : IRendererLayer { - + // ... } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs index 081aca3..6cd6f0d 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs @@ -68,54 +68,70 @@ public interface IRenderTarget { /// 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 index eb092c1..5ad0e6b 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs @@ -58,16 +58,19 @@ public interface IRenderer : IDisposable { /// /// Gets or sets the render target for the renderer. /// - IRenderTarget Target { get; set; } + /// 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; } /// @@ -93,6 +96,7 @@ public interface IRenderer : IDisposable { /// where possible, and to use active rendering for real-time /// applications such as video games or simulations. /// + /// /// /// /// 0 represents passive rendering. @@ -100,12 +104,12 @@ public interface IRenderer : IDisposable { /// 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; } @@ -116,6 +120,7 @@ public interface IRenderer : IDisposable { /// /// Only available if is enabled. /// + /// The average frame rate in frames per second (FPS). /// double AvgFrameRate { get; } @@ -125,6 +130,7 @@ public interface IRenderer : IDisposable { /// /// Only available if is enabled. /// + /// The average delta time in seconds. /// double AvgDeltaTime { get; } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs index 6656f32..1731273 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs @@ -119,7 +119,6 @@ private static void Initialize() { if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) { throw new TypeInitializationException(nameof(Glfw3), new InvalidOperationException("Failed to initialize Glfw3 API on the main thread.")); } - } [CachedDelegate] diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs index 27f720d..1a726d3 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs @@ -90,24 +90,24 @@ public static unsafe GlfwDisplay FromMonitor(Glfw3 glfw, in Monitor* pMonitor) { 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 handler for this system - IGlfw3NativeConnector>? nativeHandler; + // Check if we have a native connector for this system + IGlfw3NativeConnector>? nativeConnector; try { - nativeHandler = ModelRegistry.RequestConnector>>(); - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native handler: {nativeHandler}"); + nativeConnector = ModelRegistry.RequestConnector>>(); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native connector: {nativeConnector}"); } catch { - nativeHandler = null; - Glfw3.DebugContext.Log(LogLevel.Warning, "No native handler found for Glfw3. Some QOL features may not be available."); + 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 (nativeHandler != null) { - rotation = Angle.FromDegrees(nativeHandler.GetDisplayRotation(glfw, pMonitor)); + if (nativeConnector != null) { + rotation = Angle.FromDegrees(nativeConnector.GetDisplayRotation(glfw, pMonitor)); orientation = rotation.ToOrientation(); - } else { + } else { throw new PlatformNotSupportedException(); } } catch { @@ -125,8 +125,8 @@ public static unsafe GlfwDisplay FromMonitor(Glfw3 glfw, in Monitor* pMonitor) { // Get EDID descriptor and manufacturer string descriptor; try { - if (nativeHandler != null) { - descriptor = nativeHandler.GetDisplayDescriptor(glfw, pMonitor); + if (nativeConnector != null) { + descriptor = nativeConnector.GetDisplayDescriptor(glfw, pMonitor); } else { throw new PlatformNotSupportedException(); } @@ -136,8 +136,8 @@ public static unsafe GlfwDisplay FromMonitor(Glfw3 glfw, in Monitor* pMonitor) { Glfw3.DebugContext.Log(LogLevel.Verbose, $"Parsed monitor descriptor: {descriptor}"); string? manufacturer; try { - if (nativeHandler != null) { - manufacturer = nativeHandler.GetDisplayManufacturer(glfw, pMonitor); + if (nativeConnector != null) { + manufacturer = nativeConnector.GetDisplayManufacturer(glfw, pMonitor); } else { throw new PlatformNotSupportedException(); } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs index c08b2d5..8d232d0 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs @@ -104,7 +104,7 @@ public unsafe class GlfwWindow : IWindow { /// Gets a generated string used for logging purposes. /// /// The window class name followed by the native handle in hexadecimal format. - protected string LogHandle => $"{nameof(GlfwWindow)} {(NativeHandle.Length > 0 ? $"0x{NativeHandle[0]:X}" : "0x????")}"; + protected string LogHandle => $"{nameof(GlfwWindow)} {(NativeHandle.Length > 0 ? $"0x{NativeHandle[0]:X}" : "(No Native Handle)")}"; /// /// Internal reference for . From b312e0894a543253d6dcb57f5924a24e5d93f53c Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sat, 1 Nov 2025 23:32:06 -0600 Subject: [PATCH 08/15] Generalized code cleanup --- .../CatalystUI.Core/Debugging/DebugContext.cs | 4 +- .../CatalystUI.Debug/CatalystSerilogDebug.cs | 10 ++-- .../CatalystUI.Debug/SerilogDebugContext.cs | 21 +++---- .../CatalystUI.Mathematics/Geometry/Angle.cs | 33 +++++----- .../DataConnector/BindingContract.cs | 4 +- .../CatalystUI.Threading/DelegateQueue.cs | 12 ++-- .../ThreadDelegateDispatcher.cs | 60 ++++++++----------- .../Components/IniComponent.cs | 12 ++-- 8 files changed, 67 insertions(+), 89 deletions(-) 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.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..a7bf9c0 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(' '); @@ -113,7 +106,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/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..5d74408 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; +// 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 { From cf6d6e1bd874b5b6aaeb6e3aa8e2944847e13a9d Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sat, 1 Nov 2025 23:33:13 -0600 Subject: [PATCH 09/15] Optimize imports --- CatalystUI/Core/CatalystUI.Core/ModelRegistry.cs | 3 +-- .../Connectors/DataConnector/BindingContract.cs | 2 +- .../Connectors/DataConnector/FileDataConnectorBase.cs | 2 +- .../Connectors/DataConnector/IFileDataConnector.cs | 2 +- .../Interactions/Common/IDigitalInputData.cs | 2 +- .../Interactions/Devices/Mouse/MouseMovedInputData.cs | 2 +- .../Layers/SystemLayer/ILinuxSystemLayer.cs | 2 +- .../Layers/SystemLayer/IMacSystemLayer.cs | 2 +- .../Extensions/CatalystAppBuilderExtensions.cs | 2 +- .../NativeConnectors/Glfw3WindowsNativeConnector.cs | 2 +- .../CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs | 7 ++++--- 11 files changed, 14 insertions(+), 14 deletions(-) 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.Supplementary/Connectors/DataConnector/BindingContract.cs b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs index 5d74408..cfb7e43 100644 --- a/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs +++ b/CatalystUI/Core/CatalystUI.Supplementary/Connectors/DataConnector/BindingContract.cs @@ -9,8 +9,8 @@ // For full terms, see the LICENSE and NOTICE files in the project root. // ------------------------------------------------------------------------------------------------- -using System.Threading; 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/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs index 4037cc3..33a1a74 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs @@ -9,8 +9,8 @@ // For full terms, see the LICENSE and NOTICE files in the project root. // ------------------------------------------------------------------------------------------------- -using Catalyst.Supplementary; using Catalyst.Modules.Crystal.Glfw3; +using Catalyst.Supplementary; using System; // ReSharper disable once CheckNamespace diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs index 1873cfa..fdfb9fe 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs @@ -178,7 +178,7 @@ public static double GetDisplayRotation(string deviceName) { /// 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 = WindowsImports.GetDISPLAY_DEVICEW(deviceName); + 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 = [ ]; diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs index 8d232d0..2a08a6c 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs @@ -20,6 +20,7 @@ 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 { @@ -686,11 +687,11 @@ public virtual void SetFullscreen(WindowFullscreenMode mode, IDisplay? display = case WindowFullscreenMode.Borderless: // If we match existing monitor specs, GLFW will use borderless. Glfw.Api.WindowHint(WindowHintBool.Decorated, false); - Glfw.Api.SetWindowMonitor((WindowHandle*) GlfwHandle, (Silk.NET.GLFW.Monitor*) ((GlfwDisplay) display).Monitor, 0, 0, (int) display.Width, (int) display.Height, (int) Math.Round(display.RefreshRate)); + 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, (Silk.NET.GLFW.Monitor*) ((GlfwDisplay) display).Monitor, 0, 0, (int) display.Width, (int) display.Height, (int) Math.Round(display.RefreshRate)); + 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); @@ -1022,7 +1023,7 @@ protected virtual void HandleWindowFocus(WindowHandle* handle, bool focused) { } /// - protected virtual void HandleMonitorCallback(Silk.NET.GLFW.Monitor* monitor, ConnectedState state) { + protected virtual void HandleMonitorCallback(Monitor* monitor, ConnectedState state) { // Propagate the event _previousMonitorCallback?.Invoke(monitor, state); From 4bb9e36e606726503df00d2ff7e76231b40a44ec Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sun, 2 Nov 2025 19:31:28 -0700 Subject: [PATCH 10/15] Significant refactoring to GlfwWindow * Resolved prefix not being appended in the SerilogDebugContext * Add new variable to IWindow for a ThreadDelegateDispatcher * Removed hard requirement for the windowing API to be run on the main thread (still recommended though). * Resolved some debugging logs unnecessarily emitting the full namespace of the associated class. --- .../CatalystUI.Debug/SerilogDebugContext.cs | 3 + .../Window/IWindow.cs | 16 + .../Window/WindowOptions.cs | 9 + .../Window/GlfwDisplay.cs | 2 +- .../Window/GlfwWindow.cs | 692 +++++++++--------- 5 files changed, 367 insertions(+), 355 deletions(-) diff --git a/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs b/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs index a7bf9c0..a89f97e 100644 --- a/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs +++ b/CatalystUI/Core/CatalystUI.Debug/SerilogDebugContext.cs @@ -94,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) { diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs index bb59e8e..2c8aab0 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs @@ -10,6 +10,7 @@ // ------------------------------------------------------------------------------------------------- using Catalyst.Interactions; +using Catalyst.Threading; using System; // ReSharper disable once CheckNamespace @@ -53,6 +54,7 @@ public interface IWindow : IDisposable { /// /// Delegate for that allows cancellation of the close operation. /// + /// to cancel the close operation; otherwise, . delegate bool WindowClosingEventHandler(IWindow window); /// @@ -168,6 +170,20 @@ public interface IWindow : IDisposable { /// 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. /// diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs index 4c49af2..f0634dc 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs @@ -9,6 +9,8 @@ // For full terms, see the LICENSE and NOTICE files in the project root. // ------------------------------------------------------------------------------------------------- +using Catalyst.Threading; + // ReSharper disable once CheckNamespace namespace Catalyst.Modules.Crystal { @@ -17,6 +19,10 @@ namespace Catalyst.Modules.Crystal { /// 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; @@ -54,6 +60,7 @@ namespace Catalyst.Modules.Crystal { /// /// 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. @@ -65,6 +72,7 @@ namespace Catalyst.Modules.Crystal { /// 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, @@ -76,6 +84,7 @@ public WindowOptions( WindowIcon[]? icons = null, IWindow.WindowEventHandler? initializedHandler = null ) { + Dispatcher = dispatcher; Width = width; Height = height; Title = title; diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs index 1a726d3..5aad99d 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs @@ -94,7 +94,7 @@ public static unsafe GlfwDisplay FromMonitor(Glfw3 glfw, in Monitor* pMonitor) { IGlfw3NativeConnector>? nativeConnector; try { nativeConnector = ModelRegistry.RequestConnector>>(); - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native connector: {nativeConnector}"); + 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."); diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs index 2a08a6c..2621b18 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs @@ -26,20 +26,19 @@ namespace Catalyst.Modules.Crystal.Glfw3 { /// - /// An implementation of using the Glfw3 windowing API. + /// An implementation of using + /// the Glfw3 windowing API. /// public unsafe class GlfwWindow : IWindow { /// - /// The minimum allowed poll rate in milliseconds. + /// The minimum polling rate for the window in milliseconds. /// /// - /// In passive polling mode, to prevent lockups of the - /// main thread if the window is not responding, waiting - /// for events will time out after the minimum poll rate - /// in milliseconds. + /// Prevents lockups of the main thread when the + /// window may not be responding. /// - public const int MINIMUM_POLL_RATE = 3000; + public const ushort MINIMUM_POLL_RATE = 3000; /// public event IWindow.WindowErroredEventHandler? Errored; @@ -89,6 +88,12 @@ public unsafe class GlfwWindow : IWindow { /// 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. /// @@ -96,16 +101,13 @@ public unsafe class GlfwWindow : IWindow { public Glfw3 Glfw { get; private set; } /// - /// Gets or sets the internal Glfw3 window handle. + /// Gets or sets the Glfw3 window handle. /// /// The Glfw3 window handle. public nint GlfwHandle { get; private set; } - /// - /// Gets a generated string used for logging purposes. - /// - /// The window class name followed by the native handle in hexadecimal format. - protected string LogHandle => $"{nameof(GlfwWindow)} {(NativeHandle.Length > 0 ? $"0x{NativeHandle[0]:X}" : "(No Native Handle)")}"; + /// + public ThreadDelegateDispatcher Dispatcher { get; init; } /// /// Internal reference for . @@ -125,14 +127,11 @@ public virtual ushort PollRate { get => _pollRate; set { ObjectDisposedException.ThrowIf(_disposed, this); - _pollRate = value; - if (ThreadDelegateDispatcher.MainThreadDispatcher != null) { - // Simply enqueuing a no-op will cause the main thread dispatcher to wake up - // and re-evaluate the poll rate on the next iteration. - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { - // ... - }); - } + _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); } } @@ -155,11 +154,11 @@ public virtual string Title { set { ObjectDisposedException.ThrowIf(_disposed, this); if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Title)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { Glfw.Api.SetWindowTitle((WindowHandle*) GlfwHandle, value); _title = value; - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Set title to \"{value}\""); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, $"Window title set to \"{value}\".", prefix: LogId); + }, wait: true); } } @@ -320,89 +319,24 @@ public virtual WindowFullscreenMode FullscreenMode { public virtual bool IsClosed => _disposed; /// - /// A handle used to reset the poll wait for active polling. - /// - protected readonly ManualResetEvent _resetPollEventHandle; - - /// - /// The state of the reset poll event handle. - /// - protected bool _resetPollEventHandleState; - - /// - /// Cached delegate reference to the passive wait for events method. - /// - protected readonly ThreadDelegateDispatcher.DispatcherEventHandler _preExecuteHandler; - - /// - /// Cached delegate reference to the pre-execute handler for the main thread dispatcher. + /// Used to cache the list of known displays. /// - protected readonly ThreadDelegateDispatcher.DispatcherQueueEventHandler _delegateEnqueuedHandler; + protected GlfwDisplay[] _cachedDisplays; /// - /// Cached delegate reference to the error callback. - /// - protected readonly GlfwCallbacks.ErrorCallback _errorCallback; - - /// - /// Cached delegate reference to the window close callback. - /// - protected readonly GlfwCallbacks.WindowCloseCallback _windowCloseCallback; - - /// - /// Cached delegate reference to the window position callback. - /// - protected readonly GlfwCallbacks.WindowRefreshCallback _windowRefreshCallback; - - /// - /// Cached delegate reference to the framebuffer size callback. - /// - protected readonly GlfwCallbacks.FramebufferSizeCallback _framebufferSizeCallback; - - /// - /// Cached delegate reference to the window size callback. - /// - protected readonly GlfwCallbacks.WindowSizeCallback _windowSizeCallback; - - /// - /// Cached delegate reference to the window content scale callback. - /// - protected readonly GlfwCallbacks.WindowPosCallback _windowPosCallback; - - /// - /// Cached delegate reference to the window maximize callback. - /// - protected readonly GlfwCallbacks.WindowMaximizeCallback _windowMaximizeCallback; - - /// - /// Cached delegate reference to the window minimize (iconify) callback. - /// - protected readonly GlfwCallbacks.WindowIconifyCallback _windowIconifyCallback; - - /// - /// Cached delegate reference to the window focus callback. - /// - protected readonly GlfwCallbacks.WindowFocusCallback _windowFocusCallback; - - /// - /// Cached delegate reference to the monitor callback. - /// - protected readonly GlfwCallbacks.MonitorCallback _monitorCallback; - - /// - /// Used to store the previous monitor callback for GLFW, if any. + /// Used to cache the primary display. /// - protected GlfwCallbacks.MonitorCallback? _previousMonitorCallback; + protected GlfwDisplay? _cachedPrimaryDisplay; /// - /// Used to cache the current list of known displays. + /// Used to signal polling resets. /// - protected GlfwDisplay[] _cachedDisplays; + protected readonly ManualResetEvent _resetPollEventHandle; /// - /// Used to cache the primary display. + /// Used to track the current state of the poll event handle. /// - protected GlfwDisplay? _cachedPrimaryDisplay; + protected bool _resetPollEventHandleState; /// /// Used to store the previous fullscreen mode for the window. @@ -434,6 +368,23 @@ public virtual WindowFullscreenMode FullscreenMode { /// 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. /// @@ -445,9 +396,11 @@ public virtual WindowFullscreenMode FullscreenMode { protected readonly ReaderWriterLockSlim _lock; /// - /// Constructs a new . + /// Constructs a new with + /// the specified options. /// - internal GlfwWindow(WindowOptions options) { + /// The options to use when creating the window. + public GlfwWindow(WindowOptions options) { // Fields _nativeHandle = []; _pollRate = options.PollRate; @@ -468,35 +421,45 @@ internal GlfwWindow(WindowOptions options) { _isMinimized = false; _isMaximized = false; _isVisible = !options.Hidden; + _cachedDisplays = []; _resetPollEventHandle = new(false); _resetPollEventHandleState = false; - _preExecuteHandler = HandlePreExecute; - _delegateEnqueuedHandler = HandleDelegateEnqueued; - _errorCallback = HandleError; - _windowCloseCallback = HandleWindowClose; - _windowRefreshCallback = HandleWindowRefresh; - _framebufferSizeCallback = HandleFramebufferSize; - _windowSizeCallback = HandleWindowSize; - _windowPosCallback = HandleWindowPos; - _windowMaximizeCallback = HandleWindowMaximize; - _windowIconifyCallback = HandleWindowIconify; - _windowFocusCallback = HandleWindowFocus; - _monitorCallback = HandleMonitorCallback; - _previousMonitorCallback = null; + _cachedDisplays = []; + _cachedPrimaryDisplay = null; _previousFullscreenMode = options.FullscreenMode; _restorePos = (0, 0); _restoreSize = (0, 0); _restoreDecorated = options.Decorated; - _cachedDisplays = []; - _cachedPrimaryDisplay = null; _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 = 0; + 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); @@ -507,95 +470,95 @@ internal GlfwWindow(WindowOptions options) { } try { // Perform window initialization - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), "constructor"); - if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { - // Get a Glfw3 API instance - Glfw3.DebugContext.Log(LogLevel.Verbose, "Requesting new Glfw3 instance for window creation..."); + if (!Dispatcher.Execute(() => { + // Request a Glfw3 API instance + Glfw3.DebugContext.Log(LogLevel.Verbose, "Requesting Glfw3 instance..."); Glfw = Glfw3.GetInstance(); Glfw glfw = Glfw.Api; - glfw.SetErrorCallback(_errorCallback); + glfw.SetErrorCallback(_handleError); Glfw3.DebugContext.Log(LogLevel.Verbose, "Done."); - - // Now assign the cached displays to avoid creating then destroying the API a bunch + + // Assign cached displays to avoid multiple creations/destruction of the API _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(); - _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(); - - // Specify no client api + + // Specify no client API glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi); - Glfw3.DebugContext.Log(LogLevel.Verbose, "Set Glfw3 ClientApi hint to NoApi."); - - // TODO: Initial visibility on Wayland compat + 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, $"Set Glfw3 Visible hint to {!options.Hidden}."); - - // Resizable + 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, $"Set Glfw3 Resizable hint to {options.Resizable}."); - - // Decorations + 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, $"Set Glfw3 Decorated hint to {options.Decorated}."); - - // Initialized + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowHintBool.Decorated} window hint to {options.Decorated}"); + + // Fire the initialization handler options.InitializedHandler?.Invoke(this); - - // TODO: Handle decorated and resizable - + // Create the window - Glfw3.DebugContext.Log(LogLevel.Verbose, "Creating Glfw3 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; - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Created Glfw3 window with handle 0x{GlfwHandle:X}"); - if (handle == null || GlfwHandle == 0) throw new WindowException("Failed to create the window!"); - + 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>? nativeHandler; + IGlfw3NativeConnector>? nativeConnector; try { - nativeHandler = ModelRegistry.RequestConnector>>(); - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native handler: {nativeHandler}"); + nativeConnector = ModelRegistry.RequestConnector>>(); + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Found Glfw3 native connector: {nativeConnector.GetType().Name}"); } catch { - nativeHandler = null; - Glfw3.DebugContext.Log(LogLevel.Warning, "No native handler found for Glfw3. Native windowing functionality will be limited, and renderer compatibility may be null."); + 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 (nativeHandler != null) { + if (nativeConnector != null) { _nativeHandle = [ - nativeHandler.GetNativeHandle(Glfw, handle) + nativeConnector.GetNativeHandle(Glfw, handle) ]; + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Retrieved native handle(s): {string.Join(", ", _nativeHandle.Select(h => $"0x{h:X}"))}"); } - - // TODO: Wait for Wayland - + + // TODO: Wayland (Linux) check. + // Wait for wayland... + // Attach events - glfw.SetWindowCloseCallback(handle, _windowCloseCallback); - glfw.SetWindowRefreshCallback(handle, _windowRefreshCallback); - glfw.SetFramebufferSizeCallback(handle, _framebufferSizeCallback); - glfw.SetWindowSizeCallback(handle, _windowSizeCallback); - glfw.SetWindowPosCallback(handle, _windowPosCallback); - glfw.SetWindowMaximizeCallback(handle, _windowMaximizeCallback); - glfw.SetWindowIconifyCallback(handle, _windowIconifyCallback); - glfw.SetWindowFocusCallback(handle, _windowFocusCallback); - _previousMonitorCallback = glfw.SetMonitorCallback(_monitorCallback); - - // Update initial window size limits + 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); - - // Update initial fullscreen mode + + // Set initial fullscreen mode SetFullscreen(_fullscreenMode); - + // Set window icons if (options.Icons is { Length: > 0 }) SetIcons(options.Icons); - - // Initial properties refresh + + // Refresh properties RefreshProperties(); - + // Fire created OnCreated(); - // Attach threading events - ThreadDelegateDispatcher.MainThreadDispatcher.PreExecute += _preExecuteHandler; - ThreadDelegateDispatcher.MainThreadDispatcher.DelegateEnqueued += _delegateEnqueuedHandler; + // 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 on the main thread."); + throw new WindowException("Failed to initialize the window."); } } finally { // Release the mutex @@ -613,37 +576,36 @@ internal GlfwWindow(WindowOptions options) { /// public virtual void SetPosition(double x, double y) { - // TODO: Add Wayland linux check + // TODO: Wayland (Linux) check. ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetPosition)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.SetWindowPos((WindowHandle*) GlfwHandle, (int) x, (int) y); - Glfw3.DebugContext.Log(LogLevel.Debug, $"Set position to {x}, {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: Add Wayland linux check + // TODO: Wayland (Linux) check. ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetSize)); if (width == 0) width = IWindow.DEFAULT_WIDTH; if (height == 0) height = IWindow.DEFAULT_HEIGHT; - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.SetWindowSize((WindowHandle*) GlfwHandle, (int) width, (int) height); - Glfw3.DebugContext.Log(LogLevel.Debug, $"Set size to {width}x{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 (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetSizeLimits)); if (minWidth == 0) minWidth = uint.MinValue; if (minHeight == 0) minHeight = uint.MinValue; if (maxWidth == 0) maxWidth = uint.MaxValue; if (maxHeight == 0) maxHeight = uint.MaxValue; - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { _minimumWidth = minWidth; _minimumHeight = minHeight; _maximumWidth = maxWidth; @@ -654,28 +616,28 @@ public virtual void SetSizeLimits(uint minWidth, uint minHeight, uint maxWidth, _maximumWidth == uint.MaxValue ? Silk.NET.GLFW.Glfw.DontCare : (int) maxWidth, _maximumHeight == uint.MaxValue ? Silk.NET.GLFW.Glfw.DontCare : (int) maxHeight ); - Glfw3.DebugContext.Log(LogLevel.Debug, $"Set size limits to {(_minimumWidth == uint.MinValue ? "Unlimited" : _minimumWidth)}x{(_minimumHeight == uint.MinValue ? "Unlimited" : _minimumHeight)} - {(_maximumWidth == uint.MaxValue ? "Unlimited" : _maximumWidth)}x{(_maximumHeight == uint.MaxValue ? "Unlimited" : _maximumHeight)}"); - }); + 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 (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetFullscreen)); if (_fullscreenMode == mode) return; - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { if (display == null) { if (_cachedPrimaryDisplay == null) throw new WindowException("Failed to fetch the primary display!"); display = _cachedPrimaryDisplay; } - - // Log the windowed position and size + + // 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: @@ -696,137 +658,148 @@ public virtual void SetFullscreen(WindowFullscreenMode mode, IDisplay? display = 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, $"Set fullscreen mode to {mode} on display {display} with resolution {(width == 0 ? _width : width)}x{(height == 0 ? _height : height)}"); - }); + + 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); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(SetIcons)); - if (icons is not { Length: > 0 }) throw new ArgumentException("At least one icon must be provided.", nameof(icons)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { - // 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(); + _ = 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); } - - Glfw3.DebugContext.Log(LogLevel.Debug, $"Set {icons.Length} window icons"); - }); + }, wait: true); } /// public virtual void RequestFocus() { ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(RequestFocus)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.FocusWindow((WindowHandle*) GlfwHandle); - Glfw3.DebugContext.Log(LogLevel.Debug, "Requested focus"); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window focus requested.", prefix: LogId); + }, wait: true); } /// public virtual void RequestAttention() { ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(RequestAttention)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.RequestWindowAttention((WindowHandle*) GlfwHandle); - Glfw3.DebugContext.Log(LogLevel.Debug, "Requested attention"); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window attention requested.", prefix: LogId); + }, wait: true); } /// public virtual void Minimize() { ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Minimize)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.IconifyWindow((WindowHandle*) GlfwHandle); - Glfw3.DebugContext.Log(LogLevel.Debug, "Minimized window"); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window minimized.", prefix: LogId); + }, wait: true); } /// public virtual void Maximize() { ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Maximize)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.MaximizeWindow((WindowHandle*) GlfwHandle); - Glfw3.DebugContext.Log(LogLevel.Debug, "Maximized window"); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window maximized.", prefix: LogId); + }, wait: true); } /// public virtual void Restore() { - throw new NotImplementedException(); + 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); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Show)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.ShowWindow((WindowHandle*) GlfwHandle); - Glfw3.DebugContext.Log(LogLevel.Debug, "Shown window"); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window shown.", prefix: LogId); + }, wait: true); } /// public virtual void Hide() { ObjectDisposedException.ThrowIf(_disposed, this); - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(GlfwWindow), nameof(Hide)); - ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => { + _ = Dispatcher.Execute(() => { Glfw.Api.HideWindow((WindowHandle*) GlfwHandle); - Glfw3.DebugContext.Log(LogLevel.Debug, "Hidden window"); - }); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window hidden.", prefix: LogId); + }, wait: true); } /// public virtual void Close() { - throw new NotImplementedException(); + 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() { - throw new NotImplementedException(); + ObjectDisposedException.ThrowIf(_disposed, this); + _ = Dispatcher.Execute(() => { + Dispose(); + OnClosed(); + Glfw3.DebugContext.Log(LogLevel.Debug, "Window exited.", prefix: LogId); + }, wait: true); } /// @@ -840,42 +813,49 @@ public virtual void Wait() { } /// - /// Refreshes the underlying variables for the property of this window. + /// Refreshes the backing fields for the window properties. /// - protected void RefreshProperties() { - // TODO: Linux Wayland check + /// + /// 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 for {LogHandle}: Pos({_x}, {_y}), Size({_width}x{_height}), Focused({_isFocused}), Minimized({_isMinimized}), Maximized({_isMaximized}), Visible({_isVisible}), Display({_display})"); + + 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 current display for this window. + /// Refreshes the property by + /// determining which display the window is currently on. /// - protected void RefreshDisplay() { + protected virtual void RefreshDisplay() { if (_cachedDisplays.Length == 0) return; - + // Determine which monitor has the most overlap double bestOverlap = 0.0f; GlfwDisplay? bestDisplay = null; @@ -888,39 +868,39 @@ protected void RefreshDisplay() { 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); } - /// - /// Various calls to Glfw.Api? are nullable due to the speed - /// at which the method is executed, causing potential - /// nullability issues during disposal. - /// /// 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); + 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(); + Glfw.Api.PollEvents(); _resetPollEventHandle.WaitOne(PollRate); if (_resetPollEventHandleState) { _resetPollEventHandle.Reset(); @@ -928,13 +908,13 @@ protected virtual void HandlePreExecute(ThreadDelegateDispatcher dispatcher) { } } else { // Fastest polling (on every loop of the main thread) - Glfw.Api?.PollEvents(); + Glfw.Api.PollEvents(); } } /// protected virtual void HandleDelegateEnqueued(ThreadDelegateDispatcher dispatcher, Delegate @delegate) { - Glfw.Api.PostEmptyEvent(); // un-block the main thread + Glfw.Api.PostEmptyEvent(); // un-block the main thread if waiting _resetPollEventHandle.Set(); _resetPollEventHandleState = true; } @@ -943,12 +923,12 @@ protected virtual void HandleDelegateEnqueued(ThreadDelegateDispatcher dispatche 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; - ThreadDelegateDispatcher.MainThreadDispatcher?.Execute(() => { + _ = Dispatcher.Execute(() => { shouldCancel = OnClosing(); }, wait: true); if (shouldCancel) { @@ -958,13 +938,13 @@ protected virtual void HandleWindowClose(WindowHandle* handle) { OnClosed(); } } - + /// protected virtual void HandleWindowRefresh(WindowHandle* handle) { if (_pendingResize) return; RefreshProperties(); } - + /// protected virtual void HandleFramebufferSize(WindowHandle* handle, int width, int height) { _resizeTimeout = 500; @@ -975,7 +955,7 @@ protected virtual void HandleFramebufferSize(WindowHandle* handle, int width, in RefreshProperties(); OnRedraw(); } - + /// /// Delays the frame-buffer size callback to allow for the window to stabilize. /// @@ -991,138 +971,143 @@ protected virtual void FramebufferResizeStabilizer() { _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 HandleMonitorCallback(Monitor* monitor, ConnectedState state) { + protected virtual void HandleMonitor(Monitor* monitor, ConnectedState state) { // Propagate the event _previousMonitorCallback?.Invoke(monitor, state); - + // Update the cached displays _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(); _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(); } /// - public virtual void OnErrored(WindowException exception) { - Glfw3.DebugContext.Log(LogLevel.Error, $"Exception occurred in {LogHandle}", args: exception); + protected virtual void OnErrored(WindowException exception) { + Glfw3.DebugContext.Log(LogLevel.Error, "Window exception occurred!", prefix: LogId, args: exception); Errored?.Invoke(this, exception); } /// - public virtual void OnCreated() { - Glfw3.DebugContext.Log(LogLevel.Info, $"Created new {LogHandle}"); + protected virtual void OnCreated() { + Glfw3.DebugContext.Log(LogLevel.Info, $"Window created.", prefix: LogId); Created?.Invoke(this); } /// - public virtual void OnRepositioned() { - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Repositioned {LogHandle}"); + protected virtual void OnRepositioned() { + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Window repositioned to ({X}, {Y}).", prefix: LogId); Repositioned?.Invoke(this); } /// - public virtual void OnResized() { - Glfw3.DebugContext.Log(LogLevel.Verbose, $"Resized {LogHandle}"); + protected virtual void OnResized() { + Glfw3.DebugContext.Log(LogLevel.Verbose, $"Window resized to {Width}x{Height}.", prefix: LogId); Resized?.Invoke(this); } - + /// - public virtual void OnRefresh() { + protected virtual void OnRefresh() { Refresh?.Invoke(this); } - + /// - public virtual void OnRedraw() { + protected virtual void OnRedraw() { Redraw?.Invoke(this); } - + /// - public virtual void OnFocused() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Focused {LogHandle}"); + protected virtual void OnFocused() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window focused.", prefix: LogId); Focused?.Invoke(this); } - + /// - public virtual void OnUnfocused() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Unfocused {LogHandle}"); + protected virtual void OnUnfocused() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window unfocused.", prefix: LogId); Unfocused?.Invoke(this); } - + /// - public virtual void OnMinimized() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Minimized {LogHandle}"); + protected virtual void OnMinimized() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window minimized.", prefix: LogId); Minimized?.Invoke(this); } - + /// - public virtual void OnMaximized() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Maximized {LogHandle}"); + protected virtual void OnMaximized() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window maximized.", prefix: LogId); Maximized?.Invoke(this); } - + /// - public virtual void OnRestored() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Restored {LogHandle}"); + protected virtual void OnRestored() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window restored.", prefix: LogId); Restored?.Invoke(this); } - + /// - public virtual void OnShown() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Shown {LogHandle}"); + protected virtual void OnShown() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window shown.", prefix: LogId); Shown?.Invoke(this); } - + /// - public virtual void OnHidden() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Hidden {LogHandle}"); + protected virtual void OnHidden() { + Glfw3.DebugContext.Log(LogLevel.Debug, "Window hidden.", prefix: LogId); Hidden?.Invoke(this); } - + /// - public virtual bool OnClosing() { - Glfw3.DebugContext.Log(LogLevel.Debug, $"Closing {LogHandle}"); + 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)); - Glfw3.DebugContext.Log(LogLevel.Debug, result ? $"Closure of {LogHandle} cancelled by event handler" : $"Closure of {LogHandle} proceeding"); + if (result) Glfw3.DebugContext.Log(LogLevel.Debug, "Window closure cancelled by event handler.", prefix: LogId); return result; } - + /// - public virtual void OnClosed() { - Glfw3.DebugContext.Log(LogLevel.Info, $"Closed {LogHandle}"); + 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 . /// @@ -1141,16 +1126,15 @@ private void Dispose(bool disposing) { // Dispose managed state (managed objects) if (disposing) { - // Detach threading events - ThreadDelegateDispatcher? dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher; - if (dispatcher != null) { - dispatcher.PreExecute -= _preExecuteHandler; - dispatcher.DelegateEnqueued -= _delegateEnqueuedHandler; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (Dispatcher != null) { + Dispatcher.PreExecute -= HandlePreExecute; + Dispatcher.DelegateEnqueued -= HandleDelegateEnqueued; } // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Glfw?.Api.PostEmptyEvent(); // always post to unblock the main thread - Glfw?.Dispose(); // dispose of our requested api instance + Glfw?.Api.PostEmptyEvent(); + Glfw?.Dispose(); } // Dispose unmanaged state (unmanaged objects) From 7f3353cf804b68aefff4fa1d5324fa78a4bc08ae Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sun, 2 Nov 2025 20:06:37 -0700 Subject: [PATCH 11/15] Resolve copilot review suggestions --- .../Renderer/RendererException.cs | 6 +++--- .../Window/GlfwWindow.cs | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs index 3b023c9..813dcbb 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererException.cs @@ -21,14 +21,14 @@ namespace Catalyst.Modules.Crystal { public class RendererException : Exception { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public RendererException() : base() { // ... } /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// with a specified error message. /// public RendererException(string message) : base(message) { @@ -36,7 +36,7 @@ public RendererException(string message) : base(message) { } /// - /// Initializes a new instance of the class + /// 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. /// diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs index 2621b18..f8f1ae4 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs @@ -422,10 +422,9 @@ public GlfwWindow(WindowOptions options) { _isMaximized = false; _isVisible = !options.Hidden; _cachedDisplays = []; + _cachedPrimaryDisplay = null; _resetPollEventHandle = new(false); _resetPollEventHandleState = false; - _cachedDisplays = []; - _cachedPrimaryDisplay = null; _previousFullscreenMode = options.FullscreenMode; _restorePos = (0, 0); _restoreSize = (0, 0); @@ -806,10 +805,15 @@ public virtual void Exit() { public virtual void Wait() { if (_disposed) return; ManualResetEvent reset = new(false); - Closed += _ => { - reset.Set(); - }; - reset.WaitOne(); + try { + Closed += _ => { + // ReSharper disable once AccessToDisposedClosure + reset.Set(); + }; + reset.WaitOne(); + } finally { + reset.Dispose(); + } } /// From 626ba9327d5b4fffe53770dce1c8e327bed907b8 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sun, 2 Nov 2025 20:27:06 -0700 Subject: [PATCH 12/15] Resolve thread delegation issues with Glfw * Resolved INativeApi in the core not giving APIs an opportunity to initialize on a different thread. * Updated IWindowLayer to allow passing a different dispatcher. --- .../Core/CatalystUI.Core/Native/INativeApi.cs | 25 +++++++++++++++- .../IWindowLayer.cs | 7 +++-- .../CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs | 29 ++++++++++++++----- .../Glfw3WindowLayer.cs | 26 ++++++++++------- .../Window/GlfwWindow.cs | 8 ++--- 5 files changed, 70 insertions(+), 25 deletions(-) diff --git a/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs b/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs index 6ad2be3..a521172 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 + /// requiring 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 that + /// is appropriate for 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/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs index 9912418..ff4a09f 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IWindowLayer.cs @@ -11,6 +11,7 @@ using Catalyst.Domains; using Catalyst.Layers; +using Catalyst.Threading; using System.Collections.Generic; namespace Catalyst.Modules.Crystal { @@ -23,14 +24,16 @@ 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(); + 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(); + IDisplay? GetPrimaryDisplay(ThreadDelegateDispatcher? dispatcher = null); /// /// Constructs a new window with the specified options. diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs index 1731273..580adb0 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs @@ -29,6 +29,11 @@ public sealed partial class Glfw3 : INativeApi { /// 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. /// @@ -100,7 +105,7 @@ private Glfw3() { } /// - public static Glfw3 GetInstance() { + public static Glfw3 GetInstance(ThreadDelegateDispatcher? dispatcher = null) { _staticLock.Enter(); try { if (_referenceCount == 0) Initialize(); @@ -114,11 +119,16 @@ public static Glfw3 GetInstance() { /// /// Initializes the Glfw3 API. /// - private static void Initialize() { - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3), nameof(GetInstance)); - if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) { + /// 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] @@ -131,9 +141,12 @@ private static void InitializeUnsafe() { /// /// Terminates the Glfw3 API. /// - private static void Terminate() { - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3), nameof(Terminate)); - if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedActionTerminateUnsafe, wait: true)) { + 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."); } } @@ -170,7 +183,7 @@ private void Dispose(bool disposing) { _staticLock.Enter(); try { _referenceCount--; - if (_referenceCount == 0) Terminate(); + if (_referenceCount == 0) Terminate(_initDispatcher); } finally { _staticLock.Exit(); } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs index 92de4be..16c4383 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3WindowLayer.cs @@ -24,17 +24,20 @@ namespace Catalyst.Modules.Crystal.Glfw3 { public sealed unsafe partial class Glfw3WindowLayer : IWindowLayer { /// - public IEnumerable GetDisplays() { - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3WindowLayer), nameof(GetDisplays)); - if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedFunctionGetDisplaysUnsafe, out GlfwDisplay[] displays)) { + 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() { - using Glfw3 glfw = Glfw3.GetInstance(); + 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]; @@ -46,17 +49,20 @@ internal static GlfwDisplay[] GetDisplaysUnsafe() { } /// - public IDisplay? GetPrimaryDisplay() { - if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Glfw3WindowLayer), nameof(GetPrimaryDisplay)); - if (!ThreadDelegateDispatcher.MainThreadDispatcher.Execute(_cachedFunctionGetPrimaryDisplayUnsafe, out GlfwDisplay? display)) { + 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() { - using Glfw3 glfw = Glfw3.GetInstance(); + 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; } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs index f8f1ae4..1fce8e0 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs @@ -472,13 +472,13 @@ public GlfwWindow(WindowOptions options) { if (!Dispatcher.Execute(() => { // Request a Glfw3 API instance Glfw3.DebugContext.Log(LogLevel.Verbose, "Requesting Glfw3 instance..."); - Glfw = Glfw3.GetInstance(); + 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(); + _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher); // Specify no client API glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi); @@ -1012,8 +1012,8 @@ protected virtual void HandleMonitor(Monitor* monitor, ConnectedState state) { _previousMonitorCallback?.Invoke(monitor, state); // Update the cached displays - _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(); - _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(); + _cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher); + _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(Dispatcher); } /// From b9e3ba70d8a68d5ec76bff4d85dad60d76467575 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sun, 2 Nov 2025 21:47:10 -0700 Subject: [PATCH 13/15] Resolve review suggestions by @ThisIsBrady --- .../CatalystUI.Mathematics/Color/Color128.cs | 21 ++++++++++--------- .../CatalystUI.Mathematics/Color/Color32.cs | 21 ++++++++++--------- .../Window/IDisplay.cs | 9 ++++---- .../Window/WindowIcon.cs | 2 +- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs index 8efc53e..16a86af 100644 --- a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs +++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color128.cs @@ -88,26 +88,27 @@ public Color128(string hex) { 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 0xRRGGBBAA format, - /// where the AA field is optional and defaults to FF. + /// The hexadecimal value should follow the 0xRRGGBB or 0xRRGGBBAA format, + /// depending on whether the parameter is set to . /// /// The hexadecimal color value. - public Color128(uint hex) { - if (hex <= 0xFFFFFF) { - R = ((hex >> 16) & 0xFF) / 255.0f; - G = ((hex >> 8) & 0xFF) / 255.0f; - B = (hex & 0xFF) / 255.0f; - A = 1.0f; - } else { + /// 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; } } diff --git a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs index abc21f2..72e946a 100644 --- a/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs +++ b/CatalystUI/Core/CatalystUI.Mathematics/Color/Color32.cs @@ -88,26 +88,27 @@ public Color32(string hex) { 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 0xRRGGBBAA format, - /// where the AA field is optional and defaults to FF. + /// The hexadecimal value should follow the 0xRRGGBB or 0xRRGGBBAA format, + /// depending on whether the parameter is set to . /// /// The hexadecimal color value. - public Color32(uint hex) { - if (hex <= 0xFFFFFF) { - R = (byte) ((hex >> 16) & 0xFF); - G = (byte) ((hex >> 8) & 0xFF); - B = (byte) (hex & 0xFF); - A = 0xFF; - } else { + /// 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; } } diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs index 9cd0414..011346f 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs @@ -107,10 +107,11 @@ public interface IDisplay { /// /// /// - /// Also known as the dots per inch (DPI) of the display, - /// the default value is 96 PPI (pixels per inch), - /// which is equivalent to 3/4 of a point in typography, - /// which is defined as 1/72 of an inch. + /// 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 diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs index 678b8e8..94bfc02 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs @@ -71,7 +71,7 @@ public WindowIcon(uint width, uint height, IReadOnlyCollection> pi /// /// 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_%dx%d.bmp. This would result in the + /// 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. /// /// From 208c2cfbf7059a8812b574ad59b8a7814d3292e0 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sat, 8 Nov 2025 21:18:13 -0700 Subject: [PATCH 14/15] Apply suggestions from code review Co-authored-by: Craze <51416587+RealCrazeOfficial@users.noreply.github.com> Signed-off-by: FireController#1847 --- .../CatalystUI.Core/Connectors/IDataConnector.cs | 4 ++-- .../CatalystUI.Core/Connectors/INativeConnector.cs | 4 ++-- CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs | 12 ++++++------ .../Renderer/IRenderLayer.cs | 6 +++--- .../Window/IDisplay.cs | 2 +- .../Window/WindowIcon.cs | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs index c365d71..c1c1a5c 100644 --- a/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/IDataConnector.cs @@ -19,9 +19,9 @@ namespace Catalyst.Connectors { /// /// /// The and - /// are the only two connectors who contain a layer without an associated layer restriction. For the native + /// 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 typically has a lower layer without restrictions, as + /// 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. /// /// diff --git a/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs b/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs index ee1f932..ade122f 100644 --- a/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs +++ b/CatalystUI/Core/CatalystUI.Core/Connectors/INativeConnector.cs @@ -19,9 +19,9 @@ namespace Catalyst.Connectors { /// /// /// The and - /// are the only two connectors who contain a layer without an associated layer restriction. For the native + /// 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 typically has a lower layer without restrictions, as + /// 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. /// /// diff --git a/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs b/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs index a521172..852cbff 100644 --- a/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs +++ b/CatalystUI/Core/CatalystUI.Core/Native/INativeApi.cs @@ -33,10 +33,10 @@ public interface INativeApi : IDisposable where TSelf : INa /// /// /// When requesting with a custom dispatcher, - /// upon first initialization the API will + /// 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 + /// will be ignored, and the originally associated /// dispatcher will be used. /// /// @@ -45,11 +45,11 @@ public interface INativeApi : IDisposable where TSelf : INa /// 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 - /// requiring a context to be current on the thread it is used on). + /// 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 that - /// is appropriate for the API's threading model. + /// 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. diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs index b256dba..e0edf99 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs @@ -13,13 +13,13 @@ namespace Catalyst.Modules.Crystal { /// - /// Represents a module or extension which will + /// 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 which - /// each have an opportunity to render content. The + /// 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 diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs index 011346f..2159863 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IDisplay.cs @@ -82,7 +82,7 @@ public interface IDisplay { /// /// /// - /// A display's rotation is a more precise value of + /// 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 diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs index 94bfc02..23fec98 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowIcon.cs @@ -65,7 +65,7 @@ public WindowIcon(uint width, uint height, IReadOnlyCollection> pi /// /// /// The resulting assembly path will be in the format: {location}.{filename}, - /// and as such the provided filename must contain at least one % placeholder + /// 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"). /// /// From b6054fc5da9a18d89d277fe42a79f542f8eaa048 Mon Sep 17 00:00:00 2001 From: FireController#1847 Date: Sat, 8 Nov 2025 21:22:40 -0700 Subject: [PATCH 15/15] Apply suggestions from code review Co-authored-by: Craze <51416587+RealCrazeOfficial@users.noreply.github.com> Signed-off-by: FireController#1847 --- .../Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs index 2c8aab0..28f0cc6 100644 --- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs +++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs @@ -190,8 +190,8 @@ public interface IWindow : IDisposable { /// /// /// 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 + /// 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. /// ///