238 lines
9.8 KiB
C#
238 lines
9.8 KiB
C#
using System.IO;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
namespace QuestAppLauncher
|
|
{
|
|
/// <summary>
|
|
/// Applies managed branding from <see cref="ManagedPolicyHandler.Branding"/>
|
|
/// to the launcher UI at startup and whenever new tabs are created.
|
|
///
|
|
/// No Unity Editor wiring required — all GameObjects are located by their
|
|
/// scene names at runtime.
|
|
///
|
|
/// <para>
|
|
/// Default colours (original art):
|
|
/// Primary <c>#3E6DA9</c> · Accent <c>#00AEEF</c>
|
|
/// </para>
|
|
/// </summary>
|
|
public class BrandingHandler : MonoBehaviour
|
|
{
|
|
// Known GameObject / child names from the scene
|
|
const string TopTabContentName = "Top_Tab_Content";
|
|
const string LeftTabContentName = "Left_Tab_Content";
|
|
const string RightTabContentName = "Right_Tab_Content";
|
|
const string LogoObjectName = "Branding_Logo"; // created at runtime if absent
|
|
const string TitleObjectName = "Branding_Title"; // created at runtime if absent
|
|
const string CanvasName = "Canvas";
|
|
|
|
// Original art colours — used when no policy is active
|
|
const string DefaultPrimary = "#3E6DA9";
|
|
const string DefaultAccent = "#00AEEF";
|
|
|
|
/// <summary>Current primary colour (tab backgrounds).</summary>
|
|
public static Color PrimaryColor { get; private set; }
|
|
/// <summary>Current accent colour (selected-tab indicator).</summary>
|
|
public static Color AccentColor { get; private set; }
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Automatically creates a persistent BrandingHandler GameObject on startup.
|
|
/// No scene setup or Unity Editor wiring required.
|
|
/// </summary>
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
private static void AutoInstall()
|
|
{
|
|
if (FindFirstObjectByType<BrandingHandler>() != null) return;
|
|
var go = new GameObject("BrandingHandler");
|
|
DontDestroyOnLoad(go);
|
|
go.AddComponent<BrandingHandler>();
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
var branding = ManagedPolicyHandler.Branding;
|
|
|
|
PrimaryColor = ParseColor(branding?.PrimaryColor, DefaultPrimary);
|
|
AccentColor = ParseColor(branding?.AccentColor, DefaultAccent);
|
|
|
|
ApplyLogo(branding?.LogoPath);
|
|
ApplyTitle(branding?.AppTitle);
|
|
|
|
// Recolour tabs already in the scene (tabs are also re-coloured by
|
|
// GridPopulation after it creates them, covering the dynamic case)
|
|
ApplyToTabParent(GameObject.Find(TopTabContentName));
|
|
ApplyToTabParent(GameObject.Find(LeftTabContentName));
|
|
ApplyToTabParent(GameObject.Find(RightTabContentName));
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Recolours every tab child of <paramref name="tabParent"/>.
|
|
/// Called by <see cref="GridPopulation"/> after it populates each tab container.
|
|
/// </summary>
|
|
public void ApplyToTabParent(GameObject tabParent)
|
|
{
|
|
if (tabParent == null) return;
|
|
foreach (Transform tab in tabParent.transform)
|
|
ApplyToTab(tab.gameObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies branding colours to a single tab (Tab.prefab structure:
|
|
/// Toggle root with <c>Background</c> and <c>Checkmark</c> children).
|
|
/// </summary>
|
|
public void ApplyToTab(GameObject tab)
|
|
{
|
|
if (tab == null) return;
|
|
|
|
var bg = tab.transform.Find("Background");
|
|
if (bg != null)
|
|
{
|
|
var img = bg.GetComponent<Image>();
|
|
if (img != null) img.color = PrimaryColor;
|
|
}
|
|
|
|
var check = tab.transform.Find("Checkmark");
|
|
if (check != null)
|
|
{
|
|
var img = check.GetComponent<Image>();
|
|
if (img != null) img.color = AccentColor;
|
|
}
|
|
|
|
var toggle = tab.GetComponent<Toggle>();
|
|
if (toggle != null)
|
|
{
|
|
var cb = toggle.colors;
|
|
cb.normalColor = PrimaryColor;
|
|
cb.highlightedColor = Brighten(PrimaryColor, 0.15f);
|
|
cb.pressedColor = Darken(PrimaryColor, 0.15f);
|
|
cb.selectedColor = AccentColor;
|
|
toggle.colors = cb;
|
|
}
|
|
}
|
|
|
|
// ── Logo & title ──────────────────────────────────────────────────────
|
|
|
|
private void ApplyLogo(string logoPath)
|
|
{
|
|
if (string.IsNullOrEmpty(logoPath) || !File.Exists(logoPath)) return;
|
|
|
|
var bytes = File.ReadAllBytes(logoPath);
|
|
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
|
tex.filterMode = FilterMode.Bilinear;
|
|
if (!tex.LoadImage(bytes)) return;
|
|
|
|
var logoObj = GetOrCreateUIImage(LogoObjectName, new Vector2(400, 100), new Vector2(0, 60));
|
|
var img = logoObj.GetComponent<Image>();
|
|
img.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
|
|
img.color = Color.white;
|
|
img.preserveAspect = true;
|
|
logoObj.SetActive(true);
|
|
}
|
|
|
|
private void ApplyTitle(string title)
|
|
{
|
|
if (string.IsNullOrEmpty(title)) return;
|
|
|
|
// Re-use an existing Branding_Title object or create one
|
|
var existing = GameObject.Find(TitleObjectName);
|
|
TextMeshProUGUI tmp;
|
|
|
|
if (existing == null)
|
|
{
|
|
var canvas = GameObject.Find(CanvasName);
|
|
if (canvas == null) return;
|
|
|
|
existing = new GameObject(TitleObjectName);
|
|
existing.transform.SetParent(canvas.transform, false);
|
|
|
|
var rt = existing.AddComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(0.5f, 1f);
|
|
rt.anchorMax = new Vector2(0.5f, 1f);
|
|
rt.pivot = new Vector2(0.5f, 1f);
|
|
rt.anchoredPosition = new Vector2(0, -10);
|
|
rt.sizeDelta = new Vector2(800, 80);
|
|
|
|
tmp = existing.AddComponent<TextMeshProUGUI>();
|
|
tmp.fontSize = 48;
|
|
tmp.fontStyle = FontStyles.Bold;
|
|
tmp.alignment = TextAlignmentOptions.Center;
|
|
tmp.color = Color.white;
|
|
}
|
|
else
|
|
{
|
|
tmp = existing.GetComponent<TextMeshProUGUI>();
|
|
if (tmp == null) tmp = existing.AddComponent<TextMeshProUGUI>();
|
|
}
|
|
|
|
tmp.text = title;
|
|
existing.SetActive(true);
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Finds an existing Image GameObject by name, or creates one anchored to the
|
|
/// top-centre of the Canvas.
|
|
/// </summary>
|
|
private GameObject GetOrCreateUIImage(string objName, Vector2 size, Vector2 anchoredPos)
|
|
{
|
|
var existing = GameObject.Find(objName);
|
|
if (existing != null) return existing;
|
|
|
|
var canvas = GameObject.Find(CanvasName);
|
|
if (canvas == null) return new GameObject(objName); // fallback
|
|
|
|
var obj = new GameObject(objName);
|
|
obj.transform.SetParent(canvas.transform, false);
|
|
|
|
var rt = obj.AddComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(0.5f, 1f);
|
|
rt.anchorMax = new Vector2(0.5f, 1f);
|
|
rt.pivot = new Vector2(0.5f, 1f);
|
|
rt.anchoredPosition = anchoredPos;
|
|
rt.sizeDelta = size;
|
|
|
|
obj.AddComponent<Image>();
|
|
return obj;
|
|
}
|
|
|
|
// ── Colour utilities ──────────────────────────────────────────────────
|
|
|
|
private static Color ParseColor(string hex, string fallback)
|
|
{
|
|
if (!string.IsNullOrEmpty(hex) && TryParseHex(hex, out var c)) return c;
|
|
if (!string.IsNullOrEmpty(fallback) && TryParseHex(fallback, out var f)) return f;
|
|
return Color.white;
|
|
}
|
|
|
|
private static bool TryParseHex(string hex, out Color color)
|
|
{
|
|
color = Color.white;
|
|
hex = hex?.TrimStart('#');
|
|
if (hex == null || (hex.Length != 6 && hex.Length != 8)) return false;
|
|
try
|
|
{
|
|
byte r = System.Convert.ToByte(hex.Substring(0, 2), 16);
|
|
byte g = System.Convert.ToByte(hex.Substring(2, 2), 16);
|
|
byte b = System.Convert.ToByte(hex.Substring(4, 2), 16);
|
|
byte a = hex.Length == 8 ? System.Convert.ToByte(hex.Substring(6, 2), 16) : (byte)255;
|
|
color = new Color32(r, g, b, a);
|
|
return true;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static Color Brighten(Color c, float v) =>
|
|
new Color(Mathf.Clamp01(c.r + v), Mathf.Clamp01(c.g + v), Mathf.Clamp01(c.b + v), c.a);
|
|
|
|
private static Color Darken(Color c, float v) =>
|
|
new Color(Mathf.Clamp01(c.r - v), Mathf.Clamp01(c.g - v), Mathf.Clamp01(c.b - v), c.a);
|
|
}
|
|
}
|