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;
}
}
}
}