Added Unity project files

This commit is contained in:
2026-06-07 16:58:24 +01:00
parent 3cc05d260b
commit 23bbcab156
3942 changed files with 453676 additions and 0 deletions

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 32b17ad88ef74de8865aa6e2193558a9
timeCreated: 1736896912

View File

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.UIElements.Experimental;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class BuilderProgress: VisualElement
{
public new class UxmlFactory : UxmlFactory<BuilderProgress, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
}
public struct ProgressBarStateData
{
public bool Visible { get; set; }
public string Text { get; set; }
public float Progress { get; set; }
}
public ProgressBarStateData State => _state;
public EventHandler OnCancel;
private ProgressBarStateData _state;
private readonly VisualElement _progressBlock;
private readonly VisualElement _progressBar;
private readonly Label _progressText;
private readonly Button _cancelButton;
private VisualElement _visualRoot;
public BuilderProgress()
{
Resources.Load<VisualTreeAsset>("BuilderProgress").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("BuilderProgressStyles"));
RegisterCallback<AttachToPanelEvent>(evt =>
{
_visualRoot = evt.destinationPanel.visualTree;
});
_progressBlock = this;
_progressBlock.AddToClassList("d-none");
_progressBar = this.Q("progress-bar");
_progressText = this.Q<Label>("progress-text");
_cancelButton = this.Q<Button>("cancel-button");
_cancelButton.clicked += () => OnCancel?.Invoke(this, EventArgs.Empty);
}
public void SetProgress(ProgressBarStateData state)
{
if (_state.Visible != state.Visible)
{
_progressBlock.EnableInClassList("d-none", !state.Visible);
if (state.Visible)
{
_progressBar.style.width = 0;
}
}
_progressText.text = state.Text;
if (Mathf.Abs(_state.Progress - state.Progress) > float.Epsilon)
{
// execute on next frame to allow for layout to calculate
_visualRoot.schedule.Execute(() =>
{
_progressBar.experimental.animation.Start(
new StyleValues {width = _progressBar.layout.width, height = 28f},
new StyleValues {width = _progressBlock.layout.width * state.Progress, height = 28f},
500
);
}).StartingIn(50);
}
_state = state;
}
public void ClearProgress()
{
_progressBar.style.width = 0;
}
public void HideProgress()
{
SetProgress(new ProgressBarStateData { Visible = false });
}
public void SetCancelButtonVisibility(bool isVisible)
{
_cancelButton.EnableInClassList("d-none", !isVisible);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 20ece8432d114cd18c506eada5258014
timeCreated: 1736896921

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4b85293d7e174d5f8b6348567c2c5df2
timeCreated: 1736897025

View File

@ -0,0 +1,9 @@
<UXML xmlns="UnityEngine.UIElements">
<Label name="progress-text" text="Refreshing data..." class="progress-text text-white mb-2" />
<VisualElement name="progress" class="progress-container">
<VisualElement name="progress-bar" class="progress-bar" />
</VisualElement>
<VisualElement class="mt-2">
<Button name="cancel-button" class="flex-grow-1 d-none m-0 text-bold" text="Cancel Upload" />
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 64cf856a19d24b1eafc2c14d18093846
timeCreated: 1736897068

View File

@ -0,0 +1,45 @@
BuilderProgress {
position: absolute;
background-color: rgba(0,0,0,0.9);
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
}
.progress-container {
background-color: #363636;
margin: 4px 0;
height: 24px;
width: 100%;
position: relative;
overflow: hidden;
justify-content: center;
align-items: center;
}
.progress-bar {
position: absolute;
left: 0;
top:0;
height: 100%;
width: 0;
background-color: #006FF8;
}
.progress-text {
-unity-text-align: middle-center;
-unity-font-style: bold;
font-size: 16px;
}
#cancel-button {
font-size: 14px;
padding: 8px 24px 8px 24px;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a7df871579544480afc16fbd395e75ae
timeCreated: 1736897033

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0a430a4db4c4ac0bc2fbd23c0d2b954
timeCreated: 1687542367

View File

@ -0,0 +1,113 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class Checklist: VisualElement
{
public new class UxmlFactory : UxmlFactory<Checklist, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
private readonly UxmlStringAttributeDescription _label = new UxmlStringAttributeDescription { name = "label" };
private readonly UxmlStringAttributeDescription _iconName = new UxmlStringAttributeDescription { name = "icon-name" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var checklistField = (Checklist) ve;
var label = _label.GetValueFromBag(bag, cc);
if (string.IsNullOrWhiteSpace(label))
{
checklistField.Q("checklist-label").AddToClassList("d-none");
}
else
{
checklistField.Q<Label>("checklist-label-text").text = label;
}
var iconName = _iconName.GetValueFromBag(bag, cc);
if (string.IsNullOrWhiteSpace(iconName))
{
checklistField.Q("checklist-label-icon").AddToClassList("d-none");
}
else
{
var icon = EditorGUIUtility.IconContent(iconName);
var darkIcon = EditorGUIUtility.IconContent($"d_{iconName}");
if (EditorGUIUtility.isProSkin && darkIcon != null)
{
checklistField.Q("checklist-label-icon").style.backgroundImage = (Texture2D) darkIcon.image;
}
}
}
}
public class ChecklistItem
{
public string Value { get; set; }
public string Label { get; set; }
public bool Checked { get; set; }
}
private VisualElement _itemsContainer;
private List<ChecklistItem> _items;
public List<ChecklistItem> Items
{
get => _items;
set
{
_items = value;
RenderItems();
}
}
public Checklist()
{
Resources.Load<VisualTreeAsset>("ChecklistLayout").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("ChecklistStyles"));
_itemsContainer = this.Q("checklist-items");
_items = new List<ChecklistItem>();
}
public void MarkItem(string value, bool checkState)
{
var itemIndex = _items.FindIndex(i => i.Value == value);
if (itemIndex < 0) return;
_items[itemIndex].Checked = checkState;
RenderItems();
}
private void RenderItems()
{
_itemsContainer.Clear();
foreach (var item in _items)
{
var container = new VisualElement();
container.AddToClassList("row");
container.AddToClassList("align-items-center");
var icon = new VisualElement();
icon.AddToClassList("icon");
icon.AddToClassList(item.Checked ? "check-icon" : "cross-icon");
container.Add(icon);
var label = new Label(item.Label)
{
name = item.Value
};
container.Add(label);
_itemsContainer.Add(container);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c1877d18642b4765bdeba2e1cb631978
timeCreated: 1687542376

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ae06c41f028e44fda8feb04e8942b483
timeCreated: 1687543126

View File

@ -0,0 +1,9 @@
<UXML xmlns="UnityEngine.UIElements">
<VisualElement class="col" name="checklist-block">
<VisualElement name="checklist-label" class="mb-2 row section-header">
<VisualElement class="icon" name="checklist-label-icon" />
<Label name="checklist-label-text" />
</VisualElement>
<VisualElement name="checklist-items" class="col w-100 mb-2" />
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa4cfed5918f428db418a0c5ee52de02
timeCreated: 1687543136

View File

@ -0,0 +1,60 @@
#checklist-block {
border-bottom-width: 1px;
}
.dark #checklist-block {
border-bottom-color: rgb(26, 26, 26);
}
.light #checklist-block {
border-bottom-color: rgb(127, 127, 127);
}
/*.light #checklist-block {*/
/* border-color: #A9A9A9;*/
/* background-color: rgba(235, 235, 235, 0.2039216);*/
/* color: #161616;*/
/*}*/
/*.dark #checklist-block {*/
/* border-color: #232323;*/
/* background-color: rgba(96, 96, 96, 0.2039216);*/
/* color: #BDBDBD;*/
/*}*/
#checklist-block #checklist-label {
-unity-font-style: bold;
border-bottom-width: 0;
}
#checklist-block #checklist-label #checklist-label-text {
padding-bottom: 1px;
}
#checklist-items .icon {
width: 18px;
height: 18px;
margin-right: 4px;
}
.cross-icon {
background-image: resource("DotFill");
}
.check-icon {
background-image: resource("P4_CheckOutRemote");
}
.light .create-icon {
background-image: resource("CreateAddNew");
}
.dark .create-icon {
background-image: resource("d_CreateAddNew");
}
#checklist-items Label {
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8b3c8c1b61ee451892e761c0d1b0d60e
timeCreated: 1687543390

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d9949ae8c0304d43a461cbf34f7588e8
timeCreated: 1687997045

View File

