diff --git a/Processes/TerminalProcess.cs b/Processes/TerminalProcess.cs index eb209f1..5ac87ee 100644 --- a/Processes/TerminalProcess.cs +++ b/Processes/TerminalProcess.cs @@ -16,6 +16,7 @@ public class TerminalProcess : Process private readonly List history = new(); private string currentInput = ""; private readonly List textLines = new(); + private readonly object textLinesLock = new object(); private const int LineHeight = 30; private Size lastSize = new Size(-1, -1); @@ -26,11 +27,11 @@ public class TerminalProcess : Process internal override void Run() { window = WindowManager.CreateWindow(this, "Terminal", new Point(50, 50), new Size(400, 300)); - window.AutoFlush = true; PrintLine("RemSox GUI Terminal v1.0"); PrintLine("Type 'help' for commands."); + window.Flush(); window.OnKeyEvent += HandleKey; while (!StopRequested) @@ -40,7 +41,7 @@ public class TerminalProcess : Process lastSize = window.Size; UpdateDisplay(); } - System.Threading.Thread.Sleep(50); + Thread.Sleep(50); } window.OnKeyEvent -= HandleKey; @@ -106,42 +107,47 @@ public class TerminalProcess : Process if (maxLines < 1) maxLines = 1; - // Ensure we have enough Text elements for maxLines + 1 (input line) - while (textLines.Count <= maxLines) + lock (textLinesLock) { - var textElement = window.CreateUIElement(t => + // Ensure we have enough Text elements for maxLines + 1 (input line) + while (textLines.Count <= maxLines) { - t.Color = Color.LightGreen; - t.Content = ""; - }); - textLines.Add(textElement); - } - - // Calculate starting Y to align everything flush to the bottom margin - int startY = window.Size.Height - ((maxLines + 1) * LineHeight) - 5; - if (startY < 20) startY = 20; - - for (int i = 0; i < maxLines; i++) - { - int historyIndex = history.Count - maxLines + i; - string content = ""; - if (historyIndex >= 0 && historyIndex < history.Count) - { - content = history[historyIndex]; + var textElement = window.CreateUIElement(t => + { + t.Color = Color.LightGreen; + t.Content = ""; + }); + textLines.Add(textElement); } - textLines[i].Content = content; - textLines[i].Position = new Point(5, startY + (i * LineHeight)); - } + // Calculate starting Y to align everything flush to the bottom margin + int startY = window.Size.Height - ((maxLines + 1) * LineHeight) - 5; + if (startY < 20) startY = 20; - // The last line is the input line - textLines[maxLines].Content = "> " + currentInput + "_"; - textLines[maxLines].Position = new Point(5, startY + (maxLines * LineHeight)); + for (int i = 0; i < maxLines; i++) + { + int historyIndex = history.Count - maxLines + i; + string content = ""; + if (historyIndex >= 0 && historyIndex < history.Count) + { + content = history[historyIndex]; + } - // Hide any extra text lines we don't need - for (int i = maxLines + 1; i < textLines.Count; i++) - { - textLines[i].Content = ""; + textLines[i].Content = content; + textLines[i].Position = new Point(5, startY + (i * LineHeight)); + } + + // The last line is the input line + textLines[maxLines].Content = "> " + currentInput + "_"; + textLines[maxLines].Position = new Point(5, startY + (maxLines * LineHeight)); + + // Hide any extra text lines we don't need + for (int i = maxLines + 1; i < textLines.Count; i++) + { + textLines[i].Content = ""; + } + + window.Flush(); } } } \ No newline at end of file diff --git a/UI/GUI/Rendering/CanvasRenderSource.cs b/UI/GUI/Rendering/CanvasRenderSource.cs index 2b17f8c..fef13ad 100644 --- a/UI/GUI/Rendering/CanvasRenderSource.cs +++ b/UI/GUI/Rendering/CanvasRenderSource.cs @@ -17,130 +17,137 @@ public sealed class CanvasRenderSource : IRenderSource private static Point lastPointerPosition = new Point(-1, -1); private static List orderedWindowsCache = new(); private static bool isZOrderDirty = true; + private static readonly object renderLock = new object(); public void Render(IEnumerable commands) { - bool changed = false; - foreach (RenderCommand command in commands) + lock (renderLock) { - changed = true; - if (command.ElementType == "WindowClose") + bool changed = false; + foreach (RenderCommand command in commands) { - 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) + changed = true; + if (command.ElementType == "WindowClose") { - windowZIndices[command.WindowId] = z; + windowCanvases.Remove(command.WindowId); + windowPositions.Remove(command.WindowId); + windowZIndices.Remove(command.WindowId); isZOrderDirty = true; + continue; } - } - if (command.ElementType == "Window") - { - Size size = command.Properties.TryGetValue("Size", out object? rawSize) && rawSize is Size windowSize - ? windowSize - : new Size(160, 120); - - if (!windowCanvases.TryGetValue(command.WindowId, out Canvas? currentCanvas) || - currentCanvas.Mode.Width != size.Width || - currentCanvas.Mode.Height != size.Height) + if (command.ElementType == "Window" || command.ElementType == "WindowMove") { - windowCanvases[command.WindowId] = new Canvas(size.Width, size.Height); + if (command.Properties.TryGetValue("ZIndex", out object? rawZIndex) && rawZIndex is int z) + { + windowZIndices[command.WindowId] = z; + isZOrderDirty = true; + } + } + + if (command.ElementType == "Window") + { + Size size = command.Properties.TryGetValue("Size", out object? rawSize) && rawSize is Size windowSize + ? windowSize + : new Size(160, 120); + + 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); + } + + Canvas windowCanvas = windowCanvases[command.WindowId]; + + if (command.ElementType == "Window") + { + RenderWindow(windowCanvas, command); + windowPositions[command.WindowId] = command.Position; + continue; + } + + if (command.ElementType == "WindowMove") + { + windowPositions[command.WindowId] = command.Position; + continue; + } + + if (command.ElementType == "Circle") + { + RenderCircle(windowCanvas, command); + } + + if (command.ElementType == "Rectangle") + { + RenderRectangle(windowCanvas, command); + } + + if (command.ElementType == "Text") + { + RenderText(windowCanvas, command); + } + + if (command.ElementType == "Line") + { + RenderLine(windowCanvas, command); + } + + if (command.ElementType == "Button") + { + RenderButton(windowCanvas, command); + } + + if (command.ElementType == "CheckBox") + { + RenderCheckBox(windowCanvas, command); } } - if (!windowCanvases.ContainsKey(command.WindowId)) + if (changed) { - windowCanvases[command.WindowId] = new Canvas(160, 120); + isDirty = true; } - - Canvas windowCanvas = windowCanvases[command.WindowId]; - - if (command.ElementType == "Window") - { - RenderWindow(windowCanvas, command); - windowPositions[command.WindowId] = command.Position; - continue; - } - - if (command.ElementType == "WindowMove") - { - windowPositions[command.WindowId] = command.Position; - continue; - } - - if (command.ElementType == "Circle") - { - RenderCircle(windowCanvas, command); - } - - if (command.ElementType == "Rectangle") - { - RenderRectangle(windowCanvas, command); - } - - if (command.ElementType == "Text") - { - RenderText(windowCanvas, command); - } - - if (command.ElementType == "Line") - { - RenderLine(windowCanvas, command); - } - - if (command.ElementType == "Button") - { - RenderButton(windowCanvas, command); - } - - if (command.ElementType == "CheckBox") - { - RenderCheckBox(windowCanvas, command); - } - } - - if (changed) - { - isDirty = true; } } public static void CompositeAndDisplay(Canvas screenCanvas, Point pointerPosition) { - if (!isDirty && pointerPosition == lastPointerPosition) + lock (renderLock) { - return; - } - - if (isZOrderDirty) - { - orderedWindowsCache = windowPositions.Keys.OrderBy(id => windowZIndices.TryGetValue(id, out int z) ? z : 0).ToList(); - isZOrderDirty = false; - } - - screenCanvas.Clear(Color.Black); - - foreach (var windowId in orderedWindowsCache) - { - if (windowPositions.TryGetValue(windowId, out Point position) && windowCanvases.TryGetValue(windowId, out Canvas? windowCanvas)) + if (!isDirty && pointerPosition == lastPointerPosition) { - screenCanvas.DrawCanvas(windowCanvas, position.X, position.Y); + return; } + + if (isZOrderDirty) + { + orderedWindowsCache = windowPositions.Keys.OrderBy(id => windowZIndices.TryGetValue(id, out int z) ? z : 0).ToList(); + isZOrderDirty = false; + } + + screenCanvas.Clear(Color.Black); + + foreach (var windowId in orderedWindowsCache) + { + if (windowPositions.TryGetValue(windowId, out Point position) && windowCanvases.TryGetValue(windowId, out Canvas? windowCanvas)) + { + screenCanvas.DrawCanvas(windowCanvas, position.X, position.Y); + } + } + + screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5); + screenCanvas.Display(); + + lastPointerPosition = pointerPosition; + isDirty = false; } - - screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5); - screenCanvas.Display(); - - lastPointerPosition = pointerPosition; - isDirty = false; } private static void RenderWindow(Canvas canvas, RenderCommand command) diff --git a/UI/GUI/Windows/Window.cs b/UI/GUI/Windows/Window.cs index 91c89e5..5770e6a 100644 --- a/UI/GUI/Windows/Window.cs +++ b/UI/GUI/Windows/Window.cs @@ -46,6 +46,7 @@ public sealed class Window(string title, int processId, int id, IRenderSource re public bool IsDragging => currentInteraction == InteractionMode.Drag; + private readonly object uiElementsLock = new object(); private readonly Dictionary uiElements = []; private int nextUIElementId = 1; @@ -87,7 +88,10 @@ public sealed class Window(string title, int processId, int id, IRenderSource re Flush(); } - uiElements.Add(uiElementId, uiElement); + lock (uiElementsLock) + { + uiElements.Add(uiElementId, uiElement); + } return uiElement; } @@ -105,8 +109,16 @@ public sealed class Window(string title, int processId, int id, IRenderSource re return; } + bool anyChildChanged; + List elementsCopy; + + lock (uiElementsLock) + { + anyChildChanged = uiElements.Values.Any(e => e.AnyPropertyChanged); + elementsCopy = uiElements.Values.ToList(); + } + bool windowStateChanged = isFirstRender || Size != lastRenderedSize || IsFocused != lastRenderedIsFocused || Title != lastRenderedTitle; - bool anyChildChanged = uiElements.Values.Any(e => e.AnyPropertyChanged); bool positionChanged = Position != lastRenderedPosition; bool zIndexChanged = ZIndex != lastRenderedZIndex; @@ -133,7 +145,7 @@ public sealed class Window(string title, int processId, int id, IRenderSource re } }); - foreach (UIElement element in uiElements.Values) + foreach (UIElement element in elementsCopy) { commands.Add(new RenderCommand { diff --git a/UI/GUI/Windows/WindowManager.cs b/UI/GUI/Windows/WindowManager.cs index dcf2603..7b1ef6c 100644 --- a/UI/GUI/Windows/WindowManager.cs +++ b/UI/GUI/Windows/WindowManager.cs @@ -13,8 +13,9 @@ namespace RemSox.UI.GUI.Windows; public static class WindowManager { + private static readonly object windowsLock = new object(); // Process ID to list of windows - private static readonly ConcurrentDictionary> windows = new(); + private static readonly Dictionary> windows = new(); private static int nextWindowId = 1; private static int nextZIndex = 1; @@ -76,21 +77,27 @@ public static class WindowManager ZIndex = nextZIndex++ }; - if (!windows.ContainsKey(process.Id)) + lock (windowsLock) { - windows[process.Id] = []; - } + if (!windows.ContainsKey(process.Id)) + { + windows[process.Id] = []; + } - windows[process.Id].Add(window); + windows[process.Id].Add(window); + } return window; } public static void CloseWindow(Window window) { - if (windows.TryGetValue(window.ProcessId, out var processWindows)) + lock (windowsLock) { - processWindows.Remove(window); + if (windows.TryGetValue(window.ProcessId, out var processWindows)) + { + processWindows.Remove(window); + } } renderSource.Render(new[] { new RenderCommand { WindowId = window.Id, ElementId = window.Id, ElementType = "WindowClose", Position = window.Position, Properties = new Dictionary() } }); @@ -98,17 +105,23 @@ public static class WindowManager public static List GetWindowsForProcess(Process process) { - if (windows.TryGetValue(process.Id, out var processWindows)) + lock (windowsLock) { - return processWindows; - } + if (windows.TryGetValue(process.Id, out var processWindows)) + { + return processWindows.ToList(); + } - return []; + return []; + } } public static void CloseWindowsForProcess(Process process) { - windows.TryRemove(process.Id, out _); + lock (windowsLock) + { + windows.Remove(process.Id); + } } public static void FocusWindow(Window? window) @@ -137,7 +150,12 @@ public static class WindowManager public static Window? TryBeginInteract(Point pointerPosition) { - var allWindows = windows.Values.SelectMany(w => w).OrderByDescending(w => w.ZIndex); + List allWindows; + lock (windowsLock) + { + allWindows = windows.Values.SelectMany(w => w).OrderByDescending(w => w.ZIndex).ToList(); + } + foreach (var window in allWindows) { if (window.TryBeginInteract(pointerPosition)) @@ -150,12 +168,15 @@ public static class WindowManager public static void InvalidateAll() { - foreach (var processWindows in windows.Values) + List allWindows; + lock (windowsLock) { - foreach (var window in processWindows) - { - window.Invalidate(); - } + allWindows = windows.Values.SelectMany(w => w).ToList(); + } + + foreach (var window in allWindows) + { + window.Invalidate(); } } @@ -166,13 +187,33 @@ public static class WindowManager sealed private class MuliRenderSource(List sources) : IRenderSource { - public void AddSource(IRenderSource source) => sources.Add(source); + private readonly object sourcesLock = new object(); - public void RemoveSource(IRenderSource source) => sources.Remove(source); + public void AddSource(IRenderSource source) + { + lock (sourcesLock) + { + sources.Add(source); + } + } + + public void RemoveSource(IRenderSource source) + { + lock (sourcesLock) + { + sources.Remove(source); + } + } public void Render(IEnumerable commands) { - foreach (IRenderSource source in sources) + List sourcesCopy; + lock (sourcesLock) + { + sourcesCopy = sources.ToList(); + } + + foreach (IRenderSource source in sourcesCopy) { source.Render(commands); }