Handling Screen.safeArea in UIDocument

Hi,

I want to handle safeArea in my UIDocument.
I’ve subscribed to GeometryChangedEvent and want to adjust selected elements paddings based on current safeArea size.

How can I convert Screen.safeArea size to a padding size taking Panel Settings screen mode into account ?

Hello, sorry it took so long to get to your question but if you haven’t found a solution yet try looking at RuntimePanelUtils, it has a few helpful methods to translate coordinates to and from panels like ScreenToPanel.

Hope that helps!

Oh, I didn’t know that class.
Thanks! It seems to be working.

Here is how I made it:

public struct RectOffsetFloat
{
    public RectOffsetFloat(float left, float right, float top, float bottom)
    {
        Left = left;
        Right = right;
        Top = top;
        Bottom = bottom;
    }

    public float Left { get; private set; }
    public float Right { get; private set; }
    public float Top { get; private set; }
    public float Bottom { get; private set; }
}
public static RectOffsetFloat GetSafeArea(this IPanel panel)
{
    var safeLeftTop = RuntimePanelUtils.ScreenToPanel(
        panel,
        new Vector2(Screen.safeArea.xMin, Screen.height - Screen.safeArea.yMax)
    );
    var safeRightBottom = RuntimePanelUtils.ScreenToPanel(
        panel,
        new Vector2(Screen.width - Screen.safeArea.xMax, Screen.safeArea.yMin)
    );

    return new RectOffsetFloat(
        safeLeftTop.x,
        safeRightBottom.x,
        safeLeftTop.y,
        safeRightBottom.y
    );
}

and how I use it:

// Update safe area related paddings
var safeAreaRectOffset = document.rootVisualElement.panel.GetSafeArea();
safeAreaElement.style.paddingLeft = safeAreaRectOffset.Left;
safeAreaElement.style.paddingRight = safeAreaRectOffset.Right;
safeAreaElement.style.paddingTop = safeAreaRectOffset.Top;

Cheers :slight_smile:

5 Likes

I’ve created a custom Visual Element, so you can use it as Control in the UI Builder
https://github.com/PregOfficial/UI-Toolkit-SafeArea

15 Likes
using System;
using UnityEngine;
using UnityEngine.UIElements;


public static class VisualElementExtensions
{       
    public static (Vector2, Vector2) SafeArea(this VisualElement elm)
    {
        var safeArea = Screen.safeArea;
        var leftTop = RuntimePanelUtils.ScreenToPanel(elm.panel,
            new Vector2(safeArea.xMin, Screen.height - safeArea.yMax));
        var rightBottom = RuntimePanelUtils.ScreenToPanel(elm.panel,
            new Vector2(Screen.width - safeArea.xMax, safeArea.yMin));
        return (leftTop, rightBottom);
    }
   
    public static void SafeAreaMargin(this VisualElement elm)
    {
        var (leftTop, rightBottom) = elm.SafeArea();
        elm.style.marginLeft = leftTop.x;
        elm.style.marginTop = leftTop.y;
        elm.style.marginBottom = rightBottom.y;
        elm.style.marginRight = rightBottom.x;
    }

    public static void SafeAreaPadding(this VisualElement elm)
    {
        var (leftTop, rightBottom) = elm.SafeArea();
        elm.style.paddingLeft = leftTop.x;
        elm.style.paddingTop = leftTop.y;
        elm.style.paddingBottom = rightBottom.y;
        elm.style.paddingRight = rightBottom.x;
    }
}
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;

[RequireComponent(typeof(UIDocument))]
public class AffiseDemo : MonoBehaviour
{
    private VisualElement? _safeArea;

    private void Start()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;
        _safeArea = root.Q<VisualElement>("safe-area");
        _safeArea.RegisterCallback<GeometryChangedEvent>(LayoutChanged);
    }

    private void OnDestroy()
    {
        _safeArea?.UnregisterCallback<GeometryChangedEvent>(LayoutChanged);
    }

    private void LayoutChanged(GeometryChangedEvent e)
    {
        (e.target as VisualElement)?.SafeAreaMargin();
    }
}
2 Likes

The best solution is this: [quote=“PregOfficial, post:4, topic: 820797, username:PregOfficial”]
https://github.com/PregOfficial/UI-Toolkit-SafeArea
[/quote]

I have made some changes to have in account the padding option and to be compatible with Unity 6 and forward:

SafeArea

using System;
using UnityEngine;
using UnityEngine.UIElements;

/*
    Disclaimer: Minimum version compatible -> Unity 6

    TODO: Make non-editable the Spacing property in the UI Builder
*/

[UxmlElement("SafeArea")] // Cannot contain special characters
public partial class SafeAreaManager : VisualElement
{
    [UxmlAttribute]
    public SafeAreaSpacing SafeAreaType { get; set; }

    public enum SafeAreaSpacing
    {
        Margin, // Makes visible the screen behind or the camera background
        Padding // Makes visible the background of the visual element (SafeArea)
    }


    public SafeAreaManager() // Constructors are called on Awake()
    {
        style.flexGrow = 1;
        style.flexShrink = 1;
        RegisterCallback<GeometryChangedEvent>(LayoutChanged);
    }

