From e90e9ab163b04ba36d122b4c50552163e5d90535 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:11:01 +0200 Subject: [PATCH] Replace string element render types with RenderCommandType enum, decompose UI elements (Button, CheckBox, chrome) into drawing primitives via ToPrimitives(), emit per-element commands on flush, and move composite into IRenderSource. --- Networking/TcpRpcServer.cs | 18 ++ UI/GUI/Rendering/CanvasRenderSource.cs | 327 +++++++++++++----------- UI/GUI/Rendering/IRenderSource.cs | 1 + UI/GUI/Rendering/NetworkRenderSource.cs | 21 ++ UI/GUI/Rendering/RenderCommand.cs | 230 ++++++++++++++++- UI/GUI/Rendering/RenderCommandType.cs | 18 ++ UI/GUI/UIEelements/Controls/Button.cs | 54 ++++ UI/GUI/UIEelements/Controls/CheckBox.cs | 70 +++++ UI/GUI/UIEelements/Shapes/Circle.cs | 26 ++ UI/GUI/UIEelements/Shapes/Line.cs | 18 ++ UI/GUI/UIEelements/Shapes/Pixel.cs | 23 ++ UI/GUI/UIEelements/Shapes/Rectangle.cs | 18 ++ UI/GUI/UIEelements/Shapes/Text.cs | 19 ++ UI/GUI/UIEelements/UIEelement.cs | 9 + UI/GUI/Windows/Window.cs | 165 +++++++++--- UI/GUI/Windows/WindowManager.cs | 20 +- 16 files changed, 850 insertions(+), 187 deletions(-) create mode 100644 UI/GUI/Rendering/NetworkRenderSource.cs create mode 100644 UI/GUI/Rendering/RenderCommandType.cs create mode 100644 UI/GUI/UIEelements/Shapes/Pixel.cs diff --git a/Networking/TcpRpcServer.cs b/Networking/TcpRpcServer.cs index ce2542e..6e25b23 100644 --- a/Networking/TcpRpcServer.cs +++ b/Networking/TcpRpcServer.cs @@ -87,6 +87,24 @@ public class TcpRpcServer(IPacketCrypto? crypto = null) : TcpRpcBase(crypto) return Task.WhenAll(sendTasks); } + /// Broadcasts raw binary payload to all connected clients. + public Task SendRawToAll(string type, byte[] payload) + { + List sendTasks = []; + + foreach (TcpConnection conn in connections.Keys) + { + sendTasks.Add(SendRaw(conn, new TcpMessage + { + Type = type, + RequestId = Guid.NewGuid().ToString(), + Payload = payload + })); + } + + return Task.WhenAll(sendTasks); + } + protected override void OnConnectionClosed(TcpConnection conn) { _ = connections.TryRemove(conn, out _); diff --git a/UI/GUI/Rendering/CanvasRenderSource.cs b/UI/GUI/Rendering/CanvasRenderSource.cs index a50a7c8..5088a18 100644 --- a/UI/GUI/Rendering/CanvasRenderSource.cs +++ b/UI/GUI/Rendering/CanvasRenderSource.cs @@ -1,6 +1,8 @@ using Cosmos.Kernel.System.Graphics; using Cosmos.Kernel.System.Graphics.Fonts; +using Cosmos.Kernel.System.Mouse; +using RemSox.UI.GUI.UIEelements; using RemSox.Utils; using System.Drawing; @@ -14,77 +16,97 @@ public sealed class CanvasRenderSource : IRenderSource private static readonly Dictionary windowZIndices = []; // Sorted list keeps windows in Z-order without re-sorting. - // Key = (zIndex << 16 | windowId) so equal Z stays insertion-stable. + // Key = (zIndex << 32 | windowId) so equal Z stays insertion-stable. private static readonly SortedList zOrderedWindows = []; - private static bool isContentDirty = true; // pixel content changed - private static bool isPositionDirty = true; // only layout changed + // Accumulated drawing primitives per window (in draw order). + private static readonly Dictionary> windowPrimitives = []; + + private static readonly HashSet dirtyWindows = []; + private static bool isPositionDirty = true; private static Point lastPointerPosition = new(-1, -1); private static readonly Lock renderLock = new(); - private static readonly Dictionary> elementRenderers = new() - { - ["Circle"] = RenderCircle, - ["Rectangle"] = RenderRectangle, - ["Text"] = RenderText, - ["Line"] = RenderLine, - ["Button"] = RenderButton, - ["CheckBox"] = RenderCheckBox, - }; - public void Render(IEnumerable commands) { lock (renderLock) { foreach (RenderCommand command in commands) { - switch (command.ElementType) + switch (command.Type) { - case "WindowClose": - RemoveWindow(command.WindowId); - continue; + case RenderCommandType.CreateWindow: + CreateOrUpdateWindow(command); + break; - case "WindowMove": + case RenderCommandType.DestroyWindow: + RemoveWindow(command.WindowId); + break; + + case RenderCommandType.MoveWindow: if (windowCanvases.ContainsKey(command.WindowId)) { windowPositions[command.WindowId] = command.Position; isPositionDirty = true; } - continue; + break; - case "Window": - ProcessWindowCommand(command); - isContentDirty = true; - continue; - } + case RenderCommandType.RemovePrimitives: + RemovePrimitives(command.WindowId, command.ElementId); + break; - if (!windowCanvases.TryGetValue(command.WindowId, out Canvas? canvas)) - { - continue; - } - - if (elementRenderers.TryGetValue(command.ElementType, out Action? renderer)) - { - renderer(canvas, command); - isContentDirty = true; + default: + if (windowCanvases.ContainsKey(command.WindowId)) + { + UpsertPrimitive(command); + } + break; } } } } - public static void CompositeAndDisplay(Canvas screenCanvas, Point pointerPosition) + public void Composite() { + Canvas screenCanvas = FullScreenCanvas.GetFullScreenCanvas(); + Point pointerPosition = new(MouseManager.X, MouseManager.Y); + lock (renderLock) { bool pointerMoved = pointerPosition != lastPointerPosition; - if (!isContentDirty && !isPositionDirty && !pointerMoved) + bool hasDirty = dirtyWindows.Count > 0; + + if (!hasDirty && !isPositionDirty && !pointerMoved) { return; } - screenCanvas.Clear(Color.Black); + // Redraw dirty windows from accumulated primitives + if (hasDirty) + { + foreach (int winId in dirtyWindows) + { + if (!windowCanvases.TryGetValue(winId, out Canvas? canvas)) + { + continue; + } + canvas.Clear(Color.Black); + + if (windowPrimitives.TryGetValue(winId, out var primitives)) + { + foreach (var (_, cmd) in primitives) + { + DrawPrimitive(canvas, cmd); + } + } + } + dirtyWindows.Clear(); + } + + // Composite all windows to screen + screenCanvas.Clear(Color.Black); foreach (int windowId in zOrderedWindows.Values) { if (windowPositions.TryGetValue(windowId, out Point pos) && @@ -98,17 +120,48 @@ public sealed class CanvasRenderSource : IRenderSource screenCanvas.Display(); lastPointerPosition = pointerPosition; - isContentDirty = false; isPositionDirty = false; } } - // Helpers + // --- Accumulated state management --- + + private static void CreateOrUpdateWindow(RenderCommand cmd) + { + int id = cmd.WindowId; + Size size = Get(cmd.Properties, "Size", new Size(160, 120)); + int zIndex = Get(cmd.Properties, "ZIndex", 0); + + if (!windowCanvases.TryGetValue(id, out Canvas? existing) || + existing.Mode.Width != size.Width || + existing.Mode.Height != size.Height) + { + windowCanvases[id] = new Canvas(size.Width, size.Height); + } + + windowPositions[id] = cmd.Position; + + if (windowZIndices.TryGetValue(id, out int oldZ)) + { + _ = zOrderedWindows.Remove(ZKey(oldZ, id)); + } + windowZIndices[id] = zIndex; + zOrderedWindows[ZKey(zIndex, id)] = id; + + if (!windowPrimitives.ContainsKey(id)) + { + windowPrimitives[id] = []; + } + + _ = dirtyWindows.Add(id); + isPositionDirty = true; + } private static void RemoveWindow(int windowId) { _ = windowCanvases.Remove(windowId); _ = windowPositions.Remove(windowId); + _ = windowPrimitives.Remove(windowId); if (windowZIndices.TryGetValue(windowId, out int z)) { @@ -119,38 +172,69 @@ public sealed class CanvasRenderSource : IRenderSource isPositionDirty = true; } - private static void ProcessWindowCommand(RenderCommand command) + private static void UpsertPrimitive(RenderCommand cmd) { - int id = command.WindowId; - - if (command.Properties.TryGetValue("ZIndex", out object? rawZ) && rawZ is int newZ) + if (!windowPrimitives.TryGetValue(cmd.WindowId, out var list)) { - if (windowZIndices.TryGetValue(id, out int oldZ)) - { - _ = zOrderedWindows.Remove(ZKey(oldZ, id)); - } - - windowZIndices[id] = newZ; - zOrderedWindows[ZKey(newZ, id)] = id; - } - else if (!windowZIndices.ContainsKey(id)) - { - windowZIndices[id] = 0; - zOrderedWindows[ZKey(0, id)] = id; + list = []; + windowPrimitives[cmd.WindowId] = list; } - Size size = Get(command.Properties, "Size", new Size(160, 120)); - - if (!windowCanvases.TryGetValue(id, out Canvas? current) || - current.Mode.Width != size.Width || - current.Mode.Height != size.Height) + int idx = list.FindIndex(p => p.ElementId == cmd.ElementId); + if (idx >= 0) { - windowCanvases[id] = new Canvas(size.Width, size.Height); + list[idx] = (cmd.ElementId, cmd); + } + else + { + list.Add((cmd.ElementId, cmd)); } - windowPositions[id] = command.Position; - isPositionDirty = true; - RenderWindow(windowCanvases[id], command); + _ = dirtyWindows.Add(cmd.WindowId); + } + + private static void RemovePrimitives(int windowId, int baseElementId) + { + if (!windowPrimitives.TryGetValue(windowId, out var list)) + { + return; + } + + list.RemoveAll(p => p.ElementId >= 0 + ? (p.ElementId >> UIElement.PrimitiveIdShift) == baseElementId + : p.ElementId == baseElementId); + + _ = dirtyWindows.Add(windowId); + } + + // --- Primitive drawing --- + + private static void DrawPrimitive(Canvas canvas, RenderCommand cmd) + { + switch (cmd.Type) + { + case RenderCommandType.DrawFilledRect: + DrawFilledRect(canvas, cmd); + break; + case RenderCommandType.DrawRectBorder: + DrawRectBorder(canvas, cmd); + break; + case RenderCommandType.DrawFilledCircle: + DrawFilledCircle(canvas, cmd); + break; + case RenderCommandType.DrawCircle: + DrawCircle(canvas, cmd); + break; + case RenderCommandType.DrawPoint: + DrawPoint(canvas, cmd); + break; + case RenderCommandType.DrawText: + DrawText(canvas, cmd); + break; + case RenderCommandType.DrawLine: + DrawLine(canvas, cmd); + break; + } } // Builds a stable sort key from Z-index and window ID. @@ -159,110 +243,61 @@ public sealed class CanvasRenderSource : IRenderSource return ((long)z << 32) | (uint)id; } - // Inline generic property getter — eliminates repeated TryGetValue + pattern-match boilerplate. private static T Get(IReadOnlyDictionary props, string key, T fallback) { return props.TryGetValue(key, out object? raw) && raw is T value ? value : fallback; } - private static void RenderWindow(Canvas canvas, RenderCommand command) + private static void DrawFilledRect(Canvas canvas, RenderCommand cmd) { - Size size = Get(command.Properties, "Size", new Size(160, 120)); - bool focused = Get(command.Properties, "IsFocused", false); - string titleText = Get(command.Properties, "Title", string.Empty); - - Color border = focused ? Color.White : Color.DarkGray; - Color title = focused ? Color.FromArgb(0, 120, 215) : Color.FromArgb(80, 80, 80); - - canvas.DrawFilledRectangle(Color.FromArgb(32, 32, 32), 0, 0, size.Width, size.Height); - canvas.DrawFilledRectangle(title, 0, 0, size.Width, 18); - canvas.DrawStringHeight(titleText, PCScreenFont.DefaultFont, Color.White, 4, 2, 18); - canvas.DrawRectangle(border, 0, 0, size.Width, size.Height - 1); + Color color = Get(cmd.Properties, "Color", Color.White); + Size size = Get(cmd.Properties, "Size", new Size(10, 10)); + canvas.DrawFilledRectangle(color, cmd.Position.X, cmd.Position.Y, size.Width, size.Height); } - private static void RenderCircle(Canvas canvas, RenderCommand command) + private static void DrawRectBorder(Canvas canvas, RenderCommand cmd) { - Color color = Get(command.Properties, "Color", Color.White); - int radius = Get(command.Properties, "Radius", 10); - - canvas.DrawFilledCircle(color, command.Position.X + radius, command.Position.Y + radius, radius); + Color color = Get(cmd.Properties, "Color", Color.White); + Size size = Get(cmd.Properties, "Size", new Size(10, 10)); + canvas.DrawRectangle(color, cmd.Position.X, cmd.Position.Y, size.Width, size.Height); } - private static void RenderRectangle(Canvas canvas, RenderCommand command) + private static void DrawFilledCircle(Canvas canvas, RenderCommand cmd) { - Color color = Get(command.Properties, "Color", Color.White); - Size size = Get(command.Properties, "Size", new Size(10, 10)); - bool isFilled = Get(command.Properties, "IsFilled", false); - - 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); - } + Color color = Get(cmd.Properties, "Color", Color.White); + int radius = Get(cmd.Properties, "Radius", 10); + canvas.DrawFilledCircle(color, cmd.Position.X + radius, cmd.Position.Y + radius, radius); } - private static void RenderText(Canvas canvas, RenderCommand command) + private static void DrawCircle(Canvas canvas, RenderCommand cmd) { - Color color = Get(command.Properties, "Color", Color.White); - string content = Get(command.Properties, "Content", string.Empty); - int fontSize = Get(command.Properties, "FontSize", 12); + Color color = Get(cmd.Properties, "Color", Color.White); + int radius = Get(cmd.Properties, "Radius", 10); + canvas.DrawCircle(color, cmd.Position.X + radius, cmd.Position.Y + radius, radius); + } + + private static void DrawPoint(Canvas canvas, RenderCommand cmd) + { + Color color = Get(cmd.Properties, "Color", Color.White); + canvas.DrawPoint(color, cmd.Position.X, cmd.Position.Y); + } + + private static void DrawText(Canvas canvas, RenderCommand cmd) + { + Color color = Get(cmd.Properties, "Color", Color.White); + string content = Get(cmd.Properties, "Content", string.Empty); + int fontSize = Get(cmd.Properties, "FontSize", 12); if (!string.IsNullOrEmpty(content)) { - canvas.DrawStringHeight(content, PCScreenFont.DefaultFont, color, command.Position.X, command.Position.Y, fontSize); + canvas.DrawStringHeight(content, PCScreenFont.DefaultFont, color, cmd.Position.X, cmd.Position.Y, fontSize); } } - private static void RenderLine(Canvas canvas, RenderCommand command) + private static void DrawLine(Canvas canvas, RenderCommand cmd) { - Color color = Get(command.Properties, "Color", Color.White); - Point end = Get(command.Properties, "EndPosition", command.Position); - - canvas.DrawLine(color, command.Position.X, command.Position.Y, end.X, end.Y); - } - - private static void RenderButton(Canvas canvas, RenderCommand command) - { - Color bg = Get(command.Properties, "BackgroundColor", Color.LightGray); - Color fg = Get(command.Properties, "TextColor", Color.Black); - Size size = Get(command.Properties, "Size", new Size(60, 20)); - string text = Get(command.Properties, "Text", string.Empty); - - canvas.DrawFilledRectangle(bg, command.Position.X, command.Position.Y, size.Width, size.Height); - canvas.DrawRectangle(Color.DarkGray, command.Position.X, command.Position.Y, size.Width, size.Height); - - if (!string.IsNullOrEmpty(text)) - { - int tx = command.Position.X + (size.Width / 2) - (text.Length * 4); - int ty = command.Position.Y + (size.Height / 2) - 8; - canvas.DrawStringHeight(text, PCScreenFont.DefaultFont, fg, tx, ty, size.Height - 8, size.Width); - } - } - - private static void RenderCheckBox(Canvas canvas, RenderCommand command) - { - Color bg = Get(command.Properties, "BackgroundColor", Color.LightGray); - Color fg = Get(command.Properties, "TextColor", Color.White); - bool isChecked = Get(command.Properties, "IsChecked", false); - string text = Get(command.Properties, "Text", string.Empty); - Size size = Get(command.Properties, "Size", new Size(12, 12)); - - int boxSize = size.Height; - - canvas.DrawFilledRectangle(bg, command.Position.X, command.Position.Y, boxSize, boxSize); - canvas.DrawRectangle(Color.DarkGray, command.Position.X, command.Position.Y, boxSize, boxSize); - - if (isChecked) - { - canvas.DrawFilledRectangle(Color.Black, command.Position.X + 3, command.Position.Y + 3, boxSize - 6, boxSize - 6); - } - - if (!string.IsNullOrEmpty(text)) - { - canvas.DrawStringHeight(text, PCScreenFont.DefaultFont, fg, command.Position.X + boxSize + 5, command.Position.Y - 2, boxSize, size.Width); - } + Color color = Get(cmd.Properties, "Color", Color.White); + Point end = Get(cmd.Properties, "EndPosition", cmd.Position); + canvas.DrawLine(color, cmd.Position.X, cmd.Position.Y, end.X, end.Y); } } \ No newline at end of file diff --git a/UI/GUI/Rendering/IRenderSource.cs b/UI/GUI/Rendering/IRenderSource.cs index 4165c05..68eb6e3 100644 --- a/UI/GUI/Rendering/IRenderSource.cs +++ b/UI/GUI/Rendering/IRenderSource.cs @@ -3,4 +3,5 @@ namespace RemSox.UI.GUI.Rendering; public interface IRenderSource { void Render(IEnumerable commands); + void Composite(); } \ No newline at end of file diff --git a/UI/GUI/Rendering/NetworkRenderSource.cs b/UI/GUI/Rendering/NetworkRenderSource.cs new file mode 100644 index 0000000..663bf42 --- /dev/null +++ b/UI/GUI/Rendering/NetworkRenderSource.cs @@ -0,0 +1,21 @@ +using RemSox.Networking; + +namespace RemSox.UI.GUI.Rendering; + +public sealed class NetworkRenderSource(TcpRpcServer server) : IRenderSource +{ + private const string MessageType = "RenderCmd"; + + public void Render(IEnumerable commands) + { + foreach (RenderCommand cmd in commands) + { + byte[] data = cmd.ToBytes(); + _ = server.SendRawToAll(MessageType, data); + } + } + + public void Composite() + { + } +} diff --git a/UI/GUI/Rendering/RenderCommand.cs b/UI/GUI/Rendering/RenderCommand.cs index ff9f5c3..d0aabd3 100644 --- a/UI/GUI/Rendering/RenderCommand.cs +++ b/UI/GUI/Rendering/RenderCommand.cs @@ -1,4 +1,5 @@ using System.Drawing; +using System.Text; namespace RemSox.UI.GUI.Rendering; @@ -6,7 +7,230 @@ 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; } - public required IReadOnlyDictionary Properties { get; set; } + public required RenderCommandType Type { get; set; } + public Point Position { get; set; } + public Dictionary Properties { get; set; } = []; + + public byte[] ToBytes() + { + using MemoryStream ms = new(); + Write(ms); + return ms.ToArray(); + } + + public void Write(Stream s) + { + s.WriteByte((byte)Type); + WriteVarint(s, WindowId); + WriteVarint(s, ElementId); + + switch (Type) + { + case RenderCommandType.CreateWindow: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + Size size = GetProp("Size", new Size(160, 120)); + WriteUInt16(s, size.Width); + WriteUInt16(s, size.Height); + WriteVarint(s, GetProp("ZIndex", 0)); + break; + + case RenderCommandType.DestroyWindow: + break; + + case RenderCommandType.MoveWindow: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + break; + + case RenderCommandType.DrawFilledRect: + case RenderCommandType.DrawRectBorder: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + Size rs = GetProp("Size", new Size(10, 10)); + WriteUInt16(s, rs.Width); + WriteUInt16(s, rs.Height); + WriteColor(s, GetProp("Color", Color.White)); + break; + + case RenderCommandType.DrawFilledCircle: + case RenderCommandType.DrawCircle: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + WriteUInt16(s, GetProp("Radius", 10)); + WriteColor(s, GetProp("Color", Color.White)); + break; + + case RenderCommandType.DrawPoint: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + WriteColor(s, GetProp("Color", Color.White)); + break; + + case RenderCommandType.DrawText: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + WriteColor(s, GetProp("Color", Color.White)); + s.WriteByte((byte)GetProp("FontSize", 12)); + string text = GetProp("Content", string.Empty); + byte[] utf8 = Encoding.UTF8.GetBytes(text); + WriteVarint(s, utf8.Length); + s.Write(utf8, 0, utf8.Length); + break; + + case RenderCommandType.DrawLine: + WriteInt16(s, Position.X); + WriteInt16(s, Position.Y); + Point end = GetProp("EndPosition", Position); + WriteInt16(s, end.X); + WriteInt16(s, end.Y); + WriteColor(s, GetProp("Color", Color.White)); + break; + + case RenderCommandType.RemovePrimitives: + break; + } + } + + public static RenderCommand FromBytes(byte[] data) + { + int offset = 0; + RenderCommandType type = (RenderCommandType)data[offset++]; + int windowId = ReadVarint(data, ref offset); + int elementId = ReadVarint(data, ref offset); + Point pos = default; + Dictionary props = []; + + switch (type) + { + case RenderCommandType.CreateWindow: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["Size"] = new Size(ReadUInt16(data, ref offset), ReadUInt16(data, ref offset)); + props["ZIndex"] = ReadVarint(data, ref offset); + break; + + case RenderCommandType.DestroyWindow: + break; + + case RenderCommandType.MoveWindow: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + break; + + case RenderCommandType.DrawFilledRect: + case RenderCommandType.DrawRectBorder: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["Size"] = new Size(ReadUInt16(data, ref offset), ReadUInt16(data, ref offset)); + props["Color"] = ReadColor(data, ref offset); + break; + + case RenderCommandType.DrawFilledCircle: + case RenderCommandType.DrawCircle: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["Radius"] = ReadUInt16(data, ref offset); + props["Color"] = ReadColor(data, ref offset); + break; + + case RenderCommandType.DrawPoint: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["Color"] = ReadColor(data, ref offset); + break; + + case RenderCommandType.DrawText: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["Color"] = ReadColor(data, ref offset); + props["FontSize"] = data[offset++]; + int textLen = ReadVarint(data, ref offset); + props["Content"] = Encoding.UTF8.GetString(data, offset, textLen); + offset += textLen; + break; + + case RenderCommandType.DrawLine: + pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["EndPosition"] = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset)); + props["Color"] = ReadColor(data, ref offset); + break; + + case RenderCommandType.RemovePrimitives: + break; + } + + return new RenderCommand + { + Type = type, + WindowId = windowId, + ElementId = elementId, + Position = pos, + Properties = props, + }; + } + + // --- Serialization helpers --- + + private T GetProp(string key, T fallback) => + Properties.TryGetValue(key, out object? raw) && raw is T val ? val : fallback; + + private static void WriteVarint(Stream s, int value) + { + uint v = (uint)value; + while (v >= 0x80) + { + s.WriteByte((byte)(v | 0x80)); + v >>= 7; + } + s.WriteByte((byte)v); + } + + private static void WriteInt16(Stream s, int value) + { + s.WriteByte((byte)(value & 0xFF)); + s.WriteByte((byte)((value >> 8) & 0xFF)); + } + + private static void WriteUInt16(Stream s, int value) + { + s.WriteByte((byte)(value & 0xFF)); + s.WriteByte((byte)((value >> 8) & 0xFF)); + } + + private static void WriteColor(Stream s, Color c) + { + s.WriteByte(c.R); + s.WriteByte(c.G); + s.WriteByte(c.B); + } + + private static int ReadVarint(byte[] data, ref int offset) + { + uint result = 0; + int shift = 0; + while (true) + { + byte b = data[offset++]; + result |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) return (int)result; + shift += 7; + } + } + + private static int ReadInt16(byte[] data, ref int offset) + { + int lo = data[offset++]; + int hi = data[offset++]; + return (short)(lo | (hi << 8)); + } + + private static int ReadUInt16(byte[] data, ref int offset) + { + int lo = data[offset++]; + int hi = data[offset++]; + return lo | (hi << 8); + } + + private static Color ReadColor(byte[] data, ref int offset) + { + byte r = data[offset++]; + byte g = data[offset++]; + byte b = data[offset++]; + return Color.FromArgb(r, g, b); + } } \ No newline at end of file diff --git a/UI/GUI/Rendering/RenderCommandType.cs b/UI/GUI/Rendering/RenderCommandType.cs new file mode 100644 index 0000000..cf1b8eb --- /dev/null +++ b/UI/GUI/Rendering/RenderCommandType.cs @@ -0,0 +1,18 @@ +namespace RemSox.UI.GUI.Rendering; + +public enum RenderCommandType : byte +{ + CreateWindow = 0x01, + DestroyWindow = 0x02, + MoveWindow = 0x03, + + DrawFilledRect = 0x10, + DrawRectBorder = 0x11, + DrawFilledCircle = 0x12, + DrawCircle = 0x13, + DrawText = 0x14, + DrawLine = 0x15, + DrawPoint = 0x16, + + RemovePrimitives = 0x20, +} diff --git a/UI/GUI/UIEelements/Controls/Button.cs b/UI/GUI/UIEelements/Controls/Button.cs index 3721722..28de063 100644 --- a/UI/GUI/UIEelements/Controls/Button.cs +++ b/UI/GUI/UIEelements/Controls/Button.cs @@ -1,3 +1,5 @@ +using RemSox.UI.GUI.Rendering; + using System.Drawing; namespace RemSox.UI.GUI.UIEelements.Controls; @@ -18,6 +20,58 @@ public class Button() : Control("Button") set => SetProperty(nameof(TextColor), ref field, value); } = Color.Black; + public override IEnumerable ToPrimitives(int windowId) + { + // Background fill + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = RenderCommandType.DrawFilledRect, + Position = Position, + Properties = new Dictionary + { + ["Color"] = BackgroundColor, + ["Size"] = Size, + } + }; + + // Border + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(1), + Type = RenderCommandType.DrawRectBorder, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color.DarkGray, + ["Size"] = Size, + } + }; + + // Text (centered) + if (!string.IsNullOrEmpty(Text)) + { + int tx = Position.X + (Size.Width / 2) - (Text.Length * 4); + int ty = Position.Y + (Size.Height / 2) - 8; + + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(2), + Type = RenderCommandType.DrawText, + Position = new Point(tx, ty), + Properties = new Dictionary + { + ["Color"] = TextColor, + ["Content"] = Text, + ["FontSize"] = Size.Height - 8, + } + }; + } + } + public override void HandleMouseEvent(MouseEvent mouseEvent) { if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left) diff --git a/UI/GUI/UIEelements/Controls/CheckBox.cs b/UI/GUI/UIEelements/Controls/CheckBox.cs index 76ef247..28cbfd8 100644 --- a/UI/GUI/UIEelements/Controls/CheckBox.cs +++ b/UI/GUI/UIEelements/Controls/CheckBox.cs @@ -1,3 +1,5 @@ +using RemSox.UI.GUI.Rendering; + using System.Drawing; namespace RemSox.UI.GUI.UIEelements.Controls; @@ -28,6 +30,74 @@ public class CheckBox() : Control("CheckBox") set => SetProperty(nameof(TextColor), ref field, value); } = Color.White; + public override IEnumerable ToPrimitives(int windowId) + { + int boxSize = Size.Height; + + // Checkbox box background + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = RenderCommandType.DrawFilledRect, + Position = Position, + Properties = new Dictionary + { + ["Color"] = BackgroundColor, + ["Size"] = new Size(boxSize, boxSize), + } + }; + + // Checkbox box border + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(1), + Type = RenderCommandType.DrawRectBorder, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color.DarkGray, + ["Size"] = new Size(boxSize, boxSize), + } + }; + + // Check mark (filled inner rect) + if (IsChecked) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(2), + Type = RenderCommandType.DrawFilledRect, + Position = new Point(Position.X + 3, Position.Y + 3), + Properties = new Dictionary + { + ["Color"] = Color.Black, + ["Size"] = new Size(boxSize - 6, boxSize - 6), + } + }; + } + + // Label text + if (!string.IsNullOrEmpty(Text)) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(3), + Type = RenderCommandType.DrawText, + Position = new Point(Position.X + boxSize + 5, Position.Y - 2), + Properties = new Dictionary + { + ["Color"] = TextColor, + ["Content"] = Text, + ["FontSize"] = boxSize, + } + }; + } + } + public override void HandleMouseEvent(MouseEvent mouseEvent) { if (mouseEvent.Type == MouseEventType.ButtonDown && mouseEvent.Button == MouseButton.Left) diff --git a/UI/GUI/UIEelements/Shapes/Circle.cs b/UI/GUI/UIEelements/Shapes/Circle.cs index 3355072..107c849 100644 --- a/UI/GUI/UIEelements/Shapes/Circle.cs +++ b/UI/GUI/UIEelements/Shapes/Circle.cs @@ -1,3 +1,7 @@ +using RemSox.UI.GUI.Rendering; + +using System.Drawing; + namespace RemSox.UI.GUI.UIEelements.Shapes; public class Circle() : Shape("Circle") @@ -7,4 +11,26 @@ public class Circle() : Shape("Circle") get; set => SetProperty(nameof(Radius), ref field, value); } + + public bool IsFilled + { + get; + set => SetProperty(nameof(IsFilled), ref field, value); + } = true; + + public override IEnumerable ToPrimitives(int windowId) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = IsFilled ? RenderCommandType.DrawFilledCircle : RenderCommandType.DrawCircle, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color, + ["Radius"] = Radius, + } + }; + } } \ No newline at end of file diff --git a/UI/GUI/UIEelements/Shapes/Line.cs b/UI/GUI/UIEelements/Shapes/Line.cs index dbd38be..dbcf322 100644 --- a/UI/GUI/UIEelements/Shapes/Line.cs +++ b/UI/GUI/UIEelements/Shapes/Line.cs @@ -1,3 +1,5 @@ +using RemSox.UI.GUI.Rendering; + using System.Drawing; namespace RemSox.UI.GUI.UIEelements.Shapes; @@ -9,4 +11,20 @@ public class Line() : Shape("Line") get; set => SetProperty(nameof(EndPosition), ref field, value); } + + public override IEnumerable ToPrimitives(int windowId) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = RenderCommandType.DrawLine, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color, + ["EndPosition"] = EndPosition, + } + }; + } } \ No newline at end of file diff --git a/UI/GUI/UIEelements/Shapes/Pixel.cs b/UI/GUI/UIEelements/Shapes/Pixel.cs new file mode 100644 index 0000000..c2410d2 --- /dev/null +++ b/UI/GUI/UIEelements/Shapes/Pixel.cs @@ -0,0 +1,23 @@ +using RemSox.UI.GUI.Rendering; + +using System.Drawing; + +namespace RemSox.UI.GUI.UIEelements.Shapes; + +public class Pixel() : Shape("Pixel") +{ + public override IEnumerable ToPrimitives(int windowId) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = RenderCommandType.DrawPoint, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color, + } + }; + } +} diff --git a/UI/GUI/UIEelements/Shapes/Rectangle.cs b/UI/GUI/UIEelements/Shapes/Rectangle.cs index be0dda8..deae391 100644 --- a/UI/GUI/UIEelements/Shapes/Rectangle.cs +++ b/UI/GUI/UIEelements/Shapes/Rectangle.cs @@ -1,3 +1,5 @@ +using RemSox.UI.GUI.Rendering; + using System.Drawing; namespace RemSox.UI.GUI.UIEelements.Shapes; @@ -15,4 +17,20 @@ public class Rectangle() : Shape("Rectangle") get; set => SetProperty(nameof(IsFilled), ref field, value); } = true; + + public override IEnumerable ToPrimitives(int windowId) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = IsFilled ? RenderCommandType.DrawFilledRect : RenderCommandType.DrawRectBorder, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color, + ["Size"] = Size, + } + }; + } } diff --git a/UI/GUI/UIEelements/Shapes/Text.cs b/UI/GUI/UIEelements/Shapes/Text.cs index 083f8e0..f6e50fe 100644 --- a/UI/GUI/UIEelements/Shapes/Text.cs +++ b/UI/GUI/UIEelements/Shapes/Text.cs @@ -1,3 +1,5 @@ +using RemSox.UI.GUI.Rendering; + using System.Drawing; namespace RemSox.UI.GUI.UIEelements.Shapes; @@ -21,4 +23,21 @@ public class Text() : UIElement("Text") get; set => SetProperty(nameof(FontSize), ref field, value); } = 12; + + public override IEnumerable ToPrimitives(int windowId) + { + yield return new RenderCommand + { + WindowId = windowId, + ElementId = PrimitiveId(0), + Type = RenderCommandType.DrawText, + Position = Position, + Properties = new Dictionary + { + ["Color"] = Color, + ["Content"] = Content, + ["FontSize"] = FontSize, + } + }; + } } diff --git a/UI/GUI/UIEelements/UIEelement.cs b/UI/GUI/UIEelements/UIEelement.cs index 35da3d6..0fc94d6 100644 --- a/UI/GUI/UIEelements/UIEelement.cs +++ b/UI/GUI/UIEelements/UIEelement.cs @@ -1,3 +1,4 @@ +using RemSox.UI.GUI.Rendering; using RemSox.Utils; using System.Drawing; @@ -6,6 +7,8 @@ namespace RemSox.UI.GUI.UIEelements; public abstract class UIElement(string type) : ChangedPropertiesTracker { + public const int PrimitiveIdShift = 6; + public int Id { get; init; } public string Type { get; set; } = type; @@ -15,4 +18,10 @@ public abstract class UIElement(string type) : ChangedPropertiesTracker get; set => SetProperty(nameof(Position), ref field, value); } + + /// Expands this UI element into drawing primitives. + public abstract IEnumerable ToPrimitives(int windowId); + + /// Builds a stable primitive ID from element ID and sub-index. + protected int PrimitiveId(int subIndex) => (Id << PrimitiveIdShift) | subIndex; } \ No newline at end of file diff --git a/UI/GUI/Windows/Window.cs b/UI/GUI/Windows/Window.cs index 4efe297..9509468 100644 --- a/UI/GUI/Windows/Window.cs +++ b/UI/GUI/Windows/Window.cs @@ -143,9 +143,19 @@ public sealed class Window(string title, int processId, int id, IRenderSource re } } - if (removed && AutoFlush) + if (removed) { - Flush(); + renderSource.Render([new RenderCommand + { + WindowId = Id, + ElementId = elementId, + Type = RenderCommandType.RemovePrimitives, + }]); + + if (AutoFlush) + { + Flush(); + } } } @@ -158,8 +168,13 @@ public sealed class Window(string title, int processId, int id, IRenderSource re Flush(); } + private const int ChromeClientBg = -1; + private const int ChromeTitleBg = -2; + private const int ChromeTitleText = -3; + private const int ChromeBorder = -4; + /// - /// Sends current window and element state to the renderer. + /// Sends current window and element state to the renderer (delta-only). /// public void Flush() { @@ -168,69 +183,83 @@ 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(); + elementsCopy = [.. uiElements.Values]; } - bool windowStateChanged = isFirstRender || Size != lastRenderedSize || IsFocused != lastRenderedIsFocused || Title != lastRenderedTitle; + bool sizeChanged = Size != lastRenderedSize; bool positionChanged = Position != lastRenderedPosition; bool zIndexChanged = ZIndex != lastRenderedZIndex; - - bool fullRedraw = windowStateChanged || anyChildChanged; + bool titleChanged = Title != lastRenderedTitle; + bool focusChanged = IsFocused != lastRenderedIsFocused; + bool chromeChanged = isFirstRender || sizeChanged || titleChanged || focusChanged; List commands = []; - if (fullRedraw) + // --- Structural commands --- + if (isFirstRender || sizeChanged || zIndexChanged) { commands.Add(new RenderCommand { WindowId = Id, ElementId = Id, - ElementType = "Window", + Type = RenderCommandType.CreateWindow, Position = Position, Properties = new Dictionary { - [nameof(Title)] = Title, - [nameof(Size)] = Size, - [nameof(IsFocused)] = IsFocused, - [nameof(IsResizable)] = IsResizable, - [nameof(IsDraggable)] = IsDraggable, - [nameof(ZIndex)] = ZIndex + ["Size"] = Size, + ["ZIndex"] = ZIndex, } }); + } + else if (positionChanged) + { + commands.Add(new RenderCommand + { + WindowId = Id, + ElementId = Id, + Type = RenderCommandType.MoveWindow, + Position = Position, + }); + } + // --- Chrome primitives (title bar, border, background) --- + if (chromeChanged) + { + ChromePrimitives(commands); + } + + // --- Child element primitives (delta) --- + if (isFirstRender) + { foreach (UIElement element in elementsCopy) { + commands.AddRange(element.ToPrimitives(Id)); + element.ClearChangedProperties(); + } + } + else + { + foreach (UIElement element in elementsCopy) + { + if (!element.AnyPropertyChanged) + { + continue; + } + + // Remove old primitives for this element, then emit new ones commands.Add(new RenderCommand { WindowId = Id, ElementId = element.Id, - ElementType = element.Type, - Position = element.Position, - Properties = element.AllProperties + Type = RenderCommandType.RemovePrimitives, }); + commands.AddRange(element.ToPrimitives(Id)); element.ClearChangedProperties(); } } - else if (positionChanged || zIndexChanged) - { - commands.Add(new RenderCommand - { - WindowId = Id, - ElementId = Id, - ElementType = "WindowMove", - Position = Position, - Properties = new Dictionary - { - [nameof(ZIndex)] = ZIndex - } - }); - } if (commands.Count > 0) { @@ -244,6 +273,72 @@ public sealed class Window(string title, int processId, int id, IRenderSource re } } + private void ChromePrimitives(List commands) + { + Color border = IsFocused ? Color.White : Color.DarkGray; + Color title = IsFocused ? Color.FromArgb(0, 120, 215) : Color.FromArgb(80, 80, 80); + + // Client area background + commands.Add(new RenderCommand + { + WindowId = Id, + ElementId = ChromeClientBg, + Type = RenderCommandType.DrawFilledRect, + Position = Point.Empty, + Properties = new Dictionary + { + ["Color"] = Color.FromArgb(32, 32, 32), + ["Size"] = Size, + } + }); + + // Title bar background + commands.Add(new RenderCommand + { + WindowId = Id, + ElementId = ChromeTitleBg, + Type = RenderCommandType.DrawFilledRect, + Position = Point.Empty, + Properties = new Dictionary + { + ["Color"] = title, + ["Size"] = new Size(Size.Width, 18), + } + }); + + // Title bar text + if (!string.IsNullOrEmpty(Title)) + { + commands.Add(new RenderCommand + { + WindowId = Id, + ElementId = ChromeTitleText, + Type = RenderCommandType.DrawText, + Position = new Point(4, 2), + Properties = new Dictionary + { + ["Color"] = Color.White, + ["Content"] = Title, + ["FontSize"] = 18, + } + }); + } + + // Window border + commands.Add(new RenderCommand + { + WindowId = Id, + ElementId = ChromeBorder, + Type = RenderCommandType.DrawRectBorder, + Position = Point.Empty, + Properties = new Dictionary + { + ["Color"] = border, + ["Size"] = Size, + } + }); + } + /// Dispatches a key event to the window's registered event handlers. public void HandleKeyEvent(Sys.Keyboard.KeyEvent keyEvent) { diff --git a/UI/GUI/Windows/WindowManager.cs b/UI/GUI/Windows/WindowManager.cs index 0df79e1..d74bfcc 100644 --- a/UI/GUI/Windows/WindowManager.cs +++ b/UI/GUI/Windows/WindowManager.cs @@ -71,7 +71,7 @@ public static class WindowManager wasRightButtonDown = rightButtonDown; wasMiddleButtonDown = middleButtonDown; - CanvasRenderSource.CompositeAndDisplay(canvas, pointerPosition); + renderSource.Composite(); lastPointerPosition = pointerPosition; } @@ -244,7 +244,7 @@ public static class WindowManager activeInteractWindow = null; } - renderSource.Render([new RenderCommand { WindowId = window.Id, ElementId = window.Id, ElementType = "WindowClose", Position = window.Position, Properties = new Dictionary() }]); + renderSource.Render([new RenderCommand { WindowId = window.Id, ElementId = window.Id, Type = RenderCommandType.DestroyWindow, Position = window.Position }]); } /// @@ -300,7 +300,7 @@ public static class WindowManager List closeCommands = []; foreach (Window window in windowsToClose) { - closeCommands.Add(new RenderCommand { WindowId = window.Id, ElementId = window.Id, ElementType = "WindowClose", Position = window.Position, Properties = new Dictionary() }); + closeCommands.Add(new RenderCommand { WindowId = window.Id, ElementId = window.Id, Type = RenderCommandType.DestroyWindow, Position = window.Position }); } renderSource.Render(closeCommands); } @@ -448,5 +448,19 @@ public static class WindowManager source.Render(commands); } } + + public void Composite() + { + List sourcesCopy; + lock (sourcesLock) + { + sourcesCopy = sources.ToList(); + } + + foreach (IRenderSource source in sourcesCopy) + { + source.Composite(); + } + } } }