Fit an EditorWindow to its content

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?

In USS:

min-width: 20px;
min-height: 20px;

In C#:

myElement.style.minWidth = 20;
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:

var minWidth = myElement.resolvedStyle.minWidth;
var minHeight = myElement.resolvedStyle.minHeight;

Doesn’t work :frowning:

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.

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace Editor
{
    internal class NewUI : EditorWindow
    {
        [MenuItem("TEST/NewUI")]
        private static void Init()
        {
            GetWindow<NewUI>();
        }

        private void OnEnable()
        {
            var xml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/NewUI.uxml");
            xml.CloneTree(rootVisualElement);
          
            var css = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/NewUI.uss");
            rootVisualElement.styleSheets.Add(css);

            var element = rootVisualElement.Q<VisualElement>("rootElement");
            element.RegisterCallback<GeometryChangedEvent>(Callback);
        }

        private void Callback(GeometryChangedEvent evt)
        {
            var target = (VisualElement) evt.target;
          
            minSize = new Vector2(
                target.resolvedStyle.minWidth.value,
                target.resolvedStyle.minHeight.value
            );
        }
    }
}
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    <ui:VisualElement name="rootElement" style="min-width: 20px; min-height: 20px;">
        <Style path="Assets/Editor/NewUI.uss" />
        <ui:Button text="Button" />
        <ui:Button text="Button" />
        <ui:Button text="Button" />
    </ui:VisualElement>
</ui:UXML>

Thank you.

I’ve been able to sort it out somehow, see subtle differences with your code!

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    <ui:VisualElement name="root" style="position: absolute;">
        <Style path="Assets/Editor/NewUI.uss" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
        <ui:Button text="Button" class="myButton" />
    </ui:VisualElement>
</ui:UXML>
.myButton {
    min-width: 222px;
}
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace Editor
{
    internal class NewUI : EditorWindow
    {
        [MenuItem("TEST/NewUI")]
        private static void Init()
        {
            GetWindow<NewUI>();
        }

        private void OnEnable()
        {
            var xml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/NewUI.uxml");
            xml.CloneTree(rootVisualElement);

            var css = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/NewUI.uss");
            rootVisualElement.styleSheets.Add(css);

            rootVisualElement.RegisterCallback<GeometryChangedEvent>(Callback);
        }

        private void Callback(GeometryChangedEvent evt)
        {
            var target = rootVisualElement.Q<VisualElement>("root");

            minSize = new Vector2(
                target.resolvedStyle.width,
                target.resolvedStyle.height
            );
        }
    }
}

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 :).

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.

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 :rage:.

I think you’re 50% right but also 200% right :smile:!

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.

