using UnityEngine; using UnityEngine.UIElements; namespace Luneraria.UI { /// /// A vector-drawn plate (port of scripts/ui/theme/forged_style.gd): chamfered corners, a /// vertical fill gradient, an edge line, an inset line, an optional top highlight and a soft /// shadow. Drawn from the element's size, so it stays crisp at every UI scale. /// public sealed class LunePlateStyle { public Color FillTop = LuneUiTheme.Hex("1b2027"); public Color FillBottom = LuneUiTheme.Hex("12161b"); public Color Edge = LuneUiTheme.Hex("8a6a3c"); public float EdgeWidth = 1.5f; public Color Inner = new Color(1f, 1f, 1f, .05f); public float InnerInset = 4f; public float Chamfer = 7f; public Color Highlight = Color.clear; public Color Shadow = Color.clear; public float ShadowSize; /// Grows the drawing outside the element (focus rings). public float Grow; public bool DrawFill = true; /// Content margins (Godot content_margin_*), applied as padding. public float PadX = 12f; public float PadY = 12f; /// SaumwachtTheme.plate (inner .045) plus LuneUiTheme.plate (inner .035, inset 3, margins). public static LunePlateStyle Hud(Color top, Color bottom, Color edge, float edgeWidth = 1f, float cut = 6f, float padX = 12f, float padY = 10f) { return new LunePlateStyle { FillTop = top, FillBottom = bottom, Edge = edge, EdgeWidth = edgeWidth, Chamfer = cut, Inner = new Color(1f, 1f, 1f, .035f), InnerInset = 3f, PadX = padX, PadY = padY }; } /// SaumwachtTheme.plate with margins: menu plates (inner .045, inset 4). public static LunePlateStyle Menu(Color top, Color bottom, Color edge, float edgeWidth = 1.5f, float cut = 7f, float padX = 12f, float padY = 12f) { return new LunePlateStyle { FillTop = top, FillBottom = bottom, Edge = edge, EdgeWidth = edgeWidth, Chamfer = cut, Inner = new Color(1f, 1f, 1f, .045f), InnerInset = 4f, PadX = padX, PadY = padY }; } /// Keyboard focus ring outside the element (SaumwachtTheme.focus_ring). public static LunePlateStyle FocusRing(float cut = 7f) { return new LunePlateStyle { DrawFill = false, Edge = LuneUiTheme.Focus, EdgeWidth = 2.5f, Inner = LuneUiTheme.WithAlpha(LuneUiTheme.Focus, .55f), InnerInset = 4.5f, Chamfer = cut + 4f, Grow = 4f, Shadow = LuneUiTheme.WithAlpha(LuneUiTheme.Focus, .22f), ShadowSize = 5f, PadX = 0f, PadY = 0f }; } public void ApplyPadding(VisualElement element) { element.style.paddingLeft = PadX; element.style.paddingRight = PadX; element.style.paddingTop = PadY; element.style.paddingBottom = PadY; } public LunePlateStyle WithShadow(Color shadow, float size) { var copy = (LunePlateStyle)MemberwiseClone(); copy.Shadow = shadow; copy.ShadowSize = size; return copy; } public LunePlateStyle WithInner(Color inner) { var copy = (LunePlateStyle)MemberwiseClone(); copy.Inner = inner; return copy; } } public static class LunePlates { static readonly Vector2[] Points = new Vector2[8]; /// The eight corners of a chamfered rectangle, clockwise from the top left cut. public static Vector2[] PlatePoints(Rect rect, float cut, Vector2[] into = null) { Vector2[] result = into ?? new Vector2[8]; float c = Mathf.Min(cut, Mathf.Min(rect.width, rect.height) * .5f); Vector2 p = rect.min; Vector2 e = rect.max; result[0] = new Vector2(p.x + c, p.y); result[1] = new Vector2(e.x - c, p.y); result[2] = new Vector2(e.x, p.y + c); result[3] = new Vector2(e.x, e.y - c); result[4] = new Vector2(e.x - c, e.y); result[5] = new Vector2(p.x + c, e.y); result[6] = new Vector2(p.x, e.y - c); result[7] = new Vector2(p.x, p.y + c); return result; } static Rect Grow(Rect rect, float amount) { return new Rect(rect.x - amount, rect.y - amount, rect.width + amount * 2f, rect.height + amount * 2f); } public static void Draw(MeshGenerationContext context, Rect rect, LunePlateStyle style) { Rect r = Grow(rect, style.Grow); if (r.width < 2f || r.height < 2f) return; Painter2D painter = context.painter2D; if (style.Shadow.a > 0f && style.ShadowSize > 0f) { for (int layer = 0; layer < 3; layer++) { float spread = style.ShadowSize * (layer + 1) / 3f; var soft = new Color(style.Shadow.r, style.Shadow.g, style.Shadow.b, style.Shadow.a / 3f); if (style.DrawFill) { Rect offset = new Rect(r.x, r.y + spread * .5f, r.width, r.height); Polygon(painter, PlatePoints(Grow(offset, spread), style.Chamfer + spread, Points), soft); } else { // Rings only glow outwards; the element underneath stays untinted. Outline(painter, PlatePoints(Grow(r, spread), style.Chamfer + spread, Points), soft, style.ShadowSize / 3f + 1f); } } } if (style.DrawFill) Gradient(context, PlatePoints(r, style.Chamfer, Points), r, style.FillTop, style.FillBottom); if (style.Highlight.a > 0f) { float y = r.y + style.EdgeWidth + 1f; Line(painter, new Vector2(r.x + style.Chamfer, y), new Vector2(r.xMax - style.Chamfer, y), style.Highlight, 1f); } if (style.Inner.a > 0f && style.InnerInset > 0f) Outline(painter, PlatePoints(Grow(r, -style.InnerInset), Mathf.Max(1f, style.Chamfer - style.InnerInset * .45f), Points), style.Inner, 1f); if (style.Edge.a > 0f && style.EdgeWidth > 0f) Outline(painter, PlatePoints(Grow(r, -style.EdgeWidth * .5f), style.Chamfer, Points), style.Edge, style.EdgeWidth); } /// Convex polygon with a vertical colour gradient (Godot: polygon with per-vertex colours). public static void Gradient(MeshGenerationContext context, Vector2[] points, Rect bounds, Color top, Color bottom) { int count = points.Length; MeshWriteData mesh = context.Allocate(count + 1, count * 3); Vector2 center = Vector2.zero; foreach (Vector2 point in points) center += point; center /= count; mesh.SetNextVertex(new Vertex { position = new Vector3(center.x, center.y, Vertex.nearZ), tint = Color.Lerp(top, bottom, Fraction(center.y, bounds)) }); foreach (Vector2 point in points) mesh.SetNextVertex(new Vertex { position = new Vector3(point.x, point.y, Vertex.nearZ), tint = Color.Lerp(top, bottom, Fraction(point.y, bounds)) }); // Clockwise triangles in screen space (y down). for (int index = 0; index < count; index++) { mesh.SetNextIndex(0); mesh.SetNextIndex((ushort)(1 + index)); mesh.SetNextIndex((ushort)(1 + (index + 1) % count)); } } static float Fraction(float y, Rect bounds) { return Mathf.Clamp01((y - bounds.y) / Mathf.Max(1f, bounds.height)); } public static void Polygon(Painter2D painter, Vector2[] points, Color color) { painter.fillColor = color; painter.BeginPath(); painter.MoveTo(points[0]); for (int index = 1; index < points.Length; index++) painter.LineTo(points[index]); painter.ClosePath(); painter.Fill(); } public static void Outline(Painter2D painter, Vector2[] points, Color color, float width) { painter.strokeColor = color; painter.lineWidth = width; painter.lineJoin = LineJoin.Miter; painter.BeginPath(); painter.MoveTo(points[0]); for (int index = 1; index < points.Length; index++) painter.LineTo(points[index]); painter.ClosePath(); painter.Stroke(); } public static void Line(Painter2D painter, Vector2 from, Vector2 to, Color color, float width) { painter.strokeColor = color; painter.lineWidth = width; painter.lineCap = LineCap.Butt; painter.BeginPath(); painter.MoveTo(from); painter.LineTo(to); painter.Stroke(); } } }