Add windowed terminal, basic text, rectangle rendering, and keyboard events

This commit is contained in:
Stone_Red
2026-06-08 17:42:08 +02:00
parent 57f011ffe8
commit fc71af028f
10 changed files with 284 additions and 12 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ public class Kernel : Sys.Kernel
if (Processes.DesktopProcess.IsRunning)
{
// Suspend the CLI while the GUI is active to prevent blocking and console corruption.
Thread.Sleep(100);
Thread.Sleep(1000);
return;
}
+8 -5
View File
@@ -29,20 +29,23 @@ public class DesktopProcess : Process
// Trigger Canvas initialization
FullScreenCanvas.GetFullScreenCanvas();
// Force existing windows to redraw onto the new canvas renderer
WindowManager.InvalidateAll();
// Start the terminal process within the desktop environment
ProcessManager.SpawnProcess<TerminalProcess>();
while (!StopRequested)
{
WindowManager.Update();
// Sleep slightly to yield CPU to the main CLI thread (approx 60 FPS)
Thread.Sleep(16);
//Thread.Sleep(16);
}
IsRunning = false;
// Cosmos doesn't robustly support switching back to text mode yet.
// We will stop updating, but the screen will remain in graphics mode.
}
+135
View File
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using Cosmos.Kernel.System;
using Cosmos.Kernel.System.Keyboard;
using RemSox.Processing;
using RemSox.UI.GUI.UIEelements;
using RemSox.UI.GUI.Windows;
using RemSox.UI.GUI.CLI;
namespace RemSox.Processes;
public class TerminalProcess : Process
{
private Window? window;
private readonly List<string> history = new();
private string currentInput = "";
private readonly List<Text> textLines = new();
private const int MaxLines = 15;
private const int LineHeight = 16;
public TerminalProcess() : base("Terminal")
{
}
internal override void Run()
{
window = WindowManager.CreateWindow(this, "Terminal", new Point(50, 50), new Size(400, 300));
for (int i = 0; i < MaxLines + 1; i++) // +1 for the input line
{
var textElement = window.CreateUIElement<Text>(t =>
{
t.Position = new Point(5, 20 + (i * LineHeight));
t.Color = Color.LightGreen;
t.Content = "";
});
textLines.Add(textElement);
}
window.AutoFlush = true;
PrintLine("RemSox GUI Terminal v1.0");
PrintLine("Type 'help' for commands.");
window.OnKeyEvent += HandleKey;
while (!StopRequested)
{
System.Threading.Thread.Sleep(50);
}
window.OnKeyEvent -= HandleKey;
WindowManager.CloseWindow(window);
}
private void HandleKey(KeyEvent keyEvent)
{
if (keyEvent.Key == ConsoleKeyEx.Enter)
{
string cmd = currentInput;
PrintLine("> " + cmd);
currentInput = "";
if (!string.IsNullOrWhiteSpace(cmd))
{
// Temporarily capture console output
// Wait, CommandManager uses Console.WriteLine.
// Redirecting Console output in Cosmos might be tricky.
// For now we will just execute it. Note: CommandManager commands write to standard Console,
// which might write over the VGA buffer or just be invisible.
// To make a proper terminal, commands should return strings or we need a custom ICommand context.
if (cmd == "exit")
{
RequestStop();
}
else
{
bool found = CommandManager.TryExecute(cmd);
if (!found)
{
PrintLine($"\"{cmd}\" is not a command");
}
else
{
PrintLine("Command executed. (Output sent to background console)");
}
}
}
UpdateDisplay();
}
else if (keyEvent.Key == ConsoleKeyEx.Backspace)
{
if (currentInput.Length > 0)
{
currentInput = currentInput.Substring(0, currentInput.Length - 1);
UpdateDisplay();
}
}
else if (keyEvent.KeyChar >= 32 && keyEvent.KeyChar <= 126) // Printable chars
{
currentInput += keyEvent.KeyChar;
UpdateDisplay();
}
}
private void PrintLine(string text)
{
history.Add(text);
if (history.Count > MaxLines)
{
history.RemoveAt(0);
}
UpdateDisplay();
}
private void UpdateDisplay()
{
for (int i = 0; i < MaxLines; i++)
{
if (i < history.Count)
{
textLines[i].Content = history[i];
}
else
{
textLines[i].Content = "";
}
}
// The last line is the input line
textLines[MaxLines].Content = "> " + currentInput + "_";
}
}
+89 -6
View File
@@ -3,6 +3,7 @@ using System.Drawing;
using System.Collections.Generic;
using System.Linq;
using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Graphics.Fonts;
namespace RemSox.UI.GUI.Rendering;
@@ -12,15 +13,32 @@ public sealed class CanvasRenderSource : IRenderSource
private static readonly Dictionary<int, Point> windowPositions = new();
private static readonly Dictionary<int, int> windowZIndices = new();
private static bool isDirty = true;
private static Point lastPointerPosition = new Point(-1, -1);
private static List<int> orderedWindowsCache = new();
private static bool isZOrderDirty = true;
public void Render(IEnumerable<RenderCommand> commands)
{
bool changed = false;
foreach (RenderCommand command in commands)
{
changed = true;
if (command.ElementType == "WindowClose")
{
windowCanvases.Remove(command.WindowId);
windowPositions.Remove(command.WindowId);
windowZIndices.Remove(command.WindowId);
isZOrderDirty = true;
continue;
}
if (command.ElementType == "Window" || command.ElementType == "WindowMove")
{
if (command.Properties.TryGetValue("ZIndex", out object? rawZIndex) && rawZIndex is int z)
{
windowZIndices[command.WindowId] = z;
isZOrderDirty = true;
}
}
@@ -30,14 +48,14 @@ public sealed class CanvasRenderSource : IRenderSource
? windowSize
: new Size(160, 120);
if (!windowCanvases.TryGetValue(command.WindowId, out Canvas? currentCanvas) ||
currentCanvas.Mode.Width != size.Width ||
if (!windowCanvases.TryGetValue(command.WindowId, out Canvas? currentCanvas) ||
currentCanvas.Mode.Width != size.Width ||
currentCanvas.Mode.Height != size.Height)
{
windowCanvases[command.WindowId] = new Canvas(size.Width, size.Height);
}
}
if (!windowCanvases.ContainsKey(command.WindowId))
{
windowCanvases[command.WindowId] = new Canvas(160, 120);
@@ -62,16 +80,40 @@ public sealed class CanvasRenderSource : IRenderSource
{
RenderCircle(windowCanvas, command);
}
if (command.ElementType == "Rectangle")
{
RenderRectangle(windowCanvas, command);
}
if (command.ElementType == "Text")
{
RenderText(windowCanvas, command);
}
}
if (changed)
{
isDirty = true;
}
}
public static void CompositeAndDisplay(Canvas screenCanvas, Point pointerPosition)
{
screenCanvas.Clear(Color.Black);
if (!isDirty && pointerPosition == lastPointerPosition)
{
return;
}
var orderedWindows = windowPositions.Keys.OrderBy(id => windowZIndices.TryGetValue(id, out int z) ? z : 0);
if (isZOrderDirty)
{
orderedWindowsCache = windowPositions.Keys.OrderBy(id => windowZIndices.TryGetValue(id, out int z) ? z : 0).ToList();
isZOrderDirty = false;
}
foreach (var windowId in orderedWindows)
//screenCanvas.Clear(Color.Black);
foreach (var windowId in orderedWindowsCache)
{
if (windowPositions.TryGetValue(windowId, out Point position) && windowCanvases.TryGetValue(windowId, out Canvas? windowCanvas))
{
@@ -81,6 +123,9 @@ public sealed class CanvasRenderSource : IRenderSource
screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5);
screenCanvas.Display();
lastPointerPosition = pointerPosition;
isDirty = false;
}
private static void RenderWindow(Canvas canvas, RenderCommand command)
@@ -115,4 +160,42 @@ public sealed class CanvasRenderSource : IRenderSource
canvas.DrawFilledCircle(color, centerX, centerY, radius);
}
private static void RenderRectangle(Canvas canvas, RenderCommand command)
{
Color color = command.Properties.TryGetValue("Color", out object? rawColor) && rawColor is Color rectColor
? rectColor
: Color.White;
Size size = command.Properties.TryGetValue("Size", out object? rawSize) && rawSize is Size rectSize
? rectSize
: new Size(10, 10);
bool isFilled = command.Properties.TryGetValue("IsFilled", out object? rawFilled) && rawFilled is bool filled && filled;
if (isFilled)
{
canvas.DrawFilledRectangle(color, command.Position.X, command.Position.Y, size.Width, size.Height);
}
else
{
canvas.DrawRectangle(color, command.Position.X, command.Position.Y, size.Width, size.Height);
}
}
private static void RenderText(Canvas canvas, RenderCommand command)
{
Color color = command.Properties.TryGetValue("Color", out object? rawColor) && rawColor is Color textColor
? textColor
: Color.White;
string content = command.Properties.TryGetValue("Content", out object? rawContent) && rawContent is string textContent
? textContent
: string.Empty;
if (!string.IsNullOrEmpty(content))
{
canvas.DrawString(content, Cosmos.Kernel.System.Graphics.Fonts.PCScreenFont.DefaultFont, color, command.Position.X, command.Position.Y);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System.Drawing;
namespace RemSox.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;
}
+18
View File
@@ -0,0 +1,18 @@
using System.Drawing;
namespace RemSox.UI.GUI.UIEelements;
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;
}
+7
View File
@@ -18,6 +18,13 @@ public sealed class Window(string title, int processId, int id, IRenderSource re
public bool AutoFlush { get; set; } = false;
public event Action<Cosmos.Kernel.System.Keyboard.KeyEvent>? OnKeyEvent;
public void HandleKeyEvent(Cosmos.Kernel.System.Keyboard.KeyEvent keyEvent)
{
OnKeyEvent?.Invoke(keyEvent);
}
public bool IsFocused
{
get => WindowManager.IsWindowFocused(this);
+8
View File
@@ -5,6 +5,7 @@ using System.Drawing;
using System.Linq;
using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Mouse;
using Cosmos.Kernel.System.Keyboard;
using RemSox.Processing;
using RemSox.UI.GUI.Rendering;
@@ -51,6 +52,11 @@ public static class WindowManager
wasLeftButtonDown = leftButtonDown;
if (KeyboardManager.TryReadKey(out KeyEvent keyEvent))
{
focusedWindow?.HandleKeyEvent(keyEvent);
}
Canvas canvas = FullScreenCanvas.GetFullScreenCanvas();
CanvasRenderSource.CompositeAndDisplay(canvas, pointerPosition);
@@ -86,6 +92,8 @@ public static class WindowManager
{
processWindows.Remove(window);
}
renderSource.Render(new[] { new RenderCommand { WindowId = window.Id, ElementId = window.Id, ElementType = "WindowClose", Position = window.Position, Properties = new Dictionary<string, object?>() } });
}
public static List<Window> GetWindowsForProcess(Process process)
Binary file not shown.
Binary file not shown.