using System;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace ZeroAG.Unity.Editor
{
    /// <summary>
    ///     Base class for an <see cref="EditorWindow" /> that uses USS and UXML.
    /// </summary>
    public abstract class EditorWindowUXML : EditorWindow
    {
        #region Private

        private static readonly PropertyInfo IsDockedProperty =
            GetProperty(typeof(EditorWindow), "docked", BindingFlags.Instance | BindingFlags.NonPublic);

        private static readonly Vector2 SizeToContentMinDefault = new Vector2(100.0f, 100.0f);

        private Vector2 SizeToContentMin
        {
            get => GetVector2(SizeToContentMinDefault);
            set => SetVector2(Vector2.Min(value, SizeToContentMinDefault));
        }

        private static readonly Vector2 SizeToContentMaxDefault = new Vector2(9999.0f, 9999.0f);

        private Vector2 SizeToContentMax
        {
            get => GetVector2(SizeToContentMaxDefault);
            set => SetVector2(Vector2.Max(value, value));
        }

        private static PropertyInfo GetProperty(Type type, string name, BindingFlags bindingFlags)
        {
            if (type == null)
                throw new ArgumentNullException(nameof(type));

            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));

            var property = type.GetProperty(name, bindingFlags);

            if (property == null)
                throw new ArgumentNullException(nameof(property));

            return property;
        }

        private string GetPropertyName([CallerMemberName] string propertyName = null)
        {
            return $"{GetType().Name}.{propertyName}";
        }

        private void SizeToContentUpdate()
        {
            var root = SizeToContentRoot;
            if (root == null)
            {
                Debug.LogError($"Cannot size to content, {nameof(SizeToContentRoot)} is null.");
                return;
            }

            const Position absolute = Position.Absolute;

            if (!SizeToContent)
            {
                minSize = SizeToContentMin;
                maxSize = SizeToContentMax;
                return;
            }

            if (root.resolvedStyle.position != absolute)
            {
                Debug.LogError($"Cannot size to content, '{root.name}' position must be '{absolute}'.");
                return;
            }

            SizeToContentMin = minSize;
            SizeToContentMax = maxSize;
            var size = new Vector2(root.resolvedStyle.width, root.resolvedStyle.height - (IsDocked ? 0.0f : 7.0f));
            minSize = size;
            maxSize = size;
        }

        #endregion

        #region Protected

        /// <summary>
        ///     Gets if this instance is docked.
        /// </summary>
        [PublicAPI]
        protected bool IsDocked => (bool) IsDockedProperty.GetValue(this);

        /// <summary>
        ///     Gets or sets whether this instance resizes itself to fit its content.
        /// </summary>
        [PublicAPI]
        protected bool SizeToContent
        {
            get => GetBool(false);
            set
            {
                if (Equals(value, SizeToContent))
                    return;

                SetBool(value);
                SizeToContentUpdate();
            }
        }

        /// <summary>
        ///     Gets the root visual element this instance should resize itself to (see Remarks).
        /// </summary>
        /// <remarks>
        ///     Element <see cref="IResolvedStyle.position" /> must be <see cref="Position.Absolute" /> for resize to be enabled.
        /// </remarks>
        [CanBeNull]
        protected virtual VisualElement SizeToContentRoot { get; } = null;

        protected virtual void OnEnable()
        {
            var root = rootVisualElement;

            var script = MonoScript.FromScriptableObject(this);
            if (script == null)
                throw new ArgumentNullException(nameof(script));

            var path = AssetDatabase.GetAssetPath(script);
            var pathCss = Path.ChangeExtension(path, "uss");
            var pathXml = Path.ChangeExtension(path, "uxml");

            if (File.Exists(pathCss) == false)
                throw new FileNotFoundException("Couldn't find associated style sheet.", pathCss);

            if (File.Exists(pathXml) == false)
                throw new FileNotFoundException("Couldn't find associated visual tree asset.", pathXml);

            var css = AssetDatabase.LoadAssetAtPath<StyleSheet>(pathCss);
            var xml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(pathXml);

            xml.CloneTree(root);

            root.styleSheets.Add(css);

            root.RegisterCallback<GeometryChangedEvent>(_ => SizeToContentUpdate());
        }

        protected virtual void OnDisable()
        {
        }

        [PublicAPI]
        protected bool GetBool(bool @default, [CallerMemberName] string propertyName = null)
        {
            return EditorPrefs.GetBool(GetPropertyName(propertyName), @default);
        }

        [PublicAPI]
        protected void SetBool(bool value, [CallerMemberName] string propertyName = null)
        {
            EditorPrefs.SetBool(GetPropertyName(propertyName), value);
        }

        [PublicAPI]
        protected Vector2 GetVector2(Vector2 @default, [CallerMemberName] string propertyName = null)
        {
            var n = GetPropertyName(propertyName);
            var x = EditorPrefs.GetFloat($"{n}.{nameof(@default.x)}", @default.x);
            var y = EditorPrefs.GetFloat($"{n}.{nameof(@default.y)}", @default.y);
            var v = new Vector2(x, y);
            return v;
        }

        [PublicAPI]
        protected void SetVector2(Vector2 value, [CallerMemberName] string propertyName = null)
        {
            var n = GetPropertyName(propertyName);
            EditorPrefs.SetFloat($"{n}.{nameof(value.x)}", value.x);
            EditorPrefs.SetFloat($"{n}.{nameof(value.y)}", value.y);
        }

        #endregion
    }
}

5029058--492872--2019-10-04_02-39-17.gif

PS it doesn’t exhibit any bad behavior when docked :slight_smile: not saying it’s proper UX but doesn’t blow!

1 Like

mmm, i’m getting SizeToContentRoot to null…
at:
var root = SizeToContentRoot;

What is the context exactly ?

I have created a cs script DecoratorResizableEditorWindow with your content.
I have created as the same place 2 files:
DecoratorResizableEditorWindow.uss
DecoratorResizableEditorWindow.uxml

in uxml:

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    <ui:VisualElement name="rootElement" style="min-width: 20px; min-height: 20px;">
        <Style src="DecoratorResizableEditorWindow.uss" />
        <ui:Button text="Button" />
        <ui:Button text="Button" />
        <ui:Button text="Button" />
    </ui:VisualElement>
</ui:UXML>

and in uss:

#rootElement
{
min-width: 20px;
min-height: 20px;
}

What is wrong with what I have done ?

Thanks !

I’m also interested in this topic, because I’m trying to make a generic popup window with UI Toolkit that fits the size of its content, but so far I’m pretty lost.