diff --git a/ArgonUI.Backends.Headless/ArgonUI.Backends.Headless.csproj b/ArgonUI.Backends.Headless/ArgonUI.Backends.Headless.csproj index db76f9d..1d3ade5 100644 --- a/ArgonUI.Backends.Headless/ArgonUI.Backends.Headless.csproj +++ b/ArgonUI.Backends.Headless/ArgonUI.Backends.Headless.csproj @@ -7,7 +7,7 @@ disable enable ArgonUI Headless Backend - 0.1.2-pre + 0.2.1-pre Thomas Mathieson Copyright © Thomas Mathieson 2025 https://github.com/space928/ArgonUI diff --git a/ArgonUI.Backends.Headless/HeadlessDrawContext.cs b/ArgonUI.Backends.Headless/HeadlessDrawContext.cs index a64653e..9b0c72a 100644 --- a/ArgonUI.Backends.Headless/HeadlessDrawContext.cs +++ b/ArgonUI.Backends.Headless/HeadlessDrawContext.cs @@ -29,22 +29,42 @@ public void DrawGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vect } - public void DrawRect(Bounds2D bounds, Vector4 colour, float rounding) + public void DrawLine(Vector2 start, Vector2 end, Vector4 colourStart, Vector4 colourEnd, float thickness) { } - public void DrawShadow(Bounds2D bounds, Vector4 colour, float rounding, float blur) + public void DrawOutlineGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vector4 colourC, Vector4 colourD, float outlineThickness, float rounding) + { + + } + + public void DrawOutlineRect(Bounds2D bounds, Vector4 colour, float outlineThickness, float rounding) + { + + } + + public void DrawPolyFill(IEnumerable points) { } - public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 colour) + public void DrawPolyLine(IEnumerable points, float thickness) + { + + } + + public void DrawRect(Bounds2D bounds, Vector4 colour, float rounding) + { + + } + + public void DrawShadow(Bounds2D bounds, Vector4 colour, float rounding, float blur) { } - public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 colour, float wordSpacing = 0, float charSpacing = 0, float skew = 0, float weight = 0.5F, float width = 1) + public void DrawText(Bounds2D bounds, ReadOnlySpan s, BMFont font, float size, Vector4 colour, float wordSpacing = 0, float charSpacing = 0, float skew = 0, float weight = 0.5F, float width = 1) { } diff --git a/ArgonUI.Backends.OpenGL/ArgonUI.Backends.OpenGL.csproj b/ArgonUI.Backends.OpenGL/ArgonUI.Backends.OpenGL.csproj index 277a5d1..5c81afd 100644 --- a/ArgonUI.Backends.OpenGL/ArgonUI.Backends.OpenGL.csproj +++ b/ArgonUI.Backends.OpenGL/ArgonUI.Backends.OpenGL.csproj @@ -7,7 +7,7 @@ disable enable ArgonUI OpenGL Backend - 0.3.1-pre + 0.4.1-pre Thomas Mathieson Copyright © Thomas Mathieson 2025 https://github.com/space928/ArgonUI diff --git a/ArgonUI.Backends.OpenGL/OpenGLDrawContext.cs b/ArgonUI.Backends.OpenGL/OpenGLDrawContext.cs index 3de96df..8e00d9a 100644 --- a/ArgonUI.Backends.OpenGL/OpenGLDrawContext.cs +++ b/ArgonUI.Backends.OpenGL/OpenGLDrawContext.cs @@ -18,14 +18,8 @@ internal class OpenGLDrawContext : IDrawContext private readonly GL gl; private UIWindow? window; private Vector2 resolution; - private Shader? rectShader; - private Shader? textShader; - private Shader? textureShader; - private VertexArrayObject? rectVAO; - private VertexArrayObject? textVAO; - - private VertexArrayObject? activeVao; - private Shader? activeShader; + private ShaderManager shaderManager; + private Texture2D? activeTexture; private int vertPos; private int vertCount; @@ -37,6 +31,7 @@ public OpenGLDrawContext(GL gl) { this.gl = gl; vertBuff = new uint[32*1024]; + shaderManager = new(); } #if DEBUG_LATENCY || DEBUG @@ -53,11 +48,6 @@ public void MarkLatencyTimerEnd(string? msg = null) public void InitRenderer(UIWindow window) { this.window = window; - //try - //{ - rectShader = new(gl, "ui_vert.glsl", "ui_frag.glsl", ["#define SUPPORT_ROUNDING", "#define SUPPORT_ALPHA"]); - textShader = new(gl, "ui_vert.glsl", "ui_frag.glsl", ["#define SUPPORT_TEXT", "#define SUPPORT_ALPHA"]); - textureShader = new(gl, "ui_vert.glsl", "ui_frag.glsl", ["#define SUPPORT_TEXTURE", "#define SUPPORT_ROUNDING", "#define SUPPORT_ALPHA"]); gl.FrontFace(FrontFaceDirection.CW); //gl.Enable(EnableCap.DepthTest); @@ -73,8 +63,7 @@ public void InitRenderer(UIWindow window) gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); gl.Enable(EnableCap.ScissorTest); - InitRectVAO(); - InitTextVAO(); + shaderManager.Init(gl); //vao = new(gl, new BufferObject(gl, [], BufferTargetARB.ArrayBuffer), null); /*} catch (Exception ex) @@ -85,12 +74,7 @@ public void InitRenderer(UIWindow window) public void Dispose() { - rectVAO?.Dispose(); - textVAO?.Dispose(); - //vao?.Dispose(); - rectShader?.Dispose(); - textShader?.Dispose(); - textureShader?.Dispose(); + shaderManager.Dispose(); gl.Dispose(); } @@ -141,95 +125,177 @@ public void Clear(Vector4 colour) /// /// Checks if the vertex buffer has enough space for this draw. /// - /// - /// + /// The type of vertex to check space for. + /// The number of vertices to check for space for. /// if the batch needs to be flushed. - private bool CheckSpace(int elems) + private bool CheckSpace(int elems) { - return (vertBuff.Length * Unsafe.SizeOf()) - vertPos < Unsafe.SizeOf() * elems; + return (vertBuff.Length * Unsafe.SizeOf()) - vertPos < Unsafe.SizeOf() * elems; } - private void InitRectVAO() + #region Rectangles + public void DrawRect(Bounds2D bounds, Vector4 colour, float rounding) { - rectVAO?.Dispose(); - - VertexArrayObject vao = new(gl, new BufferObject(gl, [], BufferTargetARB.ArrayBuffer), null); - vao.Bind(); - vao.VertexAttributePointer(0, 2, VertexAttribType.Float, 11, 0); - vao.VertexAttributePointer(1, 4, VertexAttribType.Float, 11, 2); - vao.VertexAttributePointer(3, 2, VertexAttribType.Float, 11, 6); - vao.VertexAttributePointer(4, 3, VertexAttribType.Float, 11, 8); - vao.Unbind(); - rectVAO = vao; - } + ShaderFeature shaderFeatures = ShaderFeature.Rounding | ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) + { + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); + } - private void InitTextVAO() - { - textVAO?.Dispose(); - - VertexArrayObject vao = new(gl, new BufferObject(gl, [], BufferTargetARB.ArrayBuffer), null); - vao.Bind(); - vao.VertexAttributePointer(0, 2, VertexAttribType.Float, 9, 0); - vao.VertexAttributePointer(1, 4, VertexAttribType.Float, 9, 2); - vao.VertexAttributePointer(2, 3, VertexAttribType.Float, 9, 6); - vao.Unbind(); - textVAO = vao; + if (CheckSpace(6)) + FlushBatch(); + + Vector4 rectProps = new(bounds.Size, rounding, 0); + + var writer = vertBuff.GetBinaryWriter(vertPos); + writer.Write(new RectRoundVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colour, new(0, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colour, new(1, 0), rectProps)); + vertCount += 6; + vertPos = writer.Offset; } public void DrawGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vector4 colourC, Vector4 colourD, float rounding) { - if (activeShader != rectShader || CheckSpace(6)) + ShaderFeature shaderFeatures = ShaderFeature.Rounding | ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) + { + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); + } + + if (CheckSpace(6)) FlushBatch(); - if (activeShader != rectShader) + Vector4 rectProps = new(bounds.Size, rounding, 0); + + var writer = vertBuff.GetBinaryWriter(vertPos); + writer.Write(new RectRoundVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colourB, new(0, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colourA, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colourD, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colourD, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colourA, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colourC, new(1, 0), rectProps)); + vertCount += 6; + vertPos = writer.Offset; + } + + public void DrawShadow(Bounds2D bounds, Vector4 colour, float rounding, float blur) + { + ShaderFeature shaderFeatures = ShaderFeature.Rounding | ShaderFeature.Blur | ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) { - activeShader = rectShader; - activeVao = rectVAO; - activeTexture = null; + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); } - Vector3 rectProps = new(bounds.Width, bounds.Height, rounding); + + if (CheckSpace(6)) + FlushBatch(); + + Vector4 rectProps = new(bounds.Size, rounding, blur); var writer = vertBuff.GetBinaryWriter(vertPos); - writer.Write(new RectVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colourB, new(0, 1), rectProps)); - writer.Write(new RectVert(bounds.topLeft, colourA, new(0, 0), rectProps)); - writer.Write(new RectVert(bounds.bottomRight, colourD, new(1, 1), rectProps)); - writer.Write(new RectVert(bounds.bottomRight, colourD, new(1, 1), rectProps)); - writer.Write(new RectVert(bounds.topLeft, colourA, new(0, 0), rectProps)); - writer.Write(new RectVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colourC, new(1, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colour, new(0, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colour, new(1, 0), rectProps)); vertCount += 6; vertPos = writer.Offset; } - public void DrawRect(Bounds2D bounds, Vector4 colour, float rounding) + public void DrawOutlineRect(Bounds2D bounds, Vector4 colour, float outlineThickness, float rounding) { - if (activeShader != rectShader || CheckSpace(6)) + ShaderFeature shaderFeatures = ShaderFeature.Outline | ShaderFeature.Rounding | ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) + { FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); + } + + if (CheckSpace(6)) + FlushBatch(); + + Vector4 rectProps = new(bounds.Size, rounding, outlineThickness); + + var writer = vertBuff.GetBinaryWriter(vertPos); + writer.Write(new RectRoundVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colour, new(0, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colour, new(1, 0), rectProps)); + vertCount += 6; + vertPos = writer.Offset; + } - if (activeShader != rectShader) + public void DrawOutlineGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vector4 colourC, Vector4 colourD, float outlineThickness, float rounding) + { + ShaderFeature shaderFeatures = ShaderFeature.Outline | ShaderFeature.Rounding | ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) { - activeShader = rectShader; - activeVao = rectVAO; - activeTexture = null; + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); } - Vector3 rectProps = new(bounds.Width, bounds.Height, rounding); + if (CheckSpace(6)) + FlushBatch(); + + Vector4 rectProps = new(bounds.Size, rounding, 0); var writer = vertBuff.GetBinaryWriter(vertPos); - writer.Write(new RectVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colour, new(0, 1), rectProps)); - writer.Write(new RectVert(bounds.topLeft, colour, new(0, 0), rectProps)); - writer.Write(new RectVert(bounds.bottomRight, colour, new(1, 1), rectProps)); - writer.Write(new RectVert(bounds.bottomRight, colour, new(1, 1), rectProps)); - writer.Write(new RectVert(bounds.topLeft, colour, new(0, 0), rectProps)); - writer.Write(new RectVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colour, new(1, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colourB, new(0, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colourA, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colourD, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colourD, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colourA, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colourC, new(1, 0), rectProps)); vertCount += 6; vertPos = writer.Offset; } - public void DrawShadow(Bounds2D bounds, Vector4 colour, float rounding, float blur) + public void DrawTexture(Bounds2D bounds, ITextureHandle texture, float rounding) { - throw new NotImplementedException(); + if (texture is not Texture2D tex) + throw new NotSupportedException("Attempted to call DrawTexture with an incompatible texture object!"); + + ShaderFeature shaderFeatures = ShaderFeature.Rounding | ShaderFeature.Alpha | ShaderFeature.Texture; + if (!shaderManager.IsShaderActive(shaderFeatures)) + { + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); + } + + if (CheckSpace(6)) + FlushBatch(); + + if (activeTexture != tex) + { + FlushBatch(); + activeTexture = tex; + } + + Vector4 rectProps = new(bounds.Size, rounding, 0); + var colour = Vector4.One; + + var writer = vertBuff.GetBinaryWriter(vertPos); + writer.Write(new RectRoundVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colour, new(0, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.bottomRight, colour, new(1, 1), rectProps)); + writer.Write(new RectRoundVert(bounds.topLeft, colour, new(0, 0), rectProps)); + writer.Write(new RectRoundVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colour, new(1, 0), rectProps)); + vertCount += 6; + vertPos = writer.Offset; } + #endregion + #region Text public void DrawChar(Bounds2D bounds, float size, char c, BMFont font, Vector4 colour) { float fontSize = size / font.Size; @@ -254,7 +320,7 @@ public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 } } - public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 colour, + public void DrawText(Bounds2D bounds, ReadOnlySpan s, BMFont font, float size, Vector4 colour, float wordSpacing = 0, float charSpacing = 0, float skew = 0, float weight = 0.5F, float width = 1) { var tex = font.FontTexture?.TextureHandle; @@ -290,24 +356,22 @@ private float DrawCharInternal(in Vector2 pos, float size, float skew, float wei if (tex.Width == 0 || tex.Height == 0) return 0f; - if (activeShader != textShader || CheckSpace(6)) - FlushBatch(); - - if (activeShader != textShader) + ShaderFeature shaderFeatures = ShaderFeature.Text | ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) { - activeShader = textShader; - textShader?.Use(); - textShader?.SetUniform("uFontTex", 0); - activeVao = textVAO; + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); } + if (CheckSpace(6)) + FlushBatch(); + if (activeTexture != tex) { FlushBatch(); activeTexture = tex; } - float size_x = size * width; Vector2 fontSizeVec = new(size * width, size); Vector2 tl = c.offset * fontSizeVec; Vector2 br = (c.offset + c.size) * fontSizeVec; @@ -329,58 +393,66 @@ private float DrawCharInternal(in Vector2 pos, float size, float skew, float wei return c.xAdvance * fontSizeVec.X; } + #endregion - public void DrawTexture(Bounds2D bounds, ITextureHandle texture, float rounding) + #region Lines & Polys + public void DrawLine(Vector2 start, Vector2 end, Vector4 colourStart, Vector4 colourEnd, float thickness) { - if (texture is not Texture2D tex) - throw new NotSupportedException("Attempted to call DrawTexture with an incompatible texture object!"); - - if (activeShader != textShader || CheckSpace(6)) - FlushBatch(); - - if (activeShader != textureShader) + ShaderFeature shaderFeatures = ShaderFeature.Alpha; + if (!shaderManager.IsShaderActive(shaderFeatures)) { - activeShader = textureShader; - textureShader?.Use(); - textureShader?.SetUniform("uMainTex", 0); - activeVao = rectVAO; + FlushBatch(); + shaderManager.SetActiveShader(gl, shaderFeatures); } - if (activeTexture != tex) - { + if (CheckSpace(6)) FlushBatch(); - activeTexture = tex; - } - Vector3 rectProps = new(bounds.Width, bounds.Height, rounding); - var colour = Vector4.One; + Vector2 dir = end - start; + dir = Vector2.Normalize(dir); + Vector2 perp = new Vector2(-dir.Y, dir.X) * thickness; + Vector2 a_0 = start + perp; + Vector2 a_1 = start - perp; + Vector2 b_0 = end + perp; + Vector2 b_1 = end - perp; var writer = vertBuff.GetBinaryWriter(vertPos); - writer.Write(new RectVert(new(bounds.topLeft.X, bounds.bottomRight.Y), colour, new(0, 1), rectProps)); - writer.Write(new RectVert(bounds.topLeft, colour, new(0, 0), rectProps)); - writer.Write(new RectVert(bounds.bottomRight, colour, new(1, 1), rectProps)); - writer.Write(new RectVert(bounds.bottomRight, colour, new(1, 1), rectProps)); - writer.Write(new RectVert(bounds.topLeft, colour, new(0, 0), rectProps)); - writer.Write(new RectVert(new(bounds.bottomRight.X, bounds.topLeft.Y), colour, new(1, 0), rectProps)); + writer.Write(new RectVert(a_0, colourStart, new(0, 1))); + writer.Write(new RectVert(a_1, colourStart, new(0, 0))); + writer.Write(new RectVert(b_0, colourEnd, new(1, 1))); + writer.Write(new RectVert(b_0, colourEnd, new(1, 1))); + writer.Write(new RectVert(a_1, colourStart, new(0, 0))); + writer.Write(new RectVert(b_1, colourEnd, new(1, 0))); vertCount += 6; vertPos = writer.Offset; } + public void DrawPolyFill(IEnumerable points) + { + throw new NotImplementedException(); + } + + public void DrawPolyLine(IEnumerable points, float thickness) + { + throw new NotImplementedException(); + } + #endregion + public void FlushBatch() { if (vertPos == 0) return; - if (activeShader != null) + if (shaderManager.ActiveShader != null) { - activeShader.Use(); + //shaderManager.ActiveShader.Use(); - activeShader.SetUniform("uResolution", resolution); + shaderManager.ActiveShader.SetUniform("uResolution", resolution); // Set all other program properties... // Bind textures and stuff activeTexture?.Bind(0); - var vao = activeVao!; + var vao = shaderManager.ActiveVAO!; vao.VBO.Update(vertBuff.AsSpan(0, vertPos >> 2)); @@ -402,24 +474,4 @@ public ITextureHandle LoadTexture(TextureData data, string? name = null, Texture { return Texture2DLoader.LoadTexture(gl, name, data, compression); } - - [StructLayout(LayoutKind.Explicit)] - readonly struct RectVert(Vector2 pos, Vector4 col, Vector2 texcoord, Vector3 rounding) - { - [FieldOffset(0)] public readonly Vector2 pos = pos; - [FieldOffset(0x8)] public readonly Vector4 col = col; - [FieldOffset(0x18)] public readonly Vector2 texcoord = texcoord; - [FieldOffset(0x20)] public readonly Vector3 rounding = rounding; - } - - [StructLayout(LayoutKind.Explicit)] - readonly struct CharVert(Vector2 pos, Vector4 col, Vector3 charData) - { - [FieldOffset(0)] public readonly Vector2 pos = pos; - [FieldOffset(0x8)] public readonly Vector4 col = col; - /// - /// The uv coordinates into the font texture are stored in xy, and z stores the font weight. - /// - [FieldOffset(0x18)] public readonly Vector3 charData = charData; - } } diff --git a/ArgonUI.Backends.OpenGL/Shader.cs b/ArgonUI.Backends.OpenGL/Shader.cs index 15819ca..1e1a76d 100644 --- a/ArgonUI.Backends.OpenGL/Shader.cs +++ b/ArgonUI.Backends.OpenGL/Shader.cs @@ -18,7 +18,7 @@ public partial class Shader : IDisposable private readonly StringDict uniformLocationCache = [];//Dictionary uniformLocationCache = []; - public Shader(GL gl, string vertexPath, string fragmentPath, string[]? defines = null) + public Shader(GL gl, string vertexPath, string fragmentPath, ICollection? defines = null) { this.gl = gl; @@ -112,7 +112,7 @@ public void Dispose() gl.DeleteProgram(handle); } - private uint LoadShader(ShaderType type, string path, string[]? defines) + private uint LoadShader(ShaderType type, string path, ICollection? defines) { string src; try @@ -146,9 +146,9 @@ private uint LoadShader(ShaderType type, string path, string[]? defines) /// /// /// - private static string ApplyDefines(string source, string[]? defines) + private static string ApplyDefines(string source, ICollection? defines) { - if (defines == null || defines.Length == 0) + if (defines == null || defines.Count == 0) return source; StringBuilder sb = new(source.Length); diff --git a/ArgonUI.Backends.OpenGL/ShaderManager.cs b/ArgonUI.Backends.OpenGL/ShaderManager.cs new file mode 100644 index 0000000..72f70c0 --- /dev/null +++ b/ArgonUI.Backends.OpenGL/ShaderManager.cs @@ -0,0 +1,231 @@ +using ArgonUI.Helpers; +using Silk.NET.OpenGL; +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +namespace ArgonUI.Backends.OpenGL; + +internal class ShaderManager : IDisposable +{ + private readonly Dictionary shaders = []; + private readonly Dictionary> vaos = []; + private readonly FrozenDictionary shaderFeatureDefines; + + public ShaderFeature ActiveFeatures { get; private set; } + public Shader? ActiveShader { get; private set; } + public VertexArrayObject? ActiveVAO { get; private set; } + + public readonly ShaderFeature[] PRECOMPILED_SHADERS = [ + ShaderFeature.Text | ShaderFeature.Alpha, + ShaderFeature.Rounding | ShaderFeature.Alpha, + ShaderFeature.Outline | ShaderFeature.Rounding | ShaderFeature.Alpha, + ShaderFeature.Texture | ShaderFeature.Rounding | ShaderFeature.Alpha, + ShaderFeature.Blur | ShaderFeature.Rounding | ShaderFeature.Alpha, + ]; + + public ShaderManager() + { + Dictionary shaderFeatureStrings = []; + shaderFeatureStrings.Add(ShaderFeature.Text, "#define SUPPORT_TEXT"); + shaderFeatureStrings.Add(ShaderFeature.Texture, "#define SUPPORT_TEXTURE"); + shaderFeatureStrings.Add(ShaderFeature.Outline, "#define SUPPORT_OUTLINE"); + shaderFeatureStrings.Add(ShaderFeature.TextShadow, "#define SUPPORT_TEXT_SHADOW"); + shaderFeatureStrings.Add(ShaderFeature.Alpha, "#define SUPPORT_ALPHA"); + shaderFeatureStrings.Add(ShaderFeature.Rounding, "#define SUPPORT_ROUNDING"); + shaderFeatureStrings.Add(ShaderFeature.Blur, "#define SUPPORT_BLUR"); + shaderFeatureDefines = shaderFeatureStrings.ToFrozenDictionary(); + } + + public void Init(GL gl) + { + foreach (var shader in shaders.Values) + shader.Dispose(); + foreach (var vao in vaos.Values) + vao.Dispose(); + shaders.Clear(); + vaos.Clear(); + + foreach (var precompile in PRECOMPILED_SHADERS) + SetActiveShader(gl, precompile); + } + + public bool IsShaderActive(ShaderFeature features) => features == ActiveFeatures; + + /// + /// Sets the active shader to one which supports the given features. Compiles and caches a new shader if needed. + /// + /// Also updates the active VAO to match. + /// + /// + /// The required shader features. + /// if the shader was changed. + /// + public bool SetActiveShader(GL gl, ShaderFeature features) + { + if (features == ActiveFeatures) + return false; + + VertexArrayObject? vao; + var vertLayout = GetVertexLayout(features); + if (shaders.TryGetValue(features, out var shader)) + { + vao = vaos[vertLayout]; + goto ReturnActiveShader; + } + + shader = InitShader(gl, features); + + if (!vaos.TryGetValue(vertLayout, out vao)) + { + vao = vertLayout switch + { + ShaderVertexLayout.Rect => InitRectVAO(gl), + ShaderVertexLayout.RectRound => InitRectRoundVAO(gl), + ShaderVertexLayout.Char => InitTextVAO(gl), + _ => throw new NotImplementedException(), + }; + vaos.Add(vertLayout, vao); + } + + shaders.Add(features, shader); + + ReturnActiveShader: + shader.Use(); + ActiveFeatures = features; + ActiveShader = shader; + ActiveVAO = vao; + + if ((features & ShaderFeature.Text) != 0) + shader?.SetUniform("uFontTex", 0); + if ((features & ShaderFeature.Texture) != 0) + shader?.SetUniform("uMainTex", 0); + + return true; + } + + private static ShaderVertexLayout GetVertexLayout(ShaderFeature features) + { + // TODO: This is likely to cause bugs, many features share vertex layouts + if ((features & ShaderFeature.Text) != 0) + return ShaderVertexLayout.Char; + if ((features & ShaderFeature.Rounding) != 0) + return ShaderVertexLayout.RectRound; + return ShaderVertexLayout.Rect; + } + + private Shader InitShader(GL gl, ShaderFeature features) + { + // Build a list of defines to add to the shader + using TemporaryList featuresList = []; + for (int i = 1; i < (int)ShaderFeature.MAX_VALUE; i <<= 1) + if (((int)features & i) != 0) + featuresList.Add(shaderFeatureDefines[(ShaderFeature)i]); + + return new(gl, "ui_vert.glsl", "ui_frag.glsl", featuresList); + } + + private static VertexArrayObject InitRectVAO(GL gl) + { + VertexArrayObject vao = new(gl, new BufferObject(gl, [], BufferTargetARB.ArrayBuffer), null); + vao.Bind(); + vao.VertexAttributePointer(0, 2, VertexAttribType.Float, 8, 0); + vao.VertexAttributePointer(1, 4, VertexAttribType.Float, 8, 2); + vao.VertexAttributePointer(3, 2, VertexAttribType.Float, 8, 6); + vao.Unbind(); + return vao; + } + + private static VertexArrayObject InitRectRoundVAO(GL gl) + { + VertexArrayObject vao = new(gl, new BufferObject(gl, [], BufferTargetARB.ArrayBuffer), null); + vao.Bind(); + vao.VertexAttributePointer(0, 2, VertexAttribType.Float, 12, 0); + vao.VertexAttributePointer(1, 4, VertexAttribType.Float, 12, 2); + vao.VertexAttributePointer(3, 2, VertexAttribType.Float, 12, 6); + vao.VertexAttributePointer(4, 4, VertexAttribType.Float, 12, 8); + vao.Unbind(); + return vao; + } + + private static VertexArrayObject InitTextVAO(GL gl) + { + VertexArrayObject vao = new(gl, new BufferObject(gl, [], BufferTargetARB.ArrayBuffer), null); + vao.Bind(); + vao.VertexAttributePointer(0, 2, VertexAttribType.Float, 9, 0); + vao.VertexAttributePointer(1, 4, VertexAttribType.Float, 9, 2); + vao.VertexAttributePointer(2, 3, VertexAttribType.Float, 9, 6); + vao.Unbind(); + return vao; + } + + public void Dispose() + { + foreach (var shader in shaders.Values) + shader.Dispose(); + foreach (var vao in vaos.Values) + vao.Dispose(); + shaders.Clear(); + vaos.Clear(); + } +} + +[Flags] +internal enum ShaderFeature : int +{ + None = 0, + Text = 1 << 0, + Texture = 1 << 1, + Outline = 1 << 2, + TextShadow = 1 << 3, + Alpha = 1 << 4, + Rounding = 1 << 5, + Blur = 1 << 6, + + /// + /// Used internally to count the number of possible features, do not move. + /// + MAX_VALUE +} + +internal enum ShaderVertexLayout +{ + Rect, + RectRound, + Char +} + +[StructLayout(LayoutKind.Explicit)] +readonly struct RectVert(Vector2 pos, Vector4 col, Vector2 texcoord) +{ + [FieldOffset(0)] public readonly Vector2 pos = pos; + [FieldOffset(0x8)] public readonly Vector4 col = col; + [FieldOffset(0x18)] public readonly Vector2 texcoord = texcoord; +} + +[StructLayout(LayoutKind.Explicit)] +readonly struct RectRoundVert(Vector2 pos, Vector4 col, Vector2 texcoord, Vector4 rounding) +{ + [FieldOffset(0)] public readonly Vector2 pos = pos; + [FieldOffset(0x8)] public readonly Vector4 col = col; + [FieldOffset(0x18)] public readonly Vector2 texcoord = texcoord; + /// + /// (XY: The size of the rectangle in pixels, Z: The rounding radius in pixels, W: The blur radius in pixels) + /// + [FieldOffset(0x20)] public readonly Vector4 rounding = rounding; +} + +[StructLayout(LayoutKind.Explicit)] +readonly struct CharVert(Vector2 pos, Vector4 col, Vector3 charData) +{ + [FieldOffset(0)] public readonly Vector2 pos = pos; + [FieldOffset(0x8)] public readonly Vector4 col = col; + /// + /// The uv coordinates into the font texture are stored in xy, and z stores the font weight. + /// + [FieldOffset(0x18)] public readonly Vector3 charData = charData; +} diff --git a/ArgonUI.Backends.OpenGL/Shaders/ui_frag.glsl b/ArgonUI.Backends.OpenGL/Shaders/ui_frag.glsl index 4ea0724..d137c75 100644 --- a/ArgonUI.Backends.OpenGL/Shaders/ui_frag.glsl +++ b/ArgonUI.Backends.OpenGL/Shaders/ui_frag.glsl @@ -1,24 +1,37 @@ #version 420 core +#ifdef SUPPORT_OUTLINE +#if !defined(SUPPORT_ROUNDING) +#define SUPPORT_ROUNDING +#endif +#endif + #ifdef SUPPORT_TEXT uniform sampler2D uFontTex; #endif // SUPPORT_TEXT + #ifdef SUPPORT_TEXTURE uniform sampler2D uMainTex; #endif // SUPPORT_TEXTURE out vec4 fragColor; + layout(location = 0) in vec4 v_color; + #ifdef SUPPORT_TEXT layout(location = 1) in vec3 v_char; #endif // SUPPORT_TEXT -#if defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) + +#if defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) layout(location = 2) in vec2 v_texcoord; #endif // defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) -#ifdef SUPPORT_ROUNDING -layout(location = 3) in flat vec3 v_size; + +#if defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) +layout(location = 3) in flat vec4 v_size; #endif // SUPPORT_ROUNDING + + #ifdef SUPPORT_TEXT float median(vec3 x) { return max(min(x.r, x.g), min(max(x.r, x.g), x.b)); @@ -46,6 +59,14 @@ float hint(float mask, float sdf) { } #endif // SUPPORT_TEXT +#ifdef SUPPORT_BLUR +float smootherstep(float edge0, float edge1, float x) { + x = clamp((x - edge0) / (edge1 - edge0), 0., 1.); + + return x * x * x * (x * (6. * x - 15.) + 10.); +} +#endif // SUPPORT_BLUR + void main() { fragColor = v_color; @@ -59,14 +80,14 @@ void main() { // Heuristic based hinting works, but has artifacts... //mask = hint(mask, sdf); fragColor.a *= mask; - #ifdef SUPPORT_SHADOW + #ifdef SUPPORT_TEXT_SHADOW float shadow_sdf = median(texture(uFontTex, v_char.xy-0.01).rgb); float shadow = smoothstep(max(v_char.z - smoothing*2., 0.05), min(v_char.z + smoothing*2., 0.95), shadow_sdf); float shadow_exp = smoothstep(max(v_char.z-.2 - smoothing, 0.05), min(v_char.z-.2 + smoothing, 0.95), shadow_sdf); float shadow_alpha = max(shadow_exp-fragColor.a, 0.); fragColor.rgb = (fragColor.rgb*0.2)*shadow_alpha + fragColor.rgb*(1.-shadow_alpha); fragColor.a = min(fragColor.a+shadow*0.75, 1.); - #endif // SUPPORT_SHADOW + #endif // SUPPORT_TEXT_SHADOW #endif // SUPPORT_TEXT #ifdef SUPPORT_TEXTURE @@ -74,14 +95,28 @@ void main() { #endif // SUPPORT_TEXTURE #ifdef SUPPORT_ROUNDING + #ifdef SUPPORT_BLUR + float blur = v_size.w; + #else + float blur = 0.; + #endif float radius = v_size.z; radius = min(radius, min(v_size.x, v_size.y)/4.); vec2 uv = v_texcoord * 2. - 1.; - vec2 r = abs(uv*v_size.xy/4.) - v_size.xy/4. + radius; + vec2 r = abs(uv*v_size.xy/4.) - v_size.xy/4. + radius + blur; float mask = length(max(r, 0.)) + min(max(r.x, r.y), 0.0) - radius; + + // Apply either the blur or the rounding + #ifdef SUPPORT_BLUR + mask = max(mask/blur, 0.); + fragColor.a *= exp(-(mask*mask*2.)) * (1.-mask*mask*mask); + #else fragColor.a *= smoothstep(0.5, -.25, mask); + #endif // SUPPORT_BLUR + #ifdef SUPPORT_OUTLINE - fragColor.rgb *= smoothstep(0.4, 1., abs(mask))*.5+.5; + float outlineThick = v_size.w; + fragColor.a = smoothstep(outlineThick, outlineThick-1., abs(mask+outlineThick)); #endif // SUPPORT_OUTLINE #endif // SUPPORT_ROUNDING diff --git a/ArgonUI.Backends.OpenGL/Shaders/ui_vert.glsl b/ArgonUI.Backends.OpenGL/Shaders/ui_vert.glsl index 61d11ab..1b90ff5 100644 --- a/ArgonUI.Backends.OpenGL/Shaders/ui_vert.glsl +++ b/ArgonUI.Backends.OpenGL/Shaders/ui_vert.glsl @@ -2,6 +2,12 @@ uniform vec2 uResolution; +#ifdef SUPPORT_OUTLINE +#if !defined(SUPPORT_ROUNDING) +#define SUPPORT_ROUNDING +#endif +#endif + layout(location = 0) in vec2 in_vert; layout(location = 1) in vec4 in_color; layout(location = 0) out vec4 v_color; @@ -10,16 +16,16 @@ layout(location = 0) out vec4 v_color; layout(location = 2) in vec3 in_char; layout(location = 1) out vec3 v_char; #endif // SUPPORT_TEXT -#if defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) +#if defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) layout(location = 3) in vec2 in_texcoord; layout(location = 2) out vec2 v_texcoord; -#endif // defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) -#ifdef SUPPORT_ROUNDING +#endif // defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) +#if defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) // To get the correct aspect ratio, in_size.xy stores the size of the rect to be rounded in pixels // in_size.z stores the rounding radius in pixels -layout(location = 4) in vec3 in_size; -layout(location = 3) out flat vec3 v_size; -#endif // T_SUPPORT_ROUNDING +layout(location = 4) in vec4 in_size; +layout(location = 3) out flat vec4 v_size; +#endif // SUPPORT_ROUNDING || SUPPORT_BLUR void main() { gl_Position = vec4((in_vert/uResolution.xy)*2.-1., -0.9, 1.0); @@ -28,10 +34,10 @@ void main() { #ifdef SUPPORT_TEXT v_char = in_char; #endif // SUPPORT_TEXT - #if defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) + #if defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) v_texcoord = in_texcoord; - #endif // defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) - #ifdef SUPPORT_ROUNDING + #endif // defined(SUPPORT_TEXTURE) || defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) + #if defined(SUPPORT_ROUNDING) || defined(SUPPORT_BLUR) v_size = in_size; - #endif // SUPPORT_ROUNDING + #endif // SUPPORT_ROUNDING || SUPPORT_BLUR } diff --git a/ArgonUI.Backends.OpenGL/Texture2D.cs b/ArgonUI.Backends.OpenGL/Texture2D.cs index 80eafd5..333b7db 100644 --- a/ArgonUI.Backends.OpenGL/Texture2D.cs +++ b/ArgonUI.Backends.OpenGL/Texture2D.cs @@ -31,6 +31,7 @@ public class Texture2D : ITextureHandle, IDisposable protected uint height; private static uint currentTexture = 0; + private static uint currentUnit = uint.MaxValue; private static Texture2D? missingTexture; public Texture2D(GL gl, string? name = null) @@ -117,9 +118,13 @@ public void Bind(uint unit) return; } - gl.ActiveTexture((TextureUnit)((int)TextureUnit.Texture0 + unit)); - gl.BindTexture(TextureTarget.Texture2D, handle); - currentTexture = handle; + if (currentTexture != handle || currentUnit != unit) + { + gl.ActiveTexture((TextureUnit)((int)TextureUnit.Texture0 + unit)); + gl.BindTexture(TextureTarget.Texture2D, handle); + currentTexture = handle; + currentUnit = unit; + } } internal void BindInvalid() diff --git a/ArgonUI.Docs/docfx.json b/ArgonUI.Docs/docfx.json index c1cf42c..f8a0865 100644 --- a/ArgonUI.Docs/docfx.json +++ b/ArgonUI.Docs/docfx.json @@ -7,7 +7,8 @@ "files": [ //"**/ArgonUI.csproj" "**/bin/**/ArgonUI*.dll" - ] + ], + "exclude": "*_Styles.g.cs", } ], "dest": "api", diff --git a/ArgonUI.Examples.DemoApp/Program.cs b/ArgonUI.Examples.DemoApp/Program.cs index 48d5b35..772472e 100644 --- a/ArgonUI.Examples.DemoApp/Program.cs +++ b/ArgonUI.Examples.DemoApp/Program.cs @@ -16,23 +16,24 @@ public static void Main(string[] args) wnd.RootElement.Style = new([ new Style([ - ArgonUIStyles.Rounding(10), + ArgonUIStyles.Rounding(5), //ArgonUIStyles.FontSize(30), + ArgonUIStyles.TextColour(Vector4.One) ]) ]); StyleSet buttonStyle = new([ new Style([ - ArgonUIStyles.Rounding(10), - ArgonUIStyles.Colour(new(0, 1.0f, 0.1f, 1)), + ArgonUIStyles.Rounding(5), + ArgonUIStyles.Colour(new(0.35f, 0.35f, 0.35f, 1)), ]), new Style(new HoveredSelector(), [ - ArgonUIStyles.Rounding(5), - ArgonUIStyles.Colour(new(1, .1f, 0, 1)), + ArgonUIStyles.Rounding(10), + ArgonUIStyles.Colour(new(0.45f, 0.45f, 0.45f, 1)), ]) ]); - wnd.RootElement.BGColour = new(0, 0.5f, 1, 1); + wnd.RootElement.BGColour = new(0.2f, 0.23f, 0.25f, 1); var stackPanel = new StackPanel(); stackPanel.InnerPadding = new(2); @@ -53,23 +54,25 @@ public static void Main(string[] args) int counter = 0; - btn.OnMouseDown += (im, button) => + btn.OnMouseDown += args => { #if DEBUG_LATENCY ((OpenGLWindow)wnd).LogLatency(DateTime.UtcNow.Ticks, "rect OnMouseDown"); rect.logLatencyNow = true; rect.Colour = (counter & 1) == 0 ? new(1, 0, 0, 1) : new(0, 0, 1, 1); #endif - //rect.Dirty(DirtyFlags.Content); + //rect.Dirty(DirtyFlag.Content); //Console.WriteLine("Rectangle clicked!"); label.Text = $"Hello World! {counter++}"; - btn.Colour = new(0.7f, 0.05f, 0, 1); + btn.Colour = new(0.30f, 0.30f, 0.30f, 1); + args.Handled = true; }; - btn.OnMouseUp += (im, button) => + btn.OnMouseUp += args => { btn.Colour = ((StylableProp)buttonStyle[1]["Colour"]).Value; + args.Handled = true; }; - btn.OnMouseEnter += (im, pos) => + btn.OnMouseEnter += args => { #if DEBUG_LATENCY ((OpenGLWindow)wnd).LogLatency(DateTime.UtcNow.Ticks, "rect OnMouseEnter"); @@ -87,16 +90,18 @@ public static void Main(string[] args) Button newButton = new(); newButton.Width = 20; newButton.Height = 20; + newButton.HorizontalAlignment = Alignment.Centre; + newButton.VerticalAlignment = Alignment.Centre; ((Label)newButton.Content!).FontSize = 8; newButton.Style = buttonStyle; newStackPanel.AddChild(newButton); - newButton.OnMouseEnter += (im, pos) => + newButton.OnMouseEnter += args => { newButton.Width = 24; newButton.Height = 24; }; - newButton.OnMouseLeave += (im, pos) => + newButton.OnMouseLeave += args => { newButton.Width = 20; newButton.Height = 20; diff --git a/ArgonUI.SourceGenerator.Test/ArgonUI.SourceGenerator.Test.csproj b/ArgonUI.SourceGenerator.Test/ArgonUI.SourceGenerator.Test.csproj index 684c7ab..dd00ccf 100644 --- a/ArgonUI.SourceGenerator.Test/ArgonUI.SourceGenerator.Test.csproj +++ b/ArgonUI.SourceGenerator.Test/ArgonUI.SourceGenerator.Test.csproj @@ -17,7 +17,7 @@ - + diff --git a/ArgonUI.SourceGenerator.Test/Program.cs b/ArgonUI.SourceGenerator.Test/Program.cs index 926cab5..c45085e 100644 --- a/ArgonUI.SourceGenerator.Test/Program.cs +++ b/ArgonUI.SourceGenerator.Test/Program.cs @@ -1,5 +1,5 @@ using ArgonUI.Styling; -using ArgonUI.UIElements; +using ArgonUI.UIElements.Abstract; using System.Numerics; namespace ArgonUI.SourceGenerator.Test; @@ -15,14 +15,14 @@ static void Main(string[] args) [UIClonable] public partial class ReactiveTest : UIElement { - [Reactive("SpecialExample")] private int exampleValue; + [Reactive("SpecialExample"), Stylable] private uint exampleValue; [Reactive(propName: "SpecialExample1")] private int exampleValue1; - [Reactive, Dirty(DirtyFlags.ChildContent)] private UIElement? test2; - [Reactive, Dirty(DirtyFlags.Layout), CustomGet(nameof(GetTest3))] private float test3; + [Reactive, Dirty(DirtyFlag.ChildContent)] private UIElement? test2; + [Reactive, Dirty(DirtyFlag.Layout), CustomGet(nameof(GetTest3))] private float test3; /// /// An example property. /// - [Reactive, Dirty(DirtyFlags.Layout), CustomSet(nameof(SetTest4)), Stylable] private float test4; + [Reactive, Dirty(DirtyFlag.Layout), CustomSet(nameof(SetTest4)), Stylable] private float test4; /// /// A vector example. /// @@ -68,7 +68,7 @@ internal static partial class Test_Styles public abstract class UIElement : ReactiveObject { - private DirtyFlags dirtyFlag; + private DirtyFlag dirtyFlag; public UIElement? Parent { get; set; } @@ -76,16 +76,16 @@ public abstract class UIElement : ReactiveObject /// Marks this element as dirty, forcing the UI engine to redraw this element and it's children when it's next dispatched. /// /// Which to set. - public virtual void Dirty(DirtyFlags flags) + public virtual void Dirty(DirtyFlag flags) { - UpdateProperty(ref dirtyFlag, dirtyFlag | flags, nameof(DirtyFlags)); + UpdateProperty(ref dirtyFlag, dirtyFlag | flags, nameof(DirtyFlag)); // Propagate dirty flags up - if ((flags & DirtyFlags.Layout) != 0) - Parent?.Dirty(DirtyFlags.ChildLayout); + if ((flags & DirtyFlag.Layout) != 0) + Parent?.Dirty(DirtyFlag.ChildLayout); - if ((flags & DirtyFlags.Content) != 0) - Parent?.Dirty(DirtyFlags.ChildContent); + if ((flags & DirtyFlag.Content) != 0) + Parent?.Dirty(DirtyFlag.ChildContent); } public virtual UIElement Clone() => throw new NotImplementedException(); diff --git a/ArgonUI.SourceGenerator/ArgonUI.SourceGenerator.csproj b/ArgonUI.SourceGenerator/ArgonUI.SourceGenerator.csproj index 0c23715..1f55b08 100644 --- a/ArgonUI.SourceGenerator/ArgonUI.SourceGenerator.csproj +++ b/ArgonUI.SourceGenerator/ArgonUI.SourceGenerator.csproj @@ -9,7 +9,7 @@ true true ArgonUI.SourceGenerator - 0.3.18-pre + 0.3.24-pre Thomas Mathieson Copyright © Thomas Mathieson 2025 https://github.com/space928/ArgonUI @@ -36,7 +36,7 @@ - + diff --git a/ArgonUI.SourceGenerator/MergeStylesGenerator.Emitter.cs b/ArgonUI.SourceGenerator/MergeStylesGenerator.Emitter.cs index d8a1982..0819c90 100644 --- a/ArgonUI.SourceGenerator/MergeStylesGenerator.Emitter.cs +++ b/ArgonUI.SourceGenerator/MergeStylesGenerator.Emitter.cs @@ -75,7 +75,7 @@ private static void Emit(SourceProductionContext context, MergeStylesResult resu using (sb.EnterCurlyBracket()) { foreach (var styled in prop.StyledTypes) - sb.AppendLine($"case {styled} _{styled}: _{styled}.{prop.Name} = typedProp.Value; break;"); + sb.AppendLine($"case {styled.Namespace}.{styled.Type} _{styled.Type}: _{styled.Type}.{prop.Name} = typedProp.Value; break;"); sb.AppendLine($"default: break;"); //sb.AppendLine($"default: throw new InvalidOperationException(\"Can't set property '{prop.Name}' on element of type '\" + elem.GetType().Name + \"'\");"); } diff --git a/ArgonUI.SourceGenerator/MergeStylesGenerator.Parser.cs b/ArgonUI.SourceGenerator/MergeStylesGenerator.Parser.cs index f02fe98..977b77f 100644 --- a/ArgonUI.SourceGenerator/MergeStylesGenerator.Parser.cs +++ b/ArgonUI.SourceGenerator/MergeStylesGenerator.Parser.cs @@ -88,6 +88,7 @@ public IEnumerable Parse(EquatableArray Parse(EquatableArray ReactiveFields); - public record MergedStylableProp(string Name, string Type, string? DocComment, EquatableArray StyledTypes); - public record MergedStylablePropTemp(string Name, string Type, string? DocComment, List StyledTypes); + public record MergedStylableProp(string Name, string Type, string? DocComment, EquatableArray StyledTypes); + public record MergedStylablePropTemp(string Name, string Type, string? DocComment, List StyledTypes); + public record MergedStylablePropType(string Type, string Namespace); } diff --git a/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Emitter.cs b/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Emitter.cs index ea17a87..3de7f8f 100644 --- a/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Emitter.cs +++ b/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Emitter.cs @@ -70,9 +70,9 @@ private static void Emit(SourceProductionContext context, ReactiveObjectResult r prefix = "this."; sb.AppendLine($"UpdateProperty(ref {prefix}{prop.FieldName}, value);"); } - if (prop.DirtyFlags != UIElements.DirtyFlags.None) + if (prop.DirtyFlags != UIElements.Abstract.DirtyFlag.None) { - sb.AppendLine($"Dirty(ArgonUI.UIElements.DirtyFlags.{prop.DirtyFlags});"); + sb.AppendLine($"Dirty(ArgonUI.UIElements.Abstract.DirtyFlag.{prop.DirtyFlags});"); } } } @@ -132,6 +132,9 @@ private static void EmitStylableClass(SourceProductionContext context, ReactiveO { foreach (var prop in model.ReactiveFields) { + if (prop.Stylable == null) + continue; + // Generate a factory method if (!string.IsNullOrEmpty(prop.DocComment)) Helpers.PrintDocComment(sb, prop.DocComment!); diff --git a/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Parser.cs b/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Parser.cs index 8cb4e60..72b9835 100644 --- a/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Parser.cs +++ b/ArgonUI.SourceGenerator/ReactiveObjectGenerator.Parser.cs @@ -1,4 +1,5 @@ using ArgonUI.UIElements; +using ArgonUI.UIElements.Abstract; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -73,9 +74,9 @@ public IEnumerable Parse(EquatableArray Parse(EquatableArray Parse(EquatableArray= 1) - dirtyFlags = (DirtyFlags)args[0].Value!; + dirtyFlags = (DirtyFlag)args[0].Value!; break; case nameof(StylableAttribute): stylable = new(); @@ -158,7 +159,7 @@ public record ReactiveObjectResult(ReactiveObjectClass? Class, Diagnostic? Diagn public record ReactiveObjectClass(Accessibility Accessibility, string Namespace, string Assembly, string ClassName, bool EnableNullable, EquatableArray ReactiveFields); public record ReactiveObjectField(string FieldType, string FieldName, string PropName, string? DocComment, - DirtyFlags DirtyFlags, string? OnGetFunc, bool GetInline, string? OnSetAction, bool SetInline, + DirtyFlag DirtyFlags, string? OnGetFunc, bool GetInline, string? OnSetAction, bool SetInline, string? CustomAccessibility, StylableField? Stylable); public record StylableField(); } diff --git a/ArgonUI.SourceGenerator/UIClonableGenerator.Emitter.cs b/ArgonUI.SourceGenerator/UIClonableGenerator.Emitter.cs index 5a69557..033f553 100644 --- a/ArgonUI.SourceGenerator/UIClonableGenerator.Emitter.cs +++ b/ArgonUI.SourceGenerator/UIClonableGenerator.Emitter.cs @@ -40,8 +40,11 @@ private static void Emit(SourceProductionContext context, UIClonableResult resul sb.AppendLine($"{model.Accessibility.GetText()} partial class {model.ClassName}"); using (sb.EnterCurlyBracket()) { - sb.AppendLine($"public override UIElement Clone() => Clone(new {model.ClassName}());"); - sb.AppendLine(); + if (!model.IsAbstract) + { + sb.AppendLine($"public override UIElement Clone() => Clone(new {model.ClassName}());"); + sb.AppendLine(); + } sb.AppendLine("public override UIElement Clone(UIElement target)"); using (sb.EnterCurlyBracket()) { diff --git a/ArgonUI.SourceGenerator/UIClonableGenerator.Parser.cs b/ArgonUI.SourceGenerator/UIClonableGenerator.Parser.cs index 0aae23b..a3b1c35 100644 --- a/ArgonUI.SourceGenerator/UIClonableGenerator.Parser.cs +++ b/ArgonUI.SourceGenerator/UIClonableGenerator.Parser.cs @@ -126,7 +126,7 @@ internal IEnumerable Parse(EquatableArray Parse(EquatableArray ClonableFields); + bool IsAbstract, bool EnableNullable, bool EnableCustomClone, EquatableArray ClonableFields); private record UIClonableField(string FieldName, bool HasCloneMethod); } diff --git a/ArgonUI/ArgonUI.csproj b/ArgonUI/ArgonUI.csproj index 53c577f..c0b7efb 100644 --- a/ArgonUI/ArgonUI.csproj +++ b/ArgonUI/ArgonUI.csproj @@ -7,7 +7,7 @@ disable enable ArgonUI - 0.4.1-pre + 0.5.1-pre Thomas Mathieson Copyright © Thomas Mathieson 2025 https://github.com/space928/ArgonUI diff --git a/ArgonUI/Bounds2D.cs b/ArgonUI/Bounds2D.cs index 8c3989b..4efa237 100644 --- a/ArgonUI/Bounds2D.cs +++ b/ArgonUI/Bounds2D.cs @@ -174,8 +174,8 @@ public readonly Bounds2D Union(Bounds2D bounds) /// A new bounds which has been shrunk. public readonly Bounds2D SubtractMargin(Thickness margin) { - var sub = new Vector4(-margin.left, -margin.top, margin.right, margin.bottom); - var res = _value - sub; + var add = new Vector4(margin.leftTop, -margin.right, -margin.bottom); + var res = _value + add; var ret = new Bounds2D(res); if (!ret.IsValid) ret.topLeft = ret.bottomRight = ret.Centre; @@ -189,14 +189,62 @@ public readonly Bounds2D SubtractMargin(Thickness margin) /// A new bounds which has been grown. public readonly Bounds2D AddMargin(Thickness margin) { - var sub = new Vector4(margin.left, margin.top, -margin.right, -margin.bottom); - var res = _value - sub; + var add = new Vector4(-margin.leftTop, margin.right, margin.bottom); + var res = _value + add; var ret = new Bounds2D(res); if (!ret.IsValid) ret.topLeft = ret.bottomRight = ret.Centre; return ret; } + /// + /// Creates a new with the same top-left coordinate as + /// bounds, but with the given . + /// + /// The final of the new bounds. + /// A new bounds with the given size. + public readonly Bounds2D WithSize(Vector2 size) + { + var tl = topLeft; + var br = size + tl; + return new(tl, br); + } + + /// + /// Creates a new with the same top-left coordinate as + /// bounds, but with the given . For any dimension of + /// less than or equal to zero, this bounds' original size is returned. + /// + /// The final of the new bounds. + /// A new bounds with the given size. + public readonly Bounds2D WithSizeNonZero(Vector2 size) + { + var tl = topLeft; + var br = size + tl; + if (size.X <= 0) + br.X = bottomRight.X; + if (size.Y <= 0) + br.Y = bottomRight.Y; + return new(tl, br); + } + + /// + /// Creates a new with the same coordinate as + /// bounds, but with the given . + /// + /// The final of the new bounds. + /// A new bounds with the given size. + public readonly Bounds2D WithSizeCentred(Vector2 size) + { + var centre = bottomRight + topLeft; + + var sizeV4 = new Vector4(-size, size.X, size.Y); + var res = new Vector4(centre, centre.X, centre.Y) + sizeV4; + res *= 0.5f; + + return new(res); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool Equals(Bounds2D other) => _value == other._value; [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/ArgonUI/Drawing/BMFont.cs b/ArgonUI/Drawing/BMFont.cs index f48f65c..a546f03 100644 --- a/ArgonUI/Drawing/BMFont.cs +++ b/ArgonUI/Drawing/BMFont.cs @@ -29,6 +29,7 @@ public class BMFont : Font private readonly ReadOnlyCollection kerningsRO; private FrozenDictionary> kerningsDictFrozen; private FrozenDictionary charsDictFrozen; + private readonly Dictionary prescaledAlternatives = []; /// /// This is the distance in pixels between each line of text. @@ -106,7 +107,13 @@ public class BMFont : Font /// public float Outline { get; private set; } + /// + /// The type of signed-distance field (if any) contained in the font's bitmap. + /// public BMFontSDFType SDFType { get; private set; } + /// + /// The distance range of the signed-distance field (if used) in pixels. + /// public float SDFDistanceRange { get; private set; } public ReadOnlyCollection Pages => pagesRO; @@ -124,6 +131,17 @@ public BMFont() charsDictFrozen = FrozenDictionary.Empty; } + /// + /// Trys to get a instance of this font optimised for rendering at the + /// given font size. + /// + /// The font size to target (integer values are recommended) + /// A instance for that font size or if one doesn't exist. + public BMFont? GetScaledFont(float fontSize) + { + return null; + } + /// /// Measures the approximate amount of space the font will require on screen in pixels. /// diff --git a/ArgonUI/Drawing/Gradient.cs b/ArgonUI/Drawing/Gradient.cs new file mode 100644 index 0000000..7be2bab --- /dev/null +++ b/ArgonUI/Drawing/Gradient.cs @@ -0,0 +1,79 @@ +using ArgonUI.SourceGenerator; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.Drawing; + +/// +/// Represents a gradient which can be applied to a UI element. +/// +public partial class Gradient : ReactiveObject +{ + [Reactive] private Vector4 colourTL; + [Reactive] private Vector4 colourTR; + [Reactive] private Vector4 colourBL; + [Reactive] private Vector4 colourBR; + + /// + /// Creates a new 4-point gradient. + /// + /// The top-left colour. + /// The top-right colour. + /// The bottom-left colour. + /// The bottom-right colour. + public Gradient(Vector4 colourTL, Vector4 colourTR, Vector4 colourBL, Vector4 colourBR) + { + this.colourTL = colourTL; + this.colourTR = colourTR; + this.colourBL = colourBL; + this.colourBR = colourBR; + } + + /// + /// Creates a new horizontal gradient from a start and end colour. + /// + /// + /// + /// + public static Gradient CreateHorizontal(Vector4 colourLeft, Vector4 colourRight) => new(colourLeft, colourRight, colourLeft, colourRight); + /// + /// Creates a new vertical gradient from a start and end colour. + /// + /// + /// + /// + public static Gradient CreateVertical(Vector4 colourTop, Vector4 colourBottom) => new(colourTop, colourTop, colourBottom, colourBottom); + /// + /// Creates a new diagonal gradient going from the top-left corner to the bottom-right corner. + /// + /// + /// + /// + public static Gradient CreateDiagonalTopLeft(Vector4 colourTopLeft, Vector4 colourBottomRight) + { + var mid = (colourTopLeft + colourBottomRight) * 0.5f; + return new(colourTopLeft, mid, mid, colourBottomRight); + } + /// + /// Creates a new diagonal gradient going from the bottom-left corner to the top-right corner. + /// + /// + /// + /// + public static Gradient CreateDiagonalBottomLeft(Vector4 colourBottomLeft, Vector4 colourTopRight) + { + var mid = (colourBottomLeft + colourTopRight) * 0.5f; + return new(mid, colourTopRight, colourBottomLeft, mid); + } + /// + /// Creates a new 4-point gradient. + /// + /// The top-left colour. + /// The top-right colour. + /// The bottom-left colour. + /// The bottom-right colour. + /// + public static Gradient CreateFourCorner(Vector4 colourTL, Vector4 colourTR, Vector4 colourBL, Vector4 colourBR) => new(colourTL, colourTR, colourBL, colourBR); +} diff --git a/ArgonUI/Drawing/IDrawContext.cs b/ArgonUI/Drawing/IDrawContext.cs index 2598d31..6fc4beb 100644 --- a/ArgonUI/Drawing/IDrawContext.cs +++ b/ArgonUI/Drawing/IDrawContext.cs @@ -53,23 +53,96 @@ public interface IDrawContext : IDisposable /// The corner rounding radius in pixels. public void DrawRect(Bounds2D bounds, Vector4 colour, float rounding); /// - /// Draws a string of text within the specified bounds. + /// Draws a rounded rectangle with a four-point gradient. /// - /// The bounds in which this string should be drawn. - /// Overflowing text will be truncated. - /// The font size to render the text with. - /// The string to draw. - /// The font to draw the string with. - /// The colour to draw the text in. - public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 colour); + /// The absolute window-space bounds of the rectangle. + /// The colour of the top-left corner. + /// The colour of the top-right corner. + /// The colour of the bottom-left corner. + /// The colour of the bottom-right corner. + /// The corner rounding radius in pixels. + public void DrawGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vector4 colourC, Vector4 colourD, float rounding); + /// + /// Draws a blurred, rounded rectangle. + /// + /// The absolute window-space bounds of the rectangle. + /// The colour of the rectangle. + /// The corner rounding radius in pixels. + /// The blur radius in pixels. + public void DrawShadow(Bounds2D bounds, Vector4 colour, float rounding, float blur); + /// + /// Draws an outline of a rounded rectangle. + /// + /// The absolute window-space bounds of the rectangle. + /// The colour of the rectangle. + /// The thickness of the outline in pixels. + /// The corner rounding radius in pixels. + public void DrawOutlineRect(Bounds2D bounds, Vector4 colour, float outlineThickness, float rounding); + /// + /// Draws an outline of a rounded rectangle with a four-point gradient. + /// + /// The absolute window-space bounds of the rectangle. + /// The colour of the top-left corner. + /// The colour of the top-right corner. + /// The colour of the bottom-left corner. + /// The colour of the bottom-right corner. + /// The thickness of the outline in pixels. + /// The corner rounding radius in pixels. + public void DrawOutlineGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vector4 colourC, Vector4 colourD, float outlineThickness, float rounding); + /// + /// Draws a straight line connecting the given start and end points. + /// + /// The colour of the line is interpolated from the start-point to the end-point. + /// + /// The starting point of the line, in window-space coordinates. + /// The end point of the line, in window-space coordinates. + /// The colour at the start of the line. + /// The colour at the end of the line. + /// The thickness of the line in pixels. + public void DrawLine(Vector2 start, Vector2 end, Vector4 colourStart, Vector4 colourEnd, float thickness); + /// + /// Draws a filled polygonal shape from a collection of points. + /// + /// Points should define a clockwise triangle-strip (see ). + /// + /// For instance to define the following shape, use the points described below: + /// + /// B---D---F + /// | \ | \ | + /// A---C---E + /// + /// positions = [A, B, C, D, E, F]; + /// + /// + /// The enumerable of points which defines the shape. + public void DrawPolyFill(IEnumerable points); + /// + /// Draws a series of line segments connecting the given points. + /// + /// The colour of the line is interpolated between each of the points. + /// + /// The enumerable of points which defines the line. + /// The thickness of the line in pixels. + public void DrawPolyLine(IEnumerable points, float thickness); + /// + /// Draws a rounded rectangle filled with a texture. + /// + /// The absolute window-space bounds of the rectangle. + /// The texture to fill this rectangle with. + /// The corner rounding radius in pixels. + public void DrawTexture(Bounds2D bounds, ITextureHandle texture, float rounding); + // TODO: Currently text shaping is done purely by font metrics and text is always drawn left-to-right, + // for compatibility with non-latin writing systems, shaping should really be done during the layout + // phase and passed in to this method. Maybe we could have a ShapedFont class deriving from Font + // which implements this? /// /// Draws a string of text within the specified bounds. /// /// The bounds in which this string should be drawn. /// Overflowing text will be truncated. - /// The font size to render the text with. /// The string to draw. /// The font to draw the string with. + /// The font size to render the text with. /// The colour to draw the text in. /// The space between words, represented by the size of the space character in pixels. /// An adjustment to the space between individual characters in pixels. @@ -77,7 +150,7 @@ public interface IDrawContext : IDisposable /// The weight to render the characters with, where 0.5 /// represents the font's native weight. Values close to 0 or 1 are likely to show visual artifacts. /// A width scale factor to stretch the font. A value of 1 represents no stretching. - public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 colour, + public void DrawText(Bounds2D bounds, ReadOnlySpan s, BMFont font, float size, Vector4 colour, float wordSpacing = 0, float charSpacing = 0, float skew = 0, float weight = 0.5f, float width = 1); /// /// Draws a single character within the specified bounds. @@ -88,15 +161,6 @@ public void DrawText(Bounds2D bounds, float size, string s, BMFont font, Vector4 /// The font to draw the char with. /// The colour to draw the char in. public void DrawChar(Bounds2D bounds, float size, char c, BMFont font, Vector4 colour); - /// - /// Draws a rounded rectangle filled with a texture. - /// - /// The absolute window-space bounds of the rectangle. - /// The texture to fill this rectangle with. - /// The corner rounding radius in pixels. - public void DrawTexture(Bounds2D bounds, ITextureHandle texture, float rounding); - public void DrawGradient(Bounds2D bounds, Vector4 colourA, Vector4 colourB, Vector4 colourC, Vector4 colourD, float rounding); - public void DrawShadow(Bounds2D bounds, Vector4 colour, float rounding, float blur); /// /// Draw calls are expected to be automatically batched by the implementor to improve performance. @@ -115,4 +179,10 @@ public ITextureHandle LoadTexture(TextureData data, string? name = null, #if DEBUG_LATENCY public void MarkLatencyTimerEnd(string? msg = null); #endif + + public struct PolyVert + { + public Vector2 pos; + public Vector4 colour; + } } diff --git a/ArgonUI/Helpers/ObjectPool.cs b/ArgonUI/Helpers/ObjectPool.cs new file mode 100644 index 0000000..d1c0534 --- /dev/null +++ b/ArgonUI/Helpers/ObjectPool.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.Helpers; + +/// +/// Represents a very simple, non thread-safe pool of objects. +/// +/// +public static class ObjectPool + where T : class, new() +{ + private const int MAX_POOLED_ITEMS = 128; + private static readonly Stack pooledObjects = []; + + public static Action? factoryMethod; + + public static T Rent() + { + if (pooledObjects.TryPop(out var res)) + return res; + + return new(); + } + + public static void Return(T obj) + { + if (pooledObjects.Count < MAX_POOLED_ITEMS) + pooledObjects.Push(obj); + } +} diff --git a/ArgonUI/Helpers/PropertyChangedArgsPool.cs b/ArgonUI/Helpers/PropertyChangedArgsPool.cs index 21d69f1..19e3c7e 100644 --- a/ArgonUI/Helpers/PropertyChangedArgsPool.cs +++ b/ArgonUI/Helpers/PropertyChangedArgsPool.cs @@ -1,5 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.ComponentModel; +using System.Threading; namespace ArgonUI.Helpers; @@ -11,40 +14,123 @@ namespace ArgonUI.Helpers; /// public static class PropertyChangedArgsPool { - private static readonly Stack propChangedPool = []; - private static readonly Stack propChangingPool = []; + //private static readonly Stack propChangedPool = []; + //private static readonly Stack propChangingPool = []; + + private static readonly ReusablePropertyChangedEventArgs?[] propChangedPool = new ReusablePropertyChangedEventArgs?[MAX_POOLED_ITEMS]; + private static readonly ReusablePropertyChangingEventArgs?[] propChangingPool = new ReusablePropertyChangingEventArgs?[MAX_POOLED_ITEMS]; + + private static ReusablePropertyChangedEventArgs? propChangedInst; + private static ReusablePropertyChangingEventArgs? propChangingInst; + private const int MAX_POOLED_ITEMS = 128; public static ReusablePropertyChangedEventArgs RentChanged(string? propName) { - if (propChangedPool.TryPop(out var res)) + var res = propChangedInst; + if (res != null && res == Interlocked.CompareExchange(ref propChangedInst, null, res)) { res.propertyName = propName; return res; } + return RentChangedSlow(propName); + } + + private static ReusablePropertyChangedEventArgs RentChangedSlow(string? propName) + { + for (int i = 0; i < propChangedPool.Length; i++) + { + var res = propChangedPool[i]; + if (res != null) + { + if (res == Interlocked.CompareExchange(ref propChangedPool[i], null, res)) + { + res.propertyName = propName; + return res; + } + break; + } + } return new(propName); } public static ReusablePropertyChangingEventArgs RentChanging(string? propName) { - if (propChangingPool.TryPop(out var res)) + var res = propChangingInst; + if (res != null && res == Interlocked.CompareExchange(ref propChangingInst, null, res)) { res.propertyName = propName; return res; } + return RentChangingSlow(propName); + } + + private static ReusablePropertyChangingEventArgs RentChangingSlow(string? propName) + { + for (int i = 0; i < propChangingPool.Length; i++) + { + var res = propChangingPool[i]; + if (res != null) + { + if (res == Interlocked.CompareExchange(ref propChangingPool[i], null, res)) + { + res.propertyName = propName; + return res; + } + break; + } + } return new(propName); } public static void Return(ReusablePropertyChangedEventArgs e) { - if (propChangedPool.Count < MAX_POOLED_ITEMS) - propChangedPool.Push(e); + // No need to interlock here, worst case is we overwrite an existing pooled object, no big deal. + if (propChangedInst == null) + { + propChangedInst = e; + return; + } + + ReturnSlow(e); + } + + private static void ReturnSlow(ReusablePropertyChangedEventArgs e) + { + var items = propChangedPool; + for (var i = 0; i < items.Length; i++) + { + if (items[i] == null) + { + items[i] = e; + break; + } + } } public static void Return(ReusablePropertyChangingEventArgs e) { - if (propChangingPool.Count < MAX_POOLED_ITEMS) - propChangingPool.Push(e); + // No need to interlock here, worst case is we overwrite an existing pooled object, no big deal. + if (propChangingInst == null) + { + propChangingInst = e; + return; + } + + ReturnSlow(e); + } + + private static void ReturnSlow(ReusablePropertyChangingEventArgs e) + { + var items = propChangingPool; + for (var i = 0; i < items.Length; i++) + { + if (items[i] == null) + { + items[i] = e; + break; + } + } } } diff --git a/ArgonUI/Input/InputManager.cs b/ArgonUI/Input/InputManager.cs index 418eab9..f634083 100644 --- a/ArgonUI/Input/InputManager.cs +++ b/ArgonUI/Input/InputManager.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Text; using System.Threading.Tasks; +using ArgonUI.Helpers; using ArgonUI.UIElements; namespace ArgonUI.Input; @@ -16,12 +17,13 @@ public class InputManager { private readonly ArgonManager argonManager; private long lastClickTime; - private long lastMouseMoveTime; + //private long lastMouseMoveTime; private VectorInt2 lastMousePos; private UIElement? lastHoveredElement; private UIElement? kbFocussedElement; private UIElement? mouseCaptureElement; private readonly Dictionary pressedKeys; + private readonly long doubleClickTime; public InputManager(ArgonManager argonManager) { @@ -33,20 +35,39 @@ public InputManager(ArgonManager argonManager) #else pressedKeys = new(Enum.GetValues().Select(x => new KeyValuePair(x, false))); #endif + doubleClickTime = TimeSpan.FromMilliseconds(300).Ticks; } /// /// Gets or sets whichever element currently has keyboard focus. /// A value of indicates no element has keyboard focus. /// - public UIElement? FocussedElement - { + public UIElement? FocussedElement + { get => kbFocussedElement; set { - kbFocussedElement?.InvokeOnFocusLost(this); + if (value == kbFocussedElement) + return; + + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = kbFocussedElement; + var obj = kbFocussedElement; + do + { + obj?.InvokeOnFocusLost(args); + } while (!args.Handled && (obj = obj?.Parent) != null); + kbFocussedElement = value; - kbFocussedElement?.InvokeOnFocusGot(this); + + obj = kbFocussedElement; + do + { + obj?.InvokeOnFocusGot(args); + } while (!args.Handled && (obj = obj?.Parent) != null); + + ObjectPool.Return(args); } } @@ -87,11 +108,35 @@ internal void OnMouseMove(UIWindow sender, VectorInt2 mousePos) #if DEBUG && DEBUG_PROP_UPDATES Debug.WriteLine($"[InputManager] Hovered element changed {lastHoveredElement} -> {hit}"); #endif - lastHoveredElement?.InvokeOnMouseLeave(this, mousePos); - hit?.InvokeOnMouseEnter(this, mousePos); + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = lastHoveredElement; + args.MousePosition = mousePos; + var obj = lastHoveredElement; + do + { + obj?.InvokeOnMouseLeave(args); + } while (!args.Handled && (obj = obj?.Parent) != null); + + args.Target = hit; + obj = hit; + do + { + obj?.InvokeOnMouseEnter(args); + } while (!args.Handled && (obj = obj?.Parent) != null); + ObjectPool.Return(args); } - hit?.InvokeOnMouseOver(this, mousePos); + var hoverArgs = ObjectPool.Rent(); + hoverArgs.InputManager = this; + hoverArgs.Target = hit; + hoverArgs.MousePosition = mousePos; + var hoverObj = hit; + do + { + hoverObj?.InvokeOnMouseOver(hoverArgs); + } while (!hoverArgs.Handled && (hoverObj = hoverObj?.Parent) != null); + ObjectPool.Return(hoverArgs); lastHoveredElement = hit; lastMousePos = mousePos; @@ -99,29 +144,83 @@ internal void OnMouseMove(UIWindow sender, VectorInt2 mousePos) internal void OnMouseUp(UIWindow sender, MouseButton mouseButton) { - lastHoveredElement?.InvokeOnMouseUp(this, mouseButton); + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = lastHoveredElement; + args.MouseButton = mouseButton; + var hoverObj = lastHoveredElement; + do + { + hoverObj?.InvokeOnMouseUp(args); + } while (!args.Handled && (hoverObj = hoverObj?.Parent) != null); + ObjectPool.Return(args); } internal void OnMouseDown(UIWindow sender, MouseButton mouseButton) { - lastHoveredElement?.InvokeOnMouseDown(this, mouseButton); + var now = DateTime.UtcNow.Ticks; + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = lastHoveredElement; + args.MouseButton = mouseButton; + var hoverObj = lastHoveredElement; + if (now - lastClickTime <= doubleClickTime) + { + do + { + hoverObj?.InvokeOnDoubleClick(args); + } while (!args.Handled && (hoverObj = hoverObj?.Parent) != null); + } + do + { + hoverObj?.InvokeOnMouseDown(args); + } while (!args.Handled && (hoverObj = hoverObj?.Parent) != null); + ObjectPool.Return(args); + lastClickTime = now; } internal void OnMouseWheel(UIWindow sender, Vector2 delta) { - lastHoveredElement?.InvokeOnMouseWheel(this, delta); + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = lastHoveredElement; + args.MouseScroll = delta; + var hoverObj = lastHoveredElement; + do + { + hoverObj?.InvokeOnMouseWheel(args); + } while (!args.Handled && (hoverObj = hoverObj?.Parent) != null); + ObjectPool.Return(args); } internal void OnKeyDown(UIWindow sender, KeyCode key) { pressedKeys[key] = true; - lastHoveredElement?.InvokeOnKeyDown(this, key); + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = FocussedElement ?? lastHoveredElement; + args.Key = key; + var hoverObj = args.Target; + do + { + hoverObj?.InvokeOnKeyDown(args); + } while (!args.Handled && (hoverObj = hoverObj?.Parent) != null); + ObjectPool.Return(args); } internal void OnKeyUp(UIWindow sender, KeyCode key) { pressedKeys[key] = false; - lastHoveredElement?.InvokeOnKeyUp(this, key); + var args = ObjectPool.Rent(); + args.InputManager = this; + args.Target = FocussedElement ?? lastHoveredElement; + args.Key = key; + var hoverObj = args.Target; + do + { + hoverObj?.InvokeOnKeyUp(args); + } while (!args.Handled && (hoverObj = hoverObj?.Parent) != null); + ObjectPool.Return(args); } /// @@ -181,3 +280,83 @@ public enum MouseButton Mouse8, Mouse9, } + +public class InputEventArgs(InputManager? inputManager, UIElement? target, bool handled) +{ + /// + /// The input manager which sent this event. + /// + public InputManager? InputManager { get; internal set; } = inputManager; + /// + /// The which initially received this event. + /// + public UIElement? Target { get; internal set; } = target; + /// + /// Whether this event has been handled yet. Once set to , this event + /// will stop propagating up to parent elements. + /// + public bool Handled { get; set; } = handled; + + public InputEventArgs() : this(null, null, false) { } +} + +public sealed class FocusInputEventArgs : InputEventArgs +{ + public FocusInputEventArgs(InputManager? inputManager, UIElement? target, bool handled) : base(inputManager, target, handled) + { + } + + public FocusInputEventArgs() : this(null, null, false) { } +} + +public sealed class MousePositionInputEventArgs : InputEventArgs +{ + public VectorInt2 MousePosition { get; internal set; } + + public MousePositionInputEventArgs(InputManager? inputManager, UIElement? target, bool handled, VectorInt2 mousePos) + : base(inputManager, target, handled) + { + MousePosition = mousePos; + } + + public MousePositionInputEventArgs() : this(null, null, false, VectorInt2.Zero) { } +} + +public sealed class MouseButtonInputEventArgs : InputEventArgs +{ + public MouseButton MouseButton { get; internal set; } + + public MouseButtonInputEventArgs(InputManager? inputManager, UIElement? target, bool handled, MouseButton mouseButton) + : base(inputManager, target, handled) + { + MouseButton = mouseButton; + } + + public MouseButtonInputEventArgs() : this(null, null, false, MouseButton.Mouse0) { } +} + +public sealed class MouseScrollInputEventArgs : InputEventArgs +{ + public Vector2 MouseScroll { get; internal set; } + + public MouseScrollInputEventArgs(InputManager? inputManager, UIElement? target, bool handled, Vector2 mouseScroll) + : base(inputManager, target, handled) + { + MouseScroll = mouseScroll; + } + + public MouseScrollInputEventArgs() : this(null, null, false, Vector2.Zero) { } +} + +public sealed class KeyboardInputEventArgs : InputEventArgs +{ + public KeyCode Key { get; internal set; } + + public KeyboardInputEventArgs(InputManager? inputManager, UIElement? target, bool handled, KeyCode key) + : base(inputManager, target, handled) + { + Key = key; + } + + public KeyboardInputEventArgs() : this(null, null, false, KeyCode.Unknown) { } +} diff --git a/ArgonUI/SourceGenerator/DirtyAttribute.cs b/ArgonUI/SourceGenerator/DirtyAttribute.cs index 280fdfd..000978c 100644 --- a/ArgonUI/SourceGenerator/DirtyAttribute.cs +++ b/ArgonUI/SourceGenerator/DirtyAttribute.cs @@ -1,4 +1,5 @@ using ArgonUI.UIElements; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Linq; @@ -9,14 +10,14 @@ namespace ArgonUI.SourceGenerator; /// /// When used on a property generated using a , generates a call to -/// the method when the property is set. +/// the method when the property is set. /// This attribute can only be used in classes which derive from . /// /// The dirty flags to set when the generated property is set. [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] -public sealed class DirtyAttribute(DirtyFlags dirtyFlags) : Attribute +public sealed class DirtyAttribute(DirtyFlag dirtyFlags) : Attribute { - private readonly DirtyFlags dirtyFlags = dirtyFlags; + private readonly DirtyFlag dirtyFlags = dirtyFlags; - public DirtyFlags DirtyFlags => dirtyFlags; + public DirtyFlag DirtyFlags => dirtyFlags; } diff --git a/ArgonUI/Thickness.cs b/ArgonUI/Thickness.cs index 97596cb..85301ee 100644 --- a/ArgonUI/Thickness.cs +++ b/ArgonUI/Thickness.cs @@ -1,6 +1,9 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if !NETSTANDARD +using System.Runtime.Intrinsics; +#endif namespace ArgonUI; @@ -10,15 +13,34 @@ namespace ArgonUI; [StructLayout(LayoutKind.Explicit)] public struct Thickness { - // Specified as a vector of (Top, Right, Bottom, Left). + /// + /// Specified as a vector of (Left, Top, Right, Bottom). + /// + /// This is an alias for the , , , and fields. + /// [FieldOffset(0x0)] public Vector4 value; - [FieldOffset(0x0)] public float top; + + /// + /// Alias for (, ). + /// + [FieldOffset(0x0)] public Vector2 leftTop; + /// + /// Alias for (, ). + /// + [FieldOffset(0x8)] public Vector2 rightBottom; + + [FieldOffset(0x0)] public float left; [FieldOffset(0x4)] public float right; - [FieldOffset(0x8)] public float bottom; - [FieldOffset(0xC)] public float left; + [FieldOffset(0x8)] public float top; + [FieldOffset(0xC)] public float bottom; public static Thickness Zero => new(); + /// + /// Gets the total width and height of this . + /// + public readonly Vector2 Size => leftTop + rightBottom; + public Thickness(float all) { Unsafe.SkipInit(out this); @@ -42,7 +64,7 @@ public Thickness(float top, float right, float bottom, float left) } /// - /// Constructs a thickness from a vector of (Top, Right, Bottom, Left). + /// Constructs a thickness from a vector of (Left, Top, Right, Bottom). /// /// public Thickness(Vector4 value) diff --git a/ArgonUI/UIElements/Abstract/DirtyFlag.cs b/ArgonUI/UIElements/Abstract/DirtyFlag.cs new file mode 100644 index 0000000..d83cb7d --- /dev/null +++ b/ArgonUI/UIElements/Abstract/DirtyFlag.cs @@ -0,0 +1,45 @@ +using System; + +namespace ArgonUI.UIElements.Abstract; + +/// +/// Flags used to indicate if a needs +/// to redrawing or re-laying out. +/// +[Flags] +public enum DirtyFlag +{ + None, + /// + /// The layout of this element is invalid, it's + /// may need to change. Dirtying the layout of an element implies that it's + /// is also dirty. + /// + Layout = 1 << 0, + /// + /// The content of this element is invalid, it's bounds have not changed but a property + /// affecting it's inner content (such as it's colour) has changed. + /// + Content = 1 << 1, + /// + /// A child element of this has a dirty . + /// + ChildLayout = 1 << 2, + /// + /// A child element of this has a dirty . + /// + ChildContent = 1 << 3, + + /// + /// Both the and are dirty. + /// + ContentAndLayout = Content | Layout, + /// + /// Both the and are dirty. + /// + ChildContentAndLayout = ChildContent | ChildLayout, + /// + /// All s are set. + /// + All = ContentAndLayout | ChildContentAndLayout, +} diff --git a/ArgonUI/UIElements/ElementPresenterBase.cs b/ArgonUI/UIElements/Abstract/ElementPresenterBase.cs similarity index 98% rename from ArgonUI/UIElements/ElementPresenterBase.cs rename to ArgonUI/UIElements/Abstract/ElementPresenterBase.cs index 29b3e92..e76d3a1 100644 --- a/ArgonUI/UIElements/ElementPresenterBase.cs +++ b/ArgonUI/UIElements/Abstract/ElementPresenterBase.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace ArgonUI.UIElements; +namespace ArgonUI.UIElements.Abstract; /// /// Abstract class for a UIElement which contains a single child element. diff --git a/ArgonUI/UIElements/Abstract/IRectangleProps.cs b/ArgonUI/UIElements/Abstract/IRectangleProps.cs new file mode 100644 index 0000000..7c9a161 --- /dev/null +++ b/ArgonUI/UIElements/Abstract/IRectangleProps.cs @@ -0,0 +1,41 @@ +using ArgonUI.Drawing; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.UIElements.Abstract; + +/// +/// Represents the properties which a generic rectangle should expose. +/// +/// +/// This interface exists to help ensure that s which draw a rectangle have feature and property name parity. +/// +internal interface IRectangleProps +{ + /// + /// The colour of this rectangle. + /// + public Vector4 Colour { get; set; } + /// + /// The outline colour. + /// + public Vector4 OutlineColour { get; set; } + /// + /// The thickness of the outline in pixels. + /// + public float OutlineThickness { get; set; } + /// + /// The radius of the corners of this rectangle. + /// + public float Rounding { get; set; } + /// + /// The texture to fill the rectangle with. + /// + public ArgonTexture? Texture { get; set; } + /// + /// The gradient to fill the rectangle with. + /// + public Gradient? GradientFill { get; set; } +} diff --git a/ArgonUI/UIElements/Panel.cs b/ArgonUI/UIElements/Abstract/Panel.cs similarity index 73% rename from ArgonUI/UIElements/Panel.cs rename to ArgonUI/UIElements/Abstract/Panel.cs index 56a50d9..264c77d 100644 --- a/ArgonUI/UIElements/Panel.cs +++ b/ArgonUI/UIElements/Abstract/Panel.cs @@ -1,5 +1,6 @@ using ArgonUI.Drawing; using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -9,18 +10,16 @@ using System.Text; using System.Threading.Tasks; -namespace ArgonUI.UIElements; +namespace ArgonUI.UIElements.Abstract; +/// +/// A very basic type of which draws each of it's children in order, on top of each other. +/// [UIClonable] -public partial class Panel : UIContainer +public partial class Panel : UIContainerRectangle { private readonly List children; private readonly ReadOnlyCollection childrenRO; - /// - /// The background colour of this panel. - /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] - private Vector4 colour; public override IReadOnlyList Children => childrenRO; @@ -76,22 +75,16 @@ public override void ClearChildren() } } - protected internal override VectorInt2 Measure() + protected internal override Vector2 Measure() { if (children.Count == 0) return base.Measure(); - VectorInt2 res = VectorInt2.Zero; + var res = Vector2.Zero; foreach (var child in children) - res = VectorInt2.Max(res, child.desiredSize); - res += new VectorInt2((int)(InnerPadding.left + InnerPadding.right), (int)(InnerPadding.top + InnerPadding.bottom)); + res = Vector2.Max(res, child.desiredSize); + res += InnerPadding.Size; return res; } - - protected internal override void Draw(IDrawContext ctx) - { - if (colour.W != 0) - ctx.DrawRect(RenderedBoundsAbsolute, colour, 0); - } } diff --git a/ArgonUI/UIElements/Abstract/RectangleBase.cs b/ArgonUI/UIElements/Abstract/RectangleBase.cs new file mode 100644 index 0000000..0cf4996 --- /dev/null +++ b/ArgonUI/UIElements/Abstract/RectangleBase.cs @@ -0,0 +1,89 @@ +using ArgonUI.Drawing; +using ArgonUI.SourceGenerator; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.UIElements.Abstract; + +/// +/// Represents the base class for all s which draw a rectangle background. +/// +[UIClonable] +public abstract partial class RectangleBase : UIElement, IRectangleProps +{ + /// + /// The colour of this rectangle. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected Vector4 colour; + /// + /// The outline colour. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected Vector4 outlineColour; + /// + /// The thickness of the outline in pixels. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected float outlineThickness; + /// + /// The radius of the corners of this rectangle. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected float rounding; + /// + /// The texture to fill the rectangle with. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected ArgonTexture? texture; + /// + /// The gradient to fill the rectangle with. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected Gradient? gradientFill; + + +#if DEBUG_LATENCY + public bool logLatencyNow; +#endif + + /// + /// Draws this rectangle using the given drawing context. + /// + /// This method exists outside of so that elements derived + /// from this class can call the rectangle drawing method whenever they want within their own + /// method implementation. + /// + /// + protected void DrawRectangle(IDrawContext ctx) + { + if (texture != null) + { + texture.ExecuteDrawCommands(ctx); + if (!texture.IsLoaded) + { + // Can't render yet, the font texture isn't ready, try again next frame. + Dirty(DirtyFlag.Content); + return; + } + ctx.DrawTexture(RenderedBoundsAbsolute, texture.TextureHandle!, Rounding); + } + else if (gradientFill != null) + { + ctx.DrawGradient(RenderedBoundsAbsolute, gradientFill.ColourTL, gradientFill.ColourTR, gradientFill.ColourBL, gradientFill.ColourBR, Rounding); + } + else if (Colour.W > 0) + { + ctx.DrawRect(RenderedBoundsAbsolute, Colour, Rounding); + } + + if (outlineThickness > 0 && outlineColour.W > 0) + ctx.DrawOutlineRect(RenderedBoundsAbsolute, outlineColour, outlineThickness, rounding); +#if DEBUG_LATENCY + if (logLatencyNow) + commands.Add(ctx => ctx.MarkLatencyTimerEnd($"{Colour.Y}")); + logLatencyNow = false; +#endif + } + + protected internal override void Draw(IDrawContext ctx) + { + DrawRectangle(ctx); + } +} diff --git a/ArgonUI/UIElements/Abstract/TextBase.cs b/ArgonUI/UIElements/Abstract/TextBase.cs new file mode 100644 index 0000000..de94677 --- /dev/null +++ b/ArgonUI/UIElements/Abstract/TextBase.cs @@ -0,0 +1,165 @@ +using ArgonUI.Drawing; +using ArgonUI.SourceGenerator; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.UIElements.Abstract; + +/// +/// The base class for any which draws simple text. For more complex text drawing, see . +/// +[UIClonable] +public abstract partial class TextBase : UIElement +{ + /// + /// The text represented by this element. + /// + [Reactive, Dirty(DirtyFlag.Layout)] + protected string? text; + /// + /// The font size of this element's text. + /// + [Reactive("FontSize"), Dirty(DirtyFlag.Layout), Stylable] + protected float size; + /// + /// The text colour of this element's text. + /// + [Reactive("TextColour"), Dirty(DirtyFlag.Layout), Stylable] + protected Vector4 textColour; + /// + /// The font used by this element's text. + /// + [Reactive, Dirty(DirtyFlag.Layout), Stylable] + protected BMFont font; + /// + /// Specifies how the text in this element is horizontally aligned. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected TextAlignment textAlignment; + + // TODO: Finish implementing + /// + /// Adds or subtracts space between individual words, measured in pixels. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float wordSpacing; + /// + /// Adds or subtracts space between individual characters, measured in pixels. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float charSpacing; + /// + /// The horizontal scaling factor applied to all characters in the text. Values smaller than 1 + /// will squeeze the text, and values greater than 1 will stretch it. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float stretchX = 1; + + /// + /// A horizontal skew to apply to the text, useful for creating faux-italic type. A value of + /// 0 represents no skew. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float skew = 0; + /// + /// The weight of the font when using SDF-based fonts. This is not a true variable-weight + /// axis control, but rather an adjustment made to the rendered SDF font, it's useful for + /// faux-bold type. A value of 0.5 represents the font's native weight. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float weight = 0.5f; + + // Note that in the future this will store the shaped positions of each glyph to be rendered. + /// + /// This stores the bounds of the label as measured by the font engine. This is updated automatically + /// whenever the UI engine re-measures the element. + /// + protected Bounds2D measuredBounds; + + public TextBase() + { + text = string.Empty; + size = 14; + textColour = new(0, 0, 0, 1); + font = Fonts.Default; + } + + public TextBase(string? text) : this() + { + this.text = text; + } + + protected internal override Vector2 Measure() + { + var res = Font.Measure(text, size, 1); + measuredBounds = res; + return res.Size; + } + + protected override Bounds2D ComputeBounds(Bounds2D parent) + { + var bounds = base.ComputeBounds(parent); + // Apply an adjustment to the bounds to correct the vertical centering. + switch (VerticalAlignment) + { + case Alignment.Top: + break; + case Alignment.Bottom: + break; + case Alignment.Centre: + case Alignment.Stretch: + /*float emHeight; + //if (font.CharsDict.TryGetValue('M', out var xChar)) + // emHeight = xChar.size.Y; + //else + emHeight = font.Size * 1.333f; + emHeight *= 0.5f; + emHeight = 0; + float offset = (font.Base - emHeight) * (size / font.Size); + bounds.topLeft.Y -= offset; + bounds.bottomRight.Y -= offset;*/ + float offset = measuredBounds.topLeft.Y; + bounds.topLeft.Y -= offset; + bounds.bottomRight.Y -= offset; + + break; + } + + return bounds; + } + + /// + /// Draws this text using the given drawing context. + /// + /// This method exists outside of so that elements derived + /// from this class can call the text drawing method whenever they want within their own + /// method implementation. + /// + /// + protected void DrawText(IDrawContext ctx) + { + if (string.IsNullOrEmpty(text)) + return; + + var fnt = font; + var tex = fnt.FontTexture; + if (tex == null) + return; + + tex.ExecuteDrawCommands(ctx); + if (!tex.IsLoaded) + { + // Can't render yet, the font texture isn't ready, try again next frame. + Dirty(DirtyFlag.Content); + return; + } + ctx.DrawText(RenderedBoundsAbsolute, text.AsSpan(), fnt, size, textColour, wordSpacing, charSpacing, skew, weight, stretchX); + } + + protected internal override void Draw(IDrawContext ctx) + { + DrawText(ctx); + } +} diff --git a/ArgonUI/UIElements/Abstract/TextBlockBase.cs b/ArgonUI/UIElements/Abstract/TextBlockBase.cs new file mode 100644 index 0000000..16f3a50 --- /dev/null +++ b/ArgonUI/UIElements/Abstract/TextBlockBase.cs @@ -0,0 +1,207 @@ +using ArgonUI.Drawing; +using ArgonUI.SourceGenerator; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.UIElements.Abstract; + +/// +/// The base class for any which draws text on top of a . +/// +/// +/// With the exception of text wrapping and a background rectangle, this class should have feature parity with . +/// +[UIClonable] +public abstract partial class TextBlockBase : RectangleBase +{ + /// + /// The text represented by this element. + /// + [Reactive, Dirty(DirtyFlag.Layout)] + protected string? text; + /// + /// The font size of this element's text. + /// + [Reactive("FontSize"), Dirty(DirtyFlag.Layout), Stylable] + protected float size; + /// + /// The text colour of this element's text. + /// + [Reactive("TextColour"), Dirty(DirtyFlag.Layout), Stylable] + protected Vector4 textColour; + /// + /// The font used by this element's text. + /// + [Reactive, Dirty(DirtyFlag.Layout), Stylable] + protected BMFont font; + /// + /// Specifies how the text in this element is horizontally aligned. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected TextAlignment textAlignment; + /// + /// Justifies the text of this element to fill it's width. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected bool justify; + //protected bool justifyLastLine; + + // TODO: Finish implementing + /// + /// Adds or subtracts space between individual words, measured in pixels. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float wordSpacing; + /// + /// Adds or subtracts space between individual characters, measured in pixels. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float charSpacing; + /// + /// The horizontal scaling factor applied to all characters in the text. Values smaller than 1 + /// will squeeze the text, and values greater than 1 will stretch it. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float stretchX = 1; + + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected TextOverflowMode textOverflowMode; + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float lineSpacing; + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float firstLineIndent; + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float indent; + + /// + /// A horizontal skew to apply to the text, useful for creating faux-italic type. A value of + /// 0 represents no skew. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float skew = 0; + /// + /// The weight of the font when using SDF-based fonts. This is not a true variable-weight + /// axis control, but rather an adjustment made to the rendered SDF font, it's useful for + /// faux-bold type. A value of 0.5 represents the font's native weight. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] + protected float weight = 0.5f; + + // Note that in the future this will store the shaped positions of each glyph to be rendered. + /// + /// This stores the bounds of the label as measured by the font engine. This is updated automatically + /// whenever the UI engine re-measures the element. + /// + protected Bounds2D measuredBounds; + + public TextBlockBase() + { + text = string.Empty; + size = 14; + colour = Vector4.Zero; + outlineThickness = 0; + textColour = new(0, 0, 0, 1); + font = Fonts.Default; + } + + public TextBlockBase(string? text) : this() + { + this.text = text; + } + + protected internal override Vector2 Measure() + { + var res = Font.Measure(text, size, 1); + measuredBounds = res; + return res.Size; + } + + protected override Bounds2D ComputeBounds(Bounds2D parent) + { + var bounds = base.ComputeBounds(parent); + // Apply an adjustment to the bounds to correct the vertical centering. + switch (VerticalAlignment) + { + case Alignment.Top: + break; + case Alignment.Bottom: + break; + case Alignment.Centre: + case Alignment.Stretch: + /*float emHeight; + //if (font.CharsDict.TryGetValue('M', out var xChar)) + // emHeight = xChar.size.Y; + //else + emHeight = font.Size * 1.333f; + emHeight *= 0.5f; + emHeight = 0; + float offset = (font.Base - emHeight) * (size / font.Size); + bounds.topLeft.Y -= offset; + bounds.bottomRight.Y -= offset;*/ + float offset = measuredBounds.topLeft.Y; + bounds.topLeft.Y -= offset; + bounds.bottomRight.Y -= offset; + + break; + } + + return bounds; + } + + /// + /// Draws this text using the given drawing context. + /// + /// This method exists outside of so that elements derived + /// from this class can call the text drawing method whenever they want within their own + /// method implementation. + /// + /// + protected void DrawText(IDrawContext ctx) + { + if (string.IsNullOrEmpty(text)) + return; + + var fnt = font; + var tex = fnt.FontTexture; + if (tex == null) + return; + + tex.ExecuteDrawCommands(ctx); + if (!tex.IsLoaded) + { + // Can't render yet, the font texture isn't ready, try again next frame. + Dirty(DirtyFlag.Content); + return; + } + ctx.DrawText(RenderedBoundsAbsolute, text.AsSpan(), fnt, size, textColour, wordSpacing, charSpacing, skew, weight, stretchX); + } + + protected internal override void Draw(IDrawContext ctx) + { + DrawRectangle(ctx); + DrawText(ctx); + } +} + +/// +/// Represents the horizontal alignment of a block of text. +/// +public enum TextAlignment +{ + Left, + Centre, + Right +} + +/// +/// Determines how text which overflows it's horizontal or vertical bounds should be handled. +/// +public enum TextOverflowMode +{ + Clip, + WrapAndClip, + Ellipsis, + WrapAndEllipsis +} diff --git a/ArgonUI/UIElements/UIContainer.cs b/ArgonUI/UIElements/Abstract/UIContainer.cs similarity index 97% rename from ArgonUI/UIElements/UIContainer.cs rename to ArgonUI/UIElements/Abstract/UIContainer.cs index fa6e056..a43fb3a 100644 --- a/ArgonUI/UIElements/UIContainer.cs +++ b/ArgonUI/UIElements/Abstract/UIContainer.cs @@ -1,5 +1,6 @@ using ArgonUI.SourceGenerator; using ArgonUI.Styling; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -21,18 +22,18 @@ public abstract partial class UIContainer : UIElement /// /// How much space (in pixels) to leave around each edge of each child element. /// - [Reactive, CustomAccessibility("public virtual"), Stylable, Dirty(DirtyFlags.Layout)] + [Reactive, CustomAccessibility("public virtual"), Stylable, Dirty(DirtyFlag.Layout)] private Thickness innerPadding; /// /// Whether child elements which overflow the bounds of this container should be drawn. /// - [Reactive, CustomAccessibility("public virtual"), Stylable, Dirty(DirtyFlags.Layout)] + [Reactive, CustomAccessibility("public virtual"), Stylable, Dirty(DirtyFlag.Layout)] private bool clipContents; public UIContainer() : base() { - DirtyFlags |= DirtyFlags.AllChild; + DirtyFlags |= DirtyFlag.ChildContentAndLayout; } /// @@ -104,7 +105,7 @@ public UIContainer() : base() /// protected internal virtual void BeforeLayoutChildren() { - //Dirty(DirtyFlags.Layout); + //Dirty(DirtyFlag.Layout); } /// @@ -138,7 +139,7 @@ protected internal override Bounds2D Layout(int childIndex) if (bounds != RenderedBoundsAbsolute) { foreach (var child in Children) - child.Dirty(DirtyFlags.Layout); + child.Dirty(DirtyFlag.Layout); } return bounds; diff --git a/ArgonUI/UIElements/Abstract/UIContainerRectangle.cs b/ArgonUI/UIElements/Abstract/UIContainerRectangle.cs new file mode 100644 index 0000000..4095983 --- /dev/null +++ b/ArgonUI/UIElements/Abstract/UIContainerRectangle.cs @@ -0,0 +1,78 @@ +using ArgonUI.Drawing; +using ArgonUI.SourceGenerator; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.UIElements.Abstract; + +/// +/// Represents the base class for all s which draw a rectangle background. +/// +/// This should have feature parity with . +/// +[UIClonable] +public abstract partial class UIContainerRectangle : UIContainer, IRectangleProps +{ + /// + /// The colour of this rectangle. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected Vector4 colour; + /// + /// The outline colour. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected Vector4 outlineColour; + /// + /// The thickness of the outline in pixels. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected float outlineThickness; + /// + /// The radius of the corners of this rectangle. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected float rounding; + /// + /// The texture to fill the rectangle with. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected ArgonTexture? texture; + /// + /// The gradient to fill the rectangle with. + /// + [Reactive, Dirty(DirtyFlag.Content), Stylable] protected Gradient? gradientFill; + + +#if DEBUG_LATENCY + public bool logLatencyNow; +#endif + + protected internal override void Draw(IDrawContext ctx) + { + if (texture != null) + { + texture.ExecuteDrawCommands(ctx); + if (!texture.IsLoaded) + { + // Can't render yet, the font texture isn't ready, try again next frame. + Dirty(DirtyFlag.Content); + return; + } + ctx.DrawTexture(RenderedBoundsAbsolute, texture.TextureHandle!, Rounding); + } + else if (gradientFill != null) + { + ctx.DrawGradient(RenderedBoundsAbsolute, gradientFill.ColourTL, gradientFill.ColourTR, gradientFill.ColourBL, gradientFill.ColourBR, Rounding); + } + else if (Colour.W > 0) + { + ctx.DrawRect(RenderedBoundsAbsolute, Colour, Rounding); + } + + if (outlineThickness > 0 && outlineColour.W > 0) + ctx.DrawOutlineRect(RenderedBoundsAbsolute, outlineColour, outlineThickness, rounding); +#if DEBUG_LATENCY + if (logLatencyNow) + commands.Add(ctx => ctx.MarkLatencyTimerEnd($"{Colour.Y}")); + logLatencyNow = false; +#endif + } +} diff --git a/ArgonUI/UIElements/UIElement.cs b/ArgonUI/UIElements/Abstract/UIElement.cs similarity index 83% rename from ArgonUI/UIElements/UIElement.cs rename to ArgonUI/UIElements/Abstract/UIElement.cs index 035e47b..ad569ce 100644 --- a/ArgonUI/UIElements/UIElement.cs +++ b/ArgonUI/UIElements/Abstract/UIElement.cs @@ -3,6 +3,7 @@ using ArgonUI.Input; using ArgonUI.SourceGenerator; using ArgonUI.Styling; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.ComponentModel; @@ -41,7 +42,7 @@ public abstract partial class UIElement : ReactiveObject, IDisposable /// /// Whether this element and it's children should be drawn. Elements which are not visible will not receive input events, nor will they participate in layout or drawing. /// - [Reactive, Stylable, Dirty(DirtyFlags.Layout)] + [Reactive, Stylable, Dirty(DirtyFlag.Layout)] private Visibility visible = Visibility.Visible; /// /// A set of stylable properties to be applied to this UIElement and it's decendants. @@ -63,53 +64,61 @@ public abstract partial class UIElement : ReactiveObject, IDisposable /// /// The absolute width of this element. Set to 0 to use automatic sizing. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int width; /// /// The absolute height of this element. Set to 0 to use automatic sizing. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int height; //public Vector2 Pivot { get => pivot; set => UpdateProperty(ref pivot, value); } /// /// How this element should be aligned vertically relative to it's parent. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private Alignment verticalAlignment; /// /// How this element should be aligned horizontally relative to it's parent. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private Alignment horizontalAlignment; /// /// How much space (in pixels) to leave around each edge of the element relative to the parent. Specified as a vector of (Top, Right, Bottom, Left). /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private Thickness margin; /// /// Controls which elements are shown on top of each other. Higher z-indexes will be shown on top. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int zIndex; /// /// The smallest width to shrink this element down to when using automatic sizing. + /// + /// Set to a value < 0 to disable the constraint. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int minWidth = -1; /// /// The smallest height to shrink this element down to when using automatic sizing. + /// + /// Set to a value < 0 to disable the constraint. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int minHeight = -1; /// /// The largest width to expand this element up to when using automatic sizing. + /// + /// Set to a value < 0 to disable the constraint. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int maxWidth = -1; /// /// The largest height to expand this element up to when using automatic sizing. + /// + /// Set to a value < 0 to disable the constraint. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private int maxHeight = -1; /// @@ -132,17 +141,17 @@ public abstract partial class UIElement : ReactiveObject, IDisposable /// Gets the dirty flags of this element which determine if the element needs to be re-rendered or re-laid out. /// /// - /// Use the and methods to set the dirty flags. + /// Use the and methods to set the dirty flags. /// - public DirtyFlags DirtyFlags { get => dirtyFlag; protected internal set => dirtyFlag = value; } + public DirtyFlag DirtyFlags { get => dirtyFlag; protected internal set => dirtyFlag = value; } /// /// Gets the width of the element when using automatic sizing. /// - public int DesiredWidth => desiredSize.x; + public float DesiredWidth => desiredSize.X; /// /// Gets the height of the element when using automatic sizing. /// - public int DesiredHeight => desiredSize.y; + public float DesiredHeight => desiredSize.Y; /// /// Gets whether the mouse is currently hovering over this element. /// @@ -179,8 +188,8 @@ public abstract partial class UIElement : ReactiveObject, IDisposable /// and the previous rendered bounds. /// internal Bounds2D invalidatedRenderBounds; - internal VectorInt2 desiredSize; - private DirtyFlags dirtyFlag = DirtyFlags.ContentAndLayout; + internal Vector2 desiredSize; + private DirtyFlag dirtyFlag = DirtyFlag.ContentAndLayout; private bool isHovered; private bool isPressed; private bool isFocused; @@ -304,32 +313,28 @@ public abstract partial class UIElement : ReactiveObject, IDisposable /// /// Represents the method that handles mouse down/up events. /// - /// - /// The mouse button which was pressed/released. - public delegate void MouseButtonEventHandler(InputManager inputManager, MouseButton button); + /// + public delegate void MouseButtonEventHandler(MouseButtonInputEventArgs args); /// /// Represents the method that handles mouse movement events. /// - /// - /// The new position of the mouse. - public delegate void MousePosEventHandler(InputManager inputManager, VectorInt2 pos); + /// + public delegate void MousePosEventHandler(MousePositionInputEventArgs args); /// /// Represents the method that handles mouse scroll events. /// - /// - /// How much the mouse has scrolled since this event was last invoked. - public delegate void MouseScrollEventHandler(InputManager inputManager, Vector2 delta); + /// + public delegate void MouseScrollEventHandler(MouseScrollInputEventArgs args); /// /// Represents the method that handles keyboard events. /// - /// - /// The key that was pressed/released. - public delegate void KeyEventHandler(InputManager inputManager, KeyCode key); + /// + public delegate void KeyEventHandler(KeyboardInputEventArgs args); /// /// Represents the method that handles events from the input manager. /// - /// - public delegate void InputEventHandler(InputManager inputManager); + /// + public delegate void InputEventHandler(InputEventArgs args); #endregion public UIElement() @@ -343,8 +348,8 @@ public UIElement() /// /// Marks this element as dirty, forcing the UI engine to redraw this element and it's children when it's next dispatched. /// - /// Which to set. - public virtual void Dirty(DirtyFlags flags) + /// Which to set. + public virtual void Dirty(DirtyFlag flags) { var prev = dirtyFlag; var newFlags = prev | flags; @@ -359,11 +364,11 @@ public virtual void Dirty(DirtyFlags flags) #endif // Propagate dirty flags up - var toPropagate = flags & (DirtyFlags.ChildContent | DirtyFlags.ChildLayout); - if ((flags & DirtyFlags.Layout) != 0) - toPropagate |= DirtyFlags.ChildLayout; - if ((flags & DirtyFlags.Content) != 0) - toPropagate |= DirtyFlags.ChildContent; + var toPropagate = flags & (DirtyFlag.ChildContent | DirtyFlag.ChildLayout); + if ((flags & DirtyFlag.Layout) != 0) + toPropagate |= DirtyFlag.ChildLayout; + if ((flags & DirtyFlag.Content) != 0) + toPropagate |= DirtyFlag.ChildContent; Parent?.Dirty(toPropagate); } @@ -371,8 +376,8 @@ public virtual void Dirty(DirtyFlags flags) /// /// Clears the given dirty flags from the UI element. /// - /// Which to clear. - public virtual void ClearDirtyFlag(DirtyFlags flags) + /// Which to clear. + public virtual void ClearDirtyFlag(DirtyFlag flags) { dirtyFlag &= ~flags; } @@ -392,6 +397,15 @@ public void Dispose() style?.Unregister(this); } + /// + /// Sets the 's currently (keyboard) focussed element to this . + /// + public void Focus() + { + if (window != null) + window.InputManager.FocussedElement = this; + } + /// /// Creates a deep copy of this . /// @@ -419,7 +433,7 @@ public virtual UIElement Clone(UIElement target) // I'm hesitant to fully automate parameter cloning (through reflection or source // generation), as there is some logic to apply to it which could be complex. // Not sure if we should invalidate dirty flags here or not... - target.dirtyFlag = DirtyFlags.Layout; + target.dirtyFlag = DirtyFlag.Layout; target.focusable = focusable; target.height = height; target.width = width; @@ -446,9 +460,9 @@ public virtual UIElement Clone(UIElement target) /// Layout() and Draw() occur from root node to leaves, whereas measure is invoked on the leaves first working it's way up to the root. /// /// The desired width and height of this element. - internal protected virtual VectorInt2 Measure() + internal protected virtual Vector2 Measure() { - return new VectorInt2(width, height); + return new Vector2(width, height); } // Internal @@ -461,8 +475,8 @@ protected virtual Bounds2D ComputeBounds(Bounds2D parent) { // Apply limits to the width and height var parentSize = parent.Size; - var desiredWidth = width > 0 ? width : DesiredWidth; - var desiredHeight = height > 0 ? height : DesiredHeight; + float desiredWidth = width > 0 ? width : DesiredWidth; + float desiredHeight = height > 0 ? height : DesiredHeight; if (minWidth >= 0) desiredWidth = Math.Max(desiredWidth, minWidth); @@ -548,79 +562,75 @@ internal protected virtual Bounds2D Layout(int childIndex) RenderedBounds = new(bounds.topLeft - parentBounds.topLeft, bounds.bottomRight - parentBounds.topLeft); // Invalidating the layout implies invalidating the content - Dirty(DirtyFlags.Content); + Dirty(DirtyFlag.Content); return bounds; } - internal void InvokeOnMouseDown(InputManager inputManager, MouseButton button) + internal void InvokeOnMouseDown(MouseButtonInputEventArgs args) { isPressed = true; OnStylableInputEvent?.Invoke(this, UIElementInputChange.MousePress); - OnMouseDown?.Invoke(inputManager, button); + OnMouseDown?.Invoke(args); } - internal void InvokeOnMouseUp(InputManager inputManager, MouseButton button) + internal void InvokeOnMouseUp(MouseButtonInputEventArgs args) { isPressed = false; OnStylableInputEvent?.Invoke(this, UIElementInputChange.MousePress); - OnMouseUp?.Invoke(inputManager, button); + OnMouseUp?.Invoke(args); } - internal void InvokeOnMouseEnter(InputManager inputManager, VectorInt2 pos) + internal void InvokeOnMouseEnter(MousePositionInputEventArgs args) { isHovered = true; OnStylableInputEvent?.Invoke(this, UIElementInputChange.MouseHover); - OnMouseEnter?.Invoke(inputManager, pos); - - Parent?.InvokeOnMouseEnter(inputManager, pos); + OnMouseEnter?.Invoke(args); } - internal void InvokeOnMouseLeave(InputManager inputManager, VectorInt2 pos) + internal void InvokeOnMouseLeave(MousePositionInputEventArgs args) { isHovered = false; OnStylableInputEvent?.Invoke(this, UIElementInputChange.MouseHover); - OnMouseLeave?.Invoke(inputManager, pos); - - Parent?.InvokeOnMouseLeave(inputManager, pos); + OnMouseLeave?.Invoke(args); } - internal void InvokeOnMouseOver(InputManager inputManager, VectorInt2 pos) + internal void InvokeOnMouseOver(MousePositionInputEventArgs args) { isHovered = true; //OnStylableInputEvent?.Invoke(UIElementInputChange.MouseHover); - OnMouseOver?.Invoke(inputManager, pos); - - // TODO: Bubble events up to parents - // We might consider doing the bubbling in the input manager, and passing a shared - // reference to the event args object so that subscribers can stop the bubbling - // of the event. - Parent?.InvokeOnMouseOver(inputManager, pos); + OnMouseOver?.Invoke(args); + } + internal void InvokeOnDoubleClick(MouseButtonInputEventArgs args) + { + isPressed = true; + OnStylableInputEvent?.Invoke(this, UIElementInputChange.MousePress); + OnDoubleClick?.Invoke(args); } - internal void InvokeOnDoubleClick(InputManager inputManager, MouseButton button) => OnDoubleClick?.Invoke(inputManager, button); - internal void InvokeOnMouseWheel(InputManager inputManager, Vector2 delta) => OnMouseWheel?.Invoke(inputManager, delta); - internal void InvokeOnKeyDown(InputManager inputManager, KeyCode key) + + internal void InvokeOnMouseWheel(MouseScrollInputEventArgs args) => OnMouseWheel?.Invoke(args); + internal void InvokeOnKeyDown(KeyboardInputEventArgs args) { OnStylableInputEvent?.Invoke(this, UIElementInputChange.KeyPress); - OnKeyDown?.Invoke(inputManager, key); + OnKeyDown?.Invoke(args); } - internal void InvokeOnKeyUp(InputManager inputManager, KeyCode key) + internal void InvokeOnKeyUp(KeyboardInputEventArgs args) { OnStylableInputEvent?.Invoke(this, UIElementInputChange.KeyPress); - OnKeyUp?.Invoke(inputManager, key); + OnKeyUp?.Invoke(args); } - internal void InvokeOnDragStart(InputManager inputManager) => OnDragStart?.Invoke(inputManager); - internal void InvokeOnDrop(InputManager inputManager) => OnDrop?.Invoke(inputManager); - internal void InvokeOnDragEnter(InputManager inputManager) => OnDragEnter?.Invoke(inputManager); - internal void InvokeOnDragLeave(InputManager inputManager) => OnDragLeave?.Invoke(inputManager); - internal void InvokeOnDragOver(InputManager inputManager) => OnDragOver?.Invoke(inputManager); - internal void InvokeOnFocusGot(InputManager inputManager) + internal void InvokeOnDragStart(InputEventArgs args) => OnDragStart?.Invoke(args); + internal void InvokeOnDrop(InputEventArgs args) => OnDrop?.Invoke(args); + internal void InvokeOnDragEnter(InputEventArgs args) => OnDragEnter?.Invoke(args); + internal void InvokeOnDragLeave(InputEventArgs args) => OnDragLeave?.Invoke(args); + internal void InvokeOnDragOver(InputEventArgs args) => OnDragOver?.Invoke(args); + internal void InvokeOnFocusGot(FocusInputEventArgs args) { isFocused = true; OnStylableInputEvent?.Invoke(this, UIElementInputChange.Focus); - OnFocusGot?.Invoke(inputManager); + OnFocusGot?.Invoke(args); } - internal void InvokeOnFocusLost(InputManager inputManager) + internal void InvokeOnFocusLost(FocusInputEventArgs args) { isFocused = false; OnStylableInputEvent?.Invoke(this, UIElementInputChange.Focus); - OnFocusLost?.Invoke(inputManager); + OnFocusLost?.Invoke(args); } internal void InvokeOnLoaded() { diff --git a/ArgonUI/UIElements/UIWindowElement.cs b/ArgonUI/UIElements/Abstract/UIWindowElement.cs similarity index 92% rename from ArgonUI/UIElements/UIWindowElement.cs rename to ArgonUI/UIElements/Abstract/UIWindowElement.cs index 29e1687..c9f436a 100644 --- a/ArgonUI/UIElements/UIWindowElement.cs +++ b/ArgonUI/UIElements/Abstract/UIWindowElement.cs @@ -1,5 +1,6 @@ using ArgonUI.Drawing; using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -20,7 +21,7 @@ public partial class UIWindowElement : UIContainer /// /// The background colour of this window. /// - [Reactive("BGColour"), Dirty(DirtyFlags.Content), Stylable] + [Reactive("BGColour"), Dirty(DirtyFlag.Content), Stylable] private Vector4 bgColour; //public new UIWindow Window { get; init; } @@ -41,21 +42,21 @@ internal UIWindowElement(UIWindow window) { Width = this.window!.Size.x; Height = this.window!.Size.y; - Dirty(DirtyFlags.Layout); + Dirty(DirtyFlag.Layout); }; window.OnResize += () => { Width = this.window!.Size.x; Height = this.window!.Size.y; - Dirty(DirtyFlags.Layout); + Dirty(DirtyFlag.Layout); }; } - public override void Dirty(DirtyFlags flags) + public override void Dirty(DirtyFlag flags) { base.Dirty(flags); - if (flags != DirtyFlags.None) + if (flags != DirtyFlag.None) { Window!.RequestRedraw(); } @@ -116,11 +117,11 @@ protected internal override Bounds2D Layout(int childIndex) if (bounds != RenderedBoundsAbsolute) { foreach (var child in Children) - child.Dirty(DirtyFlags.Layout); + child.Dirty(DirtyFlag.Layout); } // Invalidating the layout implies invalidating the content - Dirty(DirtyFlags.Content); + Dirty(DirtyFlag.Content); return bounds; } diff --git a/ArgonUI/UIElements/Checkbox.cs b/ArgonUI/UIElements/Checkbox.cs new file mode 100644 index 0000000..a25b761 --- /dev/null +++ b/ArgonUI/UIElements/Checkbox.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements; + +internal class Checkbox +{ +} diff --git a/ArgonUI/UIElements/ColourPicker.cs b/ArgonUI/UIElements/ColourPicker.cs new file mode 100644 index 0000000..be85b9c --- /dev/null +++ b/ArgonUI/UIElements/ColourPicker.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements; + +internal class ColourPicker +{ +} diff --git a/ArgonUI/UIElements/ContentButton.cs b/ArgonUI/UIElements/ContentButton.cs index 7700dd3..0da8a5a 100644 --- a/ArgonUI/UIElements/ContentButton.cs +++ b/ArgonUI/UIElements/ContentButton.cs @@ -1,5 +1,6 @@ using ArgonUI.Drawing; using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -16,11 +17,11 @@ public partial class ContentButton : ElementPresenterBase /// /// The colour of this button. /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] private Vector4 colour; + [Reactive, Dirty(DirtyFlag.Content), Stylable] private Vector4 colour; /// /// The radius of the corners of this button. /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] private float rounding; + [Reactive, Dirty(DirtyFlag.Content), Stylable] private float rounding; protected internal override void Draw(IDrawContext ctx) { diff --git a/ArgonUI/UIElements/DatePicker.cs b/ArgonUI/UIElements/DatePicker.cs new file mode 100644 index 0000000..f8b9431 --- /dev/null +++ b/ArgonUI/UIElements/DatePicker.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class DatePicker + { + } +} diff --git a/ArgonUI/UIElements/DirtyFlags.cs b/ArgonUI/UIElements/DirtyFlags.cs deleted file mode 100644 index 3f8e396..0000000 --- a/ArgonUI/UIElements/DirtyFlags.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace ArgonUI.UIElements; - -[Flags] -public enum DirtyFlags -{ - None, - Layout = 1 << 0, - Content = 1 << 1, - ChildLayout = 1 << 2, - ChildContent = 1 << 3, - - ContentAndLayout = Layout | Content, - AllChild = ChildContent | ChildLayout, - All = ContentAndLayout | AllChild, -} diff --git a/ArgonUI/UIElements/DropdownBox.cs b/ArgonUI/UIElements/DropdownBox.cs new file mode 100644 index 0000000..a0939fc --- /dev/null +++ b/ArgonUI/UIElements/DropdownBox.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements; + +internal class DropdownBox +{ +} diff --git a/ArgonUI/UIElements/FilePicker.cs b/ArgonUI/UIElements/FilePicker.cs new file mode 100644 index 0000000..ce98ba4 --- /dev/null +++ b/ArgonUI/UIElements/FilePicker.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class FilePicker + { + } +} diff --git a/ArgonUI/UIElements/Grid.cs b/ArgonUI/UIElements/Grid.cs new file mode 100644 index 0000000..553a0ff --- /dev/null +++ b/ArgonUI/UIElements/Grid.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Grid + { + } +} diff --git a/ArgonUI/UIElements/Image.cs b/ArgonUI/UIElements/Image.cs new file mode 100644 index 0000000..48a8ec2 --- /dev/null +++ b/ArgonUI/UIElements/Image.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Image + { + } +} diff --git a/ArgonUI/UIElements/Label.cs b/ArgonUI/UIElements/Label.cs index 8dadd72..c0484dd 100644 --- a/ArgonUI/UIElements/Label.cs +++ b/ArgonUI/UIElements/Label.cs @@ -1,5 +1,6 @@ using ArgonUI.Drawing; using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Linq; @@ -10,39 +11,13 @@ namespace ArgonUI.UIElements; [UIClonable] -public partial class Label : UIElement +public partial class Label : TextBase { - /// - /// The text represented by this label. - /// - [Reactive, Dirty(DirtyFlags.Layout)] protected string? text; - /// - /// The font size of this label. - /// - [Reactive("FontSize"), Dirty(DirtyFlags.Layout), Stylable] protected float size; - /// - /// The text colour of this label. - /// - [Reactive("TextColour"), Dirty(DirtyFlags.Layout), Stylable] protected Vector4 colour; - - //[UICloneableField] - /// - /// The font used by this label. - /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] - protected BMFont font; - - /// - /// This stores the bounds of the label as measured by the font engine. This is updated automatically - /// whenever the UI engine re-measures the element. - /// - protected Bounds2D measuredBounds; - public Label() { text = "Label"; size = 14; - colour = new(0, 0, 0, 1); + textColour = new(0, 0, 0, 1); font = Fonts.Default; } @@ -50,63 +25,4 @@ public Label(string? text) : this() { this.text = text; } - - protected internal override VectorInt2 Measure() - { - var res = Font.Measure(text, size, 1); - measuredBounds = res; - return new(res.Size); - } - - protected override Bounds2D ComputeBounds(Bounds2D parent) - { - var bounds = base.ComputeBounds(parent); - // Apply an adjustment to the bounds to correct the vertical centering. - switch (VerticalAlignment) - { - case Alignment.Top: - break; - case Alignment.Bottom: - break; - case Alignment.Centre: - case Alignment.Stretch: - /*float emHeight; - //if (font.CharsDict.TryGetValue('M', out var xChar)) - // emHeight = xChar.size.Y; - //else - emHeight = font.Size * 1.333f; - emHeight *= 0.5f; - emHeight = 0; - float offset = (font.Base - emHeight) * (size / font.Size); - bounds.topLeft.Y -= offset; - bounds.bottomRight.Y -= offset;*/ - float offset = measuredBounds.topLeft.Y; - bounds.topLeft.Y -= offset; - bounds.bottomRight.Y -= offset; - - break; - } - - return bounds; - } - - protected internal override void Draw(IDrawContext ctx) - { - if (string.IsNullOrEmpty(text)) - return; - - var fnt = font; - var tex = fnt.FontTexture; - if (tex == null) - return; - - tex.ExecuteDrawCommands(ctx); - if (!tex.IsLoaded) - { - // Can't render yet, the font texture isn't ready, try again next frame. - Dirty(DirtyFlags.Content); - return; - } - ctx.DrawText(RenderedBoundsAbsolute, size, text!, fnt, colour); - } } diff --git a/ArgonUI/UIElements/Modal.cs b/ArgonUI/UIElements/Modal.cs new file mode 100644 index 0000000..c8e4597 --- /dev/null +++ b/ArgonUI/UIElements/Modal.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Modal + { + } +} diff --git a/ArgonUI/UIElements/NumberField.cs b/ArgonUI/UIElements/NumberField.cs new file mode 100644 index 0000000..d7ce417 --- /dev/null +++ b/ArgonUI/UIElements/NumberField.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements; + +internal class NumberField : TextField +{ +} diff --git a/ArgonUI/UIElements/OverlayPanel.cs b/ArgonUI/UIElements/OverlayPanel.cs new file mode 100644 index 0000000..88e9d01 --- /dev/null +++ b/ArgonUI/UIElements/OverlayPanel.cs @@ -0,0 +1,10 @@ +using ArgonUI.UIElements.Abstract; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements; + +public partial class OverlayPanel : Panel +{ +} diff --git a/ArgonUI/UIElements/ProgressBar.cs b/ArgonUI/UIElements/ProgressBar.cs new file mode 100644 index 0000000..37bf854 --- /dev/null +++ b/ArgonUI/UIElements/ProgressBar.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class ProgressBar + { + } +} diff --git a/ArgonUI/UIElements/RadioButton.cs b/ArgonUI/UIElements/RadioButton.cs new file mode 100644 index 0000000..a297e63 --- /dev/null +++ b/ArgonUI/UIElements/RadioButton.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class RadioButton + { + } +} diff --git a/ArgonUI/UIElements/RangeSlider.cs b/ArgonUI/UIElements/RangeSlider.cs new file mode 100644 index 0000000..7651590 --- /dev/null +++ b/ArgonUI/UIElements/RangeSlider.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class RangeSlider + { + } +} diff --git a/ArgonUI/UIElements/Rectangle.cs b/ArgonUI/UIElements/Rectangle.cs index 87bbd10..748c8d5 100644 --- a/ArgonUI/UIElements/Rectangle.cs +++ b/ArgonUI/UIElements/Rectangle.cs @@ -1,5 +1,6 @@ using ArgonUI.Drawing; using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Linq; @@ -9,30 +10,11 @@ namespace ArgonUI.UIElements; +/// +/// A solid rectangle with an optional outline and rounded corners. +/// [UIClonable] -public partial class Rectangle : UIElement +public partial class Rectangle : RectangleBase { - /// - /// The colour of this rectangle. - /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] private Vector4 colour; - /// - /// The radius of the corners of this rectangle. - /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] private float rounding; - -#if DEBUG_LATENCY - public bool logLatencyNow; -#endif - - protected internal override void Draw(IDrawContext ctx) - { - if (Colour.W > 0) - ctx.DrawRect(RenderedBoundsAbsolute, Colour, Rounding); -#if DEBUG_LATENCY - if (logLatencyNow) - commands.Add(ctx => ctx.MarkLatencyTimerEnd($"{Colour.Y}")); - logLatencyNow = false; -#endif - } + } diff --git a/ArgonUI/UIElements/ScrollPanel.cs b/ArgonUI/UIElements/ScrollPanel.cs new file mode 100644 index 0000000..6482e47 --- /dev/null +++ b/ArgonUI/UIElements/ScrollPanel.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class ScrollPanel + { + } +} diff --git a/ArgonUI/UIElements/Separator.cs b/ArgonUI/UIElements/Separator.cs new file mode 100644 index 0000000..fe93ed6 --- /dev/null +++ b/ArgonUI/UIElements/Separator.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Separator + { + } +} diff --git a/ArgonUI/UIElements/Slider.cs b/ArgonUI/UIElements/Slider.cs index f0e3587..530c47f 100644 --- a/ArgonUI/UIElements/Slider.cs +++ b/ArgonUI/UIElements/Slider.cs @@ -1,5 +1,6 @@ using ArgonUI.Drawing; using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Linq; @@ -15,52 +16,52 @@ public partial class Slider : UIElement /// /// The colour of this slider. /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] + [Reactive, Dirty(DirtyFlag.Content), Stylable] private Vector4 colour; /// /// The rounding radius of the slider's handle. /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] + [Reactive, Dirty(DirtyFlag.Content), Stylable] private float handleRounding; /// /// The radius of the handle. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private Vector2 handleSize; /// /// The thickness of the slider track. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private float trackThickness; /// /// Whether the slider should slide horizontally, or vertically. /// - [Reactive, Dirty(DirtyFlags.Layout), Stylable] + [Reactive, Dirty(DirtyFlag.Layout), Stylable] private bool vertical; /// /// The current value of the slider. /// - [Reactive, Dirty(DirtyFlags.Content)] + [Reactive, Dirty(DirtyFlag.Content)] private float value; /// /// The minimum value the slider can represent. /// - [Reactive, Dirty(DirtyFlags.Content)] + [Reactive, Dirty(DirtyFlag.Content)] private float min; /// /// The maximum value the slider can represent. /// - [Reactive, Dirty(DirtyFlags.Content)] + [Reactive, Dirty(DirtyFlag.Content)] private float max; /// /// The size of steps between values of the slider. /// - [Reactive, Dirty(DirtyFlags.Content)] + [Reactive, Dirty(DirtyFlag.Content)] private float step; /// /// An exponent to raise the value of the slider to, useful for creating non-linear sliders. /// - [Reactive, Dirty(DirtyFlags.Content)] + [Reactive, Dirty(DirtyFlag.Content)] private float power; protected internal override void Draw(IDrawContext ctx) diff --git a/ArgonUI/UIElements/Spinner.cs b/ArgonUI/UIElements/Spinner.cs new file mode 100644 index 0000000..ba99784 --- /dev/null +++ b/ArgonUI/UIElements/Spinner.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Spinner + { + } +} diff --git a/ArgonUI/UIElements/StackPanel.cs b/ArgonUI/UIElements/StackPanel.cs index 54155f2..de5dee1 100644 --- a/ArgonUI/UIElements/StackPanel.cs +++ b/ArgonUI/UIElements/StackPanel.cs @@ -1,4 +1,5 @@ using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; using System; using System.Collections.Generic; using System.Linq; @@ -10,7 +11,7 @@ namespace ArgonUI.UIElements; [UIClonable] public partial class StackPanel : Panel { - [Reactive, Dirty(DirtyFlags.Layout)] + [Reactive, Dirty(DirtyFlag.Layout)] private Direction direction; public StackPanel() @@ -37,34 +38,34 @@ public StackPanel(Direction direction) protected internal override void BeforeLayoutChildren() { - Dirty(DirtyFlags.Layout); + Dirty(DirtyFlag.Layout); } - protected internal override VectorInt2 Measure() + protected internal override Vector2 Measure() { var children = Children; if (children.Count == 0) return base.Measure(); - VectorInt2 res = VectorInt2.Zero; - VectorInt2 pad = new((int)(InnerPadding.left + InnerPadding.right), (int)(InnerPadding.top + InnerPadding.bottom)); + var res = Vector2.Zero; + Vector2 pad = InnerPadding.Size; if (direction == Direction.Vertical) { foreach (var child in children) { - res.x = Math.Max(res.x, child.desiredSize.x); - res.y += child.desiredSize.y + pad.y; + res.X = Math.Max(res.X, child.desiredSize.X); + res.Y += child.desiredSize.Y + pad.Y; } - res.x += pad.x; + res.X += pad.X; } else { foreach (var child in children) { - res.x += child.desiredSize.x + pad.x; - res.y = Math.Max(res.y, child.desiredSize.y); + res.X += child.desiredSize.X + pad.X; + res.Y = Math.Max(res.Y, child.desiredSize.Y); } - res.y += pad.y; + res.Y += pad.Y; } return res; @@ -78,19 +79,19 @@ protected internal override VectorInt2 Measure() protected internal override Bounds2D RequestChildBounds(UIElement element, int index) { if (index == 0) - return RenderedBoundsAbsolute.SubtractMargin(InnerPadding); + return RenderedBoundsAbsolute.SubtractMargin(InnerPadding).WithSizeNonZero(element.desiredSize); if (direction == Direction.Vertical) { var bounds = RenderedBoundsAbsolute; bounds.topLeft.Y = Children[index - 1].RenderedBoundsAbsolute.bottomRight.Y; - return bounds.SubtractMargin(InnerPadding); + return bounds.SubtractMargin(InnerPadding).WithSizeNonZero(element.desiredSize); } else { var bounds = RenderedBoundsAbsolute; bounds.topLeft.X = Children[index - 1].RenderedBoundsAbsolute.bottomRight.X; - return bounds.SubtractMargin(InnerPadding); + return bounds.SubtractMargin(InnerPadding).WithSizeNonZero(element.desiredSize); } } } diff --git a/ArgonUI/UIElements/TabPanel.cs b/ArgonUI/UIElements/TabPanel.cs new file mode 100644 index 0000000..30a272e --- /dev/null +++ b/ArgonUI/UIElements/TabPanel.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class TabPanel + { + } +} diff --git a/ArgonUI/UIElements/Table.cs b/ArgonUI/UIElements/Table.cs new file mode 100644 index 0000000..ceda941 --- /dev/null +++ b/ArgonUI/UIElements/Table.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Table + { + } +} diff --git a/ArgonUI/UIElements/TextBlock.cs b/ArgonUI/UIElements/TextBlock.cs deleted file mode 100644 index 5e925cb..0000000 --- a/ArgonUI/UIElements/TextBlock.cs +++ /dev/null @@ -1,69 +0,0 @@ -using ArgonUI.Drawing; -using ArgonUI.SourceGenerator; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ArgonUI.UIElements; - -/// -/// Similar to a but with additional features such as text wrapping. -/// -[UIClonable] -public partial class TextBlock : Label -{ - /// - /// Specifies how the text in this text block is horizontally aligned. - /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected TextAlignment alignment; - /// - /// Justifies the contents of this text block to fill it's width. - /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected bool justify; - //protected bool justifyLastLine; - - // TODO: Finish implementing - /// - /// - /// - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float wordSpacing; - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float charSpacing; - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float stretchX; - - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float lineSpacing; - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float firstLineIndent; - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float indent; - - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float skew; - [Reactive, Dirty(DirtyFlags.Content), Stylable] - protected float weight; - - protected internal override void Draw(IDrawContext ctx) - { - if (text == null) - return; - - var fnt = font ?? Fonts.Default; - fnt.FontTexture?.ExecuteDrawCommands(ctx); - ctx.DrawText(RenderedBoundsAbsolute, size, text, fnt, colour, wordSpacing, charSpacing, skew, weight); - } -} - -public enum TextAlignment -{ - Left, - Centre, - Right -} diff --git a/ArgonUI/UIElements/TextField.cs b/ArgonUI/UIElements/TextField.cs new file mode 100644 index 0000000..c080d01 --- /dev/null +++ b/ArgonUI/UIElements/TextField.cs @@ -0,0 +1,61 @@ +using ArgonUI.Drawing; +using ArgonUI.SourceGenerator; +using ArgonUI.UIElements.Abstract; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace ArgonUI.UIElements; + +[UIClonable] +public partial class TextField : TextBlockBase +{ + protected VectorInt2 textSelection; + + public TextField() + { + text = string.Empty; + size = 14; + textColour = new(0, 0, 0, 1); + font = Fonts.Default; + + //OnKeyDown + } + + public TextField(string? text) : this() + { + this.text = text; + } + + /// + /// Gets the span of characters in this text field which are currently selected. + /// Returns an empty span if the text is , empty, or no + /// text is selected. + /// + public ReadOnlySpan SelectedText + { + get + { + if (string.IsNullOrEmpty(text)) + return default; + string t = text!; + int len = t.Length; + return text.AsSpan(Math.Min(textSelection.x, len), Math.Min(textSelection.y, len)); + } + } + + protected internal override void Draw(IDrawContext ctx) + { + // Draw rectangle + DrawRectangle(ctx); + + // Draw selection + + // Draw text + DrawText(ctx); + + // Draw caret + + } +} diff --git a/ArgonUI/UIElements/Toggle.cs b/ArgonUI/UIElements/Toggle.cs new file mode 100644 index 0000000..f8e79be --- /dev/null +++ b/ArgonUI/UIElements/Toggle.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Toggle + { + } +} diff --git a/ArgonUI/UIElements/Tooltip.cs b/ArgonUI/UIElements/Tooltip.cs new file mode 100644 index 0000000..78a3496 --- /dev/null +++ b/ArgonUI/UIElements/Tooltip.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Tooltip + { + } +} diff --git a/ArgonUI/UIElements/TreeView.cs b/ArgonUI/UIElements/TreeView.cs new file mode 100644 index 0000000..8e4f925 --- /dev/null +++ b/ArgonUI/UIElements/TreeView.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class TreeView + { + } +} diff --git a/ArgonUI/UIElements/Vector2Box.cs b/ArgonUI/UIElements/Vector2Box.cs new file mode 100644 index 0000000..42f7147 --- /dev/null +++ b/ArgonUI/UIElements/Vector2Box.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Vector2Box + { + } +} diff --git a/ArgonUI/UIElements/Vector3Box.cs b/ArgonUI/UIElements/Vector3Box.cs new file mode 100644 index 0000000..686b821 --- /dev/null +++ b/ArgonUI/UIElements/Vector3Box.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Vector3Box + { + } +} diff --git a/ArgonUI/UIElements/Vector4Box.cs b/ArgonUI/UIElements/Vector4Box.cs new file mode 100644 index 0000000..423b1e7 --- /dev/null +++ b/ArgonUI/UIElements/Vector4Box.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ArgonUI.UIElements +{ + internal class Vector4Box + { + } +} diff --git a/ArgonUI/UIRenderer.cs b/ArgonUI/UIRenderer.cs index 6e356ba..bc244a3 100644 --- a/ArgonUI/UIRenderer.cs +++ b/ArgonUI/UIRenderer.cs @@ -1,14 +1,10 @@ -using ArgonUI.UIElements; -using ArgonUI.Drawing; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using ArgonUI.Drawing; using ArgonUI.Helpers; -using System.Runtime.InteropServices; +using ArgonUI.UIElements; +using ArgonUI.UIElements.Abstract; +using System; +using System.Numerics; using System.Runtime.CompilerServices; -using System.Diagnostics; using System.Threading; namespace ArgonUI; @@ -101,7 +97,7 @@ public void RenderFrame() private static bool MeasureElementRecurse(UIElement element) { - if (element is UIContainer container && (element.DirtyFlags & DirtyFlags.ChildLayout) != 0) + if (element is UIContainer container && (element.DirtyFlags & DirtyFlag.ChildLayout) != 0) { bool changed = false; for (int i = 0; i < container.Children.Count; i++) @@ -113,13 +109,13 @@ private static bool MeasureElementRecurse(UIElement element) container.BeforeLayoutChildren(); } - if ((element.DirtyFlags & DirtyFlags.Layout) != 0) + if ((element.DirtyFlags & DirtyFlag.Layout) != 0) { var oldSize = element.desiredSize; if (element.Visible != Visibility.Hidden) element.desiredSize = element.Measure(); else - element.desiredSize = VectorInt2.Zero; + element.desiredSize = Vector2.Zero; if (element.desiredSize != oldSize) return true; } @@ -128,9 +124,9 @@ private static bool MeasureElementRecurse(UIElement element) private static void LayoutElementRecurse(UIElement element, int childIndex = 0) { - if ((element.DirtyFlags & DirtyFlags.Layout) != 0) + if ((element.DirtyFlags & DirtyFlag.Layout) != 0) { - element.ClearDirtyFlag(DirtyFlags.Layout); + element.ClearDirtyFlag(DirtyFlag.Layout); if (element.Visible != Visibility.Hidden) { var bounds = element.Layout(childIndex); @@ -147,10 +143,10 @@ private static void LayoutElementRecurse(UIElement element, int childIndex = 0) } } - if (element is UIContainer container && (element.DirtyFlags & DirtyFlags.ChildLayout) != 0) + if (element is UIContainer container && (element.DirtyFlags & DirtyFlag.ChildLayout) != 0) { container.LayoutChildren(); - element.ClearDirtyFlag(DirtyFlags.ChildLayout); + element.ClearDirtyFlag(DirtyFlag.ChildLayout); try { for (int i = 0; i < container.Children.Count; i++) @@ -166,9 +162,9 @@ private static void LayoutElementRecurse(UIElement element, int childIndex = 0) private void MeasureDrawnElementRecurse(UIElement element) { // Expand the draw bounds to include an elements with dirty content - if ((element.DirtyFlags & DirtyFlags.Content) != 0) + if ((element.DirtyFlags & DirtyFlag.Content) != 0) { - element.ClearDirtyFlag(DirtyFlags.Content); + element.ClearDirtyFlag(DirtyFlag.Content); // Draw bounds should be computed during layout... if (!drawBounds.HasValue) @@ -180,9 +176,9 @@ private void MeasureDrawnElementRecurse(UIElement element) // drawBounds won't expand any further. We still need to clear any dirty flags though. ClearContentFlagsRecursive(element); } - else if ((element.DirtyFlags & DirtyFlags.ChildContent) != 0 && element is UIContainer container) + else if ((element.DirtyFlags & DirtyFlag.ChildContent) != 0 && element is UIContainer container) { - element.ClearDirtyFlag(DirtyFlags.ChildContent); + element.ClearDirtyFlag(DirtyFlag.ChildContent); try { foreach (UIElement? child in container.Children) @@ -194,10 +190,10 @@ private void MeasureDrawnElementRecurse(UIElement element) private static void ClearContentFlagsRecursive(UIElement element) { - element.ClearDirtyFlag(DirtyFlags.Content); - if ((element.DirtyFlags & DirtyFlags.ChildContent) != 0 && element is UIContainer container) + element.ClearDirtyFlag(DirtyFlag.Content); + if ((element.DirtyFlags & DirtyFlag.ChildContent) != 0 && element is UIContainer container) { - element.ClearDirtyFlag(DirtyFlags.ChildContent); + element.ClearDirtyFlag(DirtyFlag.ChildContent); try { foreach (UIElement? child in container.Children) @@ -217,6 +213,10 @@ private void CollectDrawnRecurse(UIElement element) int index = element.treeDepth + element.ZIndex; + // TODO: A lot of UI elements are likely to layer multiple different draw commands. It would be good + // if we could expose the layering system to them so that they can take advantage of auto batching + // with neighbours. Otherwise we need to layer UI elements, which is a bit heavy weight... + // This is notably the case for decoraters like outlines or shadows. ref var cmdList = ref drawCommands.GetRef(index); if (Unsafe.IsNullRef(ref cmdList)) { diff --git a/design_notes.md b/design_notes.md index 107c40c..4298f00 100644 --- a/design_notes.md +++ b/design_notes.md @@ -243,6 +243,42 @@ For each element draw the sublist index it goes into is determined by: sense to clamp to one above and one below the current min and max index and then re-order when needed. Or I guess we could use a heap. +### Drawing Lists V3 + +The batching allowed by V2 is decent, but has some limitations. Notably, we expect many UI elements +to be composed of multiple different draw commands (eg: a rect and it's outline). Currently, this +would break the batching. We need a way of exposing the hierarchical draw lists to components +which may be able to take advantage of them. Ideally, we would hide this details from implementors +of custom `Draw()` functions by wrapping `IDrawContext` to automatically do this sorting. + +https://skia.googlesource.com/skia/+/refs/heads/main/src/gpu/graphite/DrawList.h#29 + +Conceptually, each method in `IDrawContext` should translate to a draw command of a given type, +notably, we mostly care about the different `ShaderFeature` flags involved. + +``` + Top + 1 | Rect | + 2 | Rect | | Text | | Text | | Text | + 3 | Rect | | Rect | | Rect | + 4 | Rect | + 5 | Clear | + +Squashses down into: + + Top + 1 | Text | | Text | | Text | + 2 | Rect | | Rect | | Rect | + 3 | Rect | | Rect | | Rect | + 4 | Clear | +``` + +Note that the gaps in 3 & 4 can only appear due to a `UIContainer` not pushing any draw commands. +As such collapsing the space above should be trivial, that being said you generally only want to +collapse down as far as you can while still leaving command types grouped -> see how Text in +layer 2 doesn't move down as it would no longer be in a group. Additionally, drawing system should +split layers such that only one draw type exists in each. + # Input @@ -317,7 +353,7 @@ public partial class Rectangle : UIElement /// /// The colour of this rectangle. /// - [Reactive][Dirty(DirtyFlags.Content)] + [Reactive][Dirty(DirtyFlag.Content)] [Stylable(DocString: "This element's colour.")] private Vector4 Colour; } @@ -333,7 +369,7 @@ public partial class Rectangle : UIElement get => colour; set { UpdateProperty(ref colour, value); - Dirty(DirtyFlags.Content); + Dirty(DirtyFlag.Content); } } }