    private void LayoutChanged(GeometryChangedEvent geomentryChangedEvent)
    {
        try
        {
            (Vector2 leftTop, Vector2 rightBottom) = SafeArea();

            switch (SafeAreaType)
            {
                case SafeAreaSpacing.Margin:
                    style.marginLeft = leftTop.x;
                    style.marginTop = leftTop.y;
                    style.marginRight = rightBottom.x;
                    style.marginBottom = rightBottom.y;
                    break;

                case SafeAreaSpacing.Padding:
                    style.paddingLeft = leftTop.x;
                    style.paddingTop = leftTop.y;
                    style.paddingRight = rightBottom.x;
                    style.paddingBottom = rightBottom.y;
                    break;

                default:
                    Debug.LogWarning("Not implemented");
                    break;
            }
        }
        catch (InvalidCastException){} // Throwed each time we safe the in the UI Builder
    }

    private (Vector2, Vector2) SafeArea()
    {
        Rect safeArea = Screen.safeArea;

        Vector2 leftTop = RuntimePanelUtils.ScreenToPanel(this.panel,
            new Vector2(safeArea.xMin, Screen.height - safeArea.yMax));

        Vector2 rightBottom = RuntimePanelUtils.ScreenToPanel(this.panel,
            new Vector2(Screen.width - safeArea.xMax, safeArea.yMin));

        return (leftTop, rightBottom);
    }
}

@XzenD made an alternative that is also great.

I have added to his version an extra option to shrink the content instead of displace it (SafeAreaMarginInsideViewport):

SafeAreaMarginInsideViewport

    private (Vector2, Vector2) SafeArea(VisualElement visualElement)
    {
        Rect safeArea = Screen.safeArea;

        Vector2 leftTop = RuntimePanelUtils.ScreenToPanel(visualElement.panel,
            new Vector2(safeArea.xMin, Screen.height - safeArea.yMax));

        Vector2 rightBottom = RuntimePanelUtils.ScreenToPanel(visualElement.panel,
            new Vector2(Screen.width - safeArea.xMax, safeArea.yMin));

        return (leftTop, rightBottom);
    }

    private void SafeAreaMargin(VisualElement visualElment) // This push the area (always from the zero x & y points). So the opposite part will be hidden (taken out of the screen) if your are using the 100% of the space.
    {
        (Vector2 leftTop, Vector2 rightBottom) = visualElment.SafeArea();
        visualElment.style.marginLeft = leftTop.x;
        visualElment.style.marginTop = leftTop.y;
        visualElment.style.marginBottom = rightBottom.y;
        visualElment.style.marginRight = rightBottom.x;
    }

    private void SafeAreaMarginInsideViewport(VisualElement visualElement) // Do not call via GeometryChangedEvent or you will get a recursive error. I you do it, make sure that in the function called, the first thing you do is unregister the GeometryChangedEvent (VisualElement.UnregisterCallback<GeometryChangedEvent>(FunctionCalled))
    {
        // Reset the size to 100% width and height
        visualElement.style.width = new Length(100f, LengthUnit.Percent);
        visualElement.style.height = new Length(100f, LengthUnit.Percent);

        // Force update layout to get the correct resolved size
        visualElement.MarkDirtyRepaint();

        visualElement.panel?.visualTree?.schedule.Execute(() =>
        {
            // After layout pass, we have correct resolved sizes
            (Vector2 leftTop, Vector2 rightBottom) = visualElement.SafeArea();

            // Apply the safe area margins
            visualElement.style.marginLeft = leftTop.x;
            visualElement.style.marginTop = leftTop.y;
            visualElement.style.marginBottom = rightBottom.y;
            visualElement.style.marginRight = rightBottom.x;

            // Adjust the size of the visual element to be inside the safe area
            float newWidth = visualElement.resolvedStyle.width - leftTop.x - rightBottom.x;
            float newHeight = visualElement.resolvedStyle.height - leftTop.y - rightBottom.y;

            visualElement.style.width = new Length(newWidth, LengthUnit.Pixel);
            visualElement.style.height = new Length(newHeight, LengthUnit.Pixel);
        }).StartingIn(0);
    }

    private void SafeAreaPadding(VisualElement visualElement)
    {
        var (leftTop, rightBottom) = visualElement.SafeArea();
        visualElement.style.paddingLeft = leftTop.x;
        visualElement.style.paddingTop = leftTop.y;
        visualElement.style.paddingBottom = rightBottom.y;
        visualElement.style.paddingRight = rightBottom.x;
    }

I am having some very weird behaviour when running this in the UI Builder, even if the UI builder’s resolution matches the game view screen pixels. I can get it to work in the game view, but not in both. How do you deal with this?

As far as I know, It only works with the Simulator:
Simulator config 9895644--1428792--upload_2024-6-18_10-43-21.png

In the UI Builder you are not simulating a phone or special screen. You just have a canvas.

thanks a lot! so so useful

1 Like

Updated the script with the new uxml attributes

SafeAreaManager.cs (1016 Bytes)