Today I was trying to dig up a analysis of the space that each UI event occurs in. By space, I’m referring to either:
- Viewport space, which starts at the absolute top-left corner of the window
- Screen space, which takes into account the GUI Inset
I remember this being unexpectedly tricky, and I couldn’t find anything on the dev hub to figure this out.
Here’s a quick picture showing what I’m talking about:
The viewport origin is the topmost, leftmost point. Even if there’s a top-bar, the origin position stays the same. The screen-space origin is offset by the top-bar.
BillboardGuis have their own “screen space” of sorts. When you have a TextButton inside of a BillboardGui, events will either be relative to the topmost, leftmost corner (similar to viewport), or offset by the size of the top-bar (similar to screen-space). This is disturbing because BillboardGuis don’t have any inset, but so it goes.
Here’s the breakdown of all events I tested and the space that they occur in:
Viewport
- TextButton.MouseButton1Down
- TextButton.MouseButton1Up
- TextButton.MouseButton2Down
- TextButton.MouseButton2Up
- UserInputService.GetMouseLocation
Note: if the textbutton is a descendant of a BillboardGui, the “viewport” origin is the top-left corner of the billboard GUI, NOT the top-left corner of the viewport.
Screen Space
- Mouse.Button1Down
- Mouse.Button1Up
- Mouse.Move
- TextButton.InputBegan
- TextButton.InputChanged
- TextButton.InputEnded
- UserInputService.InputBegan
- UserInputService.InputChanged
- UserInputService.InputEnded
- GuiObject.AbsolutePosition
The same note about BillboardGuis applies down here. The TextButton.InputBegan event will fire with the Position
property relative to the screen space of the BillboardGui, which is inset from the actual BillboardGui space.
Additionally, TextButton.InputBegan & all of its compatriots will be in screen space even if the ScreenGui they are descended from has IngoreGuiInset set to true.
How I gathered this data
I’m leaving this note in here in case I want to add to this list in the future. The way I gathered this info was by putting this LocalScript in the game; it will create GUIs & listen to their events, and I could press a button to spit out a summary of them all. It would also categorize them based on their origins.
--!strict
wait(1);
local UIS = game:GetService("UserInputService");
--Things to test:
-- 1. Player Mouse
-- 2. GUI interactions
-- 3. UIS interactions
type Space = "screen"|"viewport"|"billboard"|"billboard-with-inset"|"other";
local function GetBillboardGuiViewportPosition(billboardGui: BillboardGui): Vector2
local adornee = billboardGui.Adornee;
if adornee and adornee:IsA("BasePart") then
local midpoint: Vector3 = workspace.CurrentCamera:WorldToViewportPoint(adornee.Position);
return Vector2.new(midpoint.X, midpoint.Y) - billboardGui.AbsoluteSize / 2;
else
return Vector2.new();
end
end
local bgui;
local function GetSpaceOriginsRelativeToViewport(): {[Space]: Vector2}
local inset: Vector2 = game:GetService("GuiService"):GetGuiInset();
local t = {
screen = inset;
viewport = Vector2.new();
billboard = GetBillboardGuiViewportPosition(bgui);
["billboard-with-inset"] = GetBillboardGuiViewportPosition(bgui) + inset;
};
return t;
end
local Inputs: {[string]: Space} = {};
local OffsetFromViewportSpace: {[string]: Vector2} = {};
local function NotePositionOfEvent(event: string, position: Vector2|Vector3)
local mouseLocation = UIS:GetMouseLocation();
local offsetFromViewportSpace: Vector2 = (mouseLocation :: Vector2) - Vector2.new(position.X, position.Y);
OffsetFromViewportSpace[event] = offsetFromViewportSpace;
--This will find egregious errors in GetMouseLocation. I've seen a bug before where
--GetMouseLocation returns inconsistent results based on the context it's called in.
--
--It's possible the mouse actually does move before the next resumption cycle, so this has some
--wiggle room built-in. Try not to move the mouse too fast.
task.defer(function() local p = mouseLocation - UIS:GetMouseLocation(); assert(p.Magnitude <= 5, "Mouse location differed after short defer: " .. tostring(p)); end);
local eventScreenSpace: Space = "other";
for space: Space, offset in GetSpaceOriginsRelativeToViewport() do
if offset == offsetFromViewportSpace then
eventScreenSpace = space;
break;
end
end
if not Inputs[event] then
Inputs[event] = eventScreenSpace;
print("Event " .. tostring(event) .. " is part of " .. eventScreenSpace .. " space");
else
if Inputs[event] ~= eventScreenSpace then
error("Changed my mind on event " .. tostring(event) .. "; this interaction was in " .. tostring(eventScreenSpace) .. " space\nRaw position was: " .. tostring(position) .. ", relative was: " .. tostring(offsetFromViewportSpace));
end
end
end
local function DumpAllKnownEvents()
local tuples: {{Key: string; Space: Space}} = {};
for key, space: Space in Inputs do
table.insert(tuples, {Key = key; Space = space});
end
table.sort(tuples, function(a, b) return (a.Space :: string) < (b.Space :: string) or a.Space == b.Space and a.Key < b.Key; end);
local lastSpace;
for i, t in tuples do
if t.Space ~= lastSpace then
print("Events in " .. tostring(t.Space) .. " space:");
lastSpace = t.Space;
end
if lastSpace == "other" then
print("* " .. tostring(t.Key) .. " (" .. tostring(OffsetFromViewportSpace[t.Key]) .. ")");
else
print("* " .. tostring(t.Key));
end
end
end
local function CreateEventStringFromInputObject(source: string, inputObject: InputObject): string
return string.format("%s (%s, %s)", source, tostring(inputObject.UserInputType), tostring(inputObject.UserInputState));
end
--------------------------------------------------------------------------------
--Mouse
--------------------------------------------------------------------------------
local mouse = game.Players.LocalPlayer:GetMouse();
mouse.Button1Down:Connect(function()
NotePositionOfEvent("Mouse.Button1Down", Vector2.new(mouse.X, mouse.Y));
end);
mouse.Button1Up:Connect(function()
NotePositionOfEvent("Mouse.Button1Up", Vector2.new(mouse.X, mouse.Y));
end);
mouse.Move:Connect(function()
NotePositionOfEvent("Mouse.Move", Vector2.new(mouse.X, mouse.Y));
end);
--------------------------------------------------------------------------------
--UserInputService
--------------------------------------------------------------------------------
local function IgnoreEvent(io: InputObject)
if io.UserInputType == Enum.UserInputType.Focus then
return true;
else
return false;
end
end
UIS.InputBegan:Connect(function(io: InputObject, gpe: boolean)
if IgnoreEvent(io) then return; end
NotePositionOfEvent(CreateEventStringFromInputObject("UserInputService", io), io.Position);
NotePositionOfEvent("UserInputService.GetMouseLocation", UIS:GetMouseLocation());
end);
UIS.InputChanged:Connect(function(io: InputObject, gpe: boolean)
if IgnoreEvent(io) then return; end
NotePositionOfEvent(CreateEventStringFromInputObject("UserInputService", io), io.Position);
end);
UIS.InputEnded:Connect(function(io: InputObject, gpe: boolean)
if IgnoreEvent(io) then return; end
NotePositionOfEvent(CreateEventStringFromInputObject("UserInputService", io), io.Position);
end);
--------------------------------------------------------------------------------
--GUIs
--------------------------------------------------------------------------------
local function BuildAndListenToTextButton(key: string): TextButton
local textButton = Instance.new("TextButton");
textButton.Text = key;
textButton.InputBegan:Connect(function(io: InputObject)
NotePositionOfEvent(CreateEventStringFromInputObject(key, io), io.Position);
end);
textButton.InputChanged:Connect(function(io: InputObject)
NotePositionOfEvent(CreateEventStringFromInputObject(key, io), io.Position);
end);
textButton.InputEnded:Connect(function(io: InputObject)
NotePositionOfEvent(CreateEventStringFromInputObject(key, io), io.Position);
end);
textButton.MouseButton1Down:Connect(function(x, y)
NotePositionOfEvent(key .. ".MouseButton1Down", Vector2.new(x, y));
end);
textButton.MouseButton1Up:Connect(function(x, y)
NotePositionOfEvent(key .. ".MouseButton1Up", Vector2.new(x, y));
end);
textButton.MouseButton2Down:Connect(function(x, y)
NotePositionOfEvent(key .. ".MouseButton2Down", Vector2.new(x, y));
end);
textButton.MouseButton2Up:Connect(function(x, y)
NotePositionOfEvent(key .. ".MouseButton2Up", Vector2.new(x, y));
end);
return textButton;
end
local screenGui = Instance.new("ScreenGui");
local tb = BuildAndListenToTextButton("ScreenGui.TextButton");
tb.Parent = screenGui;
tb.Size = UDim2.new(0, 140, 0, 50);
tb.Position = UDim2.new(0, 120, 0, 10);
local dumpAllResults = Instance.new("TextButton", screenGui);
dumpAllResults.Size = UDim2.new(0, 100, 0, 50);
dumpAllResults.Position = UDim2.new(0, 10, 0, 10);
dumpAllResults.Text = "Dump all results";
screenGui.Parent = game.Players.LocalPlayer.PlayerGui;
dumpAllResults.MouseButton1Click:Connect(function()
DumpAllKnownEvents();
end);
local screenGui2 = Instance.new("ScreenGui");
screenGui2.IgnoreGuiInset = true;
local tb = BuildAndListenToTextButton("ScreenGui(IgnoreGuiInset).TextButton");
tb.Parent = screenGui2;
tb.Size = UDim2.new(0, 140, 0, 50);
tb.Position = UDim2.new(0, 120, 0, 108);
screenGui2.Parent = game.Players.LocalPlayer.PlayerGui;
--Do something similar for BillboardGuis & ScreenGuis.
local p = Instance.new("Part");
p.Anchored = true;
p.Size = Vector3.new(8, 8, 8);
p.CFrame = CFrame.new(0, 20, 0);
p.Parent = workspace;
bgui = Instance.new("BillboardGui");
bgui.Adornee = p;
bgui.AlwaysOnTop = true;
bgui.Active = true;
bgui.Size = UDim2.new(0, 140, 0, 50);
bgui.Parent = game.Players.LocalPlayer.PlayerGui;
local tb2 = BuildAndListenToTextButton("BillboardGui.TextButton");
tb2.Parent = bgui;
tb2.Size = UDim2.new(1, 0, 1, 0);
workspace.CurrentCamera.CameraSubject = p;
workspace.CurrentCamera.CameraType = Enum.CameraType.Fixed;
workspace.CurrentCamera.CFrame = CFrame.new(Vector3.new(20, 40, 20), Vector3.new(0, 20, 0));
workspace.CurrentCamera.Focus = CFrame.new(0, 20, 0);