RealDocs

UMG/Slate Coordinate Spaces

Almost every UMG coordinate bug comes from one of two traps. First, UE has two different things called "screen": Slate events and geometry work in desktop screen space (OS pixels, rooted at the virtual-desktop origin — the primary monitor's top-left on Windows), while PlayerController and the ScreenTo* conversion functions mean game-screen space (render-resolution pixels, origin at the top-left of the game viewport). Second, DPI scale silently multiplies or divides coordinates depending on which function you call and which flag you leave at its default.

This page gives you the mental model first, then a cookbook of the conversions you actually need — "is the mouse over this?", "what did I click on?", "put this widget at a world position" — each verified against the engine source (FGeometry, USlateBlueprintLibrary, UWidgetLayoutLibrary, UE 5.8).

1. The Mental Model

Four spaces, nested from the OS desktop down to a single widget. The picture below shows all of them at once — one point P inside a widget, with its coordinates in every space. Windowed PIE on a 4K monitor, viewport scale 2.0; all numbers are self-consistent.

OS virtual desktop — Slate "Absolute" space secondary monitor x is negative here primary monitor (3840 × 2160) desktop (0, 0) +X +Y (down!) game / editor window @ desktop (640, 300) UE Editor — PIE editor chrome game viewport — (0, 0) for px AND scaled = desktop (800, 520) 2560×1440 px = 1280×720 scaled (scale 2.0) local (0, 0) widget @ viewport (400, 150) P Point P in all four spaces Desktop / "Absolute" (1780, 900) FGeometry, pointer events, OS mouse position Game-screen px (980, 380) = desktop − viewport origin (800, 520) Viewport (scaled) (490, 190) = game-screen ÷ viewport scale (2.0) Widget local (90, 40) = viewport − (400, 150), via AbsoluteToLocal Same point, four different numbers. Every bug on this page is one row handed to a function that expected a different row. Conversions between rows: see the cheat sheet ↓

Windowed PIE on a 4K monitor: window at desktop (640, 300), viewport panel offset (160, 220) inside it, viewport scale 2.0. In fullscreen the window and chrome offsets collapse to zero — which is exactly why coordinate bugs hide on dev machines.

Outermost
Desktop space — what Slate calls "Absolute"
✏️ OS pixels. Origin = the OS virtual-desktop origin (the primary monitor's top-left on Windows); spans all monitors, so coordinates can be negative. Every FGeometry "absolute" coordinate, every FPointerEvent::GetScreenSpacePosition(), and GetMousePositionOnPlatform() live here. Yes — "Absolute" means the desktop, not the game window and not the viewport.
Game-screen space (viewport pixels)
✏️ Origin = top-left of the game viewport; units are render-resolution pixels. PlayerController::GetMousePosition, ProjectWorldLocationToScreen / DeprojectScreenPositionToWorld, every "Screen Position" input and every "Pixel Position" output in the UMG libraries. The window border and (in PIE) the editor chrome are already excluded.
÷ viewport (DPI) scale
Viewport space (scaled)
✏️ Same origin as game-screen space, but in DPI-independent UMG units: game-screen pixels ÷ GetViewportScale(). This is the space of GetMousePositionOnViewport() and of widget placement — a full-viewport Canvas Panel is laid out in these units.
via a widget's FGeometry
Widget local space
✏️ Per-widget coordinates in Slate units. Origin = the widget's own top-left. Values outside 0..Size are valid — they describe points beyond the widget's edges. Convert in and out with FGeometry::AbsoluteToLocal / LocalToAbsolute (which go through desktop space).
📝 Paint space, the fifth space: render transforms (SetRenderTransformAngle, SetRenderScale…) visually warp a widget without changing its layout. Contrary to popular belief, AbsoluteToLocal, LocalToAbsolute, IsUnderLocation, and Slate hit-testing all include render transforms — a rotated button is clickable exactly where you see it. What ignores render transforms is layout: desired size, slot arrangement, and GetLayoutBoundingRect(). Details in §4.5.
✅ The one rule that prevents the most bugs: SetPositionInViewport(Pos, bRemoveDPIScale) divides by the DPI scale when the flag is true — and true is the default. So: pixel-space input (game-screen / "Pixel Position") → use the default; scaled input ("Viewport Position", GetMousePositionOnViewport, ProjectWorldLocationToWidgetPosition) → pass false. Mixing these up divides twice and your widget drifts toward the top-left on any DPI scale ≠ 1.

2. Conversion Cheat Sheet

Find your starting point in the first column. All functions are callable from both C++ and Blueprint unless noted.

You have You want Use
Mouse cursor "Is it over widget X?" IsUnderLocation(Geom, GetMousePositionOnPlatform()) — R1
Pointer event (OnMouseDown…) Point inside the widget MyGeometry.AbsoluteToLocal(Event.GetScreenSpacePosition()) — R2
Mouse cursor Widget-placeable position GetMousePositionOnViewport() → SetPositionInViewport(…, false) — R3
3D world location Widget-placeable position ProjectWorldLocationToWidgetPosition → SetPositionInViewport(…, false) — R4
A point in some widget Viewport position (place another widget there) LocalToViewport → Viewport Position + SetPositionInViewport(…, false) — R5
A point in some widget World-space ray (trace from UI) LocalToViewport → Pixel Position → DeprojectScreenPositionToWorld — R6
Game-screen pixels (GetMousePosition, ProjectWorldLocationToScreen) Widget local point ScreenToWidgetLocal (leave bIncludeWindowPosition false) — R7
Game-screen pixels Viewport (scaled) position ScreenToViewport
Desktop / absolute point Widget local point Geometry.AbsoluteToLocal / USlateBlueprintLibrary::AbsoluteToLocal
Desktop / absolute point Viewport position (pixel + scaled) AbsoluteToViewport
Widget local point Desktop / absolute point Geometry.LocalToAbsolute
A distance or size (not a position) Same, in another space Scalar_* / Vector_* variants (no translation applied)
Mouse cursor Raw desktop coordinates GetMousePositionOnPlatform()

3. Recipes

R1 — Is the mouse over this widget?

The cursor position from GetMousePositionOnPlatform() is desktop-space — exactly the space a widget's cached geometry lives in, so you can test it directly with IsUnderLocation. Render transforms are respected: a rotated widget tests against its rotated bounds. If the widget receives pointer events anyway, IsHovered() is simpler.

const FGeometry& Geom = MyWidget->GetCachedGeometry();
const FVector2D MouseDesktop = UWidgetLayoutLibrary::GetMousePositionOnPlatform();

if (USlateBlueprintLibrary::IsUnderLocation(Geom, MouseDesktop))
{
    // cursor is over the widget (render transforms respected)
}

// Simplest alternative when the widget is hit-testable anyway:
if (MyWidget->IsHovered()) { /* ... */ }
Event Tick → Get Mouse Position on Platform → Is Under Location → Branch Blueprint
Event Tick Get Mouse Position on Platform Widget Layout Library Return Value Branch Condition Condition True False Is Under Location Slate Blueprint Library Geometry Absolute Coordinate Absolute Coordinate Return Value Get Cached Geometry Target is Widget Target Return Value

R2 — Where inside the widget did the click land?

Pointer events hand you both pieces: the widget's geometry and the event position. Despite the name, GetScreenSpacePosition() is desktop-space (Slate "absolute"), so it pairs directly with AbsoluteToLocal. No viewport, no DPI, no window offset — the geometry's transform handles all of it, including render transforms.

FReply UMyWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
    // "ScreenSpacePosition" is desktop-space — same space as InGeometry's absolute side
    const FVector2D LocalClick = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());

    // Normalised 0..1 position inside the widget (handy for sliders, colour pickers):
    const FVector2D UV = LocalClick / InGeometry.GetLocalSize();

    return FReply::Handled();
}
On Mouse Button Down → Get Screen Space Position → Absolute to Local → store the local click position Blueprint
On Mouse Button Down My Geometry Mouse Event Get Screen Space Position Target is Pointer Event In Event Return Value Absolute to Local (Coordinates) Slate Blueprint Library Geometry Absolute Coordinate Return Value Set Click Local Pos Click Local Pos Click Local Pos

