diff --git a/Kernel.cs b/Kernel.cs index 3dcb951..dfdb708 100644 --- a/Kernel.cs +++ b/Kernel.cs @@ -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; } diff --git a/Processes/DesktopProcess.cs b/Processes/DesktopProcess.cs index 1de0696..d664172 100644 --- a/Processes/DesktopProcess.cs +++ b/Processes/DesktopProcess.cs @@ -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(); + 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. } diff --git a/Processes/TerminalProcess.cs b/Processes/TerminalProcess.cs new file mode 100644 index 0000000..1c6b996 --- /dev/null +++ b/Processes/TerminalProcess.cs @@ -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 history = new(); + private string currentInput = ""; + private readonly List 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(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 + "_"; + } +} diff --git a/UI/GUI/Rendering/CanvasRenderSource.cs b/UI/GUI/Rendering/CanvasRenderSource.cs index d07be6e..21231f3 100644 --- a/UI/GUI/Rendering/CanvasRenderSource.cs +++ b/UI/GUI/Rendering/CanvasRenderSource.cs @@ -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 windowPositions = new(); private static readonly Dictionary windowZIndices = new(); + private static bool isDirty = true; + private static Point lastPointerPosition = new Point(-1, -1); + private static List orderedWindowsCache = new(); + private static bool isZOrderDirty = true; + public void Render(IEnumerable 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); + } + } } \ No newline at end of file diff --git a/UI/GUI/UIEelements/Shapes/Rectangle.cs b/UI/GUI/UIEelements/Shapes/Rectangle.cs new file mode 100644 index 0000000..be0dda8 --- /dev/null +++ b/UI/GUI/UIEelements/Shapes/Rectangle.cs @@ -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; +} diff --git a/UI/GUI/UIEelements/Text.cs b/UI/GUI/UIEelements/Text.cs new file mode 100644 index 0000000..de0e15b --- /dev/null +++ b/UI/GUI/UIEelements/Text.cs @@ -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; +} diff --git a/UI/GUI/Windows/Window.cs b/UI/GUI/Windows/Window.cs index 11c257c..91c89e5 100644 --- a/UI/GUI/Windows/Window.cs +++ b/UI/GUI/Windows/Window.cs @@ -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? OnKeyEvent; + + public void HandleKeyEvent(Cosmos.Kernel.System.Keyboard.KeyEvent keyEvent) + { + OnKeyEvent?.Invoke(keyEvent); + } + public bool IsFocused { get => WindowManager.IsWindowFocused(this); diff --git a/UI/GUI/Windows/WindowManager.cs b/UI/GUI/Windows/WindowManager.cs index 451adf0..dcf2603 100644 --- a/UI/GUI/Windows/WindowManager.cs +++ b/UI/GUI/Windows/WindowManager.cs @@ -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() } }); } public static List GetWindowsForProcess(Process process) diff --git a/output-x64/RemSox.dll b/output-x64/RemSox.dll index 52045a4..cc78e71 100644 Binary files a/output-x64/RemSox.dll and b/output-x64/RemSox.dll differ diff --git a/output-x64/RemSox.iso b/output-x64/RemSox.iso index 3759cce..4b045ef 100644 Binary files a/output-x64/RemSox.iso and b/output-x64/RemSox.iso differ