@ -0,0 +1,43 @@
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class ContentWarningsField : OptionsPopupField<string>
{
private static readonly string[] CONTENT_WARNING_TAGS = { "content_sex", "content_adult", "content_violence", "content_gore", "content_horror" };
protected override IList<string> GetOptions()
{
return CONTENT_WARNING_TAGS;
}
protected override string GetOptionName(string option)
{
return option switch
{
"content_sex" => "Sexually Suggestive",
"content_adult" => "Adult Language and Themes",
"content_violence" => "Graphic Violence",
"content_gore" => "Excessive Gore",
"content_horror" => "Extreme Horror",
_ => null
};
}
protected override bool IsOptionLocked(string option)
{
return OriginalOptions.Contains("admin_content_reviewed") && OriginalOptions.Contains(option);
}
protected override int GetOptionCount()
{
return CONTENT_WARNING_TAGS.Length;
}
public new class UxmlFactory : UxmlFactory<ContentWarningsField, UxmlTraits> { }
public new class UxmlTraits : OptionsPopupField<string>.UxmlTraits { }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9246484d22834713af8e88e23d611272
timeCreated: 1687997117

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b3fdc0c0682d4191875827b6ee5ebba4
timeCreated: 1691092601

View File

@ -0,0 +1,32 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace VRC.SDK3.Editor.Elements
{
public class GenericBuilderNotification: VisualElement
{
public GenericBuilderNotification(string text, string details = null, string actionText = null, Action action = null)
{
Resources.Load<VisualTreeAsset>("GenericBuilderNotification").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("GenericBuilderNotificationStyles"));
this.Q<Label>("main-text").text = text;
if (!string.IsNullOrWhiteSpace(details))
{
var detailsLabel = this.Q<Label>("details-text");
detailsLabel.text = details;
detailsLabel.RemoveFromClassList("d-none");
}
if (action != null)
{
var actionButton = this.Q<Button>("action-button");
actionButton.text = actionText;
actionButton.clicked += action;
actionButton.RemoveFromClassList("d-none");
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e7485de206c641469dcf232a342a69c6
timeCreated: 1691093163

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6cb017af4fd04b19835d3a5069a6880f
timeCreated: 1691092607

View File

@ -0,0 +1,7 @@
<UXML xmlns="UnityEngine.UIElements">
<VisualElement name="notification-block" class="col mt-2 mb-2 w-100 flex-shrink-0">
<Label class="text-lg" name="main-text" />
<Label name="details-text" class="mt-2 p-2 white-space-normal flex-grow-1 d-none" />
<Button class="mt-3 pl-4 pr-4 pt-2 pb-2 text-bold d-none" name="action-button" />
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6e653ccec8c748c79b9bcf3b98428441
timeCreated: 1691092618

View File

@ -0,0 +1,3 @@
#details-text {
background-color: rgba(255,255,255,0.2);
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 98fa0a3c12e04eeaac24aa52eed81e8a
timeCreated: 1691093136

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2507f59cbb8f43d88cfb5fb6974309bb
timeCreated: 1732148656

View File

@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class Modal : VisualElement
{
public new class UxmlFactory : UxmlFactory<Modal, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
private readonly UxmlStringAttributeDescription _title = new() { name = "title" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get
{
yield return new UxmlChildElementDescription(typeof(VisualElement));
}
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var modal = (Modal) ve;
modal._title.text = _title.GetValueFromBag(bag, cc);
}
}
private readonly Label _title;
private readonly Button _closeButton;
private readonly VisualElement _container;
private readonly VisualElement _contentWrapper;
private readonly VisualElement _actionButtonWrapper;
private readonly Button _actionButton;
private readonly VisualElement _icon;
private StyleLength _parentHeight;
private bool _hasActionButton;
public override VisualElement contentContainer => _container;
[PublicAPI]
public EventHandler OnClose;
[PublicAPI]
public bool IsOpen { get; private set; }
[PublicAPI]
public EventHandler OnCancel;
private VisualElement _anchor;
private VisualElement _originalParent;
private bool _isTemporary;
public Modal()
{
Resources.Load<VisualTreeAsset>("Modal").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("ModalStyles"));
AddToClassList("d-none");
AddToClassList("absolute");
AddToClassList("col");
_title = this.Q<Label>("modal-title");
_closeButton = this.Q<Button>("modal-close-btn");
_contentWrapper = this.Q("modal-content-wrapper");
_container = _contentWrapper.Q("modal-content");
_icon = this.Q<VisualElement>("modal-icon");
_actionButtonWrapper = this.Q("modal-action-button-wrapper");
_actionButton = this.Q<Button>("modal-action-button");
var backdrop = this.Q("modal-backdrop");
RegisterCallback<AttachToPanelEvent>(_ =>
{
// Only save the initial parent, ignoring the future re-parenting
if (_originalParent != null) return;
_originalParent = parent;
});
void OnCloseCancel()
{
// If there is an action button, treat the backdrop/close click as a cancel
if (_hasActionButton)
{
OnCancel?.Invoke(this, EventArgs.Empty);
}
Close();
}
_closeButton.clicked += OnCloseCancel;
backdrop.RegisterCallback<MouseDownEvent>(_ =>
{
OnCloseCancel();
});
}
public Modal(VisualElement anchor) : this()
{
_anchor = anchor;
}
public Modal(string title, string content, VisualElement anchor) : this(anchor)
{
_title.text = title;
var splitContent = content.Split('\n');
_container.AddToClassList("p-3");
foreach (var line in splitContent)
{
var label = new Label(line)
{
style =
{
whiteSpace = WhiteSpace.Normal
}
};
_container.Add(label);
}
}
public Modal(string title, string content, Action buttonAction, string buttonActionText, VisualElement anchor) : this(title, content, anchor)
{
_actionButton.clicked += () =>
{
buttonAction?.Invoke();
Close();
};
_actionButtonWrapper.RemoveFromClassList("d-none");
_actionButton.text = !string.IsNullOrWhiteSpace(buttonActionText) ? buttonActionText : "OK";
_hasActionButton = true;
_container.AddToClassList("mr-2");
}
/// <summary>
/// Shorthand method for creating and showing a modal in place
/// Calling `Close` on such a modal - immediately removes it from the hierarchy
/// </summary>
/// <param name="title"></param>
/// <param name="content"></param>
/// <param name="anchor"></param>
/// <returns></returns>
[PublicAPI]
public static Modal CreateAndShow(string title, string content, VisualElement anchor)
{
var modal = new Modal(title, content, anchor)
{
_isTemporary = true
};
anchor.Add(modal);
modal.Open();
return modal;
}
/// <summary>
/// Shorthand method for creating and showing a modal in place
/// Calling `Close` on such a modal - immediately removes it from the hierarchy
/// </summary>
/// <param name="title"></param>
/// <param name="content"></param>
/// <param name="anchor"></param>
/// <param name="buttonAction">If provided - adds a button that calls this action</param>
/// <param name="buttonActionText">Sets the text of the action button if `buttonAction` is provided</param>
/// <returns></returns>
[PublicAPI]
public static Modal CreateAndShow(string title, string content, Action buttonAction, string buttonActionText, VisualElement anchor)
{
var modal = new Modal(title, content, buttonAction, buttonActionText, anchor)
{
_isTemporary = true
};
anchor.Add(modal);
modal.Open();
return modal;
}
/// <summary>
/// Sets the element to re-anchor into
/// </summary>
/// <param name="anchor"></param>
[PublicAPI]
public void SetAnchor(VisualElement anchor)
{
_anchor = anchor;
RemoveFromHierarchy();
_anchor.Add(this);
}
[PublicAPI]
public void Open()
{
if (IsOpen) return;
IsOpen = true;
RemoveFromClassList("d-none");
_parentHeight = parent.style.height;
schedule.Execute(() =>
{
if (parent.contentRect.height < layout.height + 40)
{
parent.style.height = layout.height + 40;
}
}).ExecuteLater(1);
}
[PublicAPI]
public void Close()
{
if (!IsOpen) return;
IsOpen = false;
AddToClassList("d-none");
parent.style.height = _parentHeight;
OnClose?.Invoke(this, EventArgs.Empty);
// If there is no action button - treat any close as cancel
if (!_hasActionButton)
{
OnCancel?.Invoke(this, EventArgs.Empty);
}
if (_isTemporary)
{
RemoveFromHierarchy();
}
}
[PublicAPI]
public void SetTitle(string title)
{
_title.text = title;
}
[PublicAPI]
public void SetIcon(string resourceName)
{
_icon.RemoveFromClassList("d-none");
_icon.style.backgroundImage = new StyleBackground(Resources.Load<Texture2D>(resourceName));
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0ba9be7e15ca4b2d90dcf11932772a39
timeCreated: 1732148710

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5f55c7e8e734400eb64c21091a186b9b
timeCreated: 1732148665

View File

@ -0,0 +1,16 @@
<UXML xmlns="UnityEngine.UIElements">
<VisualElement name="modal-backdrop" class="absolute" />
<VisualElement name="modal-container" class="col">
<VisualElement class="row p-2 justify-container-between align-items-center" name="modal-header">
<VisualElement name="modal-icon" class="d-none" />
<Label name="modal-title" class="text-bold text-center flex-grow-1" />
<Button name="modal-close-btn" text=" " class="pl-2 pr-2" />
</VisualElement>
<VisualElement name="modal-content-wrapper" class="flex-grow row w-full">
<VisualElement name="modal-content" class="flex-grow flex-9 w-full col white-space-normal" />
<VisualElement name="modal-action-button-wrapper" class="flex-3 p-3 d-none">
<Button name="modal-action-button" class="text-lg text-bold flex-grow-1" />
</VisualElement>
</VisualElement>
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 240d33b1f1a64e838c456ccacd8e20bd
timeCreated: 1732148673

View File

@ -0,0 +1,53 @@
Modal {
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
}
#modal-backdrop {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
#modal-container {
margin: auto;
max-width: 95%;
width: 100%;
background-color: var(--unity-colors-window-background);
}
#modal-header {
background-color: var(--unity-colors-highlight-background-hover-lighter);
}
#modal-icon {
width: 18px;
height: 18px;
}
#modal-close-btn {
border-width: 0;
background-color: transparent;
width: 16px;
height: 16px;
}
.dark #modal-close-btn {
background-image: resource("d_winbtn_win_close");
}
.light #modal-close-btn {
background-image: resource("winbtn_win_close");
}
.dark #modal-close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.light #modal-close-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d3cce9efacf428b90b9d59443ed190c
timeCreated: 1732148684

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 187afe4ea3284a31a04e1f301ba845d1
timeCreated: 1731711575

