mirror of
https://github.com/Stone-Red-Code/RemSox.git
synced 2026-09-04 00:56:19 +02:00
Reorganize project structure
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
namespace RemSox.Kernel.UI.CLI;
|
||||
|
||||
public static class CommandManager
|
||||
{
|
||||
private static readonly Dictionary<string, ICommand> commands = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly List<string> commandHistory = [];
|
||||
|
||||
public static void RegisterCommand(ICommand command)
|
||||
{
|
||||
commands[command.Name] = command;
|
||||
}
|
||||
|
||||
public static void RegisterCommands(IEnumerable<ICommand> commandsToRegister)
|
||||
{
|
||||
foreach (ICommand command in commandsToRegister)
|
||||
{
|
||||
RegisterCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<ICommand> GetCommands()
|
||||
{
|
||||
return commands.Values.OrderBy(command => command.Name);
|
||||
}
|
||||
|
||||
public static IList<string> GetCommandHistory()
|
||||
{
|
||||
return commandHistory;
|
||||
}
|
||||
|
||||
public static async Task<bool> TryExecuteAsync(string input, Action<string> printLine, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string trimmedInput = input.Trim();
|
||||
|
||||
if (trimmedInput.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (commandHistory.Count == 0 || commandHistory[^1] != trimmedInput)
|
||||
{
|
||||
commandHistory.Add(trimmedInput);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, ICommand> entry in commands.OrderByDescending(entry => entry.Key.Length))
|
||||
{
|
||||
string commandName = entry.Key;
|
||||
|
||||
if (!trimmedInput.Equals(commandName, StringComparison.OrdinalIgnoreCase) &&
|
||||
!trimmedInput.StartsWith(commandName + " ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? arguments = null;
|
||||
|
||||
if (trimmedInput.Length > commandName.Length)
|
||||
{
|
||||
arguments = trimmedInput[commandName.Length..].TrimStart();
|
||||
}
|
||||
|
||||
CancellationTokenRegistration unused = cancellationToken.Register(async () => await entry.Value.StopAsync());
|
||||
|
||||
await entry.Value.ExecuteAsync(arguments, printLine);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public sealed class ClearCommand : ICommand
|
||||
{
|
||||
public string Name => "clear";
|
||||
|
||||
public string Description => "Clear the screen";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
Console.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public sealed class ShutdownCommand : ICommand
|
||||
{
|
||||
public string Name => "shutdown";
|
||||
|
||||
public string Description => "Shutdown the system";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
printLine("Shutting down system...");
|
||||
Sys.Power.Shutdown();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public sealed class HelpCommand : ICommand
|
||||
{
|
||||
public string Name => "help";
|
||||
|
||||
public string Description => "Show this help message";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
printLine("Available commands:");
|
||||
|
||||
foreach (ICommand command in CommandManager.GetCommands())
|
||||
{
|
||||
printLine($" {command.Name,-12} - {command.Description}");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using RemSox.Kernel.Processes;
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public sealed class SpawnProcessCommand : ICommand
|
||||
{
|
||||
public string Name => "spawn";
|
||||
|
||||
public string Description => "Spawn a new process";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
arguments = arguments?.Trim();
|
||||
|
||||
int? processId = arguments switch
|
||||
{
|
||||
"test" => ProcessManager.SpawnProcess<TestProcess>(),
|
||||
"terminal" => ProcessManager.SpawnProcess<TerminalProcess>(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (processId is not null)
|
||||
{
|
||||
printLine($"Spawned process with ID {processId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
printLine("Usage: spawn <process-name>");
|
||||
printLine("Available processes: test, terminal");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ListProcessesCommand : ICommand
|
||||
{
|
||||
public string Name => "ps";
|
||||
|
||||
public string Description => "List running processes";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
IEnumerable<Process> processes = ProcessManager.GetAllProcesses();
|
||||
|
||||
printLine("Running processes:");
|
||||
|
||||
foreach (Process process in processes)
|
||||
{
|
||||
printLine($" ID: {process.Id}, Name: {process.Name}");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StopProcessCommand : ICommand
|
||||
{
|
||||
public string Name => "stop";
|
||||
|
||||
public string Description => "Stop a process by ID";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
string? idText = arguments;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(idText))
|
||||
{
|
||||
printLine("Usage: stop <process-id>");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (int.TryParse(idText, out int processId))
|
||||
{
|
||||
ProcessManager.StopProcess(processId);
|
||||
printLine($"Stopped process with ID {processId}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
printLine("Invalid process ID");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class RebootCommand : ICommand
|
||||
{
|
||||
public string Name => "reboot";
|
||||
|
||||
public string Description => "Reboot the system";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
printLine("Rebooting system...");
|
||||
Sys.Power.Reboot();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using RemSox.Kernel.Processes;
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class StartGuiCommand : ICommand
|
||||
{
|
||||
public string Name => "start-gui";
|
||||
public string Description => "Starts the Graphical User Interface (Desktop Process)";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
printLine("Starting Desktop Process...");
|
||||
|
||||
if (ProcessManager.IsProcessRunning<DesktopProcess>())
|
||||
{
|
||||
printLine("Desktop Process is already running.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_ = ProcessManager.SpawnProcess<DesktopProcess>();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using RemSox.Kernel.Processes;
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class StartRemoteDesktopCommand : ICommand
|
||||
{
|
||||
public string Name => "rdp-start";
|
||||
public string Description => "Starts the remote desktop server on the specified port (rdp-start <port>)";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arguments) || !int.TryParse(arguments.Trim(), out int port) || port <= 0 || port > 65535)
|
||||
{
|
||||
printLine("Usage: rdp-start <port> (1-65535)");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (ProcessManager.IsProcessRunning<RemoteDesktopProcess>())
|
||||
{
|
||||
printLine("Remote desktop server is already running.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_ = ProcessManager.SpawnProcess<RemoteDesktopProcess>([port.ToString()]);
|
||||
printLine($"Remote desktop server started on port {port}.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using RemSox.Kernel.Processes;
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class StopGuiCommand : ICommand
|
||||
{
|
||||
public string Name => "stop-gui";
|
||||
public string Description => "Stops the Graphical User Interface (Desktop Process)";
|
||||
|
||||
public async Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
printLine("Stopping Desktop Process...");
|
||||
|
||||
if (!ProcessManager.IsProcessRunning<DesktopProcess>())
|
||||
{
|
||||
printLine("Desktop Process is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Process process in ProcessManager.GetProcessesOfType<DesktopProcess>())
|
||||
{
|
||||
await ProcessManager.StopProcessAndWaitAsync(process.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using RemSox.Kernel.Processes;
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class StopRemoteDesktopCommand : ICommand
|
||||
{
|
||||
public string Name => "rdp-stop";
|
||||
public string Description => "Stops the remote desktop server";
|
||||
|
||||
public async Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
if (!ProcessManager.IsProcessRunning<RemoteDesktopProcess>())
|
||||
{
|
||||
printLine("Remote desktop server is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (RemoteDesktopProcess process in ProcessManager.GetProcessesOfType<RemoteDesktopProcess>())
|
||||
{
|
||||
await ProcessManager.StopProcessAndWaitAsync(process.Id);
|
||||
}
|
||||
|
||||
printLine("Remote desktop server stopped.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using RemSox.Kernel.Logging;
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class ViewProcessLogs : ICommand
|
||||
{
|
||||
public string Name => "logs";
|
||||
|
||||
public string Description => "View logs for a process";
|
||||
|
||||
public Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
if (int.TryParse(arguments, out int processId))
|
||||
{
|
||||
IEnumerable<LogEntry> logs = ProcessManager.GetProcessLogs(processId);
|
||||
PrintLogs(logs, printLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<LogEntry> logs = ProcessManager.GetLogs();
|
||||
PrintLogs(logs, printLine);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void PrintLogs(IEnumerable<LogEntry> logs, Action<string> printLine)
|
||||
{
|
||||
if (!logs.Any())
|
||||
{
|
||||
printLine("No logs found!");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (LogEntry log in logs)
|
||||
{
|
||||
printLine($"[{log.Timestamp:HH:mm:ss}] [{log.Severity}] {log.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using RemSox.Kernel.Processing;
|
||||
|
||||
namespace RemSox.Kernel.UI.CLI.Commands;
|
||||
|
||||
public class YesNtCommand : ICommand
|
||||
{
|
||||
public string Name => "yesnt";
|
||||
|
||||
public string Description => "Start the YesNt interpreter";
|
||||
|
||||
private int processId;
|
||||
|
||||
public async Task ExecuteAsync(string? arguments, Action<string> printLine)
|
||||
{
|
||||
processId = ProcessManager.SpawnProcess<Processes.YesNtInterpreterProcess>(arguments?.Split(',') ?? []);
|
||||
await ProcessManager.WaitForProcessExitAsync(processId);
|
||||
}
|
||||
|
||||
public Task StopAsync()
|
||||
{
|
||||
ProcessManager.StopProcess(processId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace RemSox.Kernel.UI.CLI;
|
||||
/// <summary>
|
||||
/// Defines a command executable via the CLI or GUI terminal.
|
||||
/// </summary>
|
||||
public interface ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the command used to invoke it.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a brief description of the command's functionality.
|
||||
/// </summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments provided to the command.</param>
|
||||
/// <param name="printLine">A delegate to stream output lines to the current console or terminal.</param>
|
||||
Task ExecuteAsync(string? arguments, Action<string> printLine);
|
||||
|
||||
/// <summary>
|
||||
/// Interrupts the command if it's currently running. This is called when the user presses Ctrl+C in the CLI.
|
||||
/// </summary>
|
||||
Task StopAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using RemSox.Kernel.UI.GUI.UIEelements;
|
||||
using RemSox.Kernel.UI.GUI.Windows;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Layout;
|
||||
|
||||
public sealed class GridLayout
|
||||
{
|
||||
private readonly Window window;
|
||||
private readonly int originX;
|
||||
private readonly int originY;
|
||||
private readonly int[] colWidths;
|
||||
private readonly int[] rowHeights;
|
||||
private readonly int spacing;
|
||||
|
||||
public GridLayout(Window window, int x, int y, int[] colWidths, int[] rowHeights, int spacing = 0)
|
||||
{
|
||||
this.window = window;
|
||||
originX = x;
|
||||
originY = y;
|
||||
this.colWidths = colWidths;
|
||||
this.rowHeights = rowHeights;
|
||||
this.spacing = spacing;
|
||||
}
|
||||
|
||||
public T Add<T>(int col, int row, Action<T>? options = null) where T : UIElement, new()
|
||||
{
|
||||
bool wasAutoFlush = window.AutoFlush;
|
||||
window.AutoFlush = false;
|
||||
|
||||
T element = window.CreateUIElement<T>(options);
|
||||
PositionElement(element, col, row);
|
||||
|
||||
if (wasAutoFlush)
|
||||
{
|
||||
window.AutoFlush = true;
|
||||
window.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.AutoFlush = false;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
private void PositionElement(UIElement element, int col, int row)
|
||||
{
|
||||
int cellX = CellOrigin(colWidths, col);
|
||||
int cellY = CellOrigin(rowHeights, row);
|
||||
|
||||
element.Position = new Point(originX + cellX, originY + cellY);
|
||||
|
||||
if (element is Control control)
|
||||
{
|
||||
control.Size = new Size(colWidths[col], rowHeights[row]);
|
||||
}
|
||||
}
|
||||
|
||||
private int CellOrigin(int[] sizes, int index)
|
||||
{
|
||||
int offset = 0;
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
offset += sizes[i] + spacing;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using RemSox.Kernel.UI.GUI.Windows;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Layout;
|
||||
|
||||
public static class LayoutExtensions
|
||||
{
|
||||
public static StackLayout CreateStackLayout(this Window window, int x, int y, int spacing = 0, StackOrientation orientation = StackOrientation.Vertical)
|
||||
{
|
||||
return new StackLayout(window, x, y, spacing, orientation);
|
||||
}
|
||||
|
||||
public static GridLayout CreateGridLayout(this Window window, int x, int y, int[] colWidths, int[] rowHeights, int spacing = 0)
|
||||
{
|
||||
return new GridLayout(window, x, y, colWidths, rowHeights, spacing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using RemSox.Kernel.UI.GUI.UIEelements;
|
||||
using RemSox.Kernel.UI.GUI.Windows;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Layout;
|
||||
|
||||
public enum StackOrientation
|
||||
{
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
public sealed class StackLayout
|
||||
{
|
||||
private readonly Window window;
|
||||
private int nextX;
|
||||
private int nextY;
|
||||
private readonly int spacing;
|
||||
private readonly StackOrientation orientation;
|
||||
|
||||
public int? UniformWidth { get; set; }
|
||||
public int? UniformHeight { get; set; }
|
||||
|
||||
public StackLayout(Window window, int x, int y, int spacing = 0, StackOrientation orientation = StackOrientation.Vertical)
|
||||
{
|
||||
this.window = window;
|
||||
nextX = x;
|
||||
nextY = y;
|
||||
this.spacing = spacing;
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
public T Add<T>(Action<T>? options = null) where T : UIElement, new()
|
||||
{
|
||||
bool wasAutoFlush = window.AutoFlush;
|
||||
window.AutoFlush = false;
|
||||
|
||||
T element = window.CreateUIElement<T>(options);
|
||||
PositionElement(element);
|
||||
|
||||
if (wasAutoFlush)
|
||||
{
|
||||
window.AutoFlush = true;
|
||||
window.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.AutoFlush = false;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
private void PositionElement(UIElement element)
|
||||
{
|
||||
element.Position = new Point(nextX, nextY);
|
||||
|
||||
if (element is Control control)
|
||||
{
|
||||
if (UniformWidth.HasValue)
|
||||
{
|
||||
control.Size = new Size(UniformWidth.Value, control.Size.Height);
|
||||
}
|
||||
if (UniformHeight.HasValue)
|
||||
{
|
||||
control.Size = new Size(control.Size.Width, UniformHeight.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (orientation == StackOrientation.Vertical)
|
||||
{
|
||||
int h = element is Control c ? c.Size.Height : 0;
|
||||
nextY += h + spacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
int w = element is Control c ? c.Size.Width : 0;
|
||||
nextX += w + spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
using Cosmos.Kernel.System.Graphics;
|
||||
using Cosmos.Kernel.System.Graphics.Fonts;
|
||||
|
||||
using RemSox.Kernel.UI.GUI.UIEelements;
|
||||
using RemSox.Kernel.Utils;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Rendering;
|
||||
|
||||
public sealed class CanvasRenderSource : IRenderSource
|
||||
{
|
||||
private readonly Dictionary<int, Canvas> windowCanvases = [];
|
||||
private readonly Dictionary<int, Point> windowPositions = [];
|
||||
private readonly Dictionary<int, int> windowZIndices = [];
|
||||
|
||||
// Sorted list keeps windows in Z-order without re-sorting.
|
||||
// Key = (zIndex << 32 | windowId) so equal Z stays insertion-stable.
|
||||
private readonly SortedList<long, int> zOrderedWindows = [];
|
||||
|
||||
// Accumulated drawing primitives per window (in draw order).
|
||||
private readonly Dictionary<int, List<(int ElementId, RenderCommand Command)>> windowPrimitives = [];
|
||||
|
||||
private readonly HashSet<int> dirtyWindows = [];
|
||||
private bool isPositionDirty = true;
|
||||
private Point lastPointerPosition = new(-1, -1);
|
||||
|
||||
private Point cursorPosition;
|
||||
private int screenWidth, screenHeight;
|
||||
|
||||
private readonly Lock renderLock = new();
|
||||
|
||||
public void Render(IEnumerable<RenderCommand> commands)
|
||||
{
|
||||
lock (renderLock)
|
||||
{
|
||||
foreach (RenderCommand command in commands)
|
||||
{
|
||||
switch (command.Type)
|
||||
{
|
||||
case RenderCommandType.CreateWindow:
|
||||
CreateOrUpdateWindow(command);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DestroyWindow:
|
||||
RemoveWindow(command.WindowId);
|
||||
break;
|
||||
|
||||
case RenderCommandType.MoveWindow:
|
||||
if (windowCanvases.ContainsKey(command.WindowId))
|
||||
{
|
||||
windowPositions[command.WindowId] = command.Position;
|
||||
isPositionDirty = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case RenderCommandType.RemovePrimitives:
|
||||
RemovePrimitives(command.WindowId, command.ElementId);
|
||||
break;
|
||||
|
||||
case RenderCommandType.SetCursor:
|
||||
cursorPosition = command.Position;
|
||||
isPositionDirty = true;
|
||||
break;
|
||||
|
||||
case RenderCommandType.ScreenInfo:
|
||||
screenWidth = Get(command.Properties, "Width", 640);
|
||||
screenHeight = Get(command.Properties, "Height", 480);
|
||||
isPositionDirty = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (windowCanvases.ContainsKey(command.WindowId))
|
||||
{
|
||||
UpsertPrimitive(command);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Composite()
|
||||
{
|
||||
Canvas screenCanvas = FullScreenCanvas.GetFullScreenCanvas();
|
||||
Point pointerPosition = cursorPosition;
|
||||
|
||||
lock (renderLock)
|
||||
{
|
||||
bool pointerMoved = pointerPosition != lastPointerPosition;
|
||||
bool hasDirty = dirtyWindows.Count > 0;
|
||||
|
||||
if (!hasDirty && !isPositionDirty && !pointerMoved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Redraw dirty windows from accumulated primitives
|
||||
if (hasDirty)
|
||||
{
|
||||
foreach (int winId in dirtyWindows)
|
||||
{
|
||||
if (!windowCanvases.TryGetValue(winId, out Canvas? canvas))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
canvas.Clear(Color.Black);
|
||||
|
||||
if (windowPrimitives.TryGetValue(winId, out List<(int ElementId, RenderCommand Command)>? primitives))
|
||||
{
|
||||
foreach ((int _, RenderCommand? cmd) in primitives)
|
||||
{
|
||||
DrawPrimitive(canvas, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
dirtyWindows.Clear();
|
||||
}
|
||||
|
||||
// Draw desktop background
|
||||
int w = (int)screenCanvas.Mode.Width;
|
||||
int h = (int)screenCanvas.Mode.Height;
|
||||
screenCanvas.Clear(Color.FromArgb(45, 45, 48));
|
||||
|
||||
// Subtle horizontal gradient effect (4 bands)
|
||||
Color[] bands = [
|
||||
Color.FromArgb(30, 30, 35),
|
||||
Color.FromArgb(45, 45, 48),
|
||||
Color.FromArgb(60, 60, 65),
|
||||
Color.FromArgb(45, 45, 48),
|
||||
];
|
||||
int bandH = h / bands.Length;
|
||||
for (int i = 0; i < bands.Length; i++)
|
||||
{
|
||||
screenCanvas.DrawFilledRectangle(bands[i], 0, i * bandH, w, bandH + 1);
|
||||
}
|
||||
|
||||
// Composite all windows to screen
|
||||
foreach (int windowId in zOrderedWindows.Values)
|
||||
{
|
||||
if (windowPositions.TryGetValue(windowId, out Point pos) &&
|
||||
windowCanvases.TryGetValue(windowId, out Canvas? wc))
|
||||
{
|
||||
screenCanvas.DrawCanvas(wc, pos.X, pos.Y);
|
||||
}
|
||||
}
|
||||
|
||||
screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5);
|
||||
screenCanvas.Display();
|
||||
|
||||
lastPointerPosition = pointerPosition;
|
||||
isPositionDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Accumulated state management ---
|
||||
|
||||
private void CreateOrUpdateWindow(RenderCommand cmd)
|
||||
{
|
||||
int id = cmd.WindowId;
|
||||
Size size = Get(cmd.Properties, "Size", new Size(160, 120));
|
||||
int zIndex = Get(cmd.Properties, "ZIndex", 0);
|
||||
|
||||
if (!windowCanvases.TryGetValue(id, out Canvas? existing) ||
|
||||
existing.Mode.Width != size.Width ||
|
||||
existing.Mode.Height != size.Height)
|
||||
{
|
||||
windowCanvases[id] = new Canvas(size.Width, size.Height);
|
||||
}
|
||||
|
||||
windowPositions[id] = cmd.Position;
|
||||
|
||||
if (windowZIndices.TryGetValue(id, out int oldZ))
|
||||
{
|
||||
_ = zOrderedWindows.Remove(ZKey(oldZ, id));
|
||||
}
|
||||
windowZIndices[id] = zIndex;
|
||||
zOrderedWindows[ZKey(zIndex, id)] = id;
|
||||
|
||||
if (!windowPrimitives.ContainsKey(id))
|
||||
{
|
||||
windowPrimitives[id] = [];
|
||||
}
|
||||
|
||||
_ = dirtyWindows.Add(id);
|
||||
isPositionDirty = true;
|
||||
}
|
||||
|
||||
private void RemoveWindow(int windowId)
|
||||
{
|
||||
_ = windowCanvases.Remove(windowId);
|
||||
_ = windowPositions.Remove(windowId);
|
||||
_ = windowPrimitives.Remove(windowId);
|
||||
|
||||
if (windowZIndices.TryGetValue(windowId, out int z))
|
||||
{
|
||||
_ = zOrderedWindows.Remove(ZKey(z, windowId));
|
||||
_ = windowZIndices.Remove(windowId);
|
||||
}
|
||||
|
||||
isPositionDirty = true;
|
||||
}
|
||||
|
||||
private void UpsertPrimitive(RenderCommand cmd)
|
||||
{
|
||||
if (!windowPrimitives.TryGetValue(cmd.WindowId, out List<(int ElementId, RenderCommand Command)>? list))
|
||||
{
|
||||
list = [];
|
||||
windowPrimitives[cmd.WindowId] = list;
|
||||
}
|
||||
|
||||
int idx = list.FindIndex(p => p.ElementId == cmd.ElementId);
|
||||
if (idx >= 0)
|
||||
{
|
||||
list[idx] = (cmd.ElementId, cmd);
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add((cmd.ElementId, cmd));
|
||||
}
|
||||
|
||||
_ = dirtyWindows.Add(cmd.WindowId);
|
||||
}
|
||||
|
||||
private void RemovePrimitives(int windowId, int baseElementId)
|
||||
{
|
||||
if (!windowPrimitives.TryGetValue(windowId, out List<(int ElementId, RenderCommand Command)>? list))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = list.RemoveAll(p => p.ElementId >= 0
|
||||
? (p.ElementId >> UIElement.PrimitiveIdShift) == baseElementId
|
||||
: p.ElementId == baseElementId);
|
||||
|
||||
_ = dirtyWindows.Add(windowId);
|
||||
}
|
||||
|
||||
// --- Primitive drawing ---
|
||||
|
||||
private static void DrawPrimitive(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
switch (cmd.Type)
|
||||
{
|
||||
case RenderCommandType.DrawFilledRect:
|
||||
DrawFilledRect(canvas, cmd);
|
||||
break;
|
||||
case RenderCommandType.DrawRectBorder:
|
||||
DrawRectBorder(canvas, cmd);
|
||||
break;
|
||||
case RenderCommandType.DrawFilledCircle:
|
||||
DrawFilledCircle(canvas, cmd);
|
||||
break;
|
||||
case RenderCommandType.DrawCircle:
|
||||
DrawCircle(canvas, cmd);
|
||||
break;
|
||||
case RenderCommandType.DrawPoint:
|
||||
DrawPoint(canvas, cmd);
|
||||
break;
|
||||
case RenderCommandType.DrawText:
|
||||
DrawText(canvas, cmd);
|
||||
break;
|
||||
case RenderCommandType.DrawLine:
|
||||
DrawLine(canvas, cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a stable sort key from Z-index and window ID.
|
||||
private static long ZKey(int z, int id)
|
||||
{
|
||||
return ((long)z << 32) | (uint)id;
|
||||
}
|
||||
|
||||
private static T Get<T>(IReadOnlyDictionary<string, object?> props, string key, T fallback)
|
||||
{
|
||||
return props.TryGetValue(key, out object? raw) && raw is T value ? value : fallback;
|
||||
}
|
||||
|
||||
private static void DrawFilledRect(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
Size size = Get(cmd.Properties, "Size", new Size(10, 10));
|
||||
canvas.DrawFilledRectangle(color, cmd.Position.X, cmd.Position.Y, size.Width, size.Height);
|
||||
}
|
||||
|
||||
private static void DrawRectBorder(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
Size size = Get(cmd.Properties, "Size", new Size(10, 10));
|
||||
canvas.DrawRectangle(color, cmd.Position.X, cmd.Position.Y, size.Width, size.Height);
|
||||
}
|
||||
|
||||
private static void DrawFilledCircle(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
int radius = Get(cmd.Properties, "Radius", 10);
|
||||
canvas.DrawFilledCircle(color, cmd.Position.X + radius, cmd.Position.Y + radius, radius);
|
||||
}
|
||||
|
||||
private static void DrawCircle(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
int radius = Get(cmd.Properties, "Radius", 10);
|
||||
canvas.DrawCircle(color, cmd.Position.X + radius, cmd.Position.Y + radius, radius);
|
||||
}
|
||||
|
||||
private static void DrawPoint(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
canvas.DrawPoint(color, cmd.Position.X, cmd.Position.Y);
|
||||
}
|
||||
|
||||
private static void DrawText(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
string content = Get(cmd.Properties, "Content", string.Empty);
|
||||
int fontSize = Get(cmd.Properties, "FontSize", 12);
|
||||
int maxWidth = Get(cmd.Properties, "MaxWidth", int.MaxValue);
|
||||
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
canvas.DrawStringHeight(content, PCScreenFont.DefaultFont, color, cmd.Position.X, cmd.Position.Y, fontSize, maxWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawLine(Canvas canvas, RenderCommand cmd)
|
||||
{
|
||||
Color color = Get(cmd.Properties, "Color", Color.White);
|
||||
Point end = Get(cmd.Properties, "EndPosition", cmd.Position);
|
||||
canvas.DrawLine(color, cmd.Position.X, cmd.Position.Y, end.X, end.Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
using RemSox.Shared.Networking;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Rendering;
|
||||
|
||||
public sealed class NetworkRenderSource(TcpRpcServer server) : IRenderSource
|
||||
{
|
||||
private const string MessageType = "RenderCmd";
|
||||
|
||||
public void Render(IEnumerable<RenderCommand> commands)
|
||||
{
|
||||
foreach (RenderCommand cmd in commands)
|
||||
{
|
||||
byte[] data = cmd.ToBytes();
|
||||
server.SendRawToAll(MessageType, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void Composite()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using RemSox.Shared.UI;
|
||||
using Cosmos.Kernel.System.Keyboard;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements;
|
||||
|
||||
public abstract class Control(string type) : UIElement(type)
|
||||
{
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(BackgroundColor), ref field, value);
|
||||
} = Color.LightGray;
|
||||
|
||||
public Size Size
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Size), ref field, value);
|
||||
}
|
||||
|
||||
public virtual void HandleMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void HandleKeyEvent(KeyEvent keyEvent)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using RemSox.Shared.UI;
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Controls;
|
||||
|
||||
public class Button() : Control("Button")
|
||||
{
|
||||
public event EventHandler? OnClick;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Text), ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public Color TextColor
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(TextColor), ref field, value);
|
||||
} = Color.Black;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
// Background fill
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = BackgroundColor,
|
||||
["Size"] = Size,
|
||||
}
|
||||
};
|
||||
|
||||
// Border
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(1),
|
||||
Type = RenderCommandType.DrawRectBorder,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.DarkGray,
|
||||
["Size"] = Size,
|
||||
}
|
||||
};
|
||||
|
||||
// Text (centered, falls back to left-aligned if too wide)
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
{
|
||||
int fontSize = Size.Height - 8;
|
||||
int charWidth = 8 * fontSize / 14;
|
||||
int maxChars = Size.Width / charWidth;
|
||||
string display = Text;
|
||||
if (display.Length > maxChars && maxChars > 0)
|
||||
{
|
||||
display = Text[..maxChars];
|
||||
}
|
||||
int textWidth = display.Length * charWidth;
|
||||
int tx = textWidth >= Size.Width
|
||||
? Position.X
|
||||
: Position.X + ((Size.Width - textWidth) / 2);
|
||||
int ty = Position.Y + (Size.Height / 2) - 8;
|
||||
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(2),
|
||||
Type = RenderCommandType.DrawText,
|
||||
Position = new Point(tx, ty),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = TextColor,
|
||||
["Content"] = Text,
|
||||
["FontSize"] = Size.Height - 8,
|
||||
["MaxWidth"] = Size.Width,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
OnClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using RemSox.Shared.UI;
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Controls;
|
||||
|
||||
public class CheckBox() : Control("CheckBox")
|
||||
{
|
||||
public event EventHandler? OnCheckedChanged;
|
||||
|
||||
public bool IsChecked
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
SetProperty(nameof(IsChecked), ref field, value);
|
||||
OnCheckedChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
} = false;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Text), ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public Color TextColor
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(TextColor), ref field, value);
|
||||
} = Color.White;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
int boxSize = Size.Height;
|
||||
|
||||
// Checkbox box background
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = BackgroundColor,
|
||||
["Size"] = new Size(boxSize, boxSize),
|
||||
}
|
||||
};
|
||||
|
||||
// Checkbox box border
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(1),
|
||||
Type = RenderCommandType.DrawRectBorder,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.DarkGray,
|
||||
["Size"] = new Size(boxSize, boxSize),
|
||||
}
|
||||
};
|
||||
|
||||
// Check mark (filled inner rect)
|
||||
if (IsChecked)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(2),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = new Point(Position.X + 3, Position.Y + 3),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.Black,
|
||||
["Size"] = new Size(boxSize - 6, boxSize - 6),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Label text
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(3),
|
||||
Type = RenderCommandType.DrawText,
|
||||
Position = new Point(Position.X + boxSize + 5, Position.Y - 2),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = TextColor,
|
||||
["Content"] = Text,
|
||||
["FontSize"] = boxSize,
|
||||
["MaxWidth"] = Size.Width - boxSize - 5,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
IsChecked = !IsChecked;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Controls;
|
||||
|
||||
public class Panel() : Control("Panel")
|
||||
{
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = BackgroundColor,
|
||||
["Size"] = Size,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Controls;
|
||||
|
||||
public class ProgressBar() : Control("ProgressBar")
|
||||
{
|
||||
public int Value
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Value), ref field, value);
|
||||
}
|
||||
|
||||
public Color FillColor
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(FillColor), ref field, value);
|
||||
} = Color.Green;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
// Background
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = BackgroundColor,
|
||||
["Size"] = Size,
|
||||
}
|
||||
};
|
||||
|
||||
// Fill
|
||||
int fillWidth = Size.Width * Math.Clamp(Value, 0, 100) / 100;
|
||||
|
||||
if (fillWidth > 0)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(1),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = FillColor,
|
||||
["Size"] = new Size(fillWidth, Size.Height),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Border
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(2),
|
||||
Type = RenderCommandType.DrawRectBorder,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.DarkGray,
|
||||
["Size"] = Size,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using RemSox.Shared.UI;
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Controls;
|
||||
|
||||
public class RadioButton() : Control("RadioButton")
|
||||
{
|
||||
public event EventHandler? OnCheckedChanged;
|
||||
|
||||
public bool IsChecked
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
SetProperty(nameof(IsChecked), ref field, value);
|
||||
OnCheckedChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Text), ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public Color TextColor
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(TextColor), ref field, value);
|
||||
} = Color.White;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
int diameter = Size.Height;
|
||||
int radius = diameter / 2;
|
||||
Point center = new(Position.X + radius, Position.Y + radius);
|
||||
|
||||
// Outer circle
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawCircle,
|
||||
Position = new Point(center.X - radius, center.Y - radius),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.DarkGray,
|
||||
["Radius"] = radius,
|
||||
}
|
||||
};
|
||||
|
||||
// Inner fill when checked
|
||||
if (IsChecked)
|
||||
{
|
||||
int innerRadius = radius - 3;
|
||||
if (innerRadius > 0)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(1),
|
||||
Type = RenderCommandType.DrawFilledCircle,
|
||||
Position = new Point(center.X - innerRadius, center.Y - innerRadius),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.Black,
|
||||
["Radius"] = innerRadius,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Label text
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
{
|
||||
int textX = Position.X + diameter + 5;
|
||||
int textY = Position.Y + (Size.Height / 2) - 8;
|
||||
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(2),
|
||||
Type = RenderCommandType.DrawText,
|
||||
Position = new Point(textX, textY),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = TextColor,
|
||||
["Content"] = Text,
|
||||
["FontSize"] = Size.Height,
|
||||
["MaxWidth"] = Size.Width - Size.Height - 5,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
IsChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using RemSox.Shared.UI;
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Controls;
|
||||
|
||||
public class Slider() : Control("Slider")
|
||||
{
|
||||
public event EventHandler? OnValueChanged;
|
||||
|
||||
public int MinValue
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(MinValue), ref field, value);
|
||||
}
|
||||
|
||||
public int MaxValue
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(MaxValue), ref field, value);
|
||||
} = 100;
|
||||
|
||||
public int Value
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
int clamped = Math.Clamp(value, MinValue, MaxValue);
|
||||
SetProperty(nameof(Value), ref field, clamped);
|
||||
if (Value == clamped)
|
||||
{
|
||||
OnValueChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Color ThumbColor
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(ThumbColor), ref field, value);
|
||||
} = Color.LightGray;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
int trackHeight = 4;
|
||||
int trackY = Position.Y + (Size.Height / 2) - (trackHeight / 2);
|
||||
|
||||
// Track background
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = new Point(Position.X, trackY),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.DimGray,
|
||||
["Size"] = new Size(Size.Width, trackHeight),
|
||||
}
|
||||
};
|
||||
|
||||
// Filled portion
|
||||
int range = MaxValue - MinValue;
|
||||
int fillWidth = range > 0 ? Size.Width * (Value - MinValue) / range : 0;
|
||||
|
||||
if (fillWidth > 0)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(1),
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = new Point(Position.X, trackY),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = BackgroundColor,
|
||||
["Size"] = new Size(fillWidth, trackHeight),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Thumb (centered at the fill edge)
|
||||
int thumbSize = Size.Height;
|
||||
int thumbX = fillWidth - (thumbSize / 2);
|
||||
int thumbY = Position.Y;
|
||||
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(2),
|
||||
Type = RenderCommandType.DrawFilledCircle,
|
||||
Position = new Point(Position.X + thumbX, thumbY),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = ThumbColor,
|
||||
["Radius"] = thumbSize / 2,
|
||||
}
|
||||
};
|
||||
|
||||
// Thumb border
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(3),
|
||||
Type = RenderCommandType.DrawCircle,
|
||||
Position = new Point(Position.X + thumbX, thumbY),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.DarkGray,
|
||||
["Radius"] = thumbSize / 2,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private bool isDragging;
|
||||
|
||||
public override void HandleMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
isDragging = true;
|
||||
SetValueFromX(mouseEvent.X);
|
||||
}
|
||||
else if (mouseEvent.Type == MouseEventType.Move && isDragging)
|
||||
{
|
||||
SetValueFromX(mouseEvent.X);
|
||||
}
|
||||
else if (mouseEvent.Type == MouseEventType.ButtonUp && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetValueFromX(int windowX)
|
||||
{
|
||||
int localX = windowX - Position.X;
|
||||
int range = MaxValue - MinValue;
|
||||
Value = range > 0
|
||||
? MinValue + (localX * range / Size.Width)
|
||||
: MinValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements;
|
||||
|
||||
public abstract class Shape(string type) : UIElement(type)
|
||||
{
|
||||
public Color Color
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Color), ref field, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Shapes;
|
||||
|
||||
public class Circle() : Shape("Circle")
|
||||
{
|
||||
public int Radius
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Radius), ref field, value);
|
||||
}
|
||||
|
||||
public bool IsFilled
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(IsFilled), ref field, value);
|
||||
} = true;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = IsFilled ? RenderCommandType.DrawFilledCircle : RenderCommandType.DrawCircle,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color,
|
||||
["Radius"] = Radius,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Shapes;
|
||||
|
||||
public class Line() : Shape("Line")
|
||||
{
|
||||
public Point EndPosition
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(EndPosition), ref field, value);
|
||||
}
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawLine,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color,
|
||||
["EndPosition"] = EndPosition,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Shapes;
|
||||
|
||||
public class Pixel() : Shape("Pixel")
|
||||
{
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawPoint,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Shapes;
|
||||
|
||||
public class Rectangle() : Shape("Rectangle")
|
||||
{
|
||||
public Size Size
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Size), ref field, value);
|
||||
}
|
||||
|
||||
public bool IsFilled
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(IsFilled), ref field, value);
|
||||
} = true;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = IsFilled ? RenderCommandType.DrawFilledRect : RenderCommandType.DrawRectBorder,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color,
|
||||
["Size"] = Size,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements.Shapes;
|
||||
|
||||
public class Text() : UIElement("Text")
|
||||
{
|
||||
public string Content
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Content), ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public Color Color
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Color), ref field, value);
|
||||
} = Color.White;
|
||||
|
||||
public int FontSize
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(FontSize), ref field, value);
|
||||
} = 12;
|
||||
|
||||
public int MaxWidth
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(MaxWidth), ref field, value);
|
||||
} = int.MaxValue;
|
||||
|
||||
public override IEnumerable<RenderCommand> ToPrimitives(int windowId)
|
||||
{
|
||||
yield return new RenderCommand
|
||||
{
|
||||
WindowId = windowId,
|
||||
ElementId = PrimitiveId(0),
|
||||
Type = RenderCommandType.DrawText,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color,
|
||||
["Content"] = Content,
|
||||
["FontSize"] = FontSize,
|
||||
["MaxWidth"] = MaxWidth,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
using RemSox.Kernel.Utils;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.UIEelements;
|
||||
|
||||
public abstract class UIElement(string type) : ChangedPropertiesTracker
|
||||
{
|
||||
public const int PrimitiveIdShift = 6;
|
||||
|
||||
public int Id { get; init; }
|
||||
|
||||
public string Type { get; set; } = type;
|
||||
|
||||
public Point Position
|
||||
{
|
||||
get;
|
||||
set => SetProperty(nameof(Position), ref field, value);
|
||||
}
|
||||
|
||||
/// <summary> Expands this UI element into drawing primitives. </summary>
|
||||
public abstract IEnumerable<RenderCommand> ToPrimitives(int windowId);
|
||||
|
||||
/// <summary> Builds a stable primitive ID from element ID and sub-index. </summary>
|
||||
protected int PrimitiveId(int subIndex)
|
||||
{
|
||||
return (Id << PrimitiveIdShift) | subIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
using RemSox.Shared.UI;
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
using RemSox.Kernel.UI.GUI.UIEelements;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a window within the GUI system, managing its state, UI elements, and interactions.
|
||||
/// </summary>
|
||||
public sealed class Window(string title, int processId, int id, IRenderSource renderSource)
|
||||
{
|
||||
/// <summary> Gets the unique identifier for this window. </summary>
|
||||
public int Id { get; } = id;
|
||||
|
||||
/// <summary> Gets the ID of the process that owns this window. </summary>
|
||||
public int ProcessId { get; } = processId;
|
||||
|
||||
/// <summary> Gets or sets the title of the window. </summary>
|
||||
public string Title { get; set; } = title;
|
||||
|
||||
/// <summary> Gets or sets the Z-order index of the window (higher means more foreground). </summary>
|
||||
public int ZIndex { get; set; }
|
||||
|
||||
/// <summary> Gets or sets a value indicating whether changes should automatically trigger a redraw. </summary>
|
||||
public bool AutoFlush { get; set; } = false;
|
||||
|
||||
/// <summary> Event raised when a keyboard event is handled by this window. </summary>
|
||||
public event Action<Sys.Keyboard.KeyEvent>? OnKeyEvent;
|
||||
|
||||
/// <summary> Event raised when a mouse event is handled by this window. </summary>
|
||||
public event Action<MouseEvent>? OnMouseEvent;
|
||||
|
||||
/// <summary> Gets or sets whether this window is currently focused. </summary>
|
||||
public bool IsFocused
|
||||
{
|
||||
get => WindowManager.IsWindowFocused(this);
|
||||
set => WindowManager.FocusWindow(value ? this : null);
|
||||
}
|
||||
|
||||
/// <summary> Gets or sets whether the window is visible. </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary> Gets or sets whether the window is resizable by the user. </summary>
|
||||
public bool IsResizable { get; set; } = true;
|
||||
|
||||
/// <summary> Gets or sets whether the window can be dragged by the user. </summary>
|
||||
public bool IsDraggable { get; set; } = true;
|
||||
|
||||
/// <summary> Gets or sets whether the window has a title bar and border. </summary>
|
||||
public bool HasChrome { get; set; } = true;
|
||||
|
||||
/// <summary> Gets or sets the position of the window. </summary>
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary> Gets or sets the size of the window. </summary>
|
||||
public Size Size { get; set; }
|
||||
|
||||
/// <summary> Gets whether the window is currently being dragged. </summary>
|
||||
public bool IsDragging => currentInteraction == InteractionMode.Drag;
|
||||
|
||||
/// <summary> Gets whether the window is currently being resized. </summary>
|
||||
public bool IsResizing => currentInteraction is InteractionMode.ResizeTop or InteractionMode.ResizeBottom or InteractionMode.ResizeLeft or InteractionMode.ResizeRight or InteractionMode.ResizeTopLeft or InteractionMode.ResizeTopRight or InteractionMode.ResizeBottomLeft or InteractionMode.ResizeBottomRight;
|
||||
|
||||
private readonly Lock uiElementsLock = new();
|
||||
private readonly Lock controlsLock = new();
|
||||
private readonly Dictionary<int, UIElement> uiElements = [];
|
||||
private readonly Dictionary<int, Control> controls = [];
|
||||
private Control? focusedControl = null;
|
||||
private Control? capturedControl = null;
|
||||
|
||||
private int nextUIElementId = 1;
|
||||
|
||||
private enum InteractionMode { None, Drag, ResizeTop, ResizeBottom, ResizeLeft, ResizeRight, ResizeTopLeft, ResizeTopRight, ResizeBottomLeft, ResizeBottomRight }
|
||||
private InteractionMode currentInteraction = InteractionMode.None;
|
||||
private Rectangle interactionStartBounds;
|
||||
private Point interactionStartPointer;
|
||||
private Point dragOffset;
|
||||
|
||||
private Point lastRenderedPosition = new(-1, -1);
|
||||
private Size lastRenderedSize = new(-1, -1);
|
||||
private bool lastRenderedIsFocused = false;
|
||||
private string lastRenderedTitle = string.Empty;
|
||||
private int lastRenderedZIndex = -1;
|
||||
private bool isFirstRender = true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and registers a new UI element within this window.
|
||||
/// </summary>
|
||||
public T CreateUIElement<T>(Action<T>? options = null) where T : UIElement, new()
|
||||
{
|
||||
int uiElementId = GetNextUIElementId();
|
||||
|
||||
T uiElement = new()
|
||||
{
|
||||
Id = uiElementId
|
||||
};
|
||||
|
||||
options?.Invoke(uiElement);
|
||||
|
||||
uiElement.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
if (AutoFlush)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
};
|
||||
|
||||
lock (uiElementsLock)
|
||||
{
|
||||
uiElements.Add(uiElementId, uiElement);
|
||||
}
|
||||
|
||||
if (uiElement is Control control)
|
||||
{
|
||||
lock (controlsLock)
|
||||
{
|
||||
controls.Add(uiElementId, control);
|
||||
}
|
||||
}
|
||||
|
||||
if (AutoFlush)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
public void RemoveUIElement(int elementId)
|
||||
{
|
||||
bool removed = false;
|
||||
|
||||
lock (uiElementsLock)
|
||||
{
|
||||
if (uiElements.Remove(elementId))
|
||||
{
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
lock (controlsLock)
|
||||
{
|
||||
if (controls.Remove(elementId))
|
||||
{
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed)
|
||||
{
|
||||
renderSource.Render([new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = elementId,
|
||||
Type = RenderCommandType.RemovePrimitives,
|
||||
}]);
|
||||
|
||||
if (AutoFlush)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the window state, forcing a full redraw on the next flush.
|
||||
/// </summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
isFirstRender = true;
|
||||
Flush();
|
||||
}
|
||||
|
||||
private const int ChromeClientBg = -1;
|
||||
private const int ChromeTitleBg = -2;
|
||||
private const int ChromeTitleText = -3;
|
||||
private const int ChromeBorder = -4;
|
||||
|
||||
/// <summary>
|
||||
/// Sends current window and element state to the renderer (delta-only).
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
if (!IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<UIElement> elementsCopy;
|
||||
lock (uiElementsLock)
|
||||
{
|
||||
elementsCopy = [.. uiElements.Values];
|
||||
}
|
||||
|
||||
bool sizeChanged = Size != lastRenderedSize;
|
||||
bool positionChanged = Position != lastRenderedPosition;
|
||||
bool zIndexChanged = ZIndex != lastRenderedZIndex;
|
||||
bool titleChanged = Title != lastRenderedTitle;
|
||||
bool focusChanged = IsFocused != lastRenderedIsFocused;
|
||||
bool chromeChanged = isFirstRender || sizeChanged || titleChanged || focusChanged;
|
||||
|
||||
List<RenderCommand> commands = [];
|
||||
|
||||
// --- Structural commands ---
|
||||
if (isFirstRender || sizeChanged || zIndexChanged)
|
||||
{
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = Id,
|
||||
Type = RenderCommandType.CreateWindow,
|
||||
Position = Position,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Size"] = Size,
|
||||
["ZIndex"] = ZIndex,
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (positionChanged)
|
||||
{
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = Id,
|
||||
Type = RenderCommandType.MoveWindow,
|
||||
Position = Position,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Chrome primitives (title bar, border, background) ---
|
||||
if (chromeChanged && HasChrome)
|
||||
{
|
||||
ChromePrimitives(commands);
|
||||
}
|
||||
|
||||
// --- Child element primitives (delta) ---
|
||||
if (isFirstRender)
|
||||
{
|
||||
foreach (UIElement element in elementsCopy)
|
||||
{
|
||||
commands.AddRange(element.ToPrimitives(Id));
|
||||
element.ClearChangedProperties();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (UIElement element in elementsCopy)
|
||||
{
|
||||
if (!element.AnyPropertyChanged)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove old primitives for this element, then emit new ones
|
||||
// TODO: optimize by diffing properties and only updating what changed instead of full remove+add
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = element.Id,
|
||||
Type = RenderCommandType.RemovePrimitives,
|
||||
});
|
||||
commands.AddRange(element.ToPrimitives(Id));
|
||||
element.ClearChangedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
if (commands.Count > 0)
|
||||
{
|
||||
renderSource.Render(commands);
|
||||
lastRenderedPosition = Position;
|
||||
lastRenderedSize = Size;
|
||||
lastRenderedIsFocused = IsFocused;
|
||||
lastRenderedTitle = Title;
|
||||
lastRenderedZIndex = ZIndex;
|
||||
isFirstRender = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ChromePrimitives(List<RenderCommand> commands)
|
||||
{
|
||||
Color border = IsFocused ? Color.White : Color.DarkGray;
|
||||
Color title = IsFocused ? Color.FromArgb(0, 120, 215) : Color.FromArgb(80, 80, 80);
|
||||
|
||||
// Client area background
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = ChromeClientBg,
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Point.Empty,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.FromArgb(32, 32, 32),
|
||||
["Size"] = Size,
|
||||
}
|
||||
});
|
||||
|
||||
// Title bar background
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = ChromeTitleBg,
|
||||
Type = RenderCommandType.DrawFilledRect,
|
||||
Position = Point.Empty,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = title,
|
||||
["Size"] = new Size(Size.Width, 18),
|
||||
}
|
||||
});
|
||||
|
||||
// Title bar text
|
||||
if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = ChromeTitleText,
|
||||
Type = RenderCommandType.DrawText,
|
||||
Position = new Point(4, 2),
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = Color.White,
|
||||
["Content"] = Title,
|
||||
["FontSize"] = 18,
|
||||
["MaxWidth"] = Size.Width - 8,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Window border
|
||||
commands.Add(new RenderCommand
|
||||
{
|
||||
WindowId = Id,
|
||||
ElementId = ChromeBorder,
|
||||
Type = RenderCommandType.DrawRectBorder,
|
||||
Position = Point.Empty,
|
||||
Properties = new Dictionary<string, object?>
|
||||
{
|
||||
["Color"] = border,
|
||||
["Size"] = Size,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary> Dispatches a key event to the window's registered event handlers. </summary>
|
||||
public void HandleKeyEvent(Sys.Keyboard.KeyEvent keyEvent)
|
||||
{
|
||||
OnKeyEvent?.Invoke(keyEvent);
|
||||
|
||||
focusedControl?.HandleKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
/// <summary> Dispatches a mouse event to the window's registered event handlers. </summary>
|
||||
public void HandleMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
OnMouseEvent?.Invoke(mouseEvent);
|
||||
|
||||
Point local = new(mouseEvent.X - Position.X, mouseEvent.Y - Position.Y);
|
||||
|
||||
if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
capturedControl = HitTestControl(local);
|
||||
focusedControl = capturedControl;
|
||||
}
|
||||
|
||||
if (mouseEvent.Type == MouseEventType.ButtonUp && mouseEvent.Button == MouseButton.Left)
|
||||
{
|
||||
capturedControl = null;
|
||||
}
|
||||
|
||||
Control? target = capturedControl ?? HitTestControl(local);
|
||||
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MouseEvent localEvent = mouseEvent with
|
||||
{
|
||||
X = local.X,
|
||||
Y = local.Y
|
||||
};
|
||||
|
||||
target.HandleMouseEvent(localEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a pointer position starts an interaction (drag/resize) and handles focus.
|
||||
/// </summary>
|
||||
public bool TryBeginInteract(Point pointerPosition)
|
||||
{
|
||||
if (!IsVisible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int resizeMargin = 5;
|
||||
|
||||
bool onLeft = pointerPosition.X >= Position.X && pointerPosition.X <= Position.X + resizeMargin;
|
||||
bool onRight = pointerPosition.X >= Position.X + Size.Width - resizeMargin && pointerPosition.X <= Position.X + Size.Width;
|
||||
bool onTop = pointerPosition.Y >= Position.Y && pointerPosition.Y <= Position.Y + resizeMargin;
|
||||
bool onBottom = pointerPosition.Y >= Position.Y + Size.Height - resizeMargin && pointerPosition.Y <= Position.Y + Size.Height;
|
||||
|
||||
bool inBounds = pointerPosition.X >= Position.X && pointerPosition.X <= Position.X + Size.Width &&
|
||||
pointerPosition.Y >= Position.Y && pointerPosition.Y <= Position.Y + Size.Height;
|
||||
|
||||
currentInteraction = InteractionMode.None;
|
||||
|
||||
if (IsResizable)
|
||||
{
|
||||
if (onTop && onLeft)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeTopLeft;
|
||||
}
|
||||
else if (onTop && onRight)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeTopRight;
|
||||
}
|
||||
else if (onBottom && onLeft)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeBottomLeft;
|
||||
}
|
||||
else if (onBottom && onRight)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeBottomRight;
|
||||
}
|
||||
else if (onLeft && inBounds)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeLeft;
|
||||
}
|
||||
else if (onRight && inBounds)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeRight;
|
||||
}
|
||||
else if (onTop && inBounds)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeTop;
|
||||
}
|
||||
else if (onBottom && inBounds)
|
||||
{
|
||||
currentInteraction = InteractionMode.ResizeBottom;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInteraction == InteractionMode.None && IsDraggable && IsPointInTitleBar(pointerPosition))
|
||||
{
|
||||
currentInteraction = InteractionMode.Drag;
|
||||
dragOffset = new Point(pointerPosition.X - Position.X, pointerPosition.Y - Position.Y);
|
||||
}
|
||||
|
||||
if (currentInteraction != InteractionMode.None || inBounds)
|
||||
{
|
||||
if (currentInteraction != InteractionMode.None)
|
||||
{
|
||||
interactionStartBounds = new Rectangle(Position, Size);
|
||||
interactionStartPointer = pointerPosition;
|
||||
}
|
||||
|
||||
WindowManager.FocusWindow(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the drag or resize interaction based on the current pointer position.
|
||||
/// </summary>
|
||||
public void UpdateInteraction(Point pointerPosition, Size screenSize)
|
||||
{
|
||||
if (currentInteraction == InteractionMode.Drag)
|
||||
{
|
||||
int newX = pointerPosition.X - dragOffset.X;
|
||||
int newY = pointerPosition.Y - dragOffset.Y;
|
||||
|
||||
// Clamp left and top edges
|
||||
newX = Math.Max(0, newX);
|
||||
newY = Math.Max(0, newY);
|
||||
|
||||
// Clamp right and bottom edges
|
||||
newX = Math.Min(screenSize.Width - Size.Width, newX);
|
||||
newY = Math.Min(screenSize.Height - Size.Height, newY);
|
||||
|
||||
Position = new Point(newX, newY);
|
||||
Flush();
|
||||
}
|
||||
else if (currentInteraction != InteractionMode.None)
|
||||
{
|
||||
int dx = pointerPosition.X - interactionStartPointer.X;
|
||||
int dy = pointerPosition.Y - interactionStartPointer.Y;
|
||||
|
||||
int newX = interactionStartBounds.X;
|
||||
int newY = interactionStartBounds.Y;
|
||||
int newW = interactionStartBounds.Width;
|
||||
int newH = interactionStartBounds.Height;
|
||||
|
||||
const int minWidth = 100;
|
||||
const int minHeight = 50;
|
||||
|
||||
if (currentInteraction is InteractionMode.ResizeRight or InteractionMode.ResizeBottomRight or InteractionMode.ResizeTopRight)
|
||||
{
|
||||
newW = Math.Max(minWidth, interactionStartBounds.Width + dx);
|
||||
newW = Math.Min(newW, screenSize.Width - newX);
|
||||
}
|
||||
if (currentInteraction is InteractionMode.ResizeBottom or InteractionMode.ResizeBottomRight or InteractionMode.ResizeBottomLeft)
|
||||
{
|
||||
newH = Math.Max(minHeight, interactionStartBounds.Height + dy);
|
||||
newH = Math.Min(newH, screenSize.Height - newY);
|
||||
}
|
||||
if (currentInteraction is InteractionMode.ResizeLeft or InteractionMode.ResizeBottomLeft or InteractionMode.ResizeTopLeft)
|
||||
{
|
||||
int maxDx = interactionStartBounds.Width - minWidth;
|
||||
int clampedDx = Math.Min(dx, maxDx);
|
||||
newX = Math.Max(0, interactionStartBounds.X + clampedDx);
|
||||
newW = interactionStartBounds.X + interactionStartBounds.Width - newX;
|
||||
}
|
||||
if (currentInteraction is InteractionMode.ResizeTop or InteractionMode.ResizeTopLeft or InteractionMode.ResizeTopRight)
|
||||
{
|
||||
int maxDy = interactionStartBounds.Height - minHeight;
|
||||
int clampedDy = Math.Min(dy, maxDy);
|
||||
newY = Math.Max(0, interactionStartBounds.Y + clampedDy);
|
||||
newH = interactionStartBounds.Y + interactionStartBounds.Height - newY;
|
||||
}
|
||||
|
||||
Position = new Point(newX, newY);
|
||||
Size = new Size(newW, newH);
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the current drag or resize interaction.
|
||||
/// </summary>
|
||||
public void EndInteraction()
|
||||
{
|
||||
currentInteraction = InteractionMode.None;
|
||||
}
|
||||
|
||||
private Control? HitTestControl(Point windowRelativePosition)
|
||||
{
|
||||
lock (controlsLock)
|
||||
{
|
||||
// top-most last (simple Z-order assumption: insertion order)
|
||||
foreach (Control control in controls.Values.Reverse())
|
||||
{
|
||||
Rectangle bounds = new(
|
||||
control.Position.X,
|
||||
control.Position.Y,
|
||||
control.Size.Width,
|
||||
control.Size.Height
|
||||
);
|
||||
|
||||
if (bounds.Contains(windowRelativePosition))
|
||||
{
|
||||
return control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsPointInTitleBar(Point pointerPosition)
|
||||
{
|
||||
return HasChrome
|
||||
&& pointerPosition.X >= Position.X
|
||||
&& pointerPosition.X < Position.X + Size.Width
|
||||
&& pointerPosition.Y >= Position.Y
|
||||
&& pointerPosition.Y < Position.Y + 18;
|
||||
}
|
||||
|
||||
private int GetNextUIElementId()
|
||||
{
|
||||
return nextUIElementId++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RemSox.Kernel.UI.GUI.Windows;
|
||||
|
||||
public enum WindowLayer
|
||||
{
|
||||
Background,
|
||||
Normal,
|
||||
Foreground
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
using RemSox.Shared.UI;
|
||||
using Cosmos.Kernel.System.Graphics;
|
||||
using Cosmos.Kernel.System.Keyboard;
|
||||
|
||||
using RemSox.Kernel.Processing;
|
||||
using RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace RemSox.Kernel.UI.GUI.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Manages window creation, focus, interaction, and rendering orchestration for the GUI system.
|
||||
/// </summary>
|
||||
public static class WindowManager
|
||||
{
|
||||
private static readonly Lock windowsLock = new();
|
||||
// Process ID to list of windows
|
||||
private static readonly Dictionary<int, List<Window>> windows = [];
|
||||
|
||||
private static int nextWindowId = 1;
|
||||
private static int nextZIndex = 1;
|
||||
|
||||
private static Window? focusedWindow;
|
||||
|
||||
private static Window? activeInteractWindow = null;
|
||||
private static Point lastPointerPosition = Point.Empty;
|
||||
private static bool wasLeftButtonDown = false;
|
||||
private static bool wasRightButtonDown = false;
|
||||
private static bool wasMiddleButtonDown = false;
|
||||
|
||||
private static readonly MuliRenderSource renderSource = new([]);
|
||||
|
||||
private static readonly Queue<MouseEvent> mouseQueue = [];
|
||||
private static readonly Queue<KeyEvent> keyQueue = [];
|
||||
private static readonly Lock inputLock = new();
|
||||
|
||||
/// <summary> Enqueues a mouse event from any input source. </summary>
|
||||
public static void EnqueueMouseEvent(MouseEvent mouseEvent)
|
||||
{
|
||||
lock (inputLock)
|
||||
{
|
||||
mouseQueue.Enqueue(mouseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Enqueues a keyboard event from any input source. </summary>
|
||||
public static void EnqueueKeyEvent(KeyEvent keyEvent)
|
||||
{
|
||||
lock (inputLock)
|
||||
{
|
||||
keyQueue.Enqueue(keyEvent);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Processes queued input and updates interaction state,
|
||||
/// and triggers rendering.
|
||||
/// </summary>
|
||||
internal static void Update()
|
||||
{
|
||||
InputState input = DrainInput();
|
||||
|
||||
Point pointerPosition = input.Position;
|
||||
bool leftButtonDown = input.LeftButton;
|
||||
bool rightButtonDown = input.RightButton;
|
||||
bool middleButtonDown = input.MiddleButton;
|
||||
|
||||
Canvas canvas = FullScreenCanvas.GetFullScreenCanvas();
|
||||
|
||||
if (leftButtonDown && !wasLeftButtonDown)
|
||||
{
|
||||
activeInteractWindow = TryBeginInteract(pointerPosition);
|
||||
}
|
||||
else if (leftButtonDown && activeInteractWindow is not null)
|
||||
{
|
||||
activeInteractWindow.UpdateInteraction(pointerPosition, new Size((int)canvas.Mode.Width, (int)canvas.Mode.Height));
|
||||
}
|
||||
else if (!leftButtonDown && activeInteractWindow is not null)
|
||||
{
|
||||
activeInteractWindow.EndInteraction();
|
||||
activeInteractWindow = null;
|
||||
}
|
||||
|
||||
foreach (KeyEvent keyEvent in input.KeyEvents)
|
||||
{
|
||||
focusedWindow?.HandleKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
if (focusedWindow is not null)
|
||||
{
|
||||
DispatchMouseEvents(focusedWindow, pointerPosition,
|
||||
leftButtonDown, wasLeftButtonDown,
|
||||
rightButtonDown, wasRightButtonDown,
|
||||
middleButtonDown, wasMiddleButtonDown,
|
||||
input.ScrollDelta);
|
||||
}
|
||||
|
||||
wasLeftButtonDown = leftButtonDown;
|
||||
wasRightButtonDown = rightButtonDown;
|
||||
wasMiddleButtonDown = middleButtonDown;
|
||||
|
||||
renderSource.Render([new RenderCommand
|
||||
{
|
||||
Type = RenderCommandType.SetCursor,
|
||||
WindowId = 0,
|
||||
ElementId = 0,
|
||||
Position = pointerPosition,
|
||||
}]);
|
||||
renderSource.Composite();
|
||||
|
||||
lastPointerPosition = pointerPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new rendering source to the compositor.
|
||||
/// </summary>
|
||||
public static void AddRenderSource(IRenderSource source)
|
||||
{
|
||||
renderSource.AddSource(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an existing rendering source from the compositor.
|
||||
/// </summary>
|
||||
public static void RemoveRenderSource(IRenderSource source)
|
||||
{
|
||||
renderSource.RemoveSource(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and registers a new window for the specified process.
|
||||
/// </summary>
|
||||
public static Window CreateWindow(Process process, string title, Size size, Point position)
|
||||
{
|
||||
Window window = new(title, process.Id, GetNextWindowId(), renderSource)
|
||||
{
|
||||
Position = position,
|
||||
Size = size,
|
||||
ZIndex = nextZIndex++
|
||||
};
|
||||
|
||||
lock (windowsLock)
|
||||
{
|
||||
if (!windows.ContainsKey(process.Id))
|
||||
{
|
||||
windows[process.Id] = [];
|
||||
}
|
||||
|
||||
windows[process.Id].Add(window);
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and registers a new window, automatically finding the most spacious, least overlapping position.
|
||||
/// </summary>
|
||||
public static Window CreateWindow(Process process, string title, Size size)
|
||||
{
|
||||
Canvas canvas = FullScreenCanvas.GetFullScreenCanvas();
|
||||
int screenWidth = (int)canvas.Mode.Width;
|
||||
int screenHeight = (int)canvas.Mode.Height;
|
||||
|
||||
List<Window> existingWindows;
|
||||
lock (windowsLock)
|
||||
{
|
||||
existingWindows = windows.Values.SelectMany(w => w).ToList();
|
||||
}
|
||||
|
||||
int bestX = 50;
|
||||
int bestY = 50;
|
||||
int minOverlap = int.MaxValue;
|
||||
int maxSpaciousness = -1;
|
||||
|
||||
const int step = 40;
|
||||
int maxX = Math.Max(0, screenWidth - size.Width);
|
||||
int maxY = Math.Max(0, screenHeight - size.Height);
|
||||
|
||||
for (int y = 0; y <= maxY; y += step)
|
||||
{
|
||||
for (int x = 0; x <= maxX; x += step)
|
||||
{
|
||||
int currentOverlap = 0;
|
||||
int minWindowDist = int.MaxValue;
|
||||
|
||||
// Edges of the proposed window
|
||||
int rectLeft = x;
|
||||
int rectTop = y;
|
||||
int rectRight = x + size.Width;
|
||||
int rectBottom = y + size.Height;
|
||||
|
||||
foreach (Window win in existingWindows)
|
||||
{
|
||||
int winLeft = win.Position.X;
|
||||
int winTop = win.Position.Y;
|
||||
int winRight = win.Position.X + win.Size.Width;
|
||||
int winBottom = win.Position.Y + win.Size.Height;
|
||||
|
||||
// Calculate Overlap (AABB)
|
||||
int intersectLeft = Math.Max(rectLeft, winLeft);
|
||||
int intersectTop = Math.Max(rectTop, winTop);
|
||||
int intersectRight = Math.Min(rectRight, winRight);
|
||||
int intersectBottom = Math.Min(rectBottom, winBottom);
|
||||
|
||||
if (intersectRight > intersectLeft && intersectBottom > intersectTop)
|
||||
{
|
||||
currentOverlap += (intersectRight - intersectLeft) * (intersectBottom - intersectTop);
|
||||
}
|
||||
|
||||
// Calculate True Edge-to-Edge Distance (Manhattan)
|
||||
// If the windows overlap, dx and dy will be 0.
|
||||
int dx = Math.Max(0, Math.Max(winLeft - rectRight, rectLeft - winRight));
|
||||
int dy = Math.Max(0, Math.Max(winTop - rectBottom, rectTop - winBottom));
|
||||
|
||||
int dist = dx + dy;
|
||||
|
||||
if (dist < minWindowDist)
|
||||
{
|
||||
minWindowDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Distance to Screen Borders
|
||||
int borderDistLeft = x;
|
||||
int borderDistTop = y;
|
||||
int borderDistRight = screenWidth - rectRight;
|
||||
int borderDistBottom = screenHeight - rectBottom;
|
||||
|
||||
int minBorderDist = Math.Min(Math.Min(borderDistLeft, borderDistTop), Math.Min(borderDistRight, borderDistBottom));
|
||||
|
||||
// Spaciousness evaluates the closest boundary (either a screen edge or another window edge)
|
||||
int currentSpaciousness = existingWindows.Count == 0
|
||||
? minBorderDist
|
||||
: Math.Min(minBorderDist, minWindowDist);
|
||||
|
||||
// Score Evaluation
|
||||
if (currentOverlap < minOverlap)
|
||||
{
|
||||
minOverlap = currentOverlap;
|
||||
maxSpaciousness = currentSpaciousness;
|
||||
bestX = x;
|
||||
bestY = y;
|
||||
}
|
||||
else if (currentOverlap == minOverlap)
|
||||
{
|
||||
// If overlap is tied (e.g., both are 0), pick the most spacious position
|
||||
if (currentSpaciousness > maxSpaciousness)
|
||||
{
|
||||
maxSpaciousness = currentSpaciousness;
|
||||
bestX = x;
|
||||
bestY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CreateWindow(process, title, size, new Point(bestX, bestY));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a specific window and notifies the renderer.
|
||||
/// </summary>
|
||||
public static void CloseWindow(Window window)
|
||||
{
|
||||
lock (windowsLock)
|
||||
{
|
||||
if (windows.TryGetValue(window.ProcessId, out List<Window>? processWindows))
|
||||
{
|
||||
_ = processWindows.Remove(window);
|
||||
}
|
||||
}
|
||||
|
||||
if (focusedWindow == window)
|
||||
{
|
||||
focusedWindow = null;
|
||||
}
|
||||
if (activeInteractWindow == window)
|
||||
{
|
||||
activeInteractWindow = null;
|
||||
}
|
||||
|
||||
renderSource.Render([new RenderCommand { WindowId = window.Id, ElementId = window.Id, Type = RenderCommandType.DestroyWindow, Position = window.Position }]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all windows belonging to the given process.
|
||||
/// </summary>
|
||||
public static List<Window> GetWindowsForProcess(Process process)
|
||||
{
|
||||
lock (windowsLock)
|
||||
{
|
||||
if (windows.TryGetValue(process.Id, out List<Window>? processWindows))
|
||||
{
|
||||
return processWindows.ToList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all windows across all processes, ordered by Z-index (highest first).
|
||||
/// </summary>
|
||||
public static List<Window> GetAllWindows()
|
||||
{
|
||||
lock (windowsLock)
|
||||
{
|
||||
return windows.Values
|
||||
.SelectMany(w => w)
|
||||
.OrderBy(w => w.Id)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes all windows belonging to the given process.
|
||||
/// </summary>
|
||||
public static void CloseWindowsForProcess(Process process)
|
||||
{
|
||||
CloseWindowsForProcess(process.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes all windows belonging to the given process ID.
|
||||
/// </summary>
|
||||
public static void CloseWindowsForProcess(int processId)
|
||||
{
|
||||
List<Window> windowsToClose = [];
|
||||
lock (windowsLock)
|
||||
{
|
||||
if (windows.TryGetValue(processId, out List<Window>? processWindows))
|
||||
{
|
||||
windowsToClose.AddRange(processWindows);
|
||||
_ = windows.Remove(processId);
|
||||
}
|
||||
}
|
||||
|
||||
if (windowsToClose.Count > 0)
|
||||
{
|
||||
if (focusedWindow != null && windowsToClose.Contains(focusedWindow))
|
||||
{
|
||||
focusedWindow = null;
|
||||
}
|
||||
if (activeInteractWindow != null && windowsToClose.Contains(activeInteractWindow))
|
||||
{
|
||||
activeInteractWindow = null;
|
||||
}
|
||||
|
||||
List<RenderCommand> closeCommands = [];
|
||||
foreach (Window window in windowsToClose)
|
||||
{
|
||||
closeCommands.Add(new RenderCommand { WindowId = window.Id, ElementId = window.Id, Type = RenderCommandType.DestroyWindow, Position = window.Position });
|
||||
}
|
||||
renderSource.Render(closeCommands);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the focus to the specified window, bringing it to the foreground.
|
||||
/// </summary>
|
||||
public static void FocusWindow(Window? window)
|
||||
{
|
||||
if (focusedWindow == window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = window?.ZIndex = nextZIndex++;
|
||||
|
||||
Window? previousFocusedWindow = focusedWindow;
|
||||
focusedWindow = window;
|
||||
|
||||
previousFocusedWindow?.Flush();
|
||||
focusedWindow?.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified window is currently focused.
|
||||
/// </summary>
|
||||
public static bool IsWindowFocused(Window window)
|
||||
{
|
||||
return focusedWindow == window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces a full redraw of all windows in the system.
|
||||
/// </summary>
|
||||
public static void InvalidateAll()
|
||||
{
|
||||
List<Window> allWindows;
|
||||
lock (windowsLock)
|
||||
{
|
||||
allWindows = windows.Values.SelectMany(w => w).ToList();
|
||||
}
|
||||
|
||||
foreach (Window window in allWindows)
|
||||
{
|
||||
window.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private static Window? TryBeginInteract(Point pointerPosition)
|
||||
{
|
||||
List<Window> allWindows;
|
||||
lock (windowsLock)
|
||||
{
|
||||
allWindows = windows.Values.SelectMany(w => w).OrderByDescending(w => w.ZIndex).ToList();
|
||||
}
|
||||
|
||||
foreach (Window window in allWindows)
|
||||
{
|
||||
if (window.TryBeginInteract(pointerPosition))
|
||||
{
|
||||
return window;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static InputState DrainInput()
|
||||
{
|
||||
List<MouseEvent> mouseEvents;
|
||||
List<KeyEvent> keyEvents;
|
||||
|
||||
lock (inputLock)
|
||||
{
|
||||
mouseEvents = [.. mouseQueue];
|
||||
mouseQueue.Clear();
|
||||
keyEvents = [.. keyQueue];
|
||||
keyQueue.Clear();
|
||||
}
|
||||
|
||||
Point pos = lastPointerPosition;
|
||||
bool left = wasLeftButtonDown, right = wasRightButtonDown, middle = wasMiddleButtonDown;
|
||||
int scroll = 0;
|
||||
|
||||
foreach (MouseEvent e in mouseEvents)
|
||||
{
|
||||
switch (e.Type)
|
||||
{
|
||||
case MouseEventType.Move:
|
||||
pos = new Point(e.X, e.Y);
|
||||
break;
|
||||
case MouseEventType.ButtonDown:
|
||||
if (e.Button == MouseButton.Left) { left = true; }
|
||||
else if (e.Button == MouseButton.Right) { right = true; }
|
||||
else if (e.Button == MouseButton.Middle) { middle = true; }
|
||||
break;
|
||||
case MouseEventType.ButtonUp:
|
||||
if (e.Button == MouseButton.Left) { left = false; }
|
||||
else if (e.Button == MouseButton.Right) { right = false; }
|
||||
else if (e.Button == MouseButton.Middle) { middle = false; }
|
||||
break;
|
||||
case MouseEventType.Wheel:
|
||||
scroll += e.Delta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new InputState(pos, left, right, middle, scroll, keyEvents);
|
||||
}
|
||||
|
||||
private static void DispatchMouseEvents(
|
||||
Window window, Point pos,
|
||||
bool leftDown, bool wasLeftDown,
|
||||
bool rightDown, bool wasRightDown,
|
||||
bool middleDown, bool wasMiddleDown,
|
||||
int scrollDelta)
|
||||
{
|
||||
if (pos != lastPointerPosition)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.Move(pos.X, pos.Y));
|
||||
}
|
||||
|
||||
if (leftDown && !wasLeftDown)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Left));
|
||||
}
|
||||
else if (!leftDown && wasLeftDown)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Left));
|
||||
}
|
||||
|
||||
if (rightDown && !wasRightDown)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Right));
|
||||
}
|
||||
else if (!rightDown && wasRightDown)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Right));
|
||||
}
|
||||
|
||||
if (middleDown && !wasMiddleDown)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Middle));
|
||||
}
|
||||
else if (!middleDown && wasMiddleDown)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Middle));
|
||||
}
|
||||
|
||||
if (scrollDelta != 0)
|
||||
{
|
||||
window.HandleMouseEvent(MouseEvent.Wheel(pos.X, pos.Y, scrollDelta));
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetNextWindowId()
|
||||
{
|
||||
return nextWindowId++;
|
||||
}
|
||||
|
||||
private readonly record struct InputState(
|
||||
Point Position,
|
||||
bool LeftButton,
|
||||
bool RightButton,
|
||||
bool MiddleButton,
|
||||
int ScrollDelta,
|
||||
List<KeyEvent> KeyEvents
|
||||
);
|
||||
|
||||
private sealed class MuliRenderSource(List<IRenderSource> sources) : IRenderSource
|
||||
{
|
||||
private readonly Lock sourcesLock = new();
|
||||
|
||||
public void AddSource(IRenderSource source)
|
||||
{
|
||||
lock (sourcesLock)
|
||||
{
|
||||
sources.Add(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSource(IRenderSource source)
|
||||
{
|
||||
lock (sourcesLock)
|
||||
{
|
||||
_ = sources.Remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(IEnumerable<RenderCommand> commands)
|
||||
{
|
||||
List<IRenderSource> sourcesCopy;
|
||||
lock (sourcesLock)
|
||||
{
|
||||
sourcesCopy = sources.ToList();
|
||||
}
|
||||
|
||||
foreach (IRenderSource source in sourcesCopy)
|
||||
{
|
||||
source.Render(commands);
|
||||
}
|
||||
}
|
||||
|
||||
public void Composite()
|
||||
{
|
||||
List<IRenderSource> sourcesCopy;
|
||||
lock (sourcesLock)
|
||||
{
|
||||
sourcesCopy = sources.ToList();
|
||||
}
|
||||
|
||||
foreach (IRenderSource source in sourcesCopy)
|
||||
{
|
||||
source.Composite();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user