Search Unity

  1. Calling all beginners! Join the FPS Beginners Mods Challenge until December 13.
    Dismiss Notice
  2. It's Cyber Week at the Asset Store!
    Dismiss Notice

Fit an EditorWindow to its content

Discussion in 'UIElements' started by aybe, Oct 3, 2019.

  1. aybe

    aybe

    Joined:
    May 24, 2015
    Posts:
    104
    I want to set minimum size of an editor window to the minimum layout size computed of the root VisualElement, but there doesn't seem to be a way to do this or it's not public.

    How can I know the minimum size of a VisualElement so I can fit my EditorWindow to it?
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    413
    In USS:
    Code (CSharp):
    1. min-width: 20px;
    2. min-height: 20px;
    In C#:
    Code (CSharp):
    1. myElement.style.minWidth = 20;
    2. myElement.style.minHeight = 20;
    You can then register for the
    GeometryChangeEvent
    on your
    rootVisualElement
    and inside the callback read the resolved min size and set it on your
    EditorWindow
    :
    Code (CSharp):
    1. var minWidth = myElement.resolvedStyle.minWidth;
    2. var minHeight = myElement.resolvedStyle.minHeight;
     
  3. aybe

    aybe

    Joined:
    May 24, 2015
    Posts:
    104
    Doesn't work :(

    The window can get as small as possible, 20*46 but never fits content.

    I've been trying to set a button min width, hoping that parent would account for it but it doesn't.

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace Editor
    6. {
    7.     internal class NewUI : EditorWindow
    8.     {
    9.         [MenuItem("TEST/NewUI")]
    10.         private static void Init()
    11.         {
    12.             GetWindow<NewUI>();
    13.         }
    14.  
    15.         private void OnEnable()
    16.         {
    17.             var xml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/NewUI.uxml");
    18.             xml.CloneTree(rootVisualElement);
    19.          
    20.             var css = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/NewUI.uss");
    21.             rootVisualElement.styleSheets.Add(css);
    22.  
    23.             var element = rootVisualElement.Q<VisualElement>("rootElement");
    24.             element.RegisterCallback<GeometryChangedEvent>(Callback);
    25.         }
    26.  
    27.         private void Callback(GeometryChangedEvent evt)
    28.         {
    29.             var target = (VisualElement) evt.target;
    30.          
    31.             minSize = new Vector2(
    32.                 target.resolvedStyle.minWidth.value,
    33.                 target.resolvedStyle.minHeight.value
    34.             );
    35.         }
    36.     }
    37. }
    Code (CSharp):
    1. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    2.     <ui:VisualElement name="rootElement" style="min-width: 20px; min-height: 20px;">
    3.         <Style path="Assets/Editor/NewUI.uss" />
    4.         <ui:Button text="Button" />
    5.         <ui:Button text="Button" />
    6.         <ui:Button text="Button" />
    7.     </ui:VisualElement>
    8. </ui:UXML>
    9.  
    Thank you.
     
  4. aybe

    aybe

    Joined:
    May 24, 2015
    Posts:
    104
    I've been able to sort it out somehow, see subtle differences with your code!

    Code (CSharp):
    1. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    2.     <ui:VisualElement name="root" style="position: absolute;">
    3.         <Style path="Assets/Editor/NewUI.uss" />
    4.         <ui:Button text="Button" class="myButton" />
    5.         <ui:Button text="Button" class="myButton" />
    6.         <ui:Button text="Button" class="myButton" />
    7.         <ui:Button text="Button" class="myButton" />
    8.         <ui:Button text="Button" class="myButton" />
    9.         <ui:Button text="Button" class="myButton" />
    10.         <ui:Button text="Button" class="myButton" />
    11.         <ui:Button text="Button" class="myButton" />
    12.         <ui:Button text="Button" class="myButton" />
    13.         <ui:Button text="Button" class="myButton" />
    14.     </ui:VisualElement>
    15. </ui:UXML>
    16.  
    Code (CSharp):
    1. .myButton {
    2.     min-width: 222px;
    3. }
    4.  
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace Editor
    6. {
    7.     internal class NewUI : EditorWindow
    8.     {
    9.         [MenuItem("TEST/NewUI")]
    10.         private static void Init()
    11.         {
    12.             GetWindow<NewUI>();
    13.         }
    14.  
    15.         private void OnEnable()
    16.         {
    17.             var xml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/NewUI.uxml");
    18.             xml.CloneTree(rootVisualElement);
    19.  
    20.             var css = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/NewUI.uss");
    21.             rootVisualElement.styleSheets.Add(css);
    22.  
    23.             rootVisualElement.RegisterCallback<GeometryChangedEvent>(Callback);
    24.         }
    25.  
    26.         private void Callback(GeometryChangedEvent evt)
    27.         {
    28.             var target = rootVisualElement.Q<VisualElement>("root");
    29.  
    30.             minSize = new Vector2(
    31.                 target.resolvedStyle.width,
    32.                 target.resolvedStyle.height
    33.             );
    34.         }
    35.     }
    36. }
    To make that root container fit its content it must go absolute positionning, else only height ever shrinks.

    Now that the container fits its content, it won't report geometry changes, instead listen to rootVisualElement and use container actual size.

    It's mostly perfect, there is a 6px height differences between root and internal rootVisualContainer and I don't really know why but at least the window won't shrink past a minimum size calculated automatically :).
     
  5. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    413
    Ah, I assumed you were setting the minWidth/minHeight styles on the rootVisualElement itself. If you set the min sizes on the buttons or other children, this won't affect the min size of the parent elements (or the root). That is, you have to read the min size styles from the elements that have them set.

    Given that, your approach makes sense. I would just say that this is not standard UX. Normally, the window drives the UI, not the other way around. For example, no standard window in Unity will magically grow and push against other docked windows when its internal UI grows. I'm just saying you might run into some issues down the road with this approach, especially if you plan to make the window dockable.
     
  6. aybe

    aybe

    Joined:
    May 24, 2015
    Posts:
    104
    Doing it on the rootVisualElement doesn't work because something is affecting it, which makes sense ... There must be a 'root' container that will encompass your visual elements else it just doesn't work :mad:.

    I think you're 50% right but also 200% right :D!

    Circumventing this is weird but many Unity windows already do that such as project or hierarchy windows.

    Here's my final shot, terrible in terms of productivity but it just works !!!

    I just got bored of endlessly assigning a fixed size to my window whenever I changed its content and I wanted it always as small as possible. There is a single requirement, to return a root container whose position is absolute.

    Code (CSharp):
    1. using System;
    2. using System.IO;
    3. using System.Reflection;
    4. using System.Runtime.CompilerServices;
    5. using JetBrains.Annotations;
    6. using UnityEditor;
    7. using UnityEngine;
    8. using UnityEngine.UIElements;
    9.  
    10. namespace ZeroAG.Unity.Editor
    11. {
    12.     /// <summary>
    13.     ///     Base class for an <see cref="EditorWindow" /> that uses USS and UXML.
    14.     /// </summary>
    15.     public abstract class EditorWindowUXML : EditorWindow
    16.     {
    17.         #region Private
    18.  
    19.         private static readonly PropertyInfo IsDockedProperty =
    20.             GetProperty(typeof(EditorWindow), "docked", BindingFlags.Instance | BindingFlags.NonPublic);
    21.  
    22.         private static readonly Vector2 SizeToContentMinDefault = new Vector2(100.0f, 100.0f);
    23.  
    24.         private Vector2 SizeToContentMin
    25.         {
    26.             get => GetVector2(SizeToContentMinDefault);
    27.             set => SetVector2(Vector2.Min(value, SizeToContentMinDefault));
    28.         }
    29.  
    30.         private static readonly Vector2 SizeToContentMaxDefault = new Vector2(9999.0f, 9999.0f);
    31.  
    32.         private Vector2 SizeToContentMax
    33.         {
    34.             get => GetVector2(SizeToContentMaxDefault);
    35.             set => SetVector2(Vector2.Max(value, value));
    36.         }
    37.  
    38.         private static PropertyInfo GetProperty(Type type, string name, BindingFlags bindingFlags)
    39.         {
    40.             if (type == null)
    41.                 throw new ArgumentNullException(nameof(type));
    42.  
    43.             if (string.IsNullOrWhiteSpace(name))
    44.                 throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
    45.  
    46.             var property = type.GetProperty(name, bindingFlags);
    47.  
    48.             if (property == null)
    49.                 throw new ArgumentNullException(nameof(property));
    50.  
    51.             return property;
    52.         }
    53.  
    54.         private string GetPropertyName([CallerMemberName] string propertyName = null)
    55.         {
    56.             return $"{GetType().Name}.{propertyName}";
    57.         }
    58.  
    59.         private void SizeToContentUpdate()
    60.         {
    61.             var root = SizeToContentRoot;
    62.             if (root == null)
    63.             {
    64.                 Debug.LogError($"Cannot size to content, {nameof(SizeToContentRoot)} is null.");
    65.                 return;
    66.             }
    67.  
    68.             const Position absolute = Position.Absolute;
    69.  
    70.             if (!SizeToContent)
    71.             {
    72.                 minSize = SizeToContentMin;
    73.                 maxSize = SizeToContentMax;
    74.                 return;
    75.             }
    76.  
    77.             if (root.resolvedStyle.position != absolute)
    78.             {
    79.                 Debug.LogError($"Cannot size to content, '{root.name}' position must be '{absolute}'.");
    80.                 return;
    81.             }
    82.  
    83.             SizeToContentMin = minSize;
    84.             SizeToContentMax = maxSize;
    85.             var size = new Vector2(root.resolvedStyle.width, root.resolvedStyle.height - (IsDocked ? 0.0f : 7.0f));
    86.             minSize = size;
    87.             maxSize = size;
    88.         }
    89.  
    90.         #endregion
    91.  
    92.         #region Protected
    93.  
    94.         /// <summary>
    95.         ///     Gets if this instance is docked.
    96.         /// </summary>
    97.         [PublicAPI]
    98.         protected bool IsDocked => (bool) IsDockedProperty.GetValue(this);
    99.  
    100.         /// <summary>
    101.         ///     Gets or sets whether this instance resizes itself to fit its content.
    102.         /// </summary>
    103.         [PublicAPI]
    104.         protected bool SizeToContent
    105.         {
    106.             get => GetBool(false);
    107.             set
    108.             {
    109.                 if (Equals(value, SizeToContent))
    110.                     return;
    111.  
    112.                 SetBool(value);
    113.                 SizeToContentUpdate();
    114.             }
    115.         }
    116.  
    117.         /// <summary>
    118.         ///     Gets the root visual element this instance should resize itself to (see Remarks).
    119.         /// </summary>
    120.         /// <remarks>
    121.         ///     Element <see cref="IResolvedStyle.position" /> must be <see cref="Position.Absolute" /> for resize to be enabled.
    122.         /// </remarks>
    123.         [CanBeNull]
    124.         protected virtual VisualElement SizeToContentRoot { get; } = null;
    125.  
    126.         protected virtual void OnEnable()
    127.         {
    128.             var root = rootVisualElement;
    129.  
    130.             var script = MonoScript.FromScriptableObject(this);
    131.             if (script == null)
    132.                 throw new ArgumentNullException(nameof(script));
    133.  
    134.             var path = AssetDatabase.GetAssetPath(script);
    135.             var pathCss = Path.ChangeExtension(path, "uss");
    136.             var pathXml = Path.ChangeExtension(path, "uxml");
    137.  
    138.             if (File.Exists(pathCss) == false)
    139.                 throw new FileNotFoundException("Couldn't find associated style sheet.", pathCss);
    140.  
    141.             if (File.Exists(pathXml) == false)
    142.                 throw new FileNotFoundException("Couldn't find associated visual tree asset.", pathXml);
    143.  
    144.             var css = AssetDatabase.LoadAssetAtPath<StyleSheet>(pathCss);
    145.             var xml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(pathXml);
    146.  
    147.             xml.CloneTree(root);
    148.  
    149.             root.styleSheets.Add(css);
    150.  
    151.             root.RegisterCallback<GeometryChangedEvent>(_ => SizeToContentUpdate());
    152.         }
    153.  
    154.         protected virtual void OnDisable()
    155.         {
    156.         }
    157.  
    158.         [PublicAPI]
    159.         protected bool GetBool(bool @default, [CallerMemberName] string propertyName = null)
    160.         {
    161.             return EditorPrefs.GetBool(GetPropertyName(propertyName), @default);
    162.         }
    163.  
    164.         [PublicAPI]
    165.         protected void SetBool(bool value, [CallerMemberName] string propertyName = null)
    166.         {
    167.             EditorPrefs.SetBool(GetPropertyName(propertyName), value);
    168.         }
    169.  
    170.         [PublicAPI]
    171.         protected Vector2 GetVector2(Vector2 @default, [CallerMemberName] string propertyName = null)
    172.         {
    173.             var n = GetPropertyName(propertyName);
    174.             var x = EditorPrefs.GetFloat($"{n}.{nameof(@default.x)}", @default.x);
    175.             var y = EditorPrefs.GetFloat($"{n}.{nameof(@default.y)}", @default.y);
    176.             var v = new Vector2(x, y);
    177.             return v;
    178.         }
    179.  
    180.         [PublicAPI]
    181.         protected void SetVector2(Vector2 value, [CallerMemberName] string propertyName = null)
    182.         {
    183.             var n = GetPropertyName(propertyName);
    184.             EditorPrefs.SetFloat($"{n}.{nameof(value.x)}", value.x);
    185.             EditorPrefs.SetFloat($"{n}.{nameof(value.y)}", value.y);
    186.         }
    187.  
    188.         #endregion
    189.     }
    190. }
    2019-10-04_02-39-17.gif

    PS it doesn't exhibit any bad behavior when docked :) not saying it's proper UX but doesn't blow!
     
    uDamian likes this.