Reorganize project structure

This commit is contained in:
Stone_Red
2026-06-19 00:56:46 +02:00
parent 6f58eefb66
commit 95d88f1a8d
84 changed files with 267 additions and 186 deletions
+580
View File
@@ -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();
}
}
}
}