R3 — Make a widget follow the mouse

GetMousePositionOnViewport() returns a scaled viewport position, so tell SetPositionInViewport not to divide again: pass bRemoveDPIScale = false. The pixel-space route via PlayerController works too — then you keep the default.

// Scaled route — flag must be false:
FVector2D ViewportPos = UWidgetLayoutLibrary::GetMousePositionOnViewport(this);
CursorWidget->SetPositionInViewport(ViewportPos, /*bRemoveDPIScale=*/ false);

// Pixel route — default flag (true) divides for you:
float PixelX, PixelY;
if (GetOwningPlayer()->GetMousePosition(PixelX, PixelY))
{
    CursorWidget->SetPositionInViewport(FVector2D(PixelX, PixelY));
}
Event Tick → Get Mouse Position on Viewport → Set Position In Viewport (Remove DPI Scale = FALSE — the input is already scaled) Blueprint
Event Tick Get Mouse Position on Viewport Widget Layout Library Return Value Set Position In Viewport Target is User Widget Target Position Remove DPI Scale false false Cursor Widget Cursor Widget Cursor Widget

R4 — Pin a widget to a world location (nameplates, pings, markers)

ProjectWorldLocationToWidgetPosition projects to game-screen pixels internally, then divides by the DPI scale — its output is already scaled viewport space, even though the output pin is labelled Screen Position (the implementation runs its result through ScreenToViewport). So again: bRemoveDPIScale = false. The return value is false when the point is behind the camera — hide the widget instead of placing it. Use the …WithDistance variant if you also want the depth for distance-based scaling.

