using System.Collections.Generic; using UnityEngine; using UnityEngine.TextCore.Text; using UnityEngine.UIElements; using TextElement = UnityEngine.UIElements.TextElement; namespace Luneraria.UI { /// /// Font roles of the HUD as lune_ui_theme.gd declares them: Cantarell 560, Cantarell 700–760 /// and Cinzel 700. Godot never applied those weights: its FontVariation used the String key /// "wght", which Godot ignores (measured in 4.7.2: the same advances at 400, 560 and 700; only /// the integer tag changes them). G3 therefore shows both fonts in their default instance, /// Regular 400, and so does the port. The roles stay apart so that U5 can give them weights. /// public enum LuneFace { /// Cantarell (declared 560). Text, /// Cantarell (declared 700–760). Bold, /// Cinzel (declared 700). Display, } /// A Godot label variation: face, size, colour, glyph spacing, optional outline. public sealed class LuneTextStyle { public LuneFace Face; public float Size; public Color Color; public float Spacing; /// Godot outline_size (black, alpha .55) for titles, the wave and gold; 0 = none. public float Outline; public LuneTextStyle(LuneFace face, float size, Color color, float spacing = 0f, float outline = 0f) { Face = face; Size = Mathf.Max(LuneUiTheme.MinFont, size); Color = color; Spacing = spacing; Outline = outline; } } /// /// Factory and text styles of the HUD (Godot LuneUiTheme.label/button and the label variations /// of lune_ui_theme.gd). Components build their elements here, so sizes, faces and colours /// stay consistent. Fonts come from Settings/LuneUi.asset (LuneUi.Assets, set by the HUD). /// public static class LuneUi { public static LuneUiAssets Assets; static readonly Color Title = LuneUiTheme.Hex("f4e6c4"); public static readonly Dictionary Styles = new Dictionary { { "HudText", new LuneTextStyle(LuneFace.Text, 15, LuneUiTheme.Text) }, { "HudMuted", new LuneTextStyle(LuneFace.Text, 14, LuneUiTheme.Muted) }, { "HudCaps", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.BronzeLight, 2f) }, { "HudBadge", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.Text, 1f) }, { "HudLane", new LuneTextStyle(LuneFace.Display, 16, Title, 1f) }, { "HudValue", new LuneTextStyle(LuneFace.Bold, 17, LuneUiTheme.Bright) }, { "HudPrimary", new LuneTextStyle(LuneFace.Bold, 16, LuneUiTheme.Ink, 1f) }, { "HudGold", new LuneTextStyle(LuneFace.Bold, 24, LuneUiTheme.Gold, 0f, 4f) }, { "HudName", new LuneTextStyle(LuneFace.Display, 18, Title, 1f) }, { "HudTitle", new LuneTextStyle(LuneFace.Display, 24, Title, 1f, 4f) }, { "HudWave", new LuneTextStyle(LuneFace.Display, 30, Title, 3f, 4f) }, // Menu theme (game-over panel, options). { "TitleLabel", new LuneTextStyle(LuneFace.Display, 78, Title, 2f, 6f) }, { "PrimaryButton", new LuneTextStyle(LuneFace.Display, 22, LuneUiTheme.Ink, 2f) }, { "HeadingLabel", new LuneTextStyle(LuneFace.Display, 34, Title, 1f) }, { "SectionLabel", new LuneTextStyle(LuneFace.Display, 16, LuneUiTheme.BronzeLight, 3f) }, { "BodyLabel", new LuneTextStyle(LuneFace.Text, 20, LuneUiTheme.Parchment) }, { "MutedLabel", new LuneTextStyle(LuneFace.Text, 16, LuneUiTheme.Hex("a9a293")) }, { "EyebrowLabel", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.BronzeLight, 2f) }, { "CardFactLabel", new LuneTextStyle(LuneFace.Text, 17, LuneUiTheme.Hex("d8cfbc")) }, }; /// The atlas of a role: Cinzel Regular for Display, Cantarell Regular otherwise (see LuneFace). public static FontAsset Font(LuneFace face) { if (Assets == null) return null; return face == LuneFace.Display ? Assets.Display : Assets.Text; } /// /// Godot's spacing_glyph (pixels per glyph) as UI Toolkit letter spacing, which counts in /// hundredths of the font size (measured: 10 widens each glyph by 0.1 em). /// public static float LetterSpacing(float pixels, float fontSize) { return fontSize > 0f ? pixels * 100f / fontSize : 0f; } /// Applies a text style; size 0 keeps the style's size. public static void Apply(TextElement element, string variation, float size = 0f) { if (!Styles.TryGetValue(variation, out LuneTextStyle style)) style = Styles["HudText"]; FontAsset font = Font(style.Face); if (font != null) element.style.unityFontDefinition = FontDefinition.FromSDFFont(font); float fontSize = size > 0f ? Mathf.Max(LuneUiTheme.MinFont, size) : style.Size; element.style.fontSize = fontSize; element.style.letterSpacing = LetterSpacing(style.Spacing, fontSize); element.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f, color = new Color(0f, 0f, 0f, .55f) }; SetColor(element, style.Color, style); } /// /// Text colour. Titles, the wave and gold get Godot's dark rim (outline_size, black .55) as /// a soft shadow: a TextCore outline straddles the glyph edge and eats the thin strokes of /// the regular faces, Godot's outline only grows outwards. /// public static void SetColor(TextElement element, Color color, LuneTextStyle style = null) { element.style.color = color; if (style == null) style = element.userData as LuneTextStyle; if (style == null) return; element.userData = style; element.style.unityTextOutlineWidth = 0f; if (style.Outline > 0f) element.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f + style.Outline * .5f, color = new Color(0f, 0f, 0f, .7f) }; } public static Label Label(string text, string variation = "HudText", VisualElement parent = null, float size = 0f) { var label = new Label(text ?? ""); label.pickingMode = PickingMode.Ignore; Zero(label); label.style.whiteSpace = WhiteSpace.NoWrap; Apply(label, variation, size); parent?.Add(label); return label; } /// Godot clip_text with OVERRUN_TRIM_ELLIPSIS: one line, cut with "…". public static Label Clip(Label label) { label.style.overflow = Overflow.Hidden; label.style.textOverflow = TextOverflow.Ellipsis; label.style.whiteSpace = WhiteSpace.NoWrap; label.style.flexShrink = 1f; label.style.minWidth = 0f; return label; } /// Godot AUTOWRAP_WORD_SMART. public static Label Wrap(Label label) { label.style.whiteSpace = WhiteSpace.Normal; label.style.flexShrink = 1f; label.style.minWidth = 0f; return label; } public static void Zero(VisualElement element) { element.style.marginLeft = 0f; element.style.marginRight = 0f; element.style.marginTop = 0f; element.style.marginBottom = 0f; element.style.paddingLeft = 0f; element.style.paddingRight = 0f; element.style.paddingTop = 0f; element.style.paddingBottom = 0f; } /// Godot HBox/VBoxContainer with a separation; ignores the mouse like Godot's boxes in the HUD. public static VisualElement Box(bool vertical, float separation, VisualElement parent = null) { var box = new LuneBox(vertical, separation); parent?.Add(box); return box; } public static VisualElement Spacer(VisualElement parent) { var spacer = new VisualElement { pickingMode = PickingMode.Ignore }; spacer.style.flexGrow = 1f; parent.Add(spacer); return spacer; } public static LuneGlyphElement Glyph(string glyph, Color color, float edge = 18f, VisualElement parent = null) { var element = new LuneGlyphElement(glyph, color, edge); parent?.Add(element); return element; } /// Godot `visible`: hidden elements take no space; the parent box closes the gap. public static void Show(VisualElement element, bool visible) { DisplayStyle display = visible ? DisplayStyle.Flex : DisplayStyle.None; if (element.style.display == display) return; element.style.display = display; (element.parent as LuneBox)?.Space(); } public static bool Visible(VisualElement element) { return element.resolvedStyle.display != DisplayStyle.None && element.style.display != DisplayStyle.None; } public static string Upper(string text) { return (text ?? "").ToUpperInvariant(); } } /// /// Box container with Godot's separation: the gap goes between children, not around them. /// Children added later get the gap too; hidden children (display none) leave no gap. /// public sealed class LuneBox : VisualElement { public readonly bool Vertical; public float Separation; public LuneBox(bool vertical, float separation) { Vertical = vertical; Separation = separation; pickingMode = PickingMode.Ignore; style.flexDirection = vertical ? FlexDirection.Column : FlexDirection.Row; if (!vertical) style.alignItems = Align.Center; RegisterCallback(_ => Space()); RegisterCallback(_ => Space()); } /// Applies the gap before every visible child except the first. public void Space() { bool first = true; foreach (VisualElement child in Children()) { float gap = first ? 0f : Separation; if (child.style.display == DisplayStyle.None) continue; // Only real changes: setting a margin dirties the layout even with the same value. if (Vertical) { StyleLength current = child.style.marginTop; if (current.keyword != StyleKeyword.Undefined || Mathf.Abs(current.value.value - gap) > .01f) child.style.marginTop = gap; } else { StyleLength current = child.style.marginLeft; if (current.keyword != StyleKeyword.Undefined || Mathf.Abs(current.value.value - gap) > .01f) child.style.marginLeft = gap; } first = false; } } } }