using System; using System.Collections.Generic; using MelonLoader; using O5Kit.Core; using O5Kit.Factory; using TMPro; using UnityEngine; using UnityEngine.UI; using UnityEngine.UIElements; using Image = UnityEngine.UI.Image; namespace ADOFAIRenderer.UI; /// /// Description of one button of a dialog: label, click action and optional color. /// internal class DialogButton { // ReSharper disable FieldCanBeMadeReadOnly.Global /// Button label, shown as given (callers pass already localized text). public string Text; /// /// Action run on the main Unity thread when the button is clicked. The dialog is destroyed after the action /// returns, also when the action throws (the exception is logged). /// public Action OnClick; /// Normal (idle) background color of the button; null keeps the O5Kit theme button color. public Color? BackgroundColor; // ReSharper restore FieldCanBeMadeReadOnly.Global /// Creates a button description. /// Button label. /// Action run on click, before the dialog closes. /// Optional normal background color; null uses the theme color. public DialogButton(string text, Action onClick, Color? backgroundColor = null) { Text = text; OnClick = onClick; BackgroundColor = backgroundColor; } } /// /// Modal message dialog with a title, a description and one or more buttons. /// /// Each call creates its own GameObject (kept across scene loads) with a screen-space overlay /// canvas: a translucent full-screen backdrop that blocks input to everything below, and an O5Kit window on top of it. /// While the dialog exists, every enabled UI Toolkit root () is disabled so that it cannot /// receive input; the roots are enabled again in . Clicking any button runs its action and then /// destroys the dialog. All members run on the main Unity thread. /// /// internal class CommonWarningDialogUI : MonoBehaviour { /// /// The most recently created dialog, or null when that dialog has been destroyed. /// FirstUpdateHook checks it to wait until a startup dialog is closed. /// If several dialogs are open at once, this refers to the newest one only. /// public static CommonWarningDialogUI? Instance { get; private set; } /// UI Toolkit root elements disabled by this dialog, enabled again when the dialog is destroyed. private readonly List _disabledElements = new(); /// O5Kit window holding the title, description and buttons; disposed in . private O5Window? _window; private void Awake() { Instance = this; } /// /// Disposes the window, enables the UI Toolkit roots disabled in and clears /// if it still refers to this dialog. /// private void OnDestroy() { _window?.Dispose(); _window = null; foreach (var elem in _disabledElements) { if (elem != null) { elem.SetEnabled(true); } } _disabledElements.Clear(); if (Instance == this) { Instance = null; } } /// /// Creates and shows a new dialog. Must be called on the main Unity thread. /// /// Window title text. /// Body text, centered and word-wrapped; TextMeshPro rich text tags are allowed. /// /// Buttons laid out left to right with equal widths. With no buttons nothing is shown, because the dialog /// has no close button and could not be dismissed. /// public static void ShowDialog(string title, string description, params DialogButton[] buttons) { if (buttons.Length == 0) return; var obj = new GameObject("CommonWarningDialogUI"); DontDestroyOnLoad(obj); var dialog = obj.AddComponent(); dialog.Initialize(title, description, buttons); } /// /// Builds the canvas, backdrop, window and contents. Layout sizes are in canvas units at the 1920x1080 reference /// resolution of the . /// private void Initialize(string title, string description, DialogButton[] buttons) { // uGUI buttons need an EventSystem to receive clicks. When the current scene has none, a child EventSystem // is created; it is destroyed together with the dialog. if (!UnityEngine.EventSystems.EventSystem.current) { var eventSystemObj = new GameObject("EventSystem"); eventSystemObj.transform.SetParent(transform); eventSystemObj.AddComponent(); eventSystemObj.AddComponent(); } // Disable every enabled UI Toolkit root so that it ignores input while the dialog is open. // Roots that are already disabled are left alone and are not re-enabled in OnDestroy. foreach (var doc in FindObjectsByType(FindObjectsSortMode.None)) { if (doc && doc.rootVisualElement is { enabledSelf: true }) { doc.rootVisualElement.SetEnabled(false); _disabledElements.Add(doc.rootVisualElement); } } // Set UI layer (5, Unity's built-in "UI" layer) gameObject.layer = 5; // Canvas setup. // Sorting order 32766 draws the dialog above the RendererUIManager window canvas (32000), the render overlays // (DebugMonitorUI 9999, EscapeHandlerUI 10000) and below FFmpegDownloadUI (32767, the largest sorting order). var canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 32766; // Scale with screen size against a 1920x1080 reference; match 0.5 blends width and height scaling equally. var scaler = gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = 0.5f; gameObject.AddComponent(); var ctx = RendererUIManager.Context; // 1. Create full screen backdrop (scrim) to block background input. // Black at 65% opacity, stretched over the whole canvas; raycastTarget makes it swallow clicks. var bgPanelObj = new GameObject("Backdrop"); bgPanelObj.transform.SetParent(transform, false); bgPanelObj.layer = 5; var bgImage = bgPanelObj.AddComponent(); bgImage.color = new Color(0f, 0f, 0f, 0.65f); bgImage.raycastTarget = true; var bgRect = bgPanelObj.GetComponent(); bgRect.anchorMin = Vector2.zero; bgRect.anchorMax = Vector2.one; bgRect.offsetMin = Vector2.zero; bgRect.offsetMax = Vector2.zero; // 2. Create the O5Kit window as a child of the backdrop: 540x240 units, draggable, fixed size, // no close button (the dialog closes only through its buttons), 16-unit inner padding. _window = O5Window.Create(ctx, bgPanelObj.transform, new O5WindowOptions { Id = "RendererMod_WarningDialog", Title = title, Size = new Vector2(540, 240), Draggable = true, Resizable = false, ShowCloseButton = false, ShowOutline = true, Padding = new RectOffset(16, 16, 16, 16) }); // 3. Build content inside window.Content: a vertical stack of the description (takes the remaining height) // and the button row (40 units high), 16 units apart. var content = _window.Content; var vLayout = content.gameObject.AddComponent(); vLayout.spacing = 16f; vLayout.padding = new RectOffset(16, 16, 12, 12); vLayout.childControlWidth = true; vLayout.childControlHeight = true; vLayout.childForceExpandWidth = true; vLayout.childForceExpandHeight = false; // Description text: 18 pt, centered, word-wrapped, light gray (88% white) var descText = O5Factory.ControlText(ctx, content, 18f, noPad: true); descText.text = description; descText.alignment = TextAlignmentOptions.Center; descText.textWrappingMode = TextWrappingModes.Normal; descText.lineSpacing = 6; descText.color = new Color(0.88f, 0.88f, 0.88f, 1f); // ReSharper disable once InconsistentNaming var descLE = descText.gameObject.AddComponent(); descLE.flexibleWidth = 1f; descLE.flexibleHeight = 1f; // Button action row: buttons share the row width equally, 10 units apart var btnRowObj = new GameObject("ButtonRow"); btnRowObj.transform.SetParent(content, false); var btnRowRect = btnRowObj.AddComponent(); var btnRowLE = btnRowObj.AddComponent(); btnRowLE.preferredHeight = 40f; btnRowLE.flexibleWidth = 1f; var hLayout = btnRowObj.AddComponent(); hLayout.spacing = 10f; hLayout.childAlignment = TextAnchor.MiddleCenter; hLayout.childControlWidth = true; hLayout.childControlHeight = true; hLayout.childForceExpandWidth = true; hLayout.childForceExpandHeight = true; for (var i = 0; i < buttons.Length; i++) { var btnData = buttons[i]; // The click handler runs the caller's action, logs any exception from it, and always destroys the dialog. var btn = O5Factory.Button( ctx: ctx, parent: btnRowRect, onClick: () => { try { btnData.OnClick(); } catch (Exception ex) { Melon.Logger.Error($"Dialog Button Click Event Error: {ex}"); } Destroy(gameObject); }, text: btnData.Text, id: $"btn_dialog_{i}", height: 40f ); if (btnData.BackgroundColor.HasValue) { btn.NormalColor = btnData.BackgroundColor.Value; btn.UpdateVisual(); } var le = btn.Rect.gameObject.AddComponent(); le.flexibleWidth = 1f; le.preferredHeight = 40f; } } }