Add unified input event queue and cursor support for window manager

This commit is contained in:
Stone_Red
2026-06-17 00:50:05 +02:00
parent 8d1cd6d369
commit 633ded2d62
7 changed files with 253 additions and 27 deletions
+57
View File
@@ -1,6 +1,9 @@
using Cosmos.Kernel.System.Graphics; using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Keyboard;
using Cosmos.Kernel.System.Mouse;
using RemSox.Processing; using RemSox.Processing;
using RemSox.UI;
using RemSox.UI.GUI.Layout; using RemSox.UI.GUI.Layout;
using RemSox.UI.GUI.Rendering; using RemSox.UI.GUI.Rendering;
using RemSox.UI.GUI.UIEelements.Controls; using RemSox.UI.GUI.UIEelements.Controls;
@@ -115,8 +118,13 @@ internal class DesktopProcess() : Process("Desktop Manager")
} }
} }
private Point lastLocalMousePos = Point.Empty;
private bool lastLocalLeft, lastLocalRight, lastLocalMiddle;
internal override void Tick() internal override void Tick()
{ {
PollLocalHardware();
WindowManager.Update(); WindowManager.Update();
if (tickCount % 5 == 0) if (tickCount % 5 == 0)
@@ -132,6 +140,55 @@ internal class DesktopProcess() : Process("Desktop Manager")
} }
} }
private void PollLocalHardware()
{
Point pos = new(MouseManager.X, MouseManager.Y);
if (pos != lastLocalMousePos)
{
WindowManager.EnqueueMouseEvent(MouseEvent.Move(pos.X, pos.Y));
lastLocalMousePos = pos;
}
bool left = MouseManager.LeftButton;
if (left != lastLocalLeft)
{
WindowManager.EnqueueMouseEvent(left
? MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Left)
: MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Left));
lastLocalLeft = left;
}
bool right = MouseManager.RightButton;
if (right != lastLocalRight)
{
WindowManager.EnqueueMouseEvent(right
? MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Right)
: MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Right));
lastLocalRight = right;
}
bool middle = MouseManager.MiddleButton;
if (middle != lastLocalMiddle)
{
WindowManager.EnqueueMouseEvent(middle
? MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Middle)
: MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Middle));
lastLocalMiddle = middle;
}
int scroll = MouseManager.ScrollDelta;
if (scroll != 0)
{
WindowManager.EnqueueMouseEvent(MouseEvent.Wheel(pos.X, pos.Y, scroll));
}
while (KeyboardManager.TryReadKey(out KeyEvent? keyEvent) && keyEvent is not null)
{
WindowManager.EnqueueKeyEvent(keyEvent);
}
}
private void UpdateWindowButtons() private void UpdateWindowButtons()
{ {
List<Window> allWindows = WindowManager.GetAllWindows() List<Window> allWindows = WindowManager.GetAllWindows()
+62
View File
@@ -1,5 +1,8 @@
using Cosmos.Kernel.System.Keyboard;
using RemSox.Networking; using RemSox.Networking;
using RemSox.Processing; using RemSox.Processing;
using RemSox.UI;
using RemSox.UI.GUI.Rendering; using RemSox.UI.GUI.Rendering;
using RemSox.UI.GUI.Windows; using RemSox.UI.GUI.Windows;
@@ -13,6 +16,12 @@ internal sealed class RemoteDesktopProcess() : Process("Remote Desktop Server")
public int Port { get; private set; } public int Port { get; private set; }
// Remote input message record types (JSON-serialized over TCP)
private sealed record MouseMoveMsg(int X, int Y);
private sealed record MouseButtonMsg(int X, int Y, string Button);
private sealed record MouseWheelMsg(int X, int Y, int Delta);
private sealed record KeyEventMsg(int Key, string KeyChar, bool Shift, bool Alt, bool Control, bool Pressed);
internal override void Start(string[] args) internal override void Start(string[] args)
{ {
if (args.Length == 0 || !int.TryParse(args[0], out int port) || port <= 0 || port > 65535) if (args.Length == 0 || !int.TryParse(args[0], out int port) || port <= 0 || port > 65535)
@@ -32,6 +41,8 @@ internal sealed class RemoteDesktopProcess() : Process("Remote Desktop Server")
WindowManager.InvalidateAll(); WindowManager.InvalidateAll();
}); });
RegisterInputHandlers();
_ = Task.Run(() => server.StartAsync(port, cts.Token)); _ = Task.Run(() => server.StartAsync(port, cts.Token));
WindowManager.AddRenderSource(networkSource); WindowManager.AddRenderSource(networkSource);
@@ -55,4 +66,55 @@ internal sealed class RemoteDesktopProcess() : Process("Remote Desktop Server")
Logger.Log("Remote desktop server stopped.", Logging.LogSeverity.Info); Logger.Log("Remote desktop server stopped.", Logging.LogSeverity.Info);
} }
private void RegisterInputHandlers()
{
if (server is null)
{
return;
}
server.ListenTo<MouseMoveMsg>("MouseMove", async (msg) =>
{
WindowManager.EnqueueMouseEvent(MouseEvent.Move(msg.X, msg.Y));
});
server.ListenTo<MouseButtonMsg>("MouseDown", async (msg) =>
{
MouseButton button = ParseButton(msg.Button);
WindowManager.EnqueueMouseEvent(MouseEvent.ButtonDown(msg.X, msg.Y, button));
});
server.ListenTo<MouseButtonMsg>("MouseUp", async (msg) =>
{
MouseButton button = ParseButton(msg.Button);
WindowManager.EnqueueMouseEvent(MouseEvent.ButtonUp(msg.X, msg.Y, button));
});
server.ListenTo<MouseWheelMsg>("MouseWheel", async (msg) =>
{
WindowManager.EnqueueMouseEvent(MouseEvent.Wheel(msg.X, msg.Y, msg.Delta));
});
server.ListenTo<KeyEventMsg>("KeyEvent", async (msg) =>
{
ConsoleKeyEx key = (ConsoleKeyEx)msg.Key;
char keyChar = msg.KeyChar.Length > 0 ? msg.KeyChar[0] : '\0';
bool isPressed = msg.Pressed;
KeyEvent keyEvent = new(keyChar, key, msg.Shift, msg.Alt, msg.Control, isPressed ? KeyEvent.KeyEventType.Make : KeyEvent.KeyEventType.Break);
WindowManager.EnqueueKeyEvent(keyEvent);
});
}
private static MouseButton ParseButton(string button)
{
return button switch
{
"Left" => MouseButton.Left,
"Right" => MouseButton.Right,
"Middle" => MouseButton.Middle,
_ => MouseButton.None
};
}
} }
+8 -2
View File
@@ -1,6 +1,5 @@
using Cosmos.Kernel.System.Graphics; using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Graphics.Fonts; using Cosmos.Kernel.System.Graphics.Fonts;
using Cosmos.Kernel.System.Mouse;
using RemSox.UI.GUI.UIEelements; using RemSox.UI.GUI.UIEelements;
using RemSox.Utils; using RemSox.Utils;
@@ -26,6 +25,8 @@ public sealed class CanvasRenderSource : IRenderSource
private static bool isPositionDirty = true; private static bool isPositionDirty = true;
private static Point lastPointerPosition = new(-1, -1); private static Point lastPointerPosition = new(-1, -1);
private static Point cursorPosition;
private static readonly Lock renderLock = new(); private static readonly Lock renderLock = new();
public void Render(IEnumerable<RenderCommand> commands) public void Render(IEnumerable<RenderCommand> commands)
@@ -56,6 +57,11 @@ public sealed class CanvasRenderSource : IRenderSource
RemovePrimitives(command.WindowId, command.ElementId); RemovePrimitives(command.WindowId, command.ElementId);
break; break;
case RenderCommandType.SetCursor:
cursorPosition = command.Position;
isPositionDirty = true;
break;
default: default:
if (windowCanvases.ContainsKey(command.WindowId)) if (windowCanvases.ContainsKey(command.WindowId))
{ {
@@ -70,7 +76,7 @@ public sealed class CanvasRenderSource : IRenderSource
public void Composite() public void Composite()
{ {
Canvas screenCanvas = FullScreenCanvas.GetFullScreenCanvas(); Canvas screenCanvas = FullScreenCanvas.GetFullScreenCanvas();
Point pointerPosition = new(MouseManager.X, MouseManager.Y); Point pointerPosition = cursorPosition;
lock (renderLock) lock (renderLock)
{ {
+9
View File
@@ -90,6 +90,11 @@ public class RenderCommand
case RenderCommandType.RemovePrimitives: case RenderCommandType.RemovePrimitives:
break; break;
case RenderCommandType.SetCursor:
WriteInt16(s, Position.X);
WriteInt16(s, Position.Y);
break;
} }
} }
@@ -154,6 +159,10 @@ public class RenderCommand
case RenderCommandType.RemovePrimitives: case RenderCommandType.RemovePrimitives:
break; break;
case RenderCommandType.SetCursor:
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
break;
} }
return new RenderCommand return new RenderCommand
+2
View File
@@ -15,4 +15,6 @@ public enum RenderCommandType : byte
DrawPoint = 0x16, DrawPoint = 0x16,
RemovePrimitives = 0x20, RemovePrimitives = 0x20,
SetCursor = 0x30,
} }
+1
View File
@@ -254,6 +254,7 @@ public sealed class Window(string title, int processId, int id, IRenderSource re
} }
// Remove old primitives for this element, then emit new ones // 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 commands.Add(new RenderCommand
{ {
WindowId = Id, WindowId = Id,
+114 -25
View File
@@ -1,6 +1,5 @@
using Cosmos.Kernel.System.Graphics; using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Keyboard; using Cosmos.Kernel.System.Keyboard;
using Cosmos.Kernel.System.Mouse;
using RemSox.Processing; using RemSox.Processing;
using RemSox.UI.GUI.Rendering; using RemSox.UI.GUI.Rendering;
@@ -31,15 +30,39 @@ public static class WindowManager
private static readonly MuliRenderSource renderSource = new([]); 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> /// <summary>
/// Processes input, updates interaction state, and triggers rendering of all windows. /// Processes queued input and updates interaction state,
/// and triggers rendering.
/// </summary> /// </summary>
internal static void Update() internal static void Update()
{ {
Point pointerPosition = new(MouseManager.X, MouseManager.Y); InputState input = DrainInput();
bool leftButtonDown = MouseManager.LeftButton;
bool rightButtonDown = MouseManager.RightButton; Point pointerPosition = input.Position;
bool middleButtonDown = MouseManager.MiddleButton; bool leftButtonDown = input.LeftButton;
bool rightButtonDown = input.RightButton;
bool middleButtonDown = input.MiddleButton;
Canvas canvas = FullScreenCanvas.GetFullScreenCanvas(); Canvas canvas = FullScreenCanvas.GetFullScreenCanvas();
@@ -57,20 +80,31 @@ public static class WindowManager
activeInteractWindow = null; activeInteractWindow = null;
} }
while (KeyboardManager.TryReadKey(out KeyEvent? keyEvent) && keyEvent is not null) foreach (KeyEvent keyEvent in input.KeyEvents)
{ {
focusedWindow?.HandleKeyEvent(keyEvent); focusedWindow?.HandleKeyEvent(keyEvent);
} }
if (focusedWindow is not null) if (focusedWindow is not null)
{ {
DispatchMouseEvents(focusedWindow, pointerPosition); DispatchMouseEvents(focusedWindow, pointerPosition,
leftButtonDown, wasLeftButtonDown,
rightButtonDown, wasRightButtonDown,
middleButtonDown, wasMiddleButtonDown,
input.ScrollDelta);
} }
wasLeftButtonDown = leftButtonDown; wasLeftButtonDown = leftButtonDown;
wasRightButtonDown = rightButtonDown; wasRightButtonDown = rightButtonDown;
wasMiddleButtonDown = middleButtonDown; wasMiddleButtonDown = middleButtonDown;
renderSource.Render([new RenderCommand
{
Type = RenderCommandType.SetCursor,
WindowId = 0,
ElementId = 0,
Position = pointerPosition,
}]);
renderSource.Composite(); renderSource.Composite();
lastPointerPosition = pointerPosition; lastPointerPosition = pointerPosition;
@@ -382,45 +416,91 @@ public static class WindowManager
return null; return null;
} }
private static void DispatchMouseEvents(Window window, Point pos) private static InputState DrainInput()
{ {
int delta = MouseManager.ScrollDelta; 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) if (pos != lastPointerPosition)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.Move, pos.X, pos.Y, MouseButton.None, 0)); window.HandleMouseEvent(MouseEvent.Move(pos.X, pos.Y));
} }
if (MouseManager.LeftButton && !wasLeftButtonDown) if (leftDown && !wasLeftDown)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.ButtonDown, pos.X, pos.Y, MouseButton.Left, 0)); window.HandleMouseEvent(MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Left));
} }
else if (!MouseManager.LeftButton && wasLeftButtonDown) else if (!leftDown && wasLeftDown)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.ButtonUp, pos.X, pos.Y, MouseButton.Left, 0)); window.HandleMouseEvent(MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Left));
} }
if (MouseManager.RightButton && !wasRightButtonDown) if (rightDown && !wasRightDown)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.ButtonDown, pos.X, pos.Y, MouseButton.Right, 0)); window.HandleMouseEvent(MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Right));
} }
else if (!MouseManager.RightButton && wasRightButtonDown) else if (!rightDown && wasRightDown)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.ButtonUp, pos.X, pos.Y, MouseButton.Right, 0)); window.HandleMouseEvent(MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Right));
} }
if (MouseManager.MiddleButton && !wasMiddleButtonDown) if (middleDown && !wasMiddleDown)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.ButtonDown, pos.X, pos.Y, MouseButton.Middle, 0)); window.HandleMouseEvent(MouseEvent.ButtonDown(pos.X, pos.Y, MouseButton.Middle));
} }
else if (!MouseManager.MiddleButton && wasMiddleButtonDown) else if (!middleDown && wasMiddleDown)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.ButtonUp, pos.X, pos.Y, MouseButton.Middle, 0)); window.HandleMouseEvent(MouseEvent.ButtonUp(pos.X, pos.Y, MouseButton.Middle));
} }
if (delta != 0) if (scrollDelta != 0)
{ {
window.HandleMouseEvent(new MouseEvent(MouseEventType.Wheel, pos.X, pos.Y, MouseButton.None, delta)); window.HandleMouseEvent(MouseEvent.Wheel(pos.X, pos.Y, scrollDelta));
} }
} }
@@ -429,6 +509,15 @@ public static class WindowManager
return nextWindowId++; 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 sealed class MuliRenderSource(List<IRenderSource> sources) : IRenderSource
{ {
private readonly Lock sourcesLock = new(); private readonly Lock sourcesLock = new();