Buttons in BillboardGui miss mouse input when removed from datamodel when mouse is depressed

Reproduction Steps

Place this LocalScript in the StarterPlayerScripts. Keep in mind it should be a LocalScript, not a Script with RunContext = Client, as that will make it run twice.

Source Code
--!strict

local player = nil;
while player == nil do
	player = game.Players.LocalPlayer;
	task.wait();
end

local part = Instance.new("Part");
part.CFrame = CFrame.new(0, 20, 0);
part.Parent = workspace;

local sgui = Instance.new("BillboardGui");
sgui.Name = "BugDemo";
sgui.Parent = player.PlayerGui;
sgui.Size = UDim2.new(0, 300, 0, 200);
sgui.Active = true;
sgui.AlwaysOnTop = true;
sgui.Adornee = part;

local tb = Instance.new("TextButton");
tb.Size = UDim2.new(1, 0, 1, 0);
tb.Parent = sgui;

local text = {};
local function AddText(s: string)
	if #text > 5 then
		table.remove(text, 1);
	end
	table.insert(text, s);
	tb.Text = table.concat(text, "\n");
end

tb.InputBegan:Connect(function(io: InputObject)
	AddText("InputBegan: " .. tostring(io.UserInputType));

	if io.UserInputType == Enum.UserInputType.MouseButton1 then
		--Make the GUI disappear after a second, then re-appear a second later.
		task.wait(1);
		sgui.Parent = nil;
		task.wait(1);
		sgui.Parent = player.PlayerGui;
	end
end);

tb.InputEnded:Connect(function(io: InputObject)
	AddText("InputEnded: " .. tostring(io.UserInputType));
end);

--To make the video easier to follow, here's a little doodad that shows an animation whenever I click.
--It's not necessary for the repro, so feel free to omit it.
do
	local sgui = Instance.new("ScreenGui");
	sgui.Name = "ClickAnimation";
	sgui.Parent = player.PlayerGui;

	local function PlayAnimation(position: Vector3, color: Color3)
		local anim = Instance.new("ImageLabel");
		anim.Image = "rbxassetid://9846850752";
		anim.Position = UDim2.new(0, position.X, 0, position.Y);
		anim.AnchorPoint = Vector2.new(0.5, 0.5);
		anim.Size = UDim2.new(0, 5, 0, 5);
		anim.ImageColor3 = color;
		anim.BackgroundTransparency = 1;
		anim.Parent = sgui;
		game:GetService("TweenService"):Create(anim, TweenInfo.new(1, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out, 0, false, 0), {
			Size = UDim2.new(0, 50, 0, 50);
			ImageTransparency = 1;
			Rotation = 360;
		}):Play();
		task.delay(1, anim.Destroy, anim);
	end
	game:GetService("UserInputService").InputBegan:Connect(function(io: InputObject, gce: boolean)
		if io.UserInputType == Enum.UserInputType.MouseButton1 then
			PlayAnimation(io.Position, Color3.new(0, 1, 0));
		end
	end);
	game:GetService("UserInputService").InputEnded:Connect(function(io: InputObject, gce: boolean)
		if io.UserInputType == Enum.UserInputType.MouseButton1 then
			PlayAnimation(io.Position, Color3.new(1, 0, 0));
		end
	end);
end

In general, the set of requirements to hit this bug are:

  1. You have a BillboardGui with a button in it.
  2. The user clicks the button
  3. The BillboardGui gets removed from the data model (maybe just disabled, not sure, didn’t test it).
  4. The user releases the mouse button.
    1. Note, the InputEnded event will not fire at this point. I generally expect this event to always fire after an InputBegan event, even if the mouse moves out of the screen
  5. The BillboardGui is re-added to the data model.

When this happens, the user can click on the button, but the InputBegan event will NOT be triggered. The InputEnded event, however, WILL be triggered. This will only happen once after the BillboardGui is re-added to the data model, so all future clicks will succeed.

Expected Behavior

I expect every time I click the TextButton, the InputBegan event should fire with InputObject.UserInputType == MouseButton1.

Actual Behavior

The event doesn’t fire.

Caveat: this is ONLY an issue in BillboardGuis (I’ve tested ScreenGuis, they work fine).

I’m attaching a video that shows the issue. I added some code to show a green icon thing when the user clicks, and red icon thing when the user releases which should serve as the “source of truth”. The text button will show a short log of the events that have occurred.

Workaround

User has to click twice. I might be able to work around this by instantiating a new BillboardGui, but I haven’t tested it. This is an edge case in my code, so I think the user can suffer through this bug for the time being.

Issue Area: Engine
Issue Type: Other
Impact: Moderate
Frequency: Very Rarely

I would expect the behavior you noticed, as in general UI practice, buttons that are not click-released
on the [active] button should not register a release, which would be a click.
So an additional button press might be considered redundant.
Now if you still miss events when using UIS as opposed to the button, then I would consider that an issue.

1 Like

OK, I’m less concerned about this whether or not the InputEnded event fires, BUT it is worth pointing out that this IS how the rest of the system works. If you click on a button, move your mouse away, then release the mouse, you will get an InputEnded event for that button. This is true even if you move the mouse out of the window.

The issue that I care more about is that the second mouse click is just ignored.

Well yes, but in the case of moving out of the button, first you receive the Ended of type MouseMovement which invalidates the detection/definition of a button-click when the Ended of type MouseButton1 comes in.
Without the MouseMovement click invalidation, how do you suppress a button-click without suppressing
Ended type MouseButton1 when the button isn’t legally releaseable?
So it still seems to make sense to me.
So maybe it’s a bug, maybe it isn’t.

1 Like

Thanks for the report! We’ll investigate.

1 Like