From 930a0f6a5d4156e488013a3e68129ba5d883383b Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:03:44 +0200 Subject: [PATCH] Implement full canvas compositing and redraw only on state change --- Kernel.cs | 17 +++--- UI/GUI/Rendering/CanvasRenderSource.cs | 75 ++++++++++++++++++++------ UI/GUI/Rendering/RenderCommand.cs | 1 + UI/GUI/Windows/Window.cs | 57 ++++++++++++++++---- Utils/ChangedPropertiesTracker.cs | 32 ++++++++--- 5 files changed, 139 insertions(+), 43 deletions(-) diff --git a/Kernel.cs b/Kernel.cs index a1165fe..4c69634 100644 --- a/Kernel.cs +++ b/Kernel.cs @@ -85,17 +85,6 @@ public class Kernel : Sys.Kernel Point pointerPosition = new(Sys.Mouse.MouseManager.X, Sys.Mouse.MouseManager.Y); bool leftButtonDown = Sys.Mouse.MouseManager.LeftButton; - // Draw Cursor - - if (pointerPosition.X != lastPointerPosition.X || pointerPosition.Y != lastPointerPosition.Y) - { - Canvas canvas = FullScreenCanvas.GetFullScreenCanvas(); - canvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5); - canvas.Display(); - } - - lastPointerPosition = pointerPosition; - if (leftButtonDown && !wasLeftButtonDown) { activeDragWindow = TryBeginDrag(pointerPosition); @@ -111,6 +100,12 @@ public class Kernel : Sys.Kernel } wasLeftButtonDown = leftButtonDown; + + // Render all canvases and cursor every tick, or if changed + Canvas canvas = FullScreenCanvas.GetFullScreenCanvas(); + CanvasRenderSource.CompositeAndDisplay(canvas, pointerPosition); + + lastPointerPosition = pointerPosition; } private static Window? TryBeginDrag(Point pointerPosition) diff --git a/UI/GUI/Rendering/CanvasRenderSource.cs b/UI/GUI/Rendering/CanvasRenderSource.cs index 383b99a..d28c12b 100644 --- a/UI/GUI/Rendering/CanvasRenderSource.cs +++ b/UI/GUI/Rendering/CanvasRenderSource.cs @@ -1,57 +1,102 @@ using System; using System.Drawing; +using System.Collections.Generic; using Cosmos.Kernel.System.Graphics; namespace RemSox.UI.GUI.Rendering; public sealed class CanvasRenderSource : IRenderSource { + private static readonly Dictionary windowCanvases = new(); + private static readonly Dictionary windowPositions = new(); + public void Render(IEnumerable commands) { - Canvas canvas = FullScreenCanvas.GetFullScreenCanvas(); - foreach (RenderCommand command in commands) { if (command.ElementType == "Window") { - RenderWindow(canvas, command); + 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(canvas, command); + RenderCircle(windowCanvas, command); } } } + public static void CompositeAndDisplay(Canvas screenCanvas, Point pointerPosition) + { + screenCanvas.Clear(Color.Black); + + foreach (var kvp in windowPositions) + { + int windowId = kvp.Key; + Point position = kvp.Value; + if (windowCanvases.TryGetValue(windowId, out Canvas? windowCanvas)) + { + screenCanvas.DrawCanvas(windowCanvas, position.X, position.Y); + } + } + + screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5); + screenCanvas.Display(); + } + private static void RenderWindow(Canvas canvas, RenderCommand command) { - int x = command.Position.X; - int y = command.Position.Y; - - Size size = command.Properties.TryGetValue(nameof(Size), out object? rawSize) && rawSize is Size windowSize + Size size = command.Properties.TryGetValue("Size", out object? rawSize) && rawSize is Size windowSize ? windowSize : new Size(160, 120); - bool isFocused = command.Properties.TryGetValue(nameof(Windows.Window.IsFocused), out object? rawFocused) && rawFocused is bool focused && focused; + bool isFocused = command.Properties.TryGetValue("IsFocused", out object? rawFocused) && rawFocused is bool focused && focused; Color borderColor = isFocused ? Color.White : Color.DarkGray; Color bodyColor = Color.FromArgb(32, 32, 32); Color titleColor = isFocused ? Color.FromArgb(0, 120, 215) : Color.FromArgb(80, 80, 80); - canvas.DrawFilledRectangle(bodyColor, x, y, size.Width, size.Height); - canvas.DrawFilledRectangle(titleColor, x, y, size.Width, 18); - canvas.DrawRectangle(borderColor, x, y, size.Width, size.Height); + canvas.DrawFilledRectangle(bodyColor, 0, 0, size.Width, size.Height); + canvas.DrawFilledRectangle(titleColor, 0, 0, size.Width, 18); + canvas.DrawRectangle(borderColor, 0, 0, size.Width, size.Height); } private static void RenderCircle(Canvas canvas, RenderCommand command) { - Color color = command.Properties.TryGetValue(nameof(Color), out object? rawColor) && rawColor is Color circleColor + Color color = command.Properties.TryGetValue("Color", out object? rawColor) && rawColor is Color circleColor ? circleColor : Color.White; - int radius = command.Properties.TryGetValue(nameof(Radius), out object? rawRadius) && rawRadius is int circleRadius + int radius = command.Properties.TryGetValue("Radius", out object? rawRadius) && rawRadius is int circleRadius ? circleRadius : 10; @@ -60,6 +105,4 @@ public sealed class CanvasRenderSource : IRenderSource canvas.DrawFilledCircle(color, centerX, centerY, radius); } - - private static string Radius => nameof(Radius); } \ No newline at end of file diff --git a/UI/GUI/Rendering/RenderCommand.cs b/UI/GUI/Rendering/RenderCommand.cs index 68b3278..f19e495 100644 --- a/UI/GUI/Rendering/RenderCommand.cs +++ b/UI/GUI/Rendering/RenderCommand.cs @@ -5,6 +5,7 @@ namespace RemSox.UI.GUI.Rendering; public class RenderCommand { + public required int WindowId { get; set; } public required int ElementId { get; set; } public required string ElementType { get; set; } public required Point Position { get; set; } diff --git a/UI/GUI/Windows/Window.cs b/UI/GUI/Windows/Window.cs index d4bef11..1d5d87a 100644 --- a/UI/GUI/Windows/Window.cs +++ b/UI/GUI/Windows/Window.cs @@ -45,6 +45,12 @@ public sealed class Window(string title, int processId, int id, IRenderSource re private Point dragOffset; + private Point lastRenderedPosition = new Point(-1, -1); + private Size lastRenderedSize = new Size(-1, -1); + private bool lastRenderedIsFocused = false; + private string lastRenderedTitle = string.Empty; + private bool isFirstRender = true; + public T CreateUIElement(Action? options = null) where T : UIElement, new() { int uiElementId = GetNextUIElementId(); @@ -76,12 +82,24 @@ public sealed class Window(string title, int processId, int id, IRenderSource re public void Flush() { + if (!IsVisible) + { + return; + } + + bool windowStateChanged = isFirstRender || Size != lastRenderedSize || IsFocused != lastRenderedIsFocused || Title != lastRenderedTitle; + bool anyChildChanged = uiElements.Values.Any(e => e.AnyPropertyChanged); + bool positionChanged = Position != lastRenderedPosition; + + bool fullRedraw = windowStateChanged || anyChildChanged; + List commands = []; - if (IsVisible) + if (fullRedraw) { commands.Add(new RenderCommand { + WindowId = Id, ElementId = Id, ElementType = "Window", Position = Position, @@ -94,29 +112,40 @@ public sealed class Window(string title, int processId, int id, IRenderSource re [nameof(IsDraggable)] = IsDraggable } }); - } - foreach (UIElement element in uiElements.Values) - { - if (element.AnyPropertyChanged) + foreach (UIElement element in uiElements.Values) { - IReadOnlyDictionary changes = element.ChangedProperties; - commands.Add(new RenderCommand { + WindowId = Id, ElementId = element.Id, ElementType = element.Type, Position = element.Position, - Properties = changes + Properties = element.AllProperties }); - element.ClearChangedProperties(); } } + else if (positionChanged) + { + commands.Add(new RenderCommand + { + WindowId = Id, + ElementId = Id, + ElementType = "WindowMove", + Position = Position, + Properties = new Dictionary() + }); + } if (commands.Count > 0) { renderSource.Render(commands); + lastRenderedPosition = Position; + lastRenderedSize = Size; + lastRenderedIsFocused = IsFocused; + lastRenderedTitle = Title; + isFirstRender = false; } } @@ -142,7 +171,15 @@ public sealed class Window(string title, int processId, int id, IRenderSource re return; } - Position = new Point(pointerPosition.X - dragOffset.X, pointerPosition.Y - dragOffset.Y); + int newX = pointerPosition.X - dragOffset.X; + int newY = pointerPosition.Y - dragOffset.Y; + + // Cosmos DrawCanvas fails to render if coordinates are negative. + // Clamp to 0,0 to prevent the window from disappearing. + newX = Math.Max(0, newX); + newY = Math.Max(0, newY); + + Position = new Point(newX, newY); Flush(); } diff --git a/Utils/ChangedPropertiesTracker.cs b/Utils/ChangedPropertiesTracker.cs index 326ad1b..4941af1 100644 --- a/Utils/ChangedPropertiesTracker.cs +++ b/Utils/ChangedPropertiesTracker.cs @@ -1,29 +1,49 @@ +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; namespace RemSox.Utils; public abstract class ChangedPropertiesTracker : INotifyPropertyChanged { - private readonly Dictionary changedProperties = []; + private readonly Dictionary properties = []; + private readonly HashSet changedPropertyNames = []; - public IReadOnlyDictionary ChangedProperties => changedProperties; + public IReadOnlyDictionary ChangedProperties + { + get + { + var changes = new Dictionary(); + foreach (var name in changedPropertyNames) + { + if (properties.TryGetValue(name, out var value)) + { + changes[name] = value; + } + } + return changes; + } + } + + public IReadOnlyDictionary AllProperties => properties; public event PropertyChangedEventHandler? PropertyChanged; - public bool AnyPropertyChanged => changedProperties.Count > 0; + public bool AnyPropertyChanged => changedPropertyNames.Count > 0; protected void SetProperty(string name, ref T field, T value) { if (!EqualityComparer.Default.Equals(field, value)) { field = value; - changedProperties[name] = value; + properties[name] = value; + changedPropertyNames.Add(name); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } - public void ClearChangedProperties() => changedProperties.Clear(); + public void ClearChangedProperties() => changedPropertyNames.Clear(); - public bool IsPropertyChanged(string name) => changedProperties.ContainsKey(name); + public bool IsPropertyChanged(string name) => changedPropertyNames.Contains(name); }