Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"GUID:f1a38fcd7e85047c5a86331a3e37ddfc",
"GUID:45d3617c96fa64d3e95383450e67f25a",
"GUID:a2d87883023de4cf59b7d1962d5dd8aa",
"GUID:569bba870e5e64d0d94144057b927001",
"GUID:527f26a36b5043c2bd4d4036d04cd76d",
"GUID:94d8abc693f543a691a4645a5ff42e5c",
"GUID:aa7cf56cc5f074e57ba35272b877e116",
Expand Down
1 change: 1 addition & 0 deletions Assets/Tests/PlayMode/UnityCLILoop.Tests.PlayMode.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"GUID:45d3617c96fa64d3e95383450e67f25a",
"GUID:527f26a36b5043c2bd4d4036d04cd76d",
"GUID:a2d87883023de4cf59b7d1962d5dd8aa",
"GUID:569bba870e5e64d0d94144057b927001",
"GUID:aa7cf56cc5f074e57ba35272b877e116",
"GUID:dc412b2cd951d452b8dcec99c3adeb19",
"GUID:3b7aa330d87dc452293296dd5830859b",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Common.InputRecording.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Common.UIToolkit.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.ReplayInput.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Screenshot.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.SimulateMouseUi.Editor")]
Expand Down
8 changes: 8 additions & 0 deletions Packages/src/Editor/FirstPartyTools/Common/UIToolkit.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System.Runtime.CompilerServices;