View File

@ -0,0 +1,50 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace VRC.SDKBase.Editor.Elements
{
public class OptionsPopupContent: PopupWindowContent
{
private readonly Action<VisualElement> _setup;
private readonly Action _onClose;
private readonly Vector2 _windowSize;
public override Vector2 GetWindowSize()
{
return _windowSize;
}
public OptionsPopupContent(Action<VisualElement> setup, Vector2 size, Action onClose = null)
{
_setup = setup;
_windowSize = size;
_onClose = onClose;
}
public override void OnGUI(Rect rect)
{
// Legacy stub per unity docs
// https://docs.unity3d.com/2022.3/Documentation/Manual/UIE-create-a-popup-window.html
}
private VisualElement _root;
// Essentially the `CreateGUI` alternative
public override void OnOpen()
{
_root = editorWindow.rootVisualElement;
_root.AddToClassList("options-popup-content");
_root.styleSheets.Add(Resources.Load<StyleSheet>("OptionsPopupFieldStyles"));
_setup(_root);
}
public override void OnClose()
{
_onClose?.Invoke();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 40e6521ada904cf7ae415de1134f15c9
timeCreated: 1731712258

View File

@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using PopupWindow = UnityEditor.PopupWindow;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class OptionsPopupField<T> : VisualElement
{
#region Child API
protected virtual int GetOptionCount()
{
return 0;
}
protected virtual IList<T> GetOptions()
{
return new List<T>();
}
protected virtual string GetOptionName(T option)
{
return option.ToString();
}
protected virtual bool IsOptionLocked(T option)
{
return false;
}
protected virtual string GetPopupButtonText()
{
return $"{_selectedOptions?.Count ?? 0}/{GetOptionCount()} Selected";
}
protected virtual int GetPopupHeight()
{
return GetOptionCount() * 26;
}
#endregion
private VisualElement _optionsContainer;
private Label _label;
private readonly Button _popupButton;
private IList<T> _selectedOptions;
public IList<T> SelectedOptions
{
get => _selectedOptions;
set
{
// Ensure we're not sharing references
_selectedOptions = value.ToList();
var validOptions = GetOptions();
foreach (var option in value)
{
if (option == null)
{
_selectedOptions.Remove(option);
}
if (string.IsNullOrWhiteSpace(GetOptionName(option)))
{
_selectedOptions.Remove(option);
continue;
}
if (!validOptions.Contains(option))
{
_selectedOptions.Remove(option);
}
}
_popupButton.text = GetPopupButtonText();
UpdateOptions(ref _optionsContainer);
}
}
private IList<T> _originalOptions = new List<T>();
public IList<T> OriginalOptions
{
get => _originalOptions;
set
{
// Ensure we're not sharing references
_originalOptions = value.ToList();
var validOptions = GetOptions();
foreach (var option in value)
{
if (option == null)
{
_originalOptions.Remove(option);
}
if (string.IsNullOrWhiteSpace(GetOptionName(option)))
{
_originalOptions.Remove(option);
continue;
}
if (!validOptions.Contains(option))
{
_originalOptions.Remove(option);
}
}
UpdateOptions(ref _optionsContainer);
}
}
private bool _loading;
public bool Loading
{
get => _loading;
set
{
_loading = value;
_popupButton.text = value ? "Loading..." : GetPopupButtonText();
}
}
public EventHandler<T> OnToggleOption;
public EventHandler<List<T>> OnPopupClosed;
private VisualElement _popupBoundsReference;
public new class UxmlFactory : UxmlFactory<OptionsPopupField<T>, UxmlTraits> { }
public new class UxmlTraits : VisualElement.UxmlTraits
{
private readonly UxmlStringAttributeDescription _label = new() { name = "label" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var optionsField = (OptionsPopupField<T>)ve;
var label = _label.GetValueFromBag(bag, cc);
if (string.IsNullOrWhiteSpace(label))
optionsField.Q<Label>("label").AddToClassList("d-none");
else
optionsField.Q<Label>("label").text = label;
}
}
public OptionsPopupField()
{
Resources.Load<VisualTreeAsset>("OptionsPopupField").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("OptionsPopupFieldStyles"));
var dropdown = this.Q("dropdown");
_popupButton = new Button
{
text = "0/5 Selected",
name = "popup-button"
};
var spacerElement = new VisualElement
{
name = "spacer"
};
var arrowElement = new VisualElement
{
name = "arrow"
};
_popupButton.Add(spacerElement);
_popupButton.Add(arrowElement);
dropdown.Add(_popupButton);
_popupButton.clicked += () =>
{
PopupWindow.Show(_popupButton.worldBound, new OptionsPopupContent(r =>
{
_optionsContainer = new VisualElement();
r.Add(_optionsContainer);
UpdateOptions(ref _optionsContainer);
}, new Vector2((_popupBoundsReference ?? _popupButton).worldBound.width, GetPopupHeight()), () =>
{
OnPopupClosed?.Invoke(this, _selectedOptions.ToList());
}));
};
}
public OptionsPopupField(string label): this()
{
this.Q<Label>("label").text = label;
}
public void SetPopupBoundsReference(VisualElement reference)
{
_popupBoundsReference = reference;
}
private VisualElement CreateOption(T option)
{
var optionElement = new VisualElement();
optionElement.AddToClassList("row");
optionElement.AddToClassList("option");
optionElement.SetEnabled(!IsOptionLocked(option));
var optionToggle = new Toggle
{
value = SelectedOptions.Contains(option)
};
optionToggle.RegisterValueChangedCallback(_ => OnToggleOption?.Invoke(this, option));
optionElement.Add(optionToggle);
var optionText = GetOptionName(option);
var optionLabel = new Label(optionText);
optionElement.Add(optionLabel);
return optionElement;
}
private void UpdateOptions(ref VisualElement optionContainer)
{
if (optionContainer == null)
return;
optionContainer.Clear();
foreach (var option in GetOptions())
optionContainer.Add(CreateOption(option));
}
public void Refresh()
{
var allOptions = GetOptions().ToList();
SelectedOptions = SelectedOptions.Where(o => allOptions.Contains(o)).ToList();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 285e5c57962044cd9d5101bc1ee375c6
timeCreated: 1731711666

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5b6808abae04eefbc31e24bf9126229
timeCreated: 1731711587

View File

@ -0,0 +1,6 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<VisualElement class="col align-items-stretch">
<Label name="label" class="mt-2 text-bold mb-2" />
<VisualElement name="dropdown" class="align-self-stretch" />
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e1c4aa2cda4444e694afc677e518ccb0
timeCreated: 1731711596

View File

@ -0,0 +1,66 @@
.row {
flex-direction: row;
align-items: flex-start;
}
.col {
flex-direction: column;
align-items: flex-start;
}
.d-none {
display: none;
}
.options-popup-content
{
padding: 4px;
}
#label {
min-width: 135px;
}
.option {
padding: 0;
margin: 1px 5px 5px 0;
flex-direction: row;
align-items: center;
}
.option Toggle {
border-bottom-width: 0;
}
.option Label {
margin-left: 4px;
}
/* We need to use a specific name to have these styles override the base USS */
#dropdown #popup-button
{
-unity-text-align: middle-left;
padding-left: 3px;
padding-right: 3px;
font-size: 12px;
margin-left: 0;
margin-right: 0;
flex-direction: row;
}
#dropdown Button VisualElement {
margin-top: 2px;
margin-bottom: 2px;
flex-direction: row;
}
#dropdown Button #spacer {
flex-grow: 1;
}
#dropdown Button #arrow {
background-image: resource("d_dropdown.png");
width: 12px;
height: 12px;
align-self: center;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea3522211f3048049d1bec21d43cbc96
timeCreated: 1731711617

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 82617318d5c746709bb74ffc8f15ef52
timeCreated: 1736370342

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class PlatformSwitcherPopup : OptionsPopupField<BuildTarget>
{
protected override IList<BuildTarget> GetOptions()
{
return VRC_EditorTools.GetBuildTargetOptionsAsEnum();
}
protected override bool IsOptionLocked(BuildTarget target)
{
var shouldLock = ShouldLockOption?.Invoke(target) ?? false;
return shouldLock || !VRC_EditorTools.IsBuildTargetSupported(target);
}
protected override int GetOptionCount()
{
return GetOptions().Count;
}
protected override string GetOptionName(BuildTarget target)
{
return VRC_EditorTools.GetTargetName(target);
}
protected override string GetPopupButtonText()
{
if (SelectedOptions.Count == 1)
{
return GetOptionName(SelectedOptions[0]);
}
if (SelectedOptions.Count == 0)
{
return "None Selected";
}
if (SelectedOptions.Count == GetOptionCount())
{
return "All Platforms";
}
return $"{SelectedOptions?.Count ?? 0}/{GetOptionCount()} Selected";
}
public new class UxmlFactory : UxmlFactory<PlatformSwitcherPopup, UxmlTraits> { }
public new class UxmlTraits : OptionsPopupField<BuildTarget>.UxmlTraits { }
public Func<BuildTarget, bool> ShouldLockOption { get; set; }
public PlatformSwitcherPopup()
{}
public PlatformSwitcherPopup(string label): base(label)
{}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b36b8f84f1234cb09558f2b7bc4a8a56
timeCreated: 1736370358

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2642084c16904ccd93071a1f2aca7247
timeCreated: 1745315447

View File

@ -0,0 +1,117 @@
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.UIElements.Experimental;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public abstract class Selector<T> : VisualElement
{
public bool PopupEnabled
{
get => _popupField.enabledSelf;
set => _popupField.SetEnabled(value);
}
private readonly string _popupFieldName;
private readonly bool _pingWhenOptionsSet;
private PopupField<T> _popupField;
private VisualElement _popupInput;
private EventCallback<ChangeEvent<T>> _changeCallback;
protected Selector(List<T> options, string popupFieldName, string styleSheetPath, string labelText, string labelName, bool pingWhenOptionsSet)
{
_popupFieldName = popupFieldName;
_pingWhenOptionsSet = pingWhenOptionsSet;
styleSheets.Add(Resources.Load<StyleSheet>(styleSheetPath));
var label = new Label(labelText)
{
name = labelName
};
Add(label);
SetOptions(options, 0);
}
private void CreateField(List<T> options, int selectedIndex)
{
if (Contains(_popupField))
{
Remove(_popupField);
}
if (options == null || options.Count == 0)
{
return;
}
_popupField = new PopupField<T>(
null,
options,
selectedIndex,
prop => FormatElementName(options, prop),
prop => FormatElementName(options, prop)
);
_popupInput = _popupField.Q<VisualElement>(null, "unity-popup-field__input");
_popupField.name = _popupFieldName;
_popupField.AddToClassList("flex-grow-1");
if (_changeCallback != null)
{
_popupField.RegisterValueChangedCallback(_changeCallback);
}
Add(_popupField);
}
protected abstract string FormatElementName(List<T> options, T element);
public void SetOptions(List<T> options, int selectedIndex)
{
CreateField(options, selectedIndex);
if (_pingWhenOptionsSet)
{
PingField();
}
}
public void SetValue(T element, bool setWithoutNotify = false)
{
if (_popupField == null) return;
if (setWithoutNotify)
{
_popupField.SetValueWithoutNotify(element);
}
else
{
_popupField.value = element;
}
}
public void RegisterValueChangedCallback(EventCallback<ChangeEvent<T>> callback)
{
_changeCallback = callback;
if (_changeCallback != null)
{
_popupField.RegisterValueChangedCallback(_changeCallback);
}
}
private void PingField()
{
if (_popupField == null) return;
_popupField.schedule.Execute(() =>
{
var baseColor = _popupInput.resolvedStyle.backgroundColor;
_popupInput.experimental.animation.Start(new StyleValues
{
backgroundColor = new Color(0.3f, 0.71f, 0.37f, 0.53f)
}, new StyleValues
{
backgroundColor = baseColor
}, 500);
}).ExecuteLater(10);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa3fe294802346828d8fca51c0f8eed6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7d8e845dfe774648a8e66f92e190671d
timeCreated: 1733441765

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b7fc41b9c6234e7b994e0c7d92839625
timeCreated: 1733442419

View File

@ -0,0 +1,6 @@
/* There is a higher level parent stylesheet that we want to override with a more specific selector */
.section-foldout > Toggle Label.step-label,
.step-label{
flex-grow: 0;
margin-left: 8px;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 12073cbba57b41bea0300fb13645ebec
timeCreated: 1733442413

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class StepFoldout: Foldout
{
public new class UxmlFactory : UxmlFactory<StepFoldout, UxmlTraits> {}
public new class UxmlTraits : Foldout.UxmlTraits
{
private readonly UxmlStringAttributeDescription _stepName = new() { name = "stepName" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var stepFoldout = (StepFoldout) ve;
stepFoldout.InsertStepName(_stepName.GetValueFromBag(bag, cc));
}
}
private string _stepName;
private Label _headerLabel;
private void InsertStepName(string stepName)
{
if (string.IsNullOrWhiteSpace(stepName)) return;
_stepName = stepName;
// We insert the step right before the main label
_headerLabel = this.Q<Toggle>().Q<Label>();
var headerIndex = _headerLabel.parent.hierarchy.IndexOf(_headerLabel);
var stepLabel = new Label(_stepName);
stepLabel.AddToClassList("step-label");
_headerLabel.parent.Insert(headerIndex, stepLabel);
}
public StepFoldout()
{
styleSheets.Add(Resources.Load<StyleSheet>("StepFoldoutStyles"));
}
public void SetTitle(string title)
{
_headerLabel.text = title;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9cb09dda70d84125b7d55497e3e7f6b0
timeCreated: 1733441771

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f921025be084aacbbc9d54d0c3a0cee
timeCreated: 1684525016

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b8fb982a3dd74475901ec426a5cbc815
timeCreated: 1687270524

View File

@ -0,0 +1,17 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:vrc="VRC.SDKBase.Editor.Elements">
<VisualElement class="col">
<Label name="tags-label" class="text-bold mb-2" />
<vrc:TagsFieldButton class="align-self-stretch row" />
<vrc:Modal name="tags-modal" title="Manage Your Tags">
<VisualElement class="col">
<VisualElement class="row p-4" name="add-tag-block">
<vrc:VRCTextField name="tag-add-field" class="flex-grow-1" placeholder="Enter a new tag..." />
<Button name="tag-add-button" text="Add Tag" />
</VisualElement>
<ScrollView>
<VisualElement name="tags-row" class="row pl-4 pr-4 pt-3 pb-2" />
</ScrollView>
</VisualElement>
</vrc:Modal>
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 36d3f6fd312f4ef48aa0cd0b8fa0c9ef
timeCreated: 1684525036

View File

@ -0,0 +1,82 @@
#tags-row {
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
}
.tag {
padding: 2px 3px 2px 7px;
flex-direction: row;
align-items: center;
-unity-text-align: middle-center;
flex-shrink: 0;
min-height: 22px;
}
.dark #add-tag-block {
background-color: hsl(0, 0%, 19%);
}
.light #add-tag-block {
background-color: hsl(0, 0%, 39%);
}
.light .tag {
background-color: rgb(228, 228, 228);
}
.dark .tag {
background-color: hsl(0, 0%, 18%);
}
.tag Button.tag-remove-button {
padding: 0;
margin: 0;
border-width: 0;
background-color: rgba(0,0,0,0);
padding-left: 18px;
width: 18px;
height: 18px;
opacity: 0.5;
cursor: link;
}
.dark .tag Button.tag-remove-button {
background-image: resource("d_winbtn_win_close");
}
.light .tag Button.tag-remove-button {
background-image: resource("winbtn_win_close");
}
.tag Button.tag-remove-button:hover {
opacity: 1;
}
#tags-label {
min-width: 135px;
}
TagsFieldButton {
-unity-text-align: middle-left;
margin-left: 0;
margin-right: 0;
}
TagsFieldButton VisualElement {
margin-top: 2px;
margin-bottom: 2px;
flex-direction: row;
}
TagsFieldButton #spacer {
flex-grow: 1;
}
TagsFieldButton #edit {
background-image: resource("vrcTagEditIcon");
width: 12px;
height: 12px;
align-self: center;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cf83c9f55a1e4586b639fab90fcc48dc
timeCreated: 1684525082

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

View File

@ -0,0 +1,140 @@
fileFormatVersion: 2
guid: 15deb94ecaaa9de4f85099145052175a
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class TagsField: VisualElement
{
private Label _tagsLabel;
private TagsFieldButton _tagsButton;
private Modal _tagsModal;
private VisualElement _tagsRow;
private Button _addTagButton;
private VRCTextField _tagInput;
private List<string> _tags;
public IList<string> tags
{
get => _tags;
set
{
var copied = new List<string>(value);
if (TagFilter != null)
{
copied = TagFilter(copied);
}
_tags = copied;
_tagsButton?.SetTagCount(_tags.Count);
UpdateTags(ref _tagsRow);
_tagsModal?.SetTitle($"Manage Your Tags ({copied.Count})");
}
}
public EventHandler<string> OnAddTag;
public EventHandler<string> OnRemoveTag;
public Func<bool> CanAddTag;
public Func<string, bool> IsProtectedTag = input => false;
public Func<string, string> FormatTagDisplay = input => input;
public Func<List<string>, List<string>> TagFilter;
public int TagLimit = 5;
public new class UxmlFactory : UxmlFactory<TagsField, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
private UxmlStringAttributeDescription _label = new UxmlStringAttributeDescription { name = "label" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var tagsField = (TagsField) ve;
var label = _label.GetValueFromBag(bag, cc);
if (string.IsNullOrWhiteSpace(label))
{
tagsField.Q<Label>("tags-label").AddToClassList("d-none");
}
else
{
tagsField.Q<Label>("tags-label").text = label;
}
}
}
public TagsField()
{
Resources.Load<VisualTreeAsset>("TagsField").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("TagsFieldStyles"));
_tagsLabel = this.Q<Label>("tags-label");
_tagsButton = this.Q<TagsFieldButton>();
_tagsRow = this.Q("tags-row");
_tagsModal = this.Q<Modal>("tags-modal");
_tagsButton.clicked += _tagsModal.Open;
_tagsModal.styleSheets.Add(Resources.Load<StyleSheet>("TagsFieldStyles"));
_tagInput = _tagsModal.Q<VRCTextField>("tag-add-field");
_tagInput.RegisterCallback<KeyDownEvent>(e =>
{
if (e.keyCode != KeyCode.Return) return;
AddTag();
});
// Comma is a valid input event, so adding a tag and clearing input on KeyDown causes internal errors
// so we do it on key up and trim the end
_tagInput.RegisterCallback<KeyUpEvent>(e =>
{
if (e.keyCode != KeyCode.Comma) return;
_tagInput.value = _tagInput.value[..^1];
AddTag();
});
_addTagButton = _tagsModal.Q<Button>("tag-add-button");
_addTagButton.clicked += AddTag;
tags = new List<string>();
// Anchor the modal to the content-info block
RegisterCallback<AttachToPanelEvent>(e =>
{
_tagsModal.SetAnchor(e.destinationPanel.visualTree.Q("content-info"));
});
}
public TagsField(List<string> tags) : this()
{
this.tags = tags;
}
/// <summary>
/// Stops the editing of the tags list, closes the modal and clears the input field
/// </summary>
[PublicAPI]
public void StopEditing()
{
_tagInput.value = string.Empty;
_tagsModal.Close();
}
private void AddTag()
{
if (tags.Count >= TagLimit)
{
return;
}
if (CanAddTag != null && !CanAddTag())
{
return;
}
if (_tagInput.IsPlaceholder()) return;
if (_tagInput.value.Length == 0) return;
OnAddTag?.Invoke(this, _tagInput.text);
_tagInput.value = string.Empty;
}
private void UpdateTags(ref VisualElement container)
{
container.Clear();
foreach (var tag in tags)
{
var tagElement = new VisualElement();
tagElement.AddToClassList("tag");
tagElement.AddToClassList("row");
tagElement.AddToClassList("mr-2");
tagElement.AddToClassList("mb-2");
tagElement.Add(new Label(FormatTagDisplay(tag)));
if (!IsProtectedTag(tag))
{
var removeButton = new Button(() =>
{
OnRemoveTag?.Invoke(this, tag);
});
removeButton.AddToClassList("tag-remove-button");
tagElement.Add(removeButton);
}
container.Add(tagElement);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cefc8575eca3497fbe2221e89b6ba018
timeCreated: 1684525020

View File

@ -0,0 +1,40 @@
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class TagsFieldButton: Button
{
public new class UxmlFactory : UxmlFactory<TagsFieldButton, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
}
public void SetTagCount(int count)
{
text = $"{count} Tag(s)";
}
public TagsFieldButton() : base()
{
var spacerElement = new VisualElement
{
name = "spacer"
};
var editElement = new VisualElement
{
name = "edit"
};
Add(spacerElement);
Add(editElement);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 87a08313d01a4f00b1307526761a64f9
timeCreated: 1736209770

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4a2a716fec9d4f7a94426c04f827c060
timeCreated: 1687701391

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b6626c41f1b84ccb95dc502c72e6b902
timeCreated: 1687701682

View File

@ -0,0 +1,11 @@
<UXML xmlns="UnityEngine.UIElements">
<VisualElement class="thumbnail" name="thumbnail-container">
<VisualElement class="thumbnail-underlay">
<Label name="thumbnail-placeholder-text" />
</VisualElement>
<VisualElement name="thumbnail-image" />
<VisualElement class="thumbnail-overlay">
<Label name="thumbnail-hover-text" />
</VisualElement>
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f436adf433bd48d69a078c8ec49814e0
timeCreated: 1687701705

View File

@ -0,0 +1,56 @@
.thumbnail {
margin: 0 auto;
position: relative;
align-self: flex-start;
}
.thumbnail-underlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
align-items: center;
justify-content: center;
-unity-text-align: middle-center;
-unity-font-style: bold;
font-size: 18px;
}
.thumbnail:disabled .thumbnail-underlay #thumbnail-placeholder-text {
opacity: 0;
}
#thumbnail-image {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
-unity-background-scale-mode: scale-and-crop;
background-color: rgba(0,0,0,0.3);
overflow: hidden;
border-radius: 4px;
}
.thumbnail-overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
opacity: 0;
-unity-text-align: middle-center;
justify-content: center;
align-items: center;
white-space: normal;
}
.thumbnail-overlay Label {
-unity-font-style: bold;
}
.thumbnail:hover .thumbnail-overlay {
opacity: 1;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f774b221f3fa4973a0931098da26177e
timeCreated: 1687702140

View File

@ -0,0 +1,176 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using VRC.SDKBase.Editor.Api;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class Thumbnail: VisualElement
{
public new class UxmlFactory : UxmlFactory<Thumbnail, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
private readonly UxmlFloatAttributeDescription _width = new UxmlFloatAttributeDescription { name = "width" };
private readonly UxmlFloatAttributeDescription _height = new UxmlFloatAttributeDescription { name = "height" };
private readonly UxmlStringAttributeDescription _placeholder = new UxmlStringAttributeDescription { name = "placeholder" };
private readonly UxmlStringAttributeDescription _loadingText = new UxmlStringAttributeDescription { name = "loading-text" };
private readonly UxmlStringAttributeDescription _hoverText = new UxmlStringAttributeDescription { name = "hover-text" };
private readonly UxmlStringAttributeDescription _imageUrl = new UxmlStringAttributeDescription { name = "image-url" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var thumbnail = (Thumbnail) ve;
var width = 0f;
if (_width.TryGetValueFromBag(bag, cc, ref width))
{
thumbnail._width = width;
}
var height = 0f;
if (_height.TryGetValueFromBag(bag, cc, ref height))
{
thumbnail._height = height;
}
var placeholder = "";
if (_placeholder.TryGetValueFromBag(bag, cc, ref placeholder))
{
thumbnail._placeholder = placeholder;
}
var loadingText = "";
if (_loadingText.TryGetValueFromBag(bag, cc, ref loadingText))
{
thumbnail._loadingText = loadingText;
}
var hoverText = "";
if (_hoverText.TryGetValueFromBag(bag, cc, ref hoverText))
{
thumbnail._hoverText = hoverText;
}
thumbnail.UpdateProps();
var imageUrl = "";
if (_imageUrl.TryGetValueFromBag(bag, cc, ref imageUrl))
{
thumbnail.SetImageUrl(imageUrl).ConfigureAwait(false);
}
}
}
private float _width = 192f;
private float _height = 144f;
private string _placeholder = "No Image";
private string _loadingText = "Loading...";
private string _hoverText = "Image Size\n(1200 x 900)";
private readonly VisualElement _container;
private readonly Label _placeholderText;
private readonly Label _hoverTextElement;
private readonly VisualElement _imageElement;
private string _imageUrl;
private Texture2D _imageTexture;
private Texture2D _transparentPlaceholder;
public string CurrentImage => _imageUrl;
public Texture2D CurrentImageTexture => _imageTexture;
private bool _loading;
public bool Loading
{
get => _loading;
set
{
_loading = value;
_placeholderText.text = _loading ? _loadingText : _placeholder;
}
}
public Thumbnail()
{
Resources.Load<VisualTreeAsset>("Thumbnail").CloneTree(this);
styleSheets.Add(Resources.Load<StyleSheet>("ThumbnailStyles"));
_container = this.Q("thumbnail-container");
_placeholderText = this.Q<Label>("thumbnail-placeholder-text");
_hoverTextElement = this.Q<Label>("thumbnail-hover-text");
_imageElement = this.Q("thumbnail-image");
_container.style.width = _width;
_container.style.height = _height;
_container.style.minWidth = _width;
_placeholderText.text = _placeholder;
_hoverTextElement.text = _hoverText;
_transparentPlaceholder = new Texture2D(1, 1);
_transparentPlaceholder.SetPixel(0, 0, new Color(0,0,0,0));
_transparentPlaceholder.Apply();
}
private void UpdateProps()
{
_container.style.width = _width;
_container.style.height = _height;
_container.style.minWidth = _width;
_placeholderText.text = _placeholder;
_hoverTextElement.text = _hoverText;
}
[PublicAPI]
public async Task SetImageUrl(string url, CancellationToken cancellationToken = default, bool forceRefresh = false)
{
if (string.IsNullOrWhiteSpace(url)) return;
if (url == _imageUrl) return;
_imageUrl = url;
_imageTexture = null;
Loading = true;
try
{
_imageElement.style.backgroundImage = await VRCApi.GetImage(url, forceRefresh, cancellationToken: cancellationToken);
}
catch (TaskCanceledException)
{
_imageUrl = null;
_imageElement.style.backgroundImage = null;
}
}
[PublicAPI]
public void SetImage(Texture2D image)
{
_imageUrl = null;
_imageTexture = image;
_imageElement.style.backgroundImage = image;
}
[PublicAPI]
public void SetImage(string imagePath)
{
var bytes = File.ReadAllBytes(imagePath);
var newThumbnail = new Texture2D(2, 2);
newThumbnail.LoadImage(bytes);
SetImage(newThumbnail);
}
[PublicAPI]
public void ClearImage()
{
_imageUrl = null;
_imageTexture = null;
_imageElement.style.backgroundImage = _transparentPlaceholder;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21784b55f82d46e0aba5b8fec85c5551
timeCreated: 1687701396

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 929f1b0665a749a5bceb45abb7b3cb8b
timeCreated: 1731031495

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 96d38749df544cc59b75494732690ae4
timeCreated: 1731031558

View File

@ -0,0 +1,34 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:vrc="VRC.SDKBase.Editor.Elements" xmlns:vrca="VRC.SDK3A.Editor.Elements">
<VisualElement class="col mb-2 align-items-start w-100">
<vrc:Thumbnail width="196" height="147" hover-text="Image Size 1200 x 900" name="content-thumbnail" />
<VisualElement class="row align-items-stretch justify-content-between mt-2" style="max-width: 100%; min-width:100%;">
<Button text="Select Image" name="select-new-thumbnail-btn" class="ml-0 mr-2 white-space-normal text-bold pt-2 pb-2" style="flex:1; font-size:14px;" />
<Button text="Capture In Scene" name="capture-thumbnail-from-scene-btn" class="ml-0 mr-0 white-space-normal text-bold pt-2 pb-2" style="flex:1; font-size:14px;" />
</VisualElement>
<vrc:Modal title="Move your scene view to capture the desired thumbnail">
<VisualElement class="col">
<VisualElement class="row p-3 align-items-center">
<VisualElement class="col flex-grow-1" style="min-width: 180px;">
<Toggle label="Fill Background" tooltip="Uses flat color as a background instead of the skybox" name="thumbnail-fill-background-toggle" class="mb-2" />
<Toggle label="Use PostProcessing" tooltip="Applies scene post processing to the thumbnail" name="thumbnail-use-post-processing-toggle" class="mb-2" />
<Toggle label="Use Custom Camera" tooltip="Utilizes a custom camera instead of a VRChat-created camera instead" name="thumbnail-use-custom-camera-toggle" class="mb-2" />
<VisualElement class="col mb-2 d-none" name="thumbnail-background-block">
<uie:ColorField name="thumbnail-background-color-field" class="mb-2" />
<Label text="Background Color" style="align-self: center text-center" />
</VisualElement>
<VisualElement class="col mb-2 d-none" name="thumbnail-custom-camera-block">
<uie:ObjectField class="mb-2" name="thumbnail-custom-camera-ref"/>
<Label text="Custom Camera" style="align-self: center text-center" />
</VisualElement>
</VisualElement>
<VisualElement class="row justify-content-center mt-2 ml-2 flex-shrink-0 flex-grow-0" style="flex-basis: auto;">
<IMGUIContainer name="thumbnail-capture-preview" />
</VisualElement>
</VisualElement>
<VisualElement class="mt-1 mb-3 ml-3 mr-3 row align-items-center" name="thumbnail-capture-confirm-block">
<Button class="p-2 m-0 flex-grow-1" text="Capture" name="thumbnail-capture-confirm-btn" style="height: 30px;" />
</VisualElement>
</VisualElement>
</vrc:Modal>
</VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 123b380a0d8e48f2bfbd20b14d940662
timeCreated: 1731031567

View File

@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
#if POST_PROCESSING_INCLUDED
using UnityEngine.Rendering.PostProcessing;
#endif
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class ThumbnailBlock: VisualElement
{
public new class UxmlFactory : UxmlFactory<ThumbnailBlock, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
}
public Thumbnail Thumbnail { get; private set; }
public Foldout Foldout { get; private set; }
public EventHandler<string> OnNewThumbnailSelected { get; set; }
private VisualElement _selectorBlock;
private Button _selectThumbnailButton;
private Button _captureThumbnailButton;
private VisualElement _modalMountPoint;
private VisualElement _captureBlock;
private IMGUIContainer _previewContainer;
private Toggle _fillBackground;
private ColorField _backgroundColor;
private Toggle _usePostProcessing;
private Toggle _useCustomCamera;
private ObjectField _customCamera;
private VisualElement _captureConfirmBlock;
private Button _captureConfirmButton;
private Button _captureCancelButton;
private Modal _captureModal;
private string _oldThumbnail;
private Texture2D _oldThumbnailTexture;
private Camera _captureCamera;
private Texture2D _bufferTexture;
private RenderTexture _targetTexture;
private bool _capturing;
private bool Capturing
{
get => _capturing;
set
{
_capturing = value;
if (value)
{
_captureModal.Open();
_targetTexture = Resources.Load<RenderTexture>("ThumbnailCapture");
_captureCamera =
VRC_EditorTools.CreateThumbnailCaptureCamera(_targetTexture, _fillBackground.value, _backgroundColor.value, _usePostProcessing.value);
_captureCamera.enabled = true;
return;
}
else
{
_captureModal.Close();
}
if (_captureCamera != null)
{
Object.DestroyImmediate(_captureCamera.gameObject);
_captureCamera = null;
}
if (_customCamera.value != null)
{
((Camera) _customCamera.value).targetTexture = null;
}
if (_bufferTexture != null)
{
Object.DestroyImmediate(_bufferTexture);
_bufferTexture = null;
}
_targetTexture = null;
}
}
private async void HandleCaptureCancel()
{
Capturing = false;
Thumbnail.ClearImage();
if (!string.IsNullOrWhiteSpace(_oldThumbnail))
{
await Thumbnail.SetImageUrl(_oldThumbnail);
}
if (_oldThumbnailTexture != null)
{
Thumbnail.SetImage(_oldThumbnailTexture);
}
_oldThumbnail = null;
_oldThumbnailTexture = null;
}
public ThumbnailBlock()
{
Resources.Load<VisualTreeAsset>("ThumbnailBlock").CloneTree(this);
Thumbnail = this.Q<Thumbnail>("content-thumbnail");
_selectorBlock = this.Q<VisualElement>("thumbnail-selector-block");
_selectThumbnailButton = this.Q<Button>("select-new-thumbnail-btn");
_captureThumbnailButton = this.Q<Button>("capture-thumbnail-from-scene-btn");
_captureModal = this.Q<Modal>();
_captureModal.OnClose += (_, _) =>
{
if (!_capturing) return;
HandleCaptureCancel();
};
// We cannot traverse the tree until the component is mounted
RegisterCallback<AttachToPanelEvent>(evt =>
{
// the panel is the root of the window tree, so we can see all elements from here
_captureModal.SetAnchor(evt.destinationPanel.visualTree.Q("content-info"));
});
_previewContainer = this.Q<IMGUIContainer>("thumbnail-capture-preview");
_fillBackground = this.Q<Toggle>("thumbnail-fill-background-toggle");
var backgroundBlock = this.Q("thumbnail-background-block");
_backgroundColor = this.Q<ColorField>("thumbnail-background-color-field");
_usePostProcessing = this.Q<Toggle>("thumbnail-use-post-processing-toggle");
var customCameraBlock = this.Q("thumbnail-custom-camera-block");
_useCustomCamera = this.Q<Toggle>("thumbnail-use-custom-camera-toggle");
_customCamera = this.Q<ObjectField>("thumbnail-custom-camera-ref");
_captureConfirmBlock = this.Q<VisualElement>("thumbnail-capture-confirm-block");
_captureConfirmButton = this.Q<Button>("thumbnail-capture-confirm-btn");
_captureCancelButton = this.Q<Button>("thumbnail-capture-cancel-btn");
#if POST_PROCESSING_INCLUDED
_usePostProcessing.RemoveFromClassList("d-none");
#endif
_selectThumbnailButton.clicked += () =>
{
var imagePath = EditorUtility.OpenFilePanel("Select thumbnail", "", "png");
if (string.IsNullOrWhiteSpace(imagePath)) return;
OnNewThumbnailSelected?.Invoke(this, imagePath);
};
_captureThumbnailButton.clicked += () =>
{
_oldThumbnail = Thumbnail.CurrentImage;
_oldThumbnailTexture = Thumbnail.CurrentImageTexture;
Capturing = true;
};
_captureConfirmButton.clicked += () =>
{
Capturing = false;
var capturedPicture = VRC_EditorTools.CaptureSceneImage(1200, 900, _fillBackground.value,
_backgroundColor.value, _usePostProcessing.value,
_useCustomCamera.value ? (Camera) _customCamera.value : null);
OnNewThumbnailSelected?.Invoke(this, capturedPicture);
};
_fillBackground.RegisterValueChangedCallback(evt =>
{
if (!Capturing || _captureCamera == null) return;
backgroundBlock.EnableInClassList("d-none", !evt.newValue);
if (evt.newValue)
{
_captureCamera.clearFlags = CameraClearFlags.SolidColor;
_captureCamera.backgroundColor = _backgroundColor.value;
}
else
{
_captureCamera.clearFlags = CameraClearFlags.Skybox;
}
});
_backgroundColor.RegisterValueChangedCallback(evt =>
{
if (!Capturing || _captureCamera == null) return;
// Enforce alpha to 1
_backgroundColor.SetValueWithoutNotify(new Color(evt.newValue.r, evt.newValue.g, evt.newValue.b, 1f));
_captureCamera.backgroundColor = _backgroundColor.value;
});
_usePostProcessing.RegisterValueChangedCallback(evt =>
{
#if POST_PROCESSING_INCLUDED
if (!Capturing || _captureCamera == null) return;
if (_captureCamera.TryGetComponent<PostProcessLayer>(out var layer))
{
layer.enabled = evt.newValue;
}
else if (evt.newValue)
{
var postProcessLayer = _captureCamera.gameObject.AddComponent<PostProcessLayer>();
postProcessLayer.volumeLayer = int.MaxValue;
postProcessLayer.volumeTrigger = _captureCamera.transform;
}
#endif
});
_useCustomCamera.RegisterValueChangedCallback(evt =>
{
if (!Capturing) return;
customCameraBlock.EnableInClassList("d-none", !evt.newValue);
});
_customCamera.objectType = typeof(Camera);
_customCamera.allowSceneObjects = true;
_previewContainer.style.width = 192;
_previewContainer.style.height = 144;
_previewContainer.onGUIHandler = () =>
{
if (!Capturing) return;
if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive) return;
var customCameraValue = (Camera) _customCamera.value;
if (customCameraValue != null && _useCustomCamera.value)
{
if (_useCustomCamera.value)
{
customCameraValue.targetTexture = _targetTexture;
_captureCamera.targetTexture = null;
}
else
{
customCameraValue.targetTexture = null;
}
}
else
{
_captureCamera.targetTexture = _targetTexture;
var copyFrom = SceneView.lastActiveSceneView.camera;
_captureCamera.transform.SetPositionAndRotation(copyFrom.transform.position,
copyFrom.transform.rotation);
}
if (!Capturing) return;
if (_targetTexture == null) return;
var rect = EditorGUILayout.GetControlRect();
var aspect = _targetTexture.width / (float) _targetTexture.height;
var previewRect = new Rect(rect.x, rect.y, rect.width, rect.width / aspect);
GUI.DrawTexture(previewRect, _targetTexture);
// Enforce repaint immediately
_previewContainer.MarkDirtyRepaint();
};
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5baa3e07caa4eeba545fef8f0bc4868
timeCreated: 1731031510

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5cd5c711d0af4ac382b9c41807cb9d29
timeCreated: 1691164551

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 83beb30e3257492bbdede1c2964ac250
timeCreated: 1691165535

View File

@ -0,0 +1,27 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:vrc="VRC.SDKBase.Editor.Elements" xmlns:vrca="VRC.SDK3A.Editor.Elements">
<Foldout text="Thumbnail" class="section-foldout" name="thumbnail-foldout">
<VisualElement class="row mt-2 mb-2 align-items-start w-100">
<vrc:Thumbnail width="192" height="144" hover-text="Image Size 1200 x 900" name="content-thumbnail" />
<VisualElement class="col ml-2 flex-grow-1">
<VisualElement class="col mb-2" name="thumbnail-selector-block">
<Label class="mb-2 white-space-normal m-unity-field " text="You can pick a new image to be used as a thumbnail" />
<Button class="mb-2" text="Select New Thumbnail" name="select-new-thumbnail-btn" />
<Label class="mb-2 white-space-normal m-unity-field " text="You can also capture the new thumbnail directly from the scene" />
<Button text="Capture From Scene" name="capture-thumbnail-from-scene-btn" />
</VisualElement>
<VisualElement class="col d-none" name="thumbnail-capture-block">
<Label class="mb-2 white-space-normal m-unity-field " text="Move your scene view to capture the desired thumbnail" />
<Toggle label="Fill Background" tooltip="Uses flat color as a background instead of the skybox" name="thumbnail-fill-background-toggle" />
<uie:ColorField label="Background Color" name="thumbnail-background-color-field" class="d-none" />
<Toggle label="Use PostProcessing" tooltip="Applies scene post processing to the thumbnail" name="thumbnail-use-post-processing-toggle" />
<Toggle label="Use Custom Camera" tooltip="Utilizes a custom camera instead of a VRChat-created camera instead" name="thumbnail-use-custom-camera-toggle" />
<uie:ObjectField class="flex-grow-1 d-none" label="Camera" name="thumbnail-custom-camera-ref"/>
<VisualElement class="mt-2 row align-items-center" name="thumbnail-capture-confirm-block">
<Button class="mr-2 flex-grow-1" text="Capture" name="thumbnail-capture-confirm-btn" />
<Button class="flex-grow-1" text="Cancel" name="thumbnail-capture-cancel-btn" />
</VisualElement>
</VisualElement>
</VisualElement>
</VisualElement>
</Foldout>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7a0a0d51d65e4ac9a9ea3861a089855c
timeCreated: 1691165551

View File

@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
#if POST_PROCESSING_INCLUDED
using UnityEngine.Rendering.PostProcessing;
#endif
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class ThumbnailFoldout: VisualElement
{
public new class UxmlFactory : UxmlFactory<ThumbnailFoldout, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
}
public Thumbnail Thumbnail { get; private set; }
public Foldout Foldout { get; private set; }
public EventHandler<string> OnNewThumbnailSelected { get; set; }
private VisualElement _selectorBlock;
private Button _selectThumbnailButton;
private Button _captureThumbnailButton;
private VisualElement _captureBlock;
private Toggle _fillBackground;
private ColorField _backgroundColor;
private Toggle _usePostProcessing;
private Toggle _useCustomCamera;
private ObjectField _customCamera;
private VisualElement _captureConfirmBlock;
private Button _captureConfirmButton;
private Button _captureCancelButton;
private string _oldThumbnail;
private Texture2D _oldThumbnailTexture;
private Camera _captureCamera;
private Texture2D _bufferTexture;
private RenderTexture _targetTexture;
private bool _capturing;
private bool Capturing
{
get => _capturing;
set
{
_capturing = value;
_selectorBlock.EnableInClassList("d-none", value);
_captureBlock.EnableInClassList("d-none", !value);
if (value)
{
_targetTexture = Resources.Load<RenderTexture>("ThumbnailCapture");
_captureCamera =
VRC_EditorTools.CreateThumbnailCaptureCamera(_targetTexture, _fillBackground.value, _backgroundColor.value, _usePostProcessing.value);
_captureCamera.enabled = true;
return;
}
if (_captureCamera != null)
{
Object.DestroyImmediate(_captureCamera.gameObject);
_captureCamera = null;
}
if (_customCamera.value != null)
{
((Camera) _customCamera.value).targetTexture = null;
}
if (_bufferTexture != null)
{
Object.DestroyImmediate(_bufferTexture);
_bufferTexture = null;
}
_targetTexture = null;
}
}
private async void HandleCaptureCancel()
{
Capturing = false;
Thumbnail.ClearImage();
if (!string.IsNullOrWhiteSpace(_oldThumbnail))
{
await Thumbnail.SetImageUrl(_oldThumbnail);
}
if (_oldThumbnailTexture != null)
{
Thumbnail.SetImage(_oldThumbnailTexture);
}
_oldThumbnail = null;
_oldThumbnailTexture = null;
}
public ThumbnailFoldout()
{
Resources.Load<VisualTreeAsset>("ThumbnailFoldout").CloneTree(this);
Foldout = this.Q<Foldout>("thumbnail-foldout");
Thumbnail = this.Q<Thumbnail>("content-thumbnail");
_selectorBlock = this.Q<VisualElement>("thumbnail-selector-block");
_selectThumbnailButton = this.Q<Button>("select-new-thumbnail-btn");
_captureThumbnailButton = this.Q<Button>("capture-thumbnail-from-scene-btn");
_captureBlock = this.Q<VisualElement>("thumbnail-capture-block");
_fillBackground = this.Q<Toggle>("thumbnail-fill-background-toggle");
_backgroundColor = this.Q<ColorField>("thumbnail-background-color-field");
_usePostProcessing = this.Q<Toggle>("thumbnail-use-post-processing-toggle");
_useCustomCamera = this.Q<Toggle>("thumbnail-use-custom-camera-toggle");
_customCamera = this.Q<ObjectField>("thumbnail-custom-camera-ref");
_captureConfirmBlock = this.Q<VisualElement>("thumbnail-capture-confirm-block");
_captureConfirmButton = this.Q<Button>("thumbnail-capture-confirm-btn");
_captureCancelButton = this.Q<Button>("thumbnail-capture-cancel-btn");
#if POST_PROCESSING_INCLUDED
_usePostProcessing.RemoveFromClassList("d-none");
#endif
_selectThumbnailButton.clicked += () =>
{
var imagePath = EditorUtility.OpenFilePanel("Select thumbnail", "", "png");
if (string.IsNullOrWhiteSpace(imagePath)) return;
OnNewThumbnailSelected?.Invoke(this, imagePath);
};
_captureThumbnailButton.clicked += () =>
{
_oldThumbnail = Thumbnail.CurrentImage;
_oldThumbnailTexture = Thumbnail.CurrentImageTexture;
Capturing = true;
};
_captureConfirmButton.clicked += () =>
{
Capturing = false;
var capturedPicture = VRC_EditorTools.CaptureSceneImage(1200, 900, _fillBackground.value,
_backgroundColor.value, _usePostProcessing.value, _useCustomCamera.value ? (Camera) _customCamera.value : null);
OnNewThumbnailSelected?.Invoke(this, capturedPicture);
};
_captureCancelButton.clicked += HandleCaptureCancel;
_fillBackground.RegisterValueChangedCallback(evt =>
{
if (!Capturing || _captureCamera == null) return;
_backgroundColor.EnableInClassList("d-none", !evt.newValue);
if (evt.newValue)
{
_captureCamera.clearFlags = CameraClearFlags.SolidColor;
_captureCamera.backgroundColor = _backgroundColor.value;
}
else
{
_captureCamera.clearFlags = CameraClearFlags.Skybox;
}
});
_backgroundColor.RegisterValueChangedCallback(evt =>
{
if (!Capturing || _captureCamera == null) return;
// Enforce alpha to 1
_backgroundColor.SetValueWithoutNotify(new Color(evt.newValue.r, evt.newValue.g, evt.newValue.b, 1f));
_captureCamera.backgroundColor = _backgroundColor.value;
});
_usePostProcessing.RegisterValueChangedCallback(evt =>
{
#if POST_PROCESSING_INCLUDED
if (!Capturing || _captureCamera == null) return;
if (_captureCamera.TryGetComponent<PostProcessLayer>(out var layer))
{
layer.enabled = evt.newValue;
}
else if (evt.newValue)
{
var postProcessLayer = _captureCamera.gameObject.AddComponent<PostProcessLayer>();
postProcessLayer.volumeLayer = int.MaxValue;
postProcessLayer.volumeTrigger = _captureCamera.transform;
}
#endif
});
_useCustomCamera.RegisterValueChangedCallback(evt =>
{
if (!Capturing) return;
_customCamera.EnableInClassList("d-none", !evt.newValue);
});
_customCamera.objectType = typeof(Camera);
_customCamera.allowSceneObjects = true;
schedule.Execute(() =>
{
if (!Capturing) return;
if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive) return;
var customCameraValue = (Camera) _customCamera.value;
if (customCameraValue != null && _useCustomCamera.value)
{
if (_useCustomCamera.value)
{
customCameraValue.targetTexture = _targetTexture;
_captureCamera.targetTexture = null;
}
else
{
customCameraValue.targetTexture = null;
}
}
else
{
_captureCamera.targetTexture = _targetTexture;
var copyFrom = SceneView.lastActiveSceneView.camera;
_captureCamera.transform.SetPositionAndRotation(copyFrom.transform.position, copyFrom.transform.rotation);
}
var req = AsyncGPUReadback.Request(_targetTexture);
AsyncGPUReadback.WaitAllRequests();
if (!Capturing) return;
var data = req.GetData<Color32>();
if (_bufferTexture == null)
{
_bufferTexture = new Texture2D(_targetTexture.width, _targetTexture.height, TextureFormat.RGBA32, false, true);
}
_bufferTexture.SetPixels32(data.ToArray());
_bufferTexture.Apply();
Thumbnail.SetImage(_bufferTexture);
}).Every(100);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8625fc371c064cb39e1c550073645721
timeCreated: 1691168569

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 56fc9bafb78a470e839e5958a78b3f92
timeCreated: 1687558315

View File

@ -0,0 +1,118 @@
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
namespace VRC.SDKBase.Editor.Elements
{
public class VRCTextField: TextField
{
public new class UxmlFactory : UxmlFactory<VRCTextField, UxmlTraits> {}
public new class UxmlTraits : TextField.UxmlTraits
{
private readonly UxmlStringAttributeDescription _placeholder = new() { name = "placeholder" };
private readonly UxmlBoolAttributeDescription _required = new() { name = "required" };
private readonly UxmlBoolAttributeDescription _vertical = new() { name="vertical" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var textField = (VRCTextField) ve;
textField._placeholder = _placeholder.GetValueFromBag(bag, cc);
textField._required = _required.GetValueFromBag(bag, cc);
var vertical = _vertical.GetValueFromBag(bag, cc);
if (textField._required)
{
textField.label += "*";
}
if (vertical)
{
textField.AddToClassList("col");
}
}
}
private string _placeholder;
private static readonly string PlaceholderClass = ussClassName + "__placeholder";
private bool _required;
private bool _loading;
private bool _vertical;
public bool Loading
{
get => _loading;
set
{
_loading = value;
SetEnabled(!_loading);
if (_loading)
{
text = "Loading...";
}
else
{
if (text == "Loading...")
{
text = "";
}
FocusOut();
}
EnableInClassList(ussClassName + "__loading", _loading);
}
}
public VRCTextField(): base()
{
RegisterCallback<FocusOutEvent>(evt => FocusOut());
RegisterCallback<FocusInEvent>(evt => FocusIn());
this.RegisterValueChangedCallback(ValueChanged);
}
public void Reset()
{
if (string.IsNullOrEmpty(text))
{
FocusOut();
return;
};
RemoveFromClassList(PlaceholderClass);
}
private void ValueChanged(ChangeEvent<string> evt)
{
if (IsPlaceholder() && !string.IsNullOrEmpty(evt.newValue))
{
RemoveFromClassList(PlaceholderClass);
}
if (!_required) return;
this.Q<TextInputBase>().EnableInClassList("border-red", string.IsNullOrWhiteSpace(evt.newValue));
}
private void FocusOut()
{
if (string.IsNullOrWhiteSpace(_placeholder)) return;
if (!string.IsNullOrEmpty(text)) return;
SetValueWithoutNotify(_placeholder);
AddToClassList(PlaceholderClass);
}
private void FocusIn()
{
if (string.IsNullOrWhiteSpace(_placeholder)) return;
if (!this.ClassListContains(PlaceholderClass)) return;
this.value = string.Empty;
this.RemoveFromClassList(PlaceholderClass);
}
public bool IsPlaceholder()
{
return ClassListContains(PlaceholderClass);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7b592ca25f524408a84f100c8580649c
timeCreated: 1687558321