This commit is contained in:
2026-06-07 13:28:25 -07:00
parent 0df0b45163
commit 25b53cb062
25 changed files with 1573 additions and 125 deletions
+136 -28
View File
@@ -4,41 +4,39 @@ using UnityEngine.UIElements;
namespace ProjectM.Client
{
/// <summary>
/// UI Toolkit factories for the in-game HUD — a thin extension of <see cref="MenuUi"/>'s Aether palette so the
/// HUD reads in the same visual language as the menu / pause / settings screens. Bars are a dark rounded track
/// + a percent-width fill; labels use the shared text weights/colours. Every element is built
/// <c>pickingMode = Ignore</c> by default so the HUD never eats clicks meant for the game world (only the
/// interactive build-palette buttons opt back into picking).
/// UI Toolkit factories for the in-game HUD — a thin extension of <see cref="MenuUi"/>'s Aether palette,
/// now skinned with the curated Synty sci-fi-soldier kit (<see cref="HudTheme"/>). Panels are 9-sliced Synty
/// boxes tinted into the Aether palette; bars are a skinned track + a percent-width fill; numerics use the
/// Orbitron display font and labels the Exo 2.0 body font. EVERYTHING is null-safe: if <see cref="HudTheme"/>
/// (or a given sprite/font) is missing, each factory falls back to the original flat-colour look, so the HUD
/// is shippable with or without the theme asset. Every element is <c>pickingMode = Ignore</c> by default so
/// the HUD never eats clicks meant for the game world (only the interactive build-palette slots opt back in).
/// </summary>
public static class HudUi
{
public static readonly Color Track = new(0f, 0f, 0f, 0.55f);
/// <summary>A dark rounded bar track with a percent-width fill child (returned via <paramref name="fill"/>).</summary>
public static VisualElement Bar(float width, float height, Color fillColor, out VisualElement fill)
{
var track = new VisualElement();
track.style.width = width;
track.style.height = height;
track.style.backgroundColor = Track;
track.style.paddingLeft = 2; track.style.paddingRight = 2;
track.style.paddingTop = 2; track.style.paddingBottom = 2;
track.style.flexDirection = FlexDirection.Row;
track.pickingMode = PickingMode.Ignore;
MenuUi.Round(track, 4);
// ---- text (Orbitron display vs Exo body, theme-driven with a bold fallback) ----
fill = new VisualElement();
fill.style.height = Length.Percent(100);
fill.style.width = Length.Percent(100);
fill.style.backgroundColor = fillColor;
fill.pickingMode = PickingMode.Ignore;
MenuUi.Round(fill, 3);
track.Add(fill);
return track;
/// <summary>A body label (Exo 2.0 when themed) — labels, captions, hints.</summary>
public static Label Text(string text, int size, Color color, TextAnchor align)
{
var l = MakeLabel(text, size, color, align);
var theme = HudTheme.Get();
if (theme != null) theme.ApplyBody(l.style);
return l;
}
/// <summary>A bold HUD label (non-interactive).</summary>
public static Label Text(string text, int size, Color color, TextAnchor align)
/// <summary>A display label (Orbitron when themed) — numerics + phase words, the authoritative HUD voice.</summary>
public static Label Display(string text, int size, Color color, TextAnchor align)
{
var l = MakeLabel(text, size, color, align);
var theme = HudTheme.Get();
if (theme != null) theme.ApplyDisplay(l.style);
return l;
}
static Label MakeLabel(string text, int size, Color color, TextAnchor align)
{
var l = new Label(text);
l.style.fontSize = size;
@@ -49,13 +47,54 @@ namespace ProjectM.Client
return l;
}
// ---- bars ----
/// <summary>A dark rounded bar track with a percent-width fill child (returned via <paramref name="fill"/>).</summary>
public static VisualElement Bar(float width, float height, Color fillColor, out VisualElement fill)
{
var theme = HudTheme.Get();
var trackSpr = theme != null ? theme.BarTrack : null;
var track = new VisualElement();
track.style.width = width;
track.style.height = height;
track.style.paddingLeft = 2; track.style.paddingRight = 2;
track.style.paddingTop = 2; track.style.paddingBottom = 2;
track.style.flexDirection = FlexDirection.Row;
track.pickingMode = PickingMode.Ignore;
fill = new VisualElement();
fill.style.height = Length.Percent(100);
fill.style.width = Length.Percent(100);
fill.style.backgroundColor = fillColor;
fill.pickingMode = PickingMode.Ignore;
MenuUi.Round(fill, 3);
if (trackSpr != null)
{
// Bar_Angled ships an 80/0 border → UITK 9-slices it horizontally from the art; no style override.
track.style.backgroundImage = new StyleBackground(Background.FromSprite(trackSpr));
track.style.unityBackgroundImageTintColor = new Color(0.04f, 0.06f, 0.09f, 0.92f);
}
else
{
track.style.backgroundColor = Track;
MenuUi.Round(track, 4);
}
track.Add(fill);
return track;
}
/// <summary>Set a fill's width to a 0..1 fraction of its track.</summary>
public static void SetFill(VisualElement fill, float frac)
{
if (fill != null) fill.style.width = Length.Percent(Mathf.Clamp01(frac) * 100f);
}
/// <summary>A semi-transparent rounded panel for grouping a cluster of HUD elements.</summary>
// ---- panels / icons / glyphs ----
/// <summary>A grouping container (no background). Transparent, click-through.</summary>
public static VisualElement Group(Align items = Align.FlexStart)
{
var g = new VisualElement();
@@ -64,5 +103,74 @@ namespace ProjectM.Client
return g;
}
/// <summary>
/// A 9-sliced Synty panel tinted into the Aether palette; falls back to a flat translucent rounded panel
/// when the theme/sprite is missing. <paramref name="tint"/> multiplies the (light-grey) skin, so a dark
/// tint reads as panel-dark while preserving the printed bevels.
/// </summary>
public static VisualElement Panel(Color tint)
{
var p = new VisualElement();
p.pickingMode = PickingMode.Ignore;
var theme = HudTheme.Get();
var box = theme != null ? theme.PanelBox : null;
if (box != null)
{
p.style.backgroundImage = new StyleBackground(Background.FromSprite(box));
p.style.unityBackgroundImageTintColor = tint;
// 9-slice uses the sprite's AUTHORED border (Box_Glass ships 25px); no style override → no
// "borders overridden by style slices" log, and the art's intended corners are preserved.
}
else
{
p.style.backgroundColor = tint;
MenuUi.Round(p, 8);
MenuUi.Border(p, new Color(1f, 1f, 1f, 0.08f), 1);
}
return p;
}
/// <summary>A fixed-size icon element backed by a Synty sprite (tinted). Returns an empty element if null.</summary>
public static VisualElement Icon(Sprite sprite, float size, Color tint)
{
var e = new VisualElement();
e.style.width = size; e.style.height = size;
e.style.flexShrink = 0;
e.pickingMode = PickingMode.Ignore;
if (sprite != null)
{
e.style.backgroundImage = new StyleBackground(Background.FromSprite(sprite));
e.style.unityBackgroundImageTintColor = tint;
e.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Contain));
}
return e;
}
/// <summary>True when the theme is loaded and a sprite is present (callers choose icon vs text fallback).</summary>
/// <summary>
/// An input-prompt chip: the Synty key/button glyph (left untinted to keep its printed face) when present,
/// else a bordered text keycap with <paramref name="fallbackText"/> (e.g. "R", "Esc").
/// </summary>
public static VisualElement Glyph(Sprite sprite, string fallbackText, float size)
{
if (sprite != null) return Icon(sprite, size, Color.white);
var cap = new VisualElement();
cap.style.minWidth = size; cap.style.height = size;
cap.style.paddingLeft = 7; cap.style.paddingRight = 7;
cap.style.alignItems = Align.Center; cap.style.justifyContent = Justify.Center;
cap.style.backgroundColor = new Color(0.14f, 0.17f, 0.22f, 0.95f);
cap.style.flexShrink = 0;
cap.pickingMode = PickingMode.Ignore;
MenuUi.Round(cap, 4);
MenuUi.Border(cap, new Color(1f, 1f, 1f, 0.25f), 1);
cap.Add(Text(fallbackText, Mathf.Max(10, (int)(size * 0.5f)), MenuUi.TextCol, TextAnchor.MiddleCenter));
return cap;
}
/// <summary>Apply a uniform 9-slice (horizontal / vertical source-texture px) to a skinned element.</summary>
}
}