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.

This commit is contained in:
Stone_Red
2026-06-16 01:11:01 +02:00
parent 1355fa06b8
commit e90e9ab163
16 changed files with 850 additions and 187 deletions
+181 -146
View File
@@ -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<int, int> 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<long, int> 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<int, List<(int ElementId, RenderCommand Command)>> windowPrimitives = [];
private static readonly HashSet<int> dirtyWindows = [];
private static bool isPositionDirty = true;
private static Point lastPointerPosition = new(-1, -1);
private static readonly Lock renderLock = new();
private static readonly Dictionary<string, Action<Canvas, RenderCommand>> elementRenderers = new()
{
["Circle"] = RenderCircle,
["Rectangle"] = RenderRectangle,
["Text"] = RenderText,
["Line"] = RenderLine,
["Button"] = RenderButton,
["CheckBox"] = RenderCheckBox,
};
public void Render(IEnumerable<RenderCommand> 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<Canvas, RenderCommand>? 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<T>(IReadOnlyDictionary<string, object?> 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);
}
}
+1
View File
@@ -3,4 +3,5 @@ namespace RemSox.UI.GUI.Rendering;
public interface IRenderSource
{
void Render(IEnumerable<RenderCommand> commands);
void Composite();
}
+21
View File
@@ -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<RenderCommand> commands)
{
foreach (RenderCommand cmd in commands)
{
byte[] data = cmd.ToBytes();
_ = server.SendRawToAll(MessageType, data);
}
}
public void Composite()
{
}
}
+227 -3
View File
@@ -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<string, object?> Properties { get; set; }
public required RenderCommandType Type { get; set; }
public Point Position { get; set; }
public Dictionary<string, object?> 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<string, object?> 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<T>(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);
}
}
+18
View File
@@ -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,
}