using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
namespace QuestAppLauncher
{
///
/// Applies managed branding from
/// 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.
///
///
/// Default colours (original art):
/// Primary #3E6DA9 · Accent #00AEEF
///
///
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";
/// Current primary colour (tab backgrounds).
public static Color PrimaryColor { get; private set; }
/// Current accent colour (selected-tab indicator).
public static Color AccentColor { get; private set; }
// ─────────────────────────────────────────────────────────────────────
///
/// Automatically creates a persistent BrandingHandler GameObject on startup.
/// No scene setup or Unity Editor wiring required.
///
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoInstall()
{
if (FindFirstObjectByType() != null) return;
var go = new GameObject("BrandingHandler");
DontDestroyOnLoad(go);
go.AddComponent();
}
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 ────────────────────────────────────────────────────────
///
/// Recolours every tab child of .
/// Called by after it populates each tab container.
///
public void ApplyToTabParent(GameObject tabParent)
{
if (tabParent == null) return;
foreach (Transform tab in tabParent.transform)
ApplyToTab(tab.gameObject);
}
///
/// Applies branding colours to a single tab (Tab.prefab structure:
/// Toggle root with Background and Checkmark children).
///
public void ApplyToTab(GameObject tab)
{
if (tab == null) return;
var bg = tab.transform.Find("Background");
if (bg != null)
{
var img = bg.GetComponent();
if (img != null) img.color = PrimaryColor;
}
var check = tab.transform.Find("Checkmark");
if (check != null)
{
var img = check.GetComponent();
if (img != null) img.color = AccentColor;
}
var toggle = tab.GetComponent();
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();
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();
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();
tmp.fontSize = 48;
tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
}
else
{
tmp = existing.GetComponent();
if (tmp == null) tmp = existing.AddComponent();
}
tmp.text = title;
existing.SetActive(true);
}
// ── Helpers ───────────────────────────────────────────────────────────
///
/// Finds an existing Image GameObject by name, or creates one anchored to the
/// top-centre of the Canvas.
///
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();
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();
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);
}
}