Update HotReload

This commit is contained in:
2026-03-28 12:54:41 +07:00
parent f2173d2c73
commit a4f2654d0b
278 changed files with 2027 additions and 1896 deletions
@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal static class ReportWindowAPI {
public static void OpenBugReport(string title = null, string description = null) {
HotReloadBugReportWindow.Open(
ReportMode.BugReport,
discordUrl: Constants.DiscordInviteUrl,
contactUrl: Constants.ContactURL,
email: HotReloadPrefs.LicenseEmail,
details: description,
title: title
);
}
public static void OpenFeedback(
Func<Report, Task<string>> submitHandler = null,
string discordUrl = null,
string contactUrl = null,
string title = null,
string email = null,
string feedback = null
) {
HotReloadBugReportWindow.Open(
ReportMode.Feedback,
discordUrl: Constants.DiscordInviteUrl,
contactUrl: Constants.ContactURL,
email: HotReloadPrefs.LicenseEmail
);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a7505b2e85b481479de6861e72e954a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,616 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using SingularityGroup.HotReload.Editor.Localization;
using Application = UnityEngine.Application;
#if UNITY_2021_3_OR_NEWER
using System.IO;
using System.IO.Compression;
using SingularityGroup.HotReload.Editor.Cli;
using SingularityGroup.HotReload.EditorDependencies;
using CompressionLevel = System.IO.Compression.CompressionLevel;
#endif
namespace SingularityGroup.HotReload.Editor {
internal enum ReportMode {
BugReport,
Feedback
}
internal class HotReloadBugReportWindow : EditorWindow {
static List<string> SeverityChoices => new List<string> {
Translations.BugReport.SeverityNormal,
Translations.BugReport.SeverityLow,
Translations.BugReport.SeverityHigh,
};
static List<string> FrequencyChoices => new List<string> {
Translations.BugReport.FrequencyFirstTime,
Translations.BugReport.FrequencySometimes,
Translations.BugReport.FrequencyAlways,
};
[SerializeField] ReportMode _mode = ReportMode.BugReport;
[SerializeField] VisualTreeAsset visualTree;
[SerializeField] StyleSheet styleSheet;
string _discordUrl;
string _contactUrl;
TextField _titleField;
PopupField<string> _severityPopup;
PopupField<string> _frequencyPopup;
TextField _emailField;
IMGUIContainer _detailsContainer;
[SerializeField] string _detailsText = "";
[SerializeField] string _emailText = "";
[SerializeField] string _titleText = "";
Label _detailsLabel;
Label _validationLabel;
Button _submitButton;
VisualElement _severitySection;
VisualElement _frequencySection;
ScrollView _scrollView;
VisualElement _detailsSlot;
VisualElement _successScreen;
VisualElement _failureScreen;
Label _failureErrorLabel;
Button _tabBugReport;
Button _tabFeedback;
bool _isOnResultScreen;
static Vector2 GetMinSize(ReportMode mode) {
return mode == ReportMode.Feedback
? new Vector2(420, 500)
: new Vector2(420, 620);
}
#region Public API
/// <summary>
/// Opens the window with the given mode and configuration.
/// Prefer using <see cref="ReportWindowAPI"/> for a cleaner call site.
/// </summary>
public static HotReloadBugReportWindow Open(
ReportMode mode,
string discordUrl = null,
string contactUrl = null,
string title = null,
string email = null,
string details = null
) {
var wnd = GetWindow<HotReloadBugReportWindow>(utility: true);
wnd._mode = mode;
wnd._discordUrl = discordUrl;
wnd._contactUrl = contactUrl;
wnd.titleContent = new GUIContent(
mode == ReportMode.Feedback
? Translations.BugReport.WindowTitleFeedback
: Translations.BugReport.WindowTitleBugReport);
wnd.minSize = wnd.maxSize = GetMinSize(mode);
wnd._titleText = title;
wnd._emailText = email;
wnd._detailsText = details;
wnd.RebuildUI();
return wnd;
}
/// <summary>
/// Prefills form fields on an already-open window. Null values are ignored.
/// </summary>
public void Prefill() {
if (_titleText != null && _titleField != null) {
_titleField.SetValueWithoutNotify(_titleText);
}
if (_emailText != null && _emailField != null) {
_emailField.SetValueWithoutNotify(_emailText);
}
}
#endregion
void OnEnable() {
RebuildUI();
}
bool HasUnsavedFormContent() {
if (_isOnResultScreen) {
return false;
}
if (!string.IsNullOrWhiteSpace(_titleField.value)) {
return true;
}
if (!string.IsNullOrWhiteSpace(_detailsText) && _detailsText != Translations.BugReport.PlaceholderDetails) {
return true;
}
return false;
}
void OnDestroy() {
if (_detailsContainer != null && _detailsSlot != null) {
_detailsSlot.Remove(_detailsContainer);
_detailsContainer = null;
}
if (!HasUnsavedFormContent()) {
return;
}
bool discard = EditorUtility.DisplayDialog(
Translations.BugReport.DiscardDialogTitle,
Translations.BugReport.DiscardDialogMessage,
Translations.BugReport.DiscardDialogConfirm,
Translations.BugReport.DiscardDialogCancel);
if (!discard) {
_emailText = _emailField.value;
_titleText = _titleField.value;
EditorApplication.delayCall += () => {
Open(_mode, _discordUrl, _contactUrl);
};
} else {
_detailsText = null;
_emailText = null;
_titleText = null;
}
}
void RebuildUI() {
DetachCallbacks();
if (visualTree == null) {
Log.Warning($"Could not open bug report. Please reach out to our support: {_contactUrl}");
return;
}
rootVisualElement.Clear();
visualTree.CloneTree(rootVisualElement);
if (styleSheet != null) {
rootVisualElement.styleSheets.Add(styleSheet);
}
QueryElements();
AttachIMGUIContainers();
ApplyTranslations();
CreatePopupFields();
ApplyMode();
SetupSubmit();
SetupResultScreenButtons();
FixSingleLineFieldAlignment(_titleField);
FixSingleLineFieldAlignment(_emailField);
Prefill();
AttachCallbacks();
ShowFormView();
}
void AttachIMGUIContainers() {
_detailsSlot = rootVisualElement.Q<VisualElement>("details-field-slot");
// Build the IMGUI textarea and inject it
_detailsContainer = new IMGUIContainer(DrawDetailsField);
_detailsSlot.Add(_detailsContainer);
}
void QueryElements() {
_titleField = rootVisualElement.Q<TextField>("title-field");
_emailField = rootVisualElement.Q<TextField>("email-field");
_detailsLabel = rootVisualElement.Q<Label>("details-label");
_validationLabel = rootVisualElement.Q<Label>("validation-label");
_submitButton = rootVisualElement.Q<Button>("submit-button");
_severitySection = rootVisualElement.Q<VisualElement>("severity-section");
_frequencySection = rootVisualElement.Q<VisualElement>("frequency-section");
_scrollView = rootVisualElement.Q<ScrollView>("scroll-view");
_successScreen = rootVisualElement.Q<VisualElement>("success-screen");
_failureScreen = rootVisualElement.Q<VisualElement>("failure-screen");
_failureErrorLabel = rootVisualElement.Q<Label>("failure-error-label");
_tabBugReport = rootVisualElement.Q<Button>("tab-bug-report");
_tabFeedback = rootVisualElement.Q<Button>("tab-feedback");
}
private void DrawDetailsField() {
// GUILayout.ExpandHeight lets it grow; clamp min so it doesn't collapse
_detailsText = EditorGUILayout.TextArea(
_detailsText,
GUILayout.ExpandWidth(true),
GUILayout.MinHeight(160)
);
}
void ApplyTranslations() {
// Form labels
rootVisualElement.Q<Label>(className: "section-label") // Title section — first one
?.SetText(Translations.BugReport.LabelTitle);
rootVisualElement.Q("severity-section")?.Q<Label>(className: "section-label")
?.SetText(Translations.BugReport.LabelIssueSeverity);
rootVisualElement.Q("frequency-section")?.Q<Label>(className: "section-label")
?.SetText(Translations.BugReport.LabelHowOften);
rootVisualElement.Q("tabs")?.Q<Button>("tab-bug-report")
?.SetText(Translations.BugReport.TabBugReport);
rootVisualElement.Q("tabs")?.Q<Button>("tab-feedback")
?.SetText(Translations.BugReport.TabFeedback);
// Contact section — scoped by class since it has no name
var contactSection = rootVisualElement.Q(className: "contact-section");
contactSection?.Q<Label>(className: "section-label")?.SetText(Translations.BugReport.LabelContactEmail);
contactSection?.Q<Label>(className: "fine-print")?.SetText(Translations.BugReport.LabelContactEmailFinePrint);
// Submit button
_submitButton.text = Translations.BugReport.ButtonSubmit;
// Success screen
var successContent = _successScreen?.Q(className: "result-content");
successContent?.Q<Label>(className: "result-title")?.SetText(Translations.BugReport.SuccessTitle);
successContent?.Q<Label>(className: "result-message")?.SetText(Translations.BugReport.SuccessMessage);
_successScreen?.Q<Button>("success-discord-button")?.SetText(Translations.BugReport.ButtonJoinDiscord);
// Failure screen
var failureContent = _failureScreen?.Q(className: "result-content");
failureContent?.Q<Label>(className: "result-title")?.SetText(Translations.BugReport.FailureTitle);
// result-message--error is the dynamic error label, skip it; translate the static one
var failureMessages = failureContent?.Query<Label>(className: "result-message").ToList();
if (failureMessages != null) {
foreach (var lbl in failureMessages) {
if (!lbl.ClassListContains("result-message--error")) {
lbl.text = Translations.BugReport.FailureMessage;
}
}
}
_failureScreen?.Q<Button>("failure-discord-button")?.SetText(Translations.BugReport.ButtonJoinDiscord);
_failureScreen?.Q<Button>("failure-contact-button")?.SetText(Translations.BugReport.ButtonContactUs);
}
#region View switching
void ShowFormView() {
_isOnResultScreen = false;
_scrollView.style.display = DisplayStyle.Flex;
_successScreen.RemoveFromClassList("result-screen--visible");
_failureScreen.RemoveFromClassList("result-screen--visible");
}
void ShowSuccessScreen() {
_isOnResultScreen = true;
_scrollView.style.display = DisplayStyle.None;
_tabFeedback.style.display = DisplayStyle.None;
_tabBugReport.style.display = DisplayStyle.None;
_failureScreen.RemoveFromClassList("result-screen--visible");
_successScreen.AddToClassList("result-screen--visible");
}
void ShowFailureScreen(string errorMessage) {
_scrollView.style.display = DisplayStyle.None;
_tabFeedback.style.display = DisplayStyle.None;
_tabBugReport.style.display = DisplayStyle.None;
_successScreen.RemoveFromClassList("result-screen--visible");
if (_failureErrorLabel != null) {
if (string.IsNullOrEmpty(errorMessage)) {
_failureErrorLabel.style.display = DisplayStyle.None;
} else {
_failureErrorLabel.text = errorMessage;
_failureErrorLabel.style.display = DisplayStyle.Flex;
}
}
_failureScreen.AddToClassList("result-screen--visible");
_isOnResultScreen = true;
}
#endregion
void ApplyMode() {
bool isBugReport = _mode == ReportMode.BugReport;
this.minSize = this.maxSize = GetMinSize(_mode);
if (isBugReport) {
_severitySection.style.display = DisplayStyle.Flex;
_frequencySection.style.display = DisplayStyle.Flex;
_severityPopup.style.display = DisplayStyle.Flex;
_frequencyPopup.style.display = DisplayStyle.Flex;
_detailsLabel.text = Translations.BugReport.LabelDetails;
_tabBugReport.AddToClassList("tab--active");
_tabFeedback.RemoveFromClassList("tab--active");
if (string.IsNullOrEmpty(_detailsText)) {
_detailsText = Translations.BugReport.PlaceholderDetails;
_detailsContainer.MarkDirtyRepaint();
}
} else {
_severitySection.style.display = DisplayStyle.None;
_frequencySection.style.display = DisplayStyle.None;
_severityPopup.style.display = DisplayStyle.None;
_frequencyPopup.style.display = DisplayStyle.None;
_detailsLabel.text = Translations.BugReport.LabelFeedback;
_tabFeedback.AddToClassList("tab--active");
_tabBugReport.RemoveFromClassList("tab--active");
if (_detailsText == Translations.BugReport.PlaceholderDetails) {
_detailsText = string.Empty;
_detailsContainer.MarkDirtyRepaint();
}
}
}
void CreatePopupFields() {
var severityContainer = rootVisualElement.Q<VisualElement>("severity-container");
_severityPopup = new PopupField<string>(
string.Empty,
SeverityChoices,
0);
_severityPopup.AddToClassList("input-field");
severityContainer.Add(_severityPopup);
var frequencyContainer = rootVisualElement.Q<VisualElement>("frequency-container");
_frequencyPopup = new PopupField<string>(
string.Empty,
FrequencyChoices,
0);
_frequencyPopup.AddToClassList("input-field");
frequencyContainer.Add(_frequencyPopup);
}
void SelectBugReport() {
_mode = ReportMode.BugReport;
ApplyMode();
}
void SelectFeedback() {
_mode = ReportMode.Feedback;
ApplyMode();
}
void AttachCallbacks() {
if (_tabBugReport != null) {
_tabBugReport.clicked += SelectBugReport;
}
if (_tabFeedback != null) {
_tabFeedback.clicked += SelectFeedback;
}
if (_emailField != null) {
_emailField.RegisterValueChangedCallback(SetEmailText);
}
if (_titleField != null) {
_titleField.RegisterValueChangedCallback(SetTitleText);
}
}
void DetachCallbacks() {
if (_tabBugReport != null) {
_tabBugReport.clicked -= SelectBugReport;
}
if (_tabFeedback != null) {
_tabFeedback.clicked -= SelectFeedback;
}
if (_emailField != null) {
_emailField.UnregisterValueChangedCallback(SetEmailText);
}
if (_titleField != null) {
_titleField.UnregisterValueChangedCallback(SetTitleText);
}
}
void SetEmailText(ChangeEvent<string> @event) {
_emailText = @event.newValue;
}
void SetTitleText(ChangeEvent<string> @event) {
_titleText = @event.newValue;
}
static void FixSingleLineFieldAlignment(TextField field) {
field.RegisterCallback<GeometryChangedEvent>(evt => {
var input = field.Q(className: "unity-text-field__input");
if (input != null) {
input.style.alignItems = Align.Center;
}
});
}
#region Validation & submit
void SetupSubmit() {
_submitButton.clickable.clicked += OnSubmitClicked;
}
async void OnSubmitClicked() {
HideValidation();
if (string.IsNullOrWhiteSpace(_titleField.value)) {
ShowValidation(Translations.BugReport.ValidationTitleRequired);
return;
}
var details = _detailsText;
var report = new Report {
Mode = _mode,
Id = Guid.NewGuid().ToString("N").Substring(0, 10),
Title = _titleField.value.Trim(),
Severity = _severityPopup != null ? _severityPopup.value : null,
Frequency = _frequencyPopup != null ? _frequencyPopup.value : null,
ContactEmail = _emailField.value != null ? _emailField.value.Trim() : null,
Details = details != null ? details.Trim() : null
};
_submitButton.SetEnabled(false);
_submitButton.text = Translations.BugReport.ButtonSubmitting;
string error;
try {
error = await HandleBugReport(report);
} catch (Exception ex) {
error = string.Format(Translations.BugReport.SubmitHandlerError, ex.Message);
}
if (this == null) {
return;
}
_submitButton.SetEnabled(true);
_submitButton.text = Translations.BugReport.ButtonSubmit;
if (error == null) {
ShowSuccessScreen();
if (report.Mode == ReportMode.BugReport) {
// Back up log and patches locally for this bug report
// They can be later used to simplify reproducing and fixing the issue, but due to our privacy policy they can't be auto uploaded so we need to ask for them
#if UNITY_2021_3_OR_NEWER
var logName = LogsHelper.FindRecentLog(HotReloadAboutTab.logsPath);
try {
CreateBugReportZip(
Path.Combine(CliUtils.GetCliTempDir(), "Backup"),
logName == null ? null : Path.Combine(HotReloadAboutTab.logsPath, logName),
Path.Combine(PackageConst.LibraryCachePath, "BugReports", $"{report.Id}.zip")
);
} catch {
// Fail silently. If zip is not available we will have to ask for a reproduce
}
#endif
}
} else {
ShowFailureScreen(error);
}
}
private static Task<string> HandleBugReport(Report report) {
var hwId = HotReloadPrefs.HardwareId;
if (string.IsNullOrEmpty(hwId)) {
hwId = "unknown";
}
return RequestHelper.SubmitBugReport(new BugReport {
reportId = report.Id,
label = report.Mode == ReportMode.Feedback ? "Feedback" : "Bug Report",
title = report.Title,
description = report.Details,
email = report.ContactEmail,
hwId = hwId,
hotReloadVersion = PackageConst.Version,
unityVersion = Application.unityVersion,
operatingSystemVersionInfo = SystemInfo.operatingSystem,
});
}
#if UNITY_2021_3_OR_NEWER
private static void CreateBugReportZip(
string sourceDir,
string sourceFile,
string outputZipPath
) {
var files = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories);
if (files.Length == 0 && sourceFile == null) {
return;
}
Directory.CreateDirectory(new FileInfo(outputZipPath).DirectoryName!);
using (var stream = new FileStream(outputZipPath, FileMode.Create, FileAccess.Write, FileShare.None, 65536)) {
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: false)) {
if (sourceFile != null) {
AddFileToArchive(archive, sourceFile, Path.GetFileName(sourceFile));
}
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) {
string relativePath = Path.GetRelativePath(sourceDir, filePath);
string entryName = $"Patches/{relativePath}".Replace('\\', '/');
AddFileToArchive(archive, filePath, entryName);
}
}
}
}
private static void AddFileToArchive(ZipArchive archive, string filePath, string entryName) {
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
using (var entryStream = entry.Open()) {
using (var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite, // allow concurrent writers
65536)) {
fileStream.CopyTo(entryStream);
}
}
}
#endif
void ShowValidation(string message) {
_validationLabel.text = message;
_validationLabel.AddToClassList("validation-label--visible");
}
void HideValidation() {
_validationLabel.RemoveFromClassList("validation-label--visible");
}
#endregion
#region Result screen buttons
void SetupResultScreenButtons() {
var successDiscord = rootVisualElement.Q<Button>("success-discord-button");
var failureDiscord = rootVisualElement.Q<Button>("failure-discord-button");
var failureContact = rootVisualElement.Q<Button>("failure-contact-button");
if (successDiscord != null) {
successDiscord.clickable.clicked += OnDiscordClicked;
}
if (failureDiscord != null) {
failureDiscord.clickable.clicked += OnDiscordClicked;
}
if (failureContact != null) {
failureContact.clickable.clicked += OnContactClicked;
}
bool hasDiscord = !string.IsNullOrEmpty(_discordUrl);
bool hasContact = !string.IsNullOrEmpty(_contactUrl);
if (successDiscord != null) {
successDiscord.style.display = hasDiscord ? DisplayStyle.Flex : DisplayStyle.None;
}
if (failureDiscord != null) {
failureDiscord.style.display = hasDiscord ? DisplayStyle.Flex : DisplayStyle.None;
}
if (failureContact != null) {
failureContact.style.display = hasContact ? DisplayStyle.Flex : DisplayStyle.None;
}
}
void OnDiscordClicked() {
if (!string.IsNullOrEmpty(_discordUrl)) {
Application.OpenURL(_discordUrl);
}
}
void OnContactClicked() {
if (!string.IsNullOrEmpty(_contactUrl)) {
Application.OpenURL(_contactUrl);
}
}
#endregion
}
[Serializable]
internal class Report {
public string Id;
public ReportMode Mode;
public string Title;
public string Severity;
public string Frequency;
public string ContactEmail;
public string Details;
}
}
// Small extension to avoid repetitive null checks on label/button text assignment
namespace SingularityGroup.HotReload.Editor {
internal static class VisualElementExtensions {
public static void SetText(this UnityEngine.UIElements.Label label, string text) {
if (label != null) label.text = text;
}
public static void SetText(this UnityEngine.UIElements.Button button, string text) {
if (button != null) button.text = text;
}
}
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 9cceb96662b8a6841a0c7d4adaf01331
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- visualTree: {fileID: 9197481963319205126, guid: d9941d55822724b41bff198971988a21, type: 3}
- styleSheet: {fileID: 7433441132597879392, guid: b8a805d0a53f99248a999c327b3a151e, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,218 @@
.root {
padding: 12px;
flex-grow: 1;
}
.section {
margin-bottom: 12px;
}
.section-label {
font-size: 13px;
-unity-font-style: bold;
margin-bottom: 4px;
color: var(--unity-colors-label-text);
}
.input-field {
height: 28px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.contact-section {
padding: 10px;
border-width: 1px;
border-color: var(--unity-colors-input_field-border);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
background-color: rgba(127, 127, 127, 0.08);
}
.fine-print {
font-size: 10px;
color: var(--unity-colors-label-text-disabled);
margin-top: 4px;
white-space: normal;
-unity-font-style: italic;
}
.details-section {
flex-grow: 1;
}
.details-input {
flex-grow: 1;
min-height: 160px;
}
.details-input > .unity-text-field__input {
flex-grow: 1;
-unity-text-align: upper-left;
padding: 6px 8px;
white-space: normal;
}
.details-input .unity-text-element {
white-space: normal;
}
.validation-label {
color: rgb(220, 60, 60);
font-size: 12px;
margin-bottom: 4px;
-unity-text-align: middle-center;
display: none;
}
.validation-label--visible {
display: flex;
}
.submit-row {
flex-direction: row-reverse;
margin-top: 4px;
padding-bottom: 8px;
}
.submit-button {
width: 100px;
height: 30px;
font-size: 13px;
-unity-font-style: bold;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* Placeholder styling */
.placeholder-active > .unity-text-field__input > .unity-text-element {
color: var(--unity-colors-label-text-disabled);
}
/* ── Result screens ── */
.result-screen {
display: none;
flex-grow: 1;
justify-content: center;
align-items: center;
padding: 32px;
}
.result-screen--visible {
display: flex;
}
.result-content {
align-items: center;
max-width: 360px;
}
.result-icon {
font-size: 48px;
margin-bottom: 12px;
}
.result-icon--success {
color: rgb(40, 160, 40);
}
.result-icon--failure {
color: rgb(200, 40, 40);
}
.result-title {
font-size: 18px;
-unity-font-style: bold;
margin-bottom: 8px;
color: var(--unity-colors-label-text);
-unity-text-align: middle-center;
}
.result-message {
font-size: 12px;
color: var(--unity-colors-label-text-disabled);
margin-bottom: 6px;
-unity-text-align: middle-center;
white-space: normal;
}
.result-message--error {
color: rgb(200, 60, 60);
-unity-font-style: italic;
margin-bottom: 12px;
}
.result-buttons {
margin-top: 16px;
align-items: center;
}
.result-button {
height: 30px;
min-width: 180px;
margin-bottom: 6px;
font-size: 13px;
-unity-font-style: bold;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.result-button--primary {
background-color: rgb(60, 120, 200);
color: rgb(240, 240, 240);
border-width: 0;
}
.result-button--primary:hover {
background-color: rgb(75, 135, 215);
}
.result-button--secondary {
background-color: rgba(0, 0, 0, 0);
color: var(--unity-colors-label-text-disabled);
border-width: 1px;
border-color: var(--unity-colors-input_field-border);
margin-top: 4px;
}
.result-button--secondary:hover {
color: var(--unity-colors-label-text);
border-color: var(--unity-colors-input_field-border-focus);
}
.tab-bar {
flex-direction: row;
border-bottom-width: 1px;
border-bottom-color: var(--unity-colors-toolbar-border);
background-color: var(--unity-colors-toolbar-background);
flex-shrink: 0;
padding: 0 4px;
}
.tab {
flex: 1;
background-color: transparent;
border-width: 0;
border-radius: 0;
color: var(--unity-colors-label-text-disabled);
padding: 6px 0 5px 0;
margin: 0 2px;
-unity-font-style: normal;
}
.tab--active {
color: var(--unity-colors-label-text);
background-color: var(--unity-colors-toolbar-button-background-checked);
border-width: 0 0 2px 0;
border-color: #3c79f2;
padding-bottom: 3px;
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b8a805d0a53f99248a999c327b3a151e
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
unsupportedSelectorAction: 0
@@ -0,0 +1,78 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<ui:VisualElement name="tabs" class="tab-bar">
<ui:Button name="tab-bug-report" text="Bug Report" class="tab tab--active" />
<ui:Button name="tab-feedback" text="Feedback" class="tab" />
</ui:VisualElement>
<!-- Form view -->
<ui:ScrollView name="scroll-view" class="root">
<!-- Title -->
<ui:VisualElement class="section">
<ui:Label text="Title" class="section-label" />
<ui:TextField name="title-field" class="input-field" />
</ui:VisualElement>
<!-- Issue Severity (PopupField added from C#, hidden in Feedback mode) -->
<ui:VisualElement name="severity-section" class="section">
<ui:Label text="Issue Severity" class="section-label" />
<ui:VisualElement name="severity-container" />
</ui:VisualElement>
<!-- How often does it happen? (hidden in Feedback mode) -->
<ui:VisualElement name="frequency-section" class="section">
<ui:Label text="How often does it happen?" class="section-label" />
<ui:VisualElement name="frequency-container" />
</ui:VisualElement>
<!-- Contact Email -->
<ui:VisualElement class="section contact-section">
<ui:Label text="Contact Email (optional)" class="section-label" />
<ui:TextField name="email-field" class="input-field" />
<ui:Label text="This email will only be used to follow up on this bug report and will not be stored or shared for any other purpose." class="fine-print" />
</ui:VisualElement>
<!-- Details / Feedback -->
<ui:VisualElement class="section details-section">
<ui:Label name="details-label" text="Details" class="section-label" />
<ui:VisualElement name="details-field-slot" class="details-input"/>
</ui:VisualElement>
<!-- Validation message -->
<ui:Label name="validation-label" class="validation-label" />
<!-- Submit -->
<ui:VisualElement class="submit-row">
<ui:Button name="submit-button" text="Submit" class="submit-button" />
</ui:VisualElement>
</ui:ScrollView>
<!-- Success screen (hidden by default) -->
<ui:VisualElement name="success-screen" class="result-screen">
<ui:VisualElement class="result-content">
<ui:Label text="&#x2713;" class="result-icon result-icon--success" />
<ui:Label text="Report Submitted" class="result-title" />
<ui:Label text="Thank you! We've accepted your report and are processing it." class="result-message" />
<ui:VisualElement class="result-buttons">
<ui:Button name="success-discord-button" text="Join Our Discord" class="result-button result-button--primary" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
<!-- Failure screen (hidden by default) -->
<ui:VisualElement name="failure-screen" class="result-screen">
<ui:VisualElement class="result-content">
<ui:Label text="&#x2717;" class="result-icon result-icon--failure" />
<ui:Label text="Submission Failed" class="result-title" />
<ui:Label name="failure-error-label" text="" class="result-message result-message--error" />
<ui:Label text="Please reach out to us directly so we can help resolve your issue." class="result-message" />
<ui:VisualElement class="result-buttons">
<ui:Button name="failure-discord-button" text="Join Our Discord" class="result-button result-button--primary" />
<ui:Button name="failure-contact-button" text="Contact Us" class="result-button result-button--primary" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d9941d55822724b41bff198971988a21
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}