FVector2D WidgetPos;
const bool bOnScreen = UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(
    PC, TargetActor->GetActorLocation(), WidgetPos, /*bPlayerViewportRelative=*/ false);

if (bOnScreen)
{
    // Output is already DPI-scaled viewport space — don't let it be divided again:
    Nameplate->SetPositionInViewport(WidgetPos, /*bRemoveDPIScale=*/ false);
}
Nameplate->SetVisibility(bOnScreen ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden);
Event Tick → Project World Location to Widget Position (pure) → Set Position In Viewport (Remove DPI Scale = FALSE) Blueprint
Event Tick Set Position In Viewport Target is User Widget Target Position Remove DPI Scale false false Project World Location to Widget Position Widget Layout Library Player Controller World Location Player Viewport Relative false false Screen Position Return Value Get Player Controller Player Index 0 0 Return Value Target World Location Target World Location Target World Location Nameplate Widget Nameplate Widget Nameplate Widget
📝 For widgets added with AddToPlayerScreen (split-screen), pass bPlayerViewportRelative = true so the result is relative to that player's sub-rect. See §7.

R5 — Place widget B at widget A's position

LocalToViewport takes a point in widget A's local space and returns both flavours of viewport position. Use Viewport Position (+ flag false) for placement. If both widgets live in the same hierarchy you can skip the viewport entirely and hop through desktop space.

const FGeometry& AnchorGeom = AnchorWidget->GetCachedGeometry();

FVector2D PixelPos, ViewportPos;
USlateBlueprintLibrary::LocalToViewport(
    this, AnchorGeom, FVector2D::ZeroVector /* anchor's top-left */, PixelPos, ViewportPos);

Follower->SetPositionInViewport(ViewportPos, /*bRemoveDPIScale=*/ false);

// Widget → widget directly, no viewport round-trip:
FVector2D InTargetSpace = TargetGeom.AbsoluteToLocal(AnchorGeom.LocalToAbsolute(SomeLocalPoint));
Event Tick → Local to Viewport (pure) → Viewport Position → Set Position In Viewport (Remove DPI Scale = FALSE) Blueprint
Event Tick Set Position In Viewport Target is User Widget Target Position Remove DPI Scale false false Local to Viewport Slate Blueprint Library Geometry Local Coordinate Pixel Position Viewport Position Get Cached Geometry Target is Anchor Widget Target Return Value Make Vector 2D X 0.0 Y 0.0 Return Value Return Value Follower Widget Follower Widget Follower Widget

R6 — From a widget point into the world (trace from UI)

Going the other way — say, line-tracing from a custom crosshair widget — DeprojectScreenPositionToWorld expects game-screen pixels, so this is the one place you use the Pixel Position output instead:

FVector2D PixelPos, ViewportPos;
USlateBlueprintLibrary::LocalToViewport(this, CrosshairGeom, CrosshairLocalPoint, PixelPos, ViewportPos);

FVector WorldOrigin, WorldDirection;
if (PC->DeprojectScreenPositionToWorld(PixelPos.X, PixelPos.Y, WorldOrigin, WorldDirection))
{
    // trace along WorldDirection from WorldOrigin...
}

R7 — Game-screen pixels → widget local

When you have a coordinate in game-screen pixels — PlayerController::GetMousePosition, ProjectWorldLocationToScreen, hit results from a scene capture — ScreenToWidgetLocal maps it into a widget's local space. Leave bIncludeWindowPosition at its default (false) — see the gotchas for why setting it to true breaks this.

