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