/* * 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 { /// /// Utility functions for dealing with Unity UIs. /// public static class PUIUtils { /// /// Adds text describing a particular component if available. /// /// The location to append the text. /// The component to describe. 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); } } /// /// Adds a hot pink rectangle over the target matching its size, to help identify it /// better. /// /// The target UI component. public static void AddPinkOverlay(GameObject parent) { var child = PUIElements.CreateUI(parent, "Overlay"); var img = child.AddComponent(); img.color = new Color(1.0f, 0.0f, 1.0f, 0.2f); } /// /// 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. /// /// 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. /// 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. public static void AddSideScreenContent(GameObject uiPrefab = null) where T : SideScreenContent { AddSideScreenContentWithOrdering(null, true, uiPrefab); } /// /// 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. /// /// 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. /// 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. /// 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. /// 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. public static void AddSideScreenContentWithOrdering(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().Params = new BoxLayoutParams() { Direction = PanelDirection.Vertical, Alignment = TextAnchor. UpperCenter, Margin = new RectOffset(1, 1, 0, 1) }; var controller = rootObject.AddComponent(); 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); } } } /// /// Builds a PLib UI object and adds it to an existing UI object. /// /// The UI object to add. /// The parent of the new object. /// The sibling index to insert the element at, if provided. /// The built version of the UI object. 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; } /// /// Calculates the size of a single game object. /// /// The object to calculate. /// The direction to calculate. /// The components of this game object. /// The object's minimum and preferred size. internal static LayoutSizes CalcSizes(GameObject obj, PanelDirection direction, IEnumerable 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 }; } /// /// Dumps information about the parent tree of the specified GameObject to the debug /// log. /// /// The item to determine hierarchy. 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); } /// /// Dumps information about the specified GameObject to the debug log. /// /// The root hierarchy to dump. public static void DebugObjectTree(this GameObject root) { string info = "null"; if (root != null) info = GetObjectTree(root, 0); LogUIDebug("Object Dump:" + Environment.NewLine + info); } /// /// Derives a font style from an existing style. The font face is copied unchanged, /// but the other settings can be optionally modified. /// /// The style to use as a template. /// The font size, or 0 to use the template size. /// The font color, or null to use the template color. /// The font style, or null to use the template style. /// A copy of the root style with the specified parameters altered. 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(); 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; } /// /// A debug function used to forcefully re-layout a UI. /// /// The UI to layout /// The UI element, for call chaining. 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; } /// /// Retrieves the estimated width of a single string character (uses 'm' as the /// standard estimation character) in the given text style. /// /// The text style to use. /// The width in pixels that should be allocated. 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; } /// /// Retrieves the estimated height of one line of text in the given text style. /// /// The text style to use. /// The height in pixels that should be allocated. 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; } /// /// Creates a string recursively describing the specified GameObject. /// /// The root GameObject hierarchy. /// The indentation to use. /// A string describing this game object. 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()) { 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(); } /// /// Determines the size for a component on a particular axis. /// /// The declared sizes. /// The space allocated. /// The size that the component should be. 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; } /// /// Gets the offset required for a component in its box. /// /// The alignment to use. /// The direction of layout. /// The remaining space. /// The offset from the edge. 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; } /// /// Retrieves the parent of the GameObject, or null if it does not have a parent. /// /// The child object. /// The parent of that object, or null if it does not have a parent. 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; } /// /// Insets a child component from its parent, and assigns a fixed size to the parent /// equal to the provided size plus the insets. /// /// The parent component. /// The child to inset. /// The vertical inset on each side. /// The horizontal inset on each side. /// The minimum component size. /// The parent component. 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(); 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; } /// /// Inserts the side screen at the target location. /// /// The current list of side screens. /// The screen to insert. /// The target class name for locating the screen. If this /// class is not found, it will be added at the end regardless of insertBefore. /// true to insert before that class, or false to insert after. private static void InsertSideScreenContent(IList 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(); 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); } } } /// /// Loads a sprite embedded in the calling assembly. /// /// It may be encoded using PNG, DXT5, or JPG format. /// /// The fully qualified path to the image to load. /// The sprite border. If there is no 9-patch border, use default(Vector4). /// true to log the sprite load, or false to load silently. /// The sprite thus loaded. /// If the image could not be loaded. public static Sprite LoadSprite(string path, Vector4 border = default, bool log = true) { return LoadSprite(Assembly.GetCallingAssembly() ?? Assembly.GetExecutingAssembly(), path, border, log); } /// /// Loads a sprite embedded in the specified assembly as a 9-slice sprite. /// /// It may be encoded using PNG, DXT5, or JPG format. /// /// The assembly containing the image. /// The fully qualified path to the image to load. /// The sprite border. /// true to log the load, or false otherwise. /// The sprite thus loaded. /// If the image could not be loaded. 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); } } /// /// Loads a sprite from the file system as a 9-slice sprite. /// /// It may be encoded using PNG, DXT5, or JPG format. /// /// The path to the image to load. /// The sprite border. /// true to log the load, or false otherwise. /// The sprite thus loaded, or null if it could not be loaded. 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; } /// /// Loads a DDS sprite embedded in the specified assembly as a 9-slice sprite. /// /// It must be encoded using the DXT5 format. /// /// The assembly containing the image. /// The fully qualified path to the DDS image to load. /// The desired width. /// The desired height. /// The sprite border. /// true to log the load, or false otherwise. /// The sprite thus loaded. /// If the image could not be loaded. 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); } } /// /// Logs a debug message encountered in PLib UI functions. /// /// The debug message. internal static void LogUIDebug(string message) { Debug.LogFormat("[PLib/UI/{0}] {1}", Assembly.GetCallingAssembly()?.GetName()?. Name ?? "?", message); } /// /// Logs a warning encountered in PLib UI functions. /// /// The warning message. internal static void LogUIWarning(string message) { Debug.LogWarningFormat("[PLib/UI/{0}] {1}", Assembly.GetCallingAssembly()?. GetName()?.Name ?? "?", message); } /// /// Aggregates layout values, replacing the value if a higher priority value is given /// and otherwise taking the largest value. /// /// The current value. /// The candidate new value. No operation if this is less than zero. /// The new value's layout priority. /// The current value's priority 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; } } /// /// Sets a UI element's flexible size. /// /// The UI element to modify. /// The flexible size as a ratio. /// The UI element, for call chaining. 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(); le.flexibleWidth = flexSize.x; le.flexibleHeight = flexSize.y; } return uiElement; } /// /// Sets a UI element's minimum size. /// /// The UI element to modify. /// The minimum size in units. /// The UI element, for call chaining. 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(); if (minX > 0.0f) le.minWidth = minX; if (minY > 0.0f) le.minHeight = minY; } return uiElement; } /// /// 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. /// /// The UI element to modify. /// The new element size. /// true to add a layout element with that size, or false /// otherwise. /// The UI element, for call chaining. 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(); // 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; } } }