float MouseX, MouseY;
if (PC->GetMousePosition(MouseX, MouseY))   // game-screen pixels
{
    FVector2D LocalCoord;
    USlateBlueprintLibrary::ScreenToWidgetLocal(
        this, MyWidget->GetCachedGeometry(), FVector2D(MouseX, MouseY), LocalCoord);
    // LocalCoord is now in MyWidget's local space
}
Event Tick → Get Mouse Position (Player Controller, game-screen pixels) → Screen to Local (pure, Include Window Position = false) Blueprint
Event Tick Get Mouse Position Target is Player Controller Target Location X Location Y Return Value Set Local Under Mouse Local Under Mouse Local Under Mouse Get Player Controller Player Index 0 0 Return Value Make Vector 2D X Y Return Value Return Value Screen to Local Slate Blueprint Library Geometry Screen Position Include Window Position Include Window Position false false Local Coordinate Get Cached Geometry Target is Widget Target Return Value

4. The Spaces in Detail

4.1 Desktop space — Slate's "Absolute"

Slate builds every widget's geometry by accumulating transforms from the root down, and the root is placed at the window's desktop position: when a widget is painted, the engine appends the window-to-desktop transform and caches the result (SWidget::PaintPersistentState.DesktopGeometry, which is what GetCachedGeometry() / GetTickSpaceGeometry() return). So an "absolute" coordinate is measured from the OS virtual-desktop origin (the primary monitor's top-left on Windows), in OS pixels — the engine's own parameter name for it is AbsoluteDesktopCoordinate.

The comment in Geometry.h hedges — "Absolute coordinates could be either desktop or window space depending on what space the root of the widget hierarchy is in" — because during the paint traversal the root is the window. But everything you touch from game or widget code (cached geometry, pointer events, the mouse functions) is desktop-rooted. That's why Geometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()) just works, in every window mode, on every monitor, in PIE and standalone alike.

📝 Desktop coordinates span all connected monitors and can be negative (a monitor left of or above the primary). This is normal — the geometry transforms handle it. Don't clamp desktop coordinates before converting.

4.2 Game-screen space (viewport pixels)

The space gameplay code calls "screen": origin at the top-left of the game viewport, units are render-resolution pixels (FViewport size). In PIE the origin is the viewport panel inside the editor window — the chrome offset is already excluded, which is why game-screen code behaves identically in PIE and standalone.

  • APlayerController::GetMousePosition — mouse in game-screen pixels
  • ProjectWorldLocationToScreen / DeprojectScreenPositionToWorld
  • Every ScreenPosition input of USlateBlueprintLibrary (ScreenToWidgetLocal, ScreenToWidgetAbsolute, ScreenToViewport)
  • Every PixelPosition output (LocalToViewport, AbsoluteToViewport)
This is not the OS screen. Feeding GetMousePositionOnPlatform() (desktop coords) into ScreenToWidgetLocal gives garbage whenever the window isn't at desktop origin — and in PIE, always. For OS/desktop input, convert through absolute space instead (AbsoluteToViewport, Geometry.AbsoluteToLocal).

4.3 Viewport space (scaled)

Same origin as game-screen space, divided by the viewport scale (GetViewportScale() — see §6). This is the coordinate system UMG lays widgets out in: a full-screen Canvas Panel spans GetViewportSize() / GetViewportScale() of these units regardless of render resolution. Conversion functions that output a Viewport Position mean this space.

Output pin Space Feed it to
Pixel Position Game-screen pixels Deproject / line traces, render targets, SetPositionInViewport(pos) with the default flag
Viewport Position Viewport scaled Widget placement: SetPositionInViewport(pos, false), Canvas Panel slot positions

4.4 Widget local space

The coordinate system internal to one widget: origin at its own top-left, X right, Y down, in Slate units. (Size.X, Size.Y) (GetLocalSize()) is the bottom-right corner. Points outside 0..Size — including negative values — are valid; they describe positions beyond the widget's edges, which you'll see when converting a point that isn't over the widget, or doing drag-offset math.

Slate units are resolution-independent layout units, the same idea as CSS pixels or Android dp: pixels = slate_units × scale, where the scale accumulates in FGeometry::Scale. At 1× scale they equal pixels; on a 4K display with a 2× UI scale, 100 Slate units cover 200 pixels.

The cached geometry is populated by the widget's first tick/paint pass. In NativeConstruct, right after AddToViewport, or in a constructor, GetCachedGeometry() returns a zeroed struct (Size = (0,0)). Defer geometry reads to NativeTick or guard with if (Geom.GetLocalSize().X > 0.f).

4.5 Render transforms and paint space

A render transform (SetRenderTransform, SetRenderTransformAngle, SetRenderScale) rotates/scales/shears the widget visually without re-flowing layout. FGeometry tracks two accumulated transforms, and which one a function uses decides whether render transforms are "seen":

Uses the render transform (sees rotation/scale) Uses the layout transform (ignores it)
AbsoluteToLocal / LocalToAbsolute
IsUnderLocation (and Slate hit-testing generally)
GetRenderBoundingRect(), GetAbsoluteSize()
Scalar_* / Vector_* conversions
Slot arrangement & desired size (layout never re-flows)
GetLayoutBoundingRect()
AbsolutePosition / Scale members
GetAccumulatedLayoutTransform()

So a button rotated 45° is still clickable exactly where you see it, and AbsoluteToLocal on a click gives correct local coordinates inside the rotated frame. What does not change is layout: siblings don't move out of the way, and the layout bounding box stays axis-aligned at the pre-transform position.

📝 GetTickSpaceGeometry() and GetPaintSpaceGeometry() differ by root, not by transforms: tick-space geometry is desktop-rooted (use it with mouse positions and other widgets' geometry), paint-space geometry is window-rooted and refreshed during paint. For coordinate conversions, use GetTickSpaceGeometry() / GetCachedGeometry().

5. FGeometry Reference

FGeometry encodes a widget's position, size, and the two accumulated transforms. Get it from GetCachedGeometry() (or the geometry parameter of a paint/input callback) and pass it to conversion functions.

Member / Method Type Space What it gives you
AbsolutePosition FVector2f Desktop Layout-transform translation: the widget's top-left in desktop space (ignores render transforms)
Size / GetLocalSize() FVector2f Local (Slate units) Widget's width and height in Slate units
Scale float Accumulated layout scale (includes UMG DPI scale): pixels = slate_units × Scale
LocalToAbsolute(local) FVector2D Local → Desktop Via the accumulated render transform — includes render rotation/scale
AbsoluteToLocal(abs) FVector2D Desktop → Local Inverse render transform; result may be negative / outside bounds — that's valid
GetAbsolutePositionAtCoordinates(uv) FVector2D Desktop Normalised UV → absolute: (0,0)=top-left, (0.5,0.5)=centre, (1,1)=bottom-right
GetAbsoluteSize() FVector2D Desktop Size pushed through the render transform (≈ Size × Scale when no render transform)
GetLayoutBoundingRect() FSlateRect Desktop AABB via the layout transform — ignores render transforms
GetRenderBoundingRect() FSlateRect Desktop AABB enclosing the rendered (possibly rotated) widget — can be larger than layout
IsUnderLocation(abs) bool Desktop Point-in-widget test through the render transform — matches what you see
📝 You can copy an FGeometry and hold it, but the copy goes stale the next time the widget is laid out or moved. Fine within a frame; re-fetch each tick for anything longer.

6. DPI Scale: Who Multiplies What

UWidgetLayoutLibrary::GetViewportScale() is the UMG DPI scale: the project's DPI curve (Project Settings → User Interface) evaluated at the current viewport resolution, times the user's application scale. Notably it does not include the OS's native DPI scale — the game layer manager absorbs that before UMG sees any coordinates, so UMG code can treat the platform scale as 1.0.

float Scale = UWidgetLayoutLibrary::GetViewportScale(this);     // UMG DPI scale
FVector2D PixelSize = UWidgetLayoutLibrary::GetViewportSize(this); // game-screen PIXELS

// The relationships:
//   viewport_scaled = game_screen_pixels / Scale
//   layout size of a fullscreen Canvas Panel = PixelSize / Scale
GetViewportSize() returns the pixel size, not the scaled size. If you want the size UMG widgets are laid out in (e.g. to centre something with SetPositionInViewport(…, false)), divide by GetViewportScale().

With a 1× scale, pixel and scaled positions are identical — which is exactly why DPI bugs ship: everything looks right on a 1080p dev machine and falls apart on a 4K display or a console UI scale. Test with a non-1 DPI curve value before shipping. The full flag logic for SetPositionInViewport is in the mental model callout; the same rule applies to SetDesiredSizeInViewport (always scaled units, no flag).

7. Multiple Viewports & AddToPlayerScreen

Widgets can be added to the full game viewport or to a per-player screen area. In split-screen these are different regions of the window; even in single-player they have different geometry origins. Pair the add-function with the matching geometry-function:

Function Scope Notes
AddToViewport(ZOrder) Full game viewport Shared across all local players. ZOrder controls draw order.
AddToPlayerScreen(ZOrder) Per-player area Correct for split-screen HUD. Requires a valid owning player.
GetViewportWidgetGeometry(World) Full viewport geometry Pair with widgets added via AddToViewport.
GetPlayerScreenWidgetGeometry(PC) Player screen geometry Pair with widgets added via AddToPlayerScreen.
In split-screen the player screen is a sub-region of the viewport with a non-zero origin. If you add a widget with AddToPlayerScreen but convert coordinates through GetViewportWidgetGeometry, every position is shifted by that sub-rect offset. Likewise, pass bPlayerViewportRelative = true to ProjectWorldLocationToWidgetPosition for player-screen widgets.
📝 PIE changes nothing here: the viewport-geometry functions return the game viewport panel's geometry, so the editor chrome offset is handled for you. Absolute coordinates are desktop-rooted in PIE exactly as in a packaged game — code that converts through geometry works in both; only hardcoded positions break.

8. Common Gotchas

⚡ There are two "screen" spaces — and they're unrelated

Slate's "screen space" (FPointerEvent::GetScreenSpacePosition, GetMousePositionOnPlatform) is the OS desktop. Gameplay "screen space" (GetMousePosition, ProjectWorldLocationToScreen, the ScreenTo* conversions) is game-viewport pixels. Desktop coordinates pair with FGeometry and the Absolute* functions; game-screen pixels pair with the ScreenTo* functions and deprojection. Crossing the streams produces offsets that happen to be zero on a fullscreen primary monitor — and nowhere else.

⚡ "Absolute" means desktop, not window and not viewport

Cached widget geometry is desktop-rooted (window geometry plus the window's desktop position). A widget at the viewport's top-left has an AbsolutePosition equal to the viewport panel's position on your monitor — in PIE that includes the editor window position and chrome. Never compare absolute coordinates against viewport sizes or hardcoded positions; convert through AbsoluteToViewport or a geometry first.

⚡ SetPositionInViewport's default flag divides by DPI — feed it pixels or say false

bRemoveDPIScale defaults to true, which divides your position by the viewport scale. That's correct for game-screen pixel input. Passing an already-scaled position (a Viewport Position output, GetMousePositionOnViewport, ProjectWorldLocationToWidgetPosition) with the default divides twice — everything works at 1× DPI, then drifts toward the top-left on HiDPI displays. Scaled input → false.

⚡ GetCachedGeometry() is zero on the first frame

Geometry is produced by the first tick/paint pass. In NativeConstruct, immediately after AddToViewport, or in any constructor context it's a zeroed struct. Defer geometry reads to NativeTick, or guard with if (Geom.GetLocalSize().X > 0.f).

⚡ Leave bIncludeWindowPosition alone (false)

On ScreenToWidgetLocal / ScreenToWidgetAbsolute, setting bIncludeWindowPosition = true subtracts the window's desktop position from the result, converting it to window-relative coordinates. Since geometry conversions expect desktop-absolute values, turning this on shifts everything by the window position in windowed mode (and by the editor window position in PIE). It exists for the rare case where you genuinely need window-relative output. Default is false; keep it.

⚡ Render transforms: conversions and hit tests see them, layout doesn't

AbsoluteToLocal, LocalToAbsolute, IsUnderLocation, and hit-testing all go through the accumulated render transform (see Geometry.h) — a rotated widget converts and clicks exactly where it's drawn. What ignores render transforms is layout: desired size, slot arrangement, GetLayoutBoundingRect(), and the raw AbsolutePosition/Scale members. If you need the visual AABB of a transformed widget, use GetRenderBoundingRect().

⚡ AddToPlayerScreen and GetViewportWidgetGeometry don't mix

The player screen is a sub-region of the viewport with its own origin. Pair AddToViewport with GetViewportWidgetGeometry, and AddToPlayerScreen with GetPlayerScreenWidgetGeometry(PC) — mixing them offsets every conversion by the player's sub-rect in split-screen.

⚡ The deprecated Transform* nodes return inverted results

TransformScalarAbsoluteToLocal, TransformVectorLocalToAbsolute, etc. were deprecated in UE 5.6 because they transform in the opposite direction of their names. Use the replacements — Scalar_AbsoluteToLocal / Vector_LocalToAbsolute ("Absolute to Local (Scalar)" etc. in Blueprint) — which are correct, and handy for converting distances and deltas without picking up a translation offset.