// Only the tests see these internals: tools use this module through its entry point alone, and the
// compiler keeps them away from the parts behind it.
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor")]
[assembly: InternalsVisibleTo("UnityCLILoop.Tests.PlayMode")]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Walks the documents of the gathered panels and lists their reachable controls in top-left Game View
/// pixels, skipping controls that pointer input could not reach.
/// </summary>
internal static class UIToolkitControlCollector
{
// Expects the panels' layouts to be settled already, because bounds and picking read them.
internal static List<UIToolkitScreenControl> Collect(
List<UIToolkitScreenPanels.PanelEntry> panels,
Func<Rect, IReadOnlyList<Vector2>> probePointsForTopLeftBounds)
{
List<UIToolkitScreenControl> controls = new List<UIToolkitScreenControl>();
foreach (UIToolkitScreenPanels.PanelEntry entry in panels)
{
if (entry.Mapping == null)
{
continue;
}

VisitContext context = new VisitContext(
entry,
entry.Mapping,
UIToolkitScreenPanels.CreateFrontPickers(panels, entry),
point => UIToolkitScreenPanels.PickInPanel(entry.Panel, point),
probePointsForTopLeftBounds,
controls);
foreach (UIDocument document in entry.Documents)
{
Visit(document.rootVisualElement, document, context);
}
}

return controls;
}

private static void Visit(VisualElement element, UIDocument document, VisitContext context)
{
// A child can still resolve to Flex under a display:none parent, so the whole subtree stops here.
if (element.resolvedStyle.display == DisplayStyle.None)
{
return;
}

string? type = UIToolkitElementClassifier.Classify(element);
if (type != null)
{
TryAdd(element, type, document, context);

// The parts inside a control (the slider and buttons of a Scroller, for example) are not
// separate controls.
return;
}

// hierarchy, not Children(): Children() lists only the content container, which hides a
// ScrollView's scrollers.
for (int index = 0; index < element.hierarchy.childCount; index++)
{
Visit(element.hierarchy[index], document, context);
}
}

private static void TryAdd(VisualElement element, string type, UIDocument document, VisitContext context)
{
if (!element.enabledInHierarchy || element.resolvedStyle.visibility == Visibility.Hidden)
{
return;
}

Rect worldBound = element.worldBound;
if (!IsFiniteNonEmpty(worldBound))
{
return;
}

Vector2 topLeftMin = context.Mapping.PanelToScreenTopLeft(new Vector2(worldBound.xMin, worldBound.yMin));
Vector2 topLeftMax = context.Mapping.PanelToScreenTopLeft(new Vector2(worldBound.xMax, worldBound.yMax));
Rect topLeftBounds = Rect.MinMaxRect(topLeftMin.x, topLeftMin.y, topLeftMax.x, topLeftMax.y);

Vector2? reachable = UIToolkitReachabilityProbe.FindReachablePoint(
context.ProbePointsForTopLeftBounds(topLeftBounds), element, context.FrontPickers, context.OwnPicker);
if (reachable == null)
{
return;
}

VisualElement? parent = element.hierarchy.parent;
context.Controls.Add(new UIToolkitScreenControl(
UIToolkitElementPath.GetDisplayName(element),
UIToolkitElementPath.Build(document, element),
type,
topLeftBounds,
reachable.Value,
Mathf.FloorToInt(context.Entry.SortingOrder),
parent == null ? 0 : parent.hierarchy.IndexOf(element)));
}

private static bool IsFiniteNonEmpty(Rect rect)
{
return float.IsFinite(rect.x)
&& float.IsFinite(rect.y)
&& float.IsFinite(rect.width)
&& float.IsFinite(rect.height)
&& rect.width > 0f
&& rect.height > 0f;
}

private sealed class VisitContext
{
internal VisitContext(
UIToolkitScreenPanels.PanelEntry entry,
UIToolkitScreenMapping mapping,
List<Func<Vector2, VisualElement?>> frontPickers,
Func<Vector2, VisualElement?> ownPicker,
Func<Rect, IReadOnlyList<Vector2>> probePointsForTopLeftBounds,
List<UIToolkitScreenControl> controls)
{
Entry = entry;
Mapping = mapping;
FrontPickers = frontPickers;
OwnPicker = ownPicker;
ProbePointsForTopLeftBounds = probePointsForTopLeftBounds;
Controls = controls;
}

internal UIToolkitScreenPanels.PanelEntry Entry { get; }
internal UIToolkitScreenMapping Mapping { get; }
internal List<Func<Vector2, VisualElement?>> FrontPickers { get; }
internal Func<Vector2, VisualElement?> OwnPicker { get; }
internal Func<Rect, IReadOnlyList<Vector2>> ProbePointsForTopLeftBounds { get; }
internal List<UIToolkitScreenControl> Controls { get; }
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#nullable enable
using System.Collections.Generic;
using UnityEngine.UIElements;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Names UI Toolkit elements the way annotations report them: the owning document's GameObject path,
/// a '#', then the element chain below the document root.
/// </summary>
internal static class UIToolkitElementPath
{
private const char PATH_SEPARATOR = '#';
private const string CHAIN_SEPARATOR = "/";

internal static string Build(UIDocument document, VisualElement element)
{
return GameObjectPathUtility.GetFullPath(document.gameObject) + PATH_SEPARATOR
+ BuildElementChain(document.rootVisualElement, element);
}

internal static string GetDisplayName(VisualElement element)
{
return string.IsNullOrEmpty(element.name) ? element.GetType().Name : element.name;
}

// Lists the elements from just below the document root down to the element, so the root itself, which
// has no name of its own, is left out.
private static string BuildElementChain(VisualElement root, VisualElement element)
{
List<string> segments = new List<string>();
VisualElement? current = element;
while (current != null && current != root)
{
segments.Add(GetDisplayName(current));
current = current.hierarchy.parent;
}

segments.Reverse();
return string.Join(CHAIN_SEPARATOR, segments);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#nullable enable
using UnityEngine;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// One interactive UI Toolkit control on screen, in top-left Game View pixels.
/// </summary>
public sealed class UIToolkitScreenControl
{
public UIToolkitScreenControl(
string name,
string path,
string type,
Rect topLeftBounds,
Vector2 topLeftReachablePoint,
int sortingOrder,
int siblingIndex)
{
Name = name;
Path = path;
Type = type;
TopLeftBounds = topLeftBounds;
TopLeftReachablePoint = topLeftReachablePoint;
SortingOrder = sortingOrder;
SiblingIndex = siblingIndex;
}

/// <summary>The element's name, or its type name when it has none.</summary>
public string Name { get; }

/// <summary>The owning document's GameObject path, '#', and the element chain below the document root.</summary>
public string Path { get; }

/// <summary>The kind of control, such as "Button", "Toggle" or "Slider".</summary>
public string Type { get; }

public Rect TopLeftBounds { get; }

/// <summary>A point inside the bounds where pointer input reaches this control.</summary>
public Vector2 TopLeftReachablePoint { get; }

/// <summary>The panel's sorting order, rounded down.</summary>
public int SortingOrder { get; }

/// <summary>The control's index among its parent's children.</summary>
public int SiblingIndex { get; }
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Entry point for the UI Toolkit controls shown over the Game View on screen-space panels. Every point and
/// rectangle is in Game View pixels with the origin at the top left, the space screenshot annotations and
/// simulated pointer input use.
/// </summary>
public static class UIToolkitScreenElements
{
/// <summary>
/// Returns the enabled, visible controls that pointer input can reach, each with a point where it does.
/// Controls inside another control (the buttons of a scrollbar, for example) are not listed separately.
/// </summary>
/// <param name="probePointsForTopLeftBounds">
/// Gives the candidate points to try for a control's bounds, in the order to try them. The first point
/// that is not covered by a panel in front or by another element becomes the reachable point.
/// </param>
public static IReadOnlyList<UIToolkitScreenControl> CollectControls(
Func<Rect, IReadOnlyList<Vector2>> probePointsForTopLeftBounds)
{
if (probePointsForTopLeftBounds == null)
{
throw new ArgumentNullException(nameof(probePointsForTopLeftBounds));
}

List<UIToolkitScreenPanels.PanelEntry> panels = UIToolkitScreenPanels.Gather();
UIToolkitScreenPanels.SettleLayouts(panels);
return UIToolkitControlCollector.Collect(panels, probePointsForTopLeftBounds);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading