Skip to content

Search.Spatial

ODD Games edited this page Oct 14, 2026 · 5 revisions

Search Spatial Filters

Filter elements by where they appear on screen, and read element positions.

Overview

Spatial filters are useful for:

  • Finding elements in specific UI regions (top bar, bottom nav, …)
  • Skipping items scrolled out of a list's viewport
  • Disambiguating identical elements by screen location

Positions are taken from each element's screen-space centre, which works for Overlay, Camera and World Space canvases and for 3D objects with a Renderer or Collider.

Filters

InRegion (Predefined)

Keep elements whose centre falls in one cell of a 3×3 screen grid.

await Click(Type<Button>().InRegion(ScreenRegion.TopRight));
await Click(Name("*Item*").InRegion(ScreenRegion.Center));
┌─────────────┬─────────────┬─────────────┐
│  TopLeft    │  TopCenter  │  TopRight   │
├─────────────┼─────────────┼─────────────┤
│ MiddleLeft  │   Center    │ MiddleRight │
├─────────────┼─────────────┼─────────────┤
│ BottomLeft  │BottomCenter │ BottomRight │
└─────────────┴─────────────┴─────────────┘

InRegion (Custom Bounds)

Keep elements whose centre lies inside normalized bounds (0–1, inclusive). Values outside 0–1 throw ArgumentOutOfRangeException.

// Left half of the screen
await Click(Type<Button>().InRegion(0f, 0f, 0.5f, 1f));

// Top quarter
await Click(Type<Button>().InRegion(0f, 0.75f, 1f, 1f));

// Custom rectangle
await Click(Type<Slider>().InRegion(0.2f, 0.3f, 0.8f, 0.7f));

Parameters: InRegion(xMin, yMin, xMax, yMax) where x runs left (0) to right (1) and y runs bottom (0) to top (1).

Visible

Keep elements whose centre is on screen and, if they sit inside a ScrollRect, whose rect overlaps that ScrollRect's viewport.

await Click(Type<Button>().Visible().First());
var visibleItems = await FindAll<RectTransform>(Name("Item*").Visible());

Visible() does not raycast, so it doesn't detect elements covered by other UI. Use RequiresReceiver for that.

RequiresReceiver

Keep elements where an EventSystem raycast at the element's screen centre hits a GameObject implementing one of the given handler interfaces. Use it to make sure a control is actually reachable rather than covered by something else.

using UnityEngine.EventSystems;

// Buttons that something will actually receive a click on
await Click(Type<Button>().RequiresReceiver(typeof(IPointerClickHandler)));

// Draggable elements
var draggables = await FindAll<RectTransform>(
    Name("Item*").RequiresReceiver(typeof(IDragHandler), typeof(IBeginDragHandler))
);

// Inspect the receivers found by the last check
var search = new Search().Name("MyButton").RequiresReceiver(typeof(IPointerClickHandler));
var element = await search.Find();
foreach (var receiver in search.Receivers)
{
    Debug.Log($"Found receiver: {receiver.name}");
}
Interface Use Case
IPointerClickHandler Click targets
IPointerUpHandler Click/tap release
IPointerDownHandler Press/hold
IDragHandler Drag operations
IBeginDragHandler Drag start
IScrollHandler Scroll targets
ISelectHandler Selection/focus

When a RequiresReceiver search fails, the failure message lists the receivers found at the element's position.

Examples

Navigation Bars

await Click(Name("*Back*|*Close*").InRegion(ScreenRegion.TopLeft));
await Click(Text("Next|Continue").InRegion(ScreenRegion.BottomRight));
await Click(Texture("settings_*").InRegion(ScreenRegion.TopRight));

Scroll Views

await Click(Name("ListItem*").Visible().First());

var visible = await FindAll<RectTransform>(Name("Item*").Visible());
Debug.Log($"{visible.Count()} items currently visible");

// Scroll until the item is in view, then click it
await ScrollTo(Name("ItemList"), Text("Target Item"));
await Click(Text("Target Item").Visible());

// Or in one call
await ScrollToAndClick(Name("ItemList"), Text("Target Item"));

Disambiguation

await Click(Text("Submit").InRegion(ScreenRegion.BottomCenter));
await Click(Name("CloseButton").InRegion(ScreenRegion.TopRight).First());

Combining with Other Filters

await Click(
    Type<Button>()
        .Visible()
        .Interactable()
        .InRegion(ScreenRegion.Center)
        .First()
);

await ClickSlider(
    Type<Slider>()
        .HasAncestor("SettingsPanel")
        .InRegion(0f, 0f, 0.5f, 1f),
    0.75f
);

Layout Checks

// Navigation stays in the bottom 15% of the screen
var navButtons = await FindAll<Button>(
    Type<Button>().HasAncestor("BottomNav").InRegion(0f, 0f, 1f, 0.15f)
);
Assert.Greater(navButtons.Count(), 0, "Navigation should be at bottom");

Position Helpers

Read positions of found elements with the static helpers on Search, or await a search's screen position directly:

Member Returns
await search.GetScreenPosition(float searchTime = 10f) Vector2 screen centre of the first match; throws TimeoutException if none
Search.GetScreenCenter(GameObject go) Vector2 screen-space centre
Search.GetScreenBounds(GameObject go) Rect screen-space bounds
Search.GetWorldBounds(GameObject go) Bounds from the Renderer, else Collider, else zero-size at the transform

Comparisons are plain Vector2/Rect maths on top of these:

var header = await Name("Header").Find();
var footer = await Name("Footer").Find();

// Header above footer (screen y grows upwards)
Assert.Greater(Search.GetScreenCenter(header).y, Search.GetScreenCenter(footer).y);

// Label and input on the same row (within 5 px)
var labelPos = await Name("Label").GetScreenPosition();
var inputPos = await Name("InputField").GetScreenPosition();
Assert.Less(Mathf.Abs(labelPos.y - inputPos.y), 5f);

// Container fully contains its child
var container = Search.GetScreenBounds(await Name("Container").Find());
var child = Search.GetScreenBounds(await Name("ChildElement").Find());
Assert.IsTrue(container.Contains(child.min) && container.Contains(child.max));

// 3D overlap
var a = Search.GetWorldBounds(await Name("Crate").IncludeOffScreen().Find());
var b = Search.GetWorldBounds(await Name("Truck").IncludeOffScreen().Find());
Assert.IsTrue(a.Intersects(b));

Related Pages

Clone this wiki locally