Initial commit
This commit is contained in:
801
mod/PLibUI/PUIUtils.cs
Normal file
801
mod/PLibUI/PUIUtils.cs
Normal file
@@ -0,0 +1,801 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using SideScreenRef = DetailsScreen.SideScreenRef;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Utility functions for dealing with Unity UIs.
|
||||
/// </summary>
|
||||
public static class PUIUtils {
|
||||
/// <summary>
|
||||
/// Adds text describing a particular component if available.
|
||||
/// </summary>
|
||||
/// <param name="result">The location to append the text.</param>
|
||||
/// <param name="component">The component to describe.</param>
|
||||
private static void AddComponentText(StringBuilder result, Component component) {
|
||||
// Include all fields
|
||||
var fields = component.GetType().GetFields(BindingFlags.DeclaredOnly |
|
||||
BindingFlags.Instance | PPatchTools.BASE_FLAGS);
|
||||
// Class specific
|
||||
if (component is TMP_Text lt)
|
||||
result.AppendFormat(", Text={0}, Color={1}, Font={2}", lt.text, lt.color,
|
||||
lt.font);
|
||||
else if (component is Image im) {
|
||||
result.AppendFormat(", Color={0}", im.color);
|
||||
if (im.sprite != null)
|
||||
result.AppendFormat(", Sprite={0}", im.sprite);
|
||||
} else if (component is HorizontalOrVerticalLayoutGroup lg)
|
||||
result.AppendFormat(", Child Align={0}, Control W={1}, Control H={2}",
|
||||
lg.childAlignment, lg.childControlWidth, lg.childControlHeight);
|
||||
foreach (var field in fields) {
|
||||
object value = field.GetValue(component) ?? "null";
|
||||
// Value type specific
|
||||
if (value is LayerMask lm)
|
||||
value = "Layer #" + lm.value;
|
||||
else if (value is System.Collections.ICollection ic)
|
||||
value = "[" + ic.Join() + "]";
|
||||
else if (value is Array ar)
|
||||
value = "[" + ar.Join() + "]";
|
||||
result.AppendFormat(", {0}={1}", field.Name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hot pink rectangle over the target matching its size, to help identify it
|
||||
/// better.
|
||||
/// </summary>
|
||||
/// <param name="parent">The target UI component.</param>
|
||||
public static void AddPinkOverlay(GameObject parent) {
|
||||
var child = PUIElements.CreateUI(parent, "Overlay");
|
||||
var img = child.AddComponent<Image>();
|
||||
img.color = new Color(1.0f, 0.0f, 1.0f, 0.2f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified side screen content to the side screen list. The side screen
|
||||
/// behavior should be defined in a class inherited from SideScreenContent.
|
||||
///
|
||||
/// The side screen will be added at the end of the list, which will cause it to
|
||||
/// appear above previous side screens in the details panel.
|
||||
///
|
||||
/// This method should be used in a postfix on DetailsScreen.OnPrefabInit.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the controller that will determine how the side
|
||||
/// screen works. A new instance will be created and added as a component to the new
|
||||
/// side screen.</typeparam>
|
||||
/// <param name="uiPrefab">The UI prefab to use. If null is passed, the UI should
|
||||
/// be created and added to the GameObject hosting the controller object in its
|
||||
/// constructor.</param>
|
||||
public static void AddSideScreenContent<T>(GameObject uiPrefab = null)
|
||||
where T : SideScreenContent {
|
||||
AddSideScreenContentWithOrdering<T>(null, true, uiPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified side screen content to the side screen list. The side screen
|
||||
/// behavior should be defined in a class inherited from SideScreenContent.
|
||||
///
|
||||
/// This method should be used in a postfix on DetailsScreen.OnPrefabInit.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the controller that will determine how the side
|
||||
/// screen works. A new instance will be created and added as a component to the new
|
||||
/// side screen.</typeparam>
|
||||
/// <param name="targetClassName">The full name of the type of side screen to based to ordering
|
||||
/// around. An example of how this method can be used is:
|
||||
/// `AddSideScreenContentWithOrdering<MySideScreen>(typeof(CapacityControlSideScreen).FullName);`
|
||||
/// `typeof(TargetedSideScreen).FullName` is the suggested value of this parameter.
|
||||
/// Side screens from other mods can be used with their qualified names, even if no
|
||||
/// reference to their type is available, but the target mod must have added their
|
||||
/// custom side screen to the list first.</param>
|
||||
/// <param name="insertBefore">Whether to insert the new screen before or after the
|
||||
/// target side screen in the list. Defaults to before (true).
|
||||
/// When inserting before the screen, if both are valid for a building then the side
|
||||
/// screen of type "T" will show below the one of type "fullName". When inserting after
|
||||
/// the screen, the reverse is true.</param>
|
||||
/// <param name="uiPrefab">The UI prefab to use. If null is passed, the UI should
|
||||
/// be created and added to the GameObject hosting the controller object in its
|
||||
/// constructor.</param>
|
||||
public static void AddSideScreenContentWithOrdering<T>(string targetClassName,
|
||||
bool insertBefore = true, GameObject uiPrefab = null)
|
||||
where T : SideScreenContent {
|
||||
var inst = DetailsScreen.Instance;
|
||||
if (inst == null)
|
||||
LogUIWarning("DetailsScreen is not yet initialized, try a postfix on DetailsScreen.OnPrefabInit");
|
||||
else {
|
||||
var screens = UIDetours.SIDE_SCREENS.Get(inst);
|
||||
var body = UIDetours.SS_CONTENT_BODY.Get(inst);
|
||||
string name = typeof(T).Name;
|
||||
if (body != null && screens != null) {
|
||||
// The ref normally contains a prefab which is instantiated
|
||||
var newScreen = new DetailsScreen.SideScreenRef();
|
||||
// Mimic the basic screens
|
||||
var rootObject = PUIElements.CreateUI(body, name);
|
||||
// Preserve the border by fitting the child
|
||||
rootObject.AddComponent<BoxLayoutGroup>().Params = new BoxLayoutParams() {
|
||||
Direction = PanelDirection.Vertical, Alignment = TextAnchor.
|
||||
UpperCenter, Margin = new RectOffset(1, 1, 0, 1)
|
||||
};
|
||||
var controller = rootObject.AddComponent<T>();
|
||||
if (uiPrefab != null) {
|
||||
// Add prefab if supplied
|
||||
UIDetours.SS_CONTENT_CONTAINER.Set(controller, uiPrefab);
|
||||
uiPrefab.transform.SetParent(rootObject.transform);
|
||||
}
|
||||
newScreen.name = name;
|
||||
// Offset is never used
|
||||
UIDetours.SS_OFFSET.Set(newScreen, Vector2.zero);
|
||||
UIDetours.SS_PREFAB.Set(newScreen, controller);
|
||||
UIDetours.SS_INSTANCE.Set(newScreen, controller);
|
||||
InsertSideScreenContent(screens, newScreen, targetClassName, insertBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a PLib UI object and adds it to an existing UI object.
|
||||
/// </summary>
|
||||
/// <param name="component">The UI object to add.</param>
|
||||
/// <param name="parent">The parent of the new object.</param>
|
||||
/// <param name="index">The sibling index to insert the element at, if provided.</param>
|
||||
/// <returns>The built version of the UI object.</returns>
|
||||
public static GameObject AddTo(this IUIComponent component, GameObject parent,
|
||||
int index = -2) {
|
||||
if (component == null)
|
||||
throw new ArgumentNullException(nameof(component));
|
||||
if (parent == null)
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
var child = component.Build();
|
||||
child.SetParent(parent);
|
||||
if (index == -1)
|
||||
child.transform.SetAsLastSibling();
|
||||
else if (index >= 0)
|
||||
child.transform.SetSiblingIndex(index);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the size of a single game object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to calculate.</param>
|
||||
/// <param name="direction">The direction to calculate.</param>
|
||||
/// <param name="components">The components of this game object.</param>
|
||||
/// <returns>The object's minimum and preferred size.</returns>
|
||||
internal static LayoutSizes CalcSizes(GameObject obj, PanelDirection direction,
|
||||
IEnumerable<Component> components) {
|
||||
float min = 0.0f, preferred = 0.0f, flexible = 0.0f, scaleFactor;
|
||||
int minPri = int.MinValue, prefPri = int.MinValue, flexPri = int.MinValue;
|
||||
var scale = obj.transform.localScale;
|
||||
// Find the correct scale direction
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
scaleFactor = Math.Abs(scale.x);
|
||||
else
|
||||
scaleFactor = Math.Abs(scale.y);
|
||||
bool ignore = false;
|
||||
foreach (var component in components) {
|
||||
if ((component as ILayoutIgnorer)?.ignoreLayout == true) {
|
||||
ignore = true;
|
||||
break;
|
||||
}
|
||||
if ((component as Behaviour)?.isActiveAndEnabled != false && component is
|
||||
ILayoutElement le) {
|
||||
int lp = le.layoutPriority;
|
||||
// Calculate must come first
|
||||
if (direction == PanelDirection.Horizontal) {
|
||||
le.CalculateLayoutInputHorizontal();
|
||||
PriValue(ref min, le.minWidth, lp, ref minPri);
|
||||
PriValue(ref preferred, le.preferredWidth, lp, ref prefPri);
|
||||
PriValue(ref flexible, le.flexibleWidth, lp, ref flexPri);
|
||||
} else { // if (direction == PanelDirection.Vertical)
|
||||
le.CalculateLayoutInputVertical();
|
||||
PriValue(ref min, le.minHeight, lp, ref minPri);
|
||||
PriValue(ref preferred, le.preferredHeight, lp, ref prefPri);
|
||||
PriValue(ref flexible, le.flexibleHeight, lp, ref flexPri);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new LayoutSizes(obj, min * scaleFactor, Math.Max(min, preferred) *
|
||||
scaleFactor, flexible) { ignore = ignore };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dumps information about the parent tree of the specified GameObject to the debug
|
||||
/// log.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to determine hierarchy.</param>
|
||||
public static void DebugObjectHierarchy(this GameObject item) {
|
||||
string info = "null";
|
||||
if (item != null) {
|
||||
var result = new StringBuilder(256);
|
||||
do {
|
||||
result.Append("- ");
|
||||
result.Append(item.name ?? "Unnamed");
|
||||
item = item.transform?.parent?.gameObject;
|
||||
if (item != null)
|
||||
result.AppendLine();
|
||||
} while (item != null);
|
||||
info = result.ToString();
|
||||
}
|
||||
LogUIDebug("Object Tree:" + Environment.NewLine + info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dumps information about the specified GameObject to the debug log.
|
||||
/// </summary>
|
||||
/// <param name="root">The root hierarchy to dump.</param>
|
||||
public static void DebugObjectTree(this GameObject root) {
|
||||
string info = "null";
|
||||
if (root != null)
|
||||
info = GetObjectTree(root, 0);
|
||||
LogUIDebug("Object Dump:" + Environment.NewLine + info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives a font style from an existing style. The font face is copied unchanged,
|
||||
/// but the other settings can be optionally modified.
|
||||
/// </summary>
|
||||
/// <param name="root">The style to use as a template.</param>
|
||||
/// <param name="size">The font size, or 0 to use the template size.</param>
|
||||
/// <param name="newColor">The font color, or null to use the template color.</param>
|
||||
/// <param name="style">The font style, or null to use the template style.</param>
|
||||
/// <returns>A copy of the root style with the specified parameters altered.</returns>
|
||||
public static TextStyleSetting DeriveStyle(this TextStyleSetting root, int size = 0,
|
||||
Color? newColor = null, FontStyles? style = null) {
|
||||
if (root == null)
|
||||
throw new ArgumentNullException(nameof(root));
|
||||
var newStyle = ScriptableObject.CreateInstance<TextStyleSetting>();
|
||||
newStyle.enableWordWrapping = root.enableWordWrapping;
|
||||
newStyle.style = (style == null) ? root.style : (FontStyles)style;
|
||||
newStyle.fontSize = (size > 0) ? size : root.fontSize;
|
||||
newStyle.sdfFont = root.sdfFont;
|
||||
newStyle.textColor = (newColor == null) ? root.textColor : (Color)newColor;
|
||||
return newStyle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A debug function used to forcefully re-layout a UI.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI to layout</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject ForceLayoutRebuild(GameObject uiElement) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var rt = uiElement.rectTransform();
|
||||
if (rt != null)
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(rt);
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the estimated width of a single string character (uses 'm' as the
|
||||
/// standard estimation character) in the given text style.
|
||||
/// </summary>
|
||||
/// <param name="style">The text style to use.</param>
|
||||
/// <returns>The width in pixels that should be allocated.</returns>
|
||||
public static float GetEmWidth(TextStyleSetting style) {
|
||||
float width = 0.0f;
|
||||
if (style == null)
|
||||
throw new ArgumentNullException(nameof(style));
|
||||
var font = style.sdfFont;
|
||||
// Use the em width
|
||||
if (font != null && font.characterDictionary.TryGetValue('m', out TMP_Glyph em)) {
|
||||
var info = font.fontInfo;
|
||||
float ptSize = style.fontSize / (info.PointSize * info.Scale);
|
||||
width = em.width * ptSize + style.fontSize * 0.01f * font.normalSpacingOffset;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the estimated height of one line of text in the given text style.
|
||||
/// </summary>
|
||||
/// <param name="style">The text style to use.</param>
|
||||
/// <returns>The height in pixels that should be allocated.</returns>
|
||||
public static float GetLineHeight(TextStyleSetting style) {
|
||||
float height = 0.0f;
|
||||
if (style == null)
|
||||
throw new ArgumentNullException(nameof(style));
|
||||
var font = style.sdfFont;
|
||||
if (font != null) {
|
||||
var info = font.fontInfo;
|
||||
height = info.LineHeight * style.fontSize / (info.Scale * info.PointSize);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string recursively describing the specified GameObject.
|
||||
/// </summary>
|
||||
/// <param name="root">The root GameObject hierarchy.</param>
|
||||
/// <param name="indent">The indentation to use.</param>
|
||||
/// <returns>A string describing this game object.</returns>
|
||||
private static string GetObjectTree(GameObject root, int indent) {
|
||||
var result = new StringBuilder(1024);
|
||||
// Calculate indent to make nested reading easier
|
||||
var solBuilder = new StringBuilder(indent);
|
||||
for (int i = 0; i < indent; i++)
|
||||
solBuilder.Append(' ');
|
||||
string sol = solBuilder.ToString();
|
||||
var transform = root.transform;
|
||||
int n = transform.childCount;
|
||||
// Basic information
|
||||
result.Append(sol).AppendFormat("GameObject[{0}, {1:D} child(ren), Layer {2:D}, " +
|
||||
"Active={3}]", root.name, n, root.layer, root.activeInHierarchy).AppendLine();
|
||||
// Transformation
|
||||
result.Append(sol).AppendFormat(" Translation={0} [{3}] Rotation={1} [{4}] " +
|
||||
"Scale={2}", transform.position, transform.rotation, transform.
|
||||
localScale, transform.localPosition, transform.localRotation).AppendLine();
|
||||
// Components
|
||||
foreach (var component in root.GetComponents<Component>()) {
|
||||
if (component is RectTransform rt) {
|
||||
// UI rectangle
|
||||
Vector2 size = rt.rect.size, aMin = rt.anchorMin, aMax = rt.anchorMax,
|
||||
oMin = rt.offsetMin, oMax = rt.offsetMax, pivot = rt.pivot;
|
||||
result.Append(sol).AppendFormat(" Rect[Size=({0:F2},{1:F2}) Min=" +
|
||||
"({2:F2},{3:F2}) ", size.x, size.y, LayoutUtility.GetMinWidth(rt),
|
||||
LayoutUtility.GetMinHeight(rt));
|
||||
result.AppendFormat("Preferred=({0:F2},{1:F2}) Flexible=({2:F2}," +
|
||||
"{3:F2}) ", LayoutUtility.GetPreferredWidth(rt), LayoutUtility.
|
||||
GetPreferredHeight(rt), LayoutUtility.GetFlexibleWidth(rt),
|
||||
LayoutUtility.GetFlexibleHeight(rt));
|
||||
result.AppendFormat("Pivot=({4:F2},{5:F2}) AnchorMin=({0:F2},{1:F2}) " +
|
||||
"AnchorMax=({2:F2},{3:F2}) ", aMin.x, aMin.y, aMax.x, aMax.y, pivot.x,
|
||||
pivot.y);
|
||||
result.AppendFormat("OffsetMin=({0:F2},{1:F2}) OffsetMax=({2:F2}," +
|
||||
"{3:F2})]", oMin.x, oMin.y, oMax.x, oMax.y).AppendLine();
|
||||
} else if (component != null && !(component is Transform)) {
|
||||
// Exclude destroyed components and Transform objects
|
||||
result.Append(sol).Append(" Component[").Append(component.GetType().
|
||||
FullName);
|
||||
AddComponentText(result, component);
|
||||
result.AppendLine("]");
|
||||
}
|
||||
}
|
||||
// Children
|
||||
if (n > 0)
|
||||
result.Append(sol).AppendLine(" Children:");
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = transform.GetChild(i).gameObject;
|
||||
if (child != null)
|
||||
// Exclude destroyed objects
|
||||
result.AppendLine(GetObjectTree(child, indent + 2));
|
||||
}
|
||||
return result.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the size for a component on a particular axis.
|
||||
/// </summary>
|
||||
/// <param name="sizes">The declared sizes.</param>
|
||||
/// <param name="allocated">The space allocated.</param>
|
||||
/// <returns>The size that the component should be.</returns>
|
||||
internal static float GetProperSize(LayoutSizes sizes, float allocated) {
|
||||
float size = sizes.min, preferred = Math.Max(sizes.preferred, size);
|
||||
// Compute size: minimum guaranteed, then preferred, then flexible
|
||||
if (allocated > size)
|
||||
size = Math.Min(preferred, allocated);
|
||||
if (allocated > preferred && sizes.flexible > 0.0f)
|
||||
size = allocated;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset required for a component in its box.
|
||||
/// </summary>
|
||||
/// <param name="alignment">The alignment to use.</param>
|
||||
/// <param name="direction">The direction of layout.</param>
|
||||
/// <param name="delta">The remaining space.</param>
|
||||
/// <returns>The offset from the edge.</returns>
|
||||
internal static float GetOffset(TextAnchor alignment, PanelDirection direction,
|
||||
float delta) {
|
||||
float offset = 0.0f;
|
||||
// Based on alignment, offset component
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
switch (alignment) {
|
||||
case TextAnchor.LowerCenter:
|
||||
case TextAnchor.MiddleCenter:
|
||||
case TextAnchor.UpperCenter:
|
||||
offset = delta * 0.5f;
|
||||
break;
|
||||
case TextAnchor.LowerRight:
|
||||
case TextAnchor.MiddleRight:
|
||||
case TextAnchor.UpperRight:
|
||||
offset = delta;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
} else
|
||||
switch (alignment) {
|
||||
case TextAnchor.MiddleLeft:
|
||||
case TextAnchor.MiddleCenter:
|
||||
case TextAnchor.MiddleRight:
|
||||
offset = delta * 0.5f;
|
||||
break;
|
||||
case TextAnchor.LowerLeft:
|
||||
case TextAnchor.LowerCenter:
|
||||
case TextAnchor.LowerRight:
|
||||
offset = delta;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the parent of the GameObject, or null if it does not have a parent.
|
||||
/// </summary>
|
||||
/// <param name="child">The child object.</param>
|
||||
/// <returns>The parent of that object, or null if it does not have a parent.</returns>
|
||||
public static GameObject GetParent(this GameObject child) {
|
||||
GameObject parent = null;
|
||||
if (child != null) {
|
||||
var newParent = child.transform.parent?.gameObject;
|
||||
// If parent is disposed, prevent crash
|
||||
if (newParent != null)
|
||||
parent = newParent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insets a child component from its parent, and assigns a fixed size to the parent
|
||||
/// equal to the provided size plus the insets.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent component.</param>
|
||||
/// <param name="child">The child to inset.</param>
|
||||
/// <param name="vertical">The vertical inset on each side.</param>
|
||||
/// <param name="horizontal">The horizontal inset on each side.</param>
|
||||
/// <param name="prefSize">The minimum component size.</param>
|
||||
/// <returns>The parent component.</returns>
|
||||
internal static GameObject InsetChild(GameObject parent, GameObject child,
|
||||
Vector2 insets, Vector2 prefSize = default(Vector2)) {
|
||||
var rt = child.rectTransform();
|
||||
float horizontal = insets.x, vertical = insets.y, width = prefSize.x, height =
|
||||
prefSize.y;
|
||||
rt.offsetMax = new Vector2(-horizontal, -vertical);
|
||||
rt.offsetMin = insets;
|
||||
var layout = parent.AddOrGet<LayoutElement>();
|
||||
layout.minWidth = layout.preferredWidth = (width <= 0.0f ? LayoutUtility.
|
||||
GetPreferredWidth(rt) : width) + horizontal * 2.0f;
|
||||
layout.minHeight = layout.preferredHeight = (height <= 0.0f ? LayoutUtility.
|
||||
GetPreferredHeight(rt) : height) + vertical * 2.0f;
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the side screen at the target location.
|
||||
/// </summary>
|
||||
/// <param name="screens">The current list of side screens.</param>
|
||||
/// <param name="newScreen">The screen to insert.</param>
|
||||
/// <param name="targetClassName">The target class name for locating the screen. If this
|
||||
/// class is not found, it will be added at the end regardless of insertBefore.</param>
|
||||
/// <param name="insertBefore">true to insert before that class, or false to insert after.</param>
|
||||
private static void InsertSideScreenContent(IList<SideScreenRef> screens,
|
||||
SideScreenRef newScreen, string targetClassName, bool insertBefore) {
|
||||
if (screens == null)
|
||||
throw new ArgumentNullException(nameof(screens));
|
||||
if (newScreen == null)
|
||||
throw new ArgumentNullException(nameof(newScreen));
|
||||
if (string.IsNullOrEmpty(targetClassName))
|
||||
// Add to end by default
|
||||
screens.Add(newScreen);
|
||||
else {
|
||||
int n = screens.Count;
|
||||
bool found = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
var screen = screens[i];
|
||||
var sideScreenPrefab = UIDetours.SS_PREFAB.Get(screen);
|
||||
if (sideScreenPrefab != null) {
|
||||
var contents = sideScreenPrefab.
|
||||
GetComponentsInChildren<SideScreenContent>();
|
||||
if (contents == null || contents.Length < 1)
|
||||
// Some naughty mod added a prefab with no side screen content!
|
||||
LogUIWarning("Could not find SideScreenContent on side screen: " +
|
||||
screen.name);
|
||||
else if (contents[0].GetType().FullName == targetClassName) {
|
||||
// Once the first matching screen is found, perform insertion
|
||||
if (insertBefore)
|
||||
screens.Insert(i, newScreen);
|
||||
else if (i >= n - 1)
|
||||
screens.Add(newScreen);
|
||||
else
|
||||
screens.Insert(i + 1, newScreen);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Warn if no match found
|
||||
if (!found) {
|
||||
LogUIWarning("No side screen with class name {0} found!".F(
|
||||
targetClassName));
|
||||
screens.Add(newScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite embedded in the calling assembly.
|
||||
///
|
||||
/// It may be encoded using PNG, DXT5, or JPG format.
|
||||
/// </summary>
|
||||
/// <param name="path">The fully qualified path to the image to load.</param>
|
||||
/// <param name="border">The sprite border. If there is no 9-patch border, use default(Vector4).</param>
|
||||
/// <param name="log">true to log the sprite load, or false to load silently.</param>
|
||||
/// <returns>The sprite thus loaded.</returns>
|
||||
/// <exception cref="ArgumentException">If the image could not be loaded.</exception>
|
||||
public static Sprite LoadSprite(string path, Vector4 border = default, bool log = true)
|
||||
{
|
||||
return LoadSprite(Assembly.GetCallingAssembly() ?? Assembly.GetExecutingAssembly(),
|
||||
path, border, log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite embedded in the specified assembly as a 9-slice sprite.
|
||||
///
|
||||
/// It may be encoded using PNG, DXT5, or JPG format.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the image.</param>
|
||||
/// <param name="path">The fully qualified path to the image to load.</param>
|
||||
/// <param name="border">The sprite border.</param>
|
||||
/// <param name="log">true to log the load, or false otherwise.</param>
|
||||
/// <returns>The sprite thus loaded.</returns>
|
||||
/// <exception cref="ArgumentException">If the image could not be loaded.</exception>
|
||||
internal static Sprite LoadSprite(Assembly assembly, string path, Vector4 border =
|
||||
default, bool log = false) {
|
||||
// Open a stream to the image
|
||||
try {
|
||||
using (var stream = assembly.GetManifestResourceStream(path)) {
|
||||
if (stream == null)
|
||||
throw new ArgumentException("Could not load image: " + path);
|
||||
// If len > int.MaxValue we will not go to space today
|
||||
int len = (int)stream.Length;
|
||||
byte[] buffer = new byte[len];
|
||||
var texture = new Texture2D(2, 2);
|
||||
// Load the texture from the stream
|
||||
stream.Read(buffer, 0, len);
|
||||
ImageConversion.LoadImage(texture, buffer, false);
|
||||
// Create a sprite centered on the texture
|
||||
int width = texture.width, height = texture.height;
|
||||
#if DEBUG
|
||||
log = true;
|
||||
#endif
|
||||
if (log)
|
||||
LogUIDebug("Loaded sprite: {0} ({1:D}x{2:D}, {3:D} bytes)".F(path,
|
||||
width, height, len));
|
||||
// pivot is in RELATIVE coordinates!
|
||||
return Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(
|
||||
0.5f, 0.5f), 100.0f, 0, SpriteMeshType.FullRect, border);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ArgumentException("Could not load image: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite from the file system as a 9-slice sprite.
|
||||
///
|
||||
/// It may be encoded using PNG, DXT5, or JPG format.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the image to load.</param>
|
||||
/// <param name="border">The sprite border.</param>
|
||||
/// <param name="log">true to log the load, or false otherwise.</param>
|
||||
/// <returns>The sprite thus loaded, or null if it could not be loaded.</returns>
|
||||
public static Sprite LoadSpriteFile(string path, Vector4 border = default) {
|
||||
Sprite sprite = null;
|
||||
// Open a stream to the image
|
||||
try {
|
||||
using (var stream = new FileStream(path, FileMode.Open)) {
|
||||
// If len > int.MaxValue we will not go to space today
|
||||
int len = (int)stream.Length;
|
||||
byte[] buffer = new byte[len];
|
||||
var texture = new Texture2D(2, 2);
|
||||
// Load the texture from the stream
|
||||
stream.Read(buffer, 0, len);
|
||||
ImageConversion.LoadImage(texture, buffer, false);
|
||||
// Create a sprite centered on the texture
|
||||
int width = texture.width, height = texture.height;
|
||||
sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(
|
||||
0.5f, 0.5f), 100.0f, 0, SpriteMeshType.FullRect, border);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
#if DEBUG
|
||||
PUtil.LogExcWarn(e);
|
||||
#endif
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a DDS sprite embedded in the specified assembly as a 9-slice sprite.
|
||||
///
|
||||
/// It must be encoded using the DXT5 format.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the image.</param>
|
||||
/// <param name="path">The fully qualified path to the DDS image to load.</param>
|
||||
/// <param name="width">The desired width.</param>
|
||||
/// <param name="height">The desired height.</param>
|
||||
/// <param name="border">The sprite border.</param>
|
||||
/// <param name="log">true to log the load, or false otherwise.</param>
|
||||
/// <returns>The sprite thus loaded.</returns>
|
||||
/// <exception cref="ArgumentException">If the image could not be loaded.</exception>
|
||||
internal static Sprite LoadSpriteLegacy(Assembly assembly, string path, int width,
|
||||
int height, Vector4 border = default) {
|
||||
// Open a stream to the image
|
||||
try {
|
||||
using (var stream = assembly.GetManifestResourceStream(path)) {
|
||||
const int SKIP = 128;
|
||||
if (stream == null)
|
||||
throw new ArgumentException("Could not load image: " + path);
|
||||
// If len > int.MaxValue we will not go to space today, skip first 128
|
||||
// bytes of stream
|
||||
int len = (int)stream.Length - SKIP;
|
||||
if (len < 0)
|
||||
throw new ArgumentException("Image is too small: " + path);
|
||||
byte[] buffer = new byte[len];
|
||||
stream.Seek(SKIP, SeekOrigin.Begin);
|
||||
stream.Read(buffer, 0, len);
|
||||
// Load the texture from the stream
|
||||
var texture = new Texture2D(width, height, TextureFormat.DXT5, false);
|
||||
texture.LoadRawTextureData(buffer);
|
||||
texture.Apply(true, true);
|
||||
// Create a sprite centered on the texture
|
||||
LogUIDebug("Loaded sprite: {0} ({1:D}x{2:D}, {3:D} bytes)".F(path,
|
||||
width, height, len));
|
||||
// pivot is in RELATIVE coordinates!
|
||||
return Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(
|
||||
0.5f, 0.5f), 100.0f, 0, SpriteMeshType.FullRect, border);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ArgumentException("Could not load image: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message encountered in PLib UI functions.
|
||||
/// </summary>
|
||||
/// <param name="message">The debug message.</param>
|
||||
internal static void LogUIDebug(string message) {
|
||||
Debug.LogFormat("[PLib/UI/{0}] {1}", Assembly.GetCallingAssembly()?.GetName()?.
|
||||
Name ?? "?", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning encountered in PLib UI functions.
|
||||
/// </summary>
|
||||
/// <param name="message">The warning message.</param>
|
||||
internal static void LogUIWarning(string message) {
|
||||
Debug.LogWarningFormat("[PLib/UI/{0}] {1}", Assembly.GetCallingAssembly()?.
|
||||
GetName()?.Name ?? "?", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates layout values, replacing the value if a higher priority value is given
|
||||
/// and otherwise taking the largest value.
|
||||
/// </summary>
|
||||
/// <param name="value">The current value.</param>
|
||||
/// <param name="newValue">The candidate new value. No operation if this is less than zero.</param>
|
||||
/// <param name="newPri">The new value's layout priority.</param>
|
||||
/// <param name="pri">The current value's priority</param>
|
||||
private static void PriValue(ref float value, float newValue, int newPri, ref int pri)
|
||||
{
|
||||
int thisPri = pri;
|
||||
if (newValue >= 0.0f) {
|
||||
if (newPri > thisPri) {
|
||||
// Priority override?
|
||||
pri = newPri;
|
||||
value = newValue;
|
||||
} else if (newValue > value && newPri == thisPri)
|
||||
// Same priority and higher value?
|
||||
value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a UI element's flexible size.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="flexSize">The flexible size as a ratio.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetFlexUISize(this GameObject uiElement, Vector2 flexSize) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
if (uiElement.TryGetComponent(out ISettableFlexSize fs)) {
|
||||
// Avoid duplicate LayoutElement on layouts
|
||||
fs.flexibleWidth = flexSize.x;
|
||||
fs.flexibleHeight = flexSize.y;
|
||||
} else {
|
||||
var le = uiElement.AddOrGet<LayoutElement>();
|
||||
le.flexibleWidth = flexSize.x;
|
||||
le.flexibleHeight = flexSize.y;
|
||||
}
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a UI element's minimum size.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="minSize">The minimum size in units.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetMinUISize(this GameObject uiElement, Vector2 minSize) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
float minX = minSize.x, minY = minSize.y;
|
||||
if (minX > 0.0f || minY > 0.0f) {
|
||||
var le = uiElement.AddOrGet<LayoutElement>();
|
||||
if (minX > 0.0f)
|
||||
le.minWidth = minX;
|
||||
if (minY > 0.0f)
|
||||
le.minHeight = minY;
|
||||
}
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately resizes a UI element. Uses the element's current anchors. If a
|
||||
/// dimension of the size is negative, the component will not be resized in that
|
||||
/// dimension.
|
||||
///
|
||||
/// If addLayout is true, a layout element is also added so that future auto layout
|
||||
/// calls will try to maintain that size. Do not set addLayout to true if either of
|
||||
/// the size dimensions are negative, as laying out components with a negative
|
||||
/// preferred size may cause unexpected behavior.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="size">The new element size.</param>
|
||||
/// <param name="addLayout">true to add a layout element with that size, or false
|
||||
/// otherwise.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetUISize(this GameObject uiElement, Vector2 size,
|
||||
bool addLayout = false) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var transform = uiElement.rectTransform();
|
||||
float width = size.x, height = size.y;
|
||||
if (transform != null) {
|
||||
if (width >= 0.0f)
|
||||
transform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
|
||||
if (height >= 0.0f)
|
||||
transform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
}
|
||||
if (addLayout) {
|
||||
var le = uiElement.AddOrGet<LayoutElement>();
|
||||
// Set minimum and preferred size
|
||||
le.minWidth = width;
|
||||
le.minHeight = height;
|
||||
le.preferredWidth = width;
|
||||
le.preferredHeight = height;
|
||||
le.flexibleHeight = 0.0f;
|
||||
le.flexibleWidth = 0.0f;
|
||||
}
|
||||
return uiElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user