Fed up with MouseEnter and MouseLeave not working? Here's a module for you!

For a long time now, I’ve found MouseEnter and MouseLeave events to be rather unreliable. I’ve created a simple module to replace these for my new game, and thought I’d share it with you all in the hopes it would save some frustration and annoyance.

Does not support touch or game pads. Please report any bugs you find!

Note: Currently, once connected the events will continue to be fired until the GuiObject is destroyed, regardless of the UIs parent or visibility.

Note 2: Does not work for rotated elements.

Quick Start Guide:

  1. Copy the code at the bottom into a module and call it ‘MouseOverModule’, or a name of your choosing. Place it wherever you normally keep your modules.
  2. Use like this:
local MouseOverModule = require(game.ReplicatedStorage.Whatever.MouseOverModule)

local MouseEnter, MouseLeave = MouseOverModule.MouseEnterLeaveEvent(GuiObject)
MouseEnter:Connect(function()
   print("Mouse enter")
end

MouseLeave:Connect(function()
   print("Mouse leave")
end
  1. Delete the GuiObject when you’re done with it.
local Player = game.Players.LocalPlayer or game.Players:GetPropertyChangedSignal("LocalPlayer"):Wait()

local Mouse = Player:GetMouse()

local CurrentItems = {}


--Private functions
local function IsInFrame(v)
	local X = Mouse.X
	local Y = Mouse.Y
	if X>v.AbsolutePosition.X and Y>v.AbsolutePosition.Y and X<v.AbsolutePosition.X+v.AbsoluteSize.X and Y<v.AbsolutePosition.Y+v.AbsoluteSize.Y then
		return true
	else 
		return false
	end
end

local function CheckMouseExited(Object)
	if not Object.MouseIsInFrame and Object.MouseWasIn then --Mouse was previously over object, fire leave event
		Object.MouseWasIn = false
		Object.LeaveEvent:Fire()
	end
end
	
local function CheckMouseEntered(Object)
	if Object.MouseIsInFrame and not Object.MouseWasIn then
		Object.MouseWasIn = true
		Object.EnteredEvent:Fire()
	end
end

game:GetService("RunService").Heartbeat:Connect(function()
	--Check each UI object
	--All exit events fire before all enter events for ease of use, so check for mouse exit events here
	for _, Object in pairs(CurrentItems) do
		Object.MouseIsInFrame = IsInFrame(Object.UIObj)
		CheckMouseExited(Object)
	end
	--Now check if the mouse entered any frames
	for _, Object in pairs(CurrentItems) do
		CheckMouseEntered(Object)
	end
end)


--Public functions
local module = {}
	
	function module.MouseEnterLeaveEvent(UIObj)
		if CurrentItems[UIObj] then
			return CurrentItems[UIObj].EnteredEvent.Event,CurrentItems[UIObj].LeaveEvent.Event
		end		
		
		local newObj = {}
		newObj.UIObj = UIObj
		local EnterEvent = Instance.new("BindableEvent")
		local LeaveEvent = Instance.new("BindableEvent")
		newObj.EnteredEvent = EnterEvent
		newObj.LeaveEvent = LeaveEvent
		newObj.MouseWasIn = false
		CurrentItems[UIObj] = newObj
		UIObj.AncestryChanged:Connect(function()
			if not UIObj.Parent then
		        --Assuming the object has been destroyed as we still dont have a .Destroyed event
				--If for some reason you parent your UI elements to nil after calling this, then parent it back again, mouse over will still have been disconnected.
				EnterEvent:Destroy()	
				LeaveEvent:Destroy()	
				CurrentItems[UIObj] = nil
		    end
		end)
		
	   return EnterEvent.Event,LeaveEvent.Event
	end
	
return module
69 Likes

Thank you for making this. I needed this

1 Like

GuiObject.InputBegan(inputObject) and GuiObject.InputEnded(inputObject) where inputObj.UserInputType == UserInputType.MouseMovement have also been reliable ways to work around this in my experience.

15 Likes

This doesn’t work for me BillboardGui TextButton MouseMovement

I found if I used Button.MouseEnter then used a step bind and some quik maf to determine if it was in the button still, that was a simpler and equally efficient way of dealing with it.

Still, good job open sourcing.

1 Like

Maybe you could account for the mouse to be on only one object at an instant.

Great work, will definitely prove to be useful. I also heard from a friend that works at Roblox that MouseEnter and MouseLeave events are being improved to fix their issues. Can’t confirm if fixes will be released soon though.

Note 4: Doesn’t work for SurfaceGui’s neither it does account for parameters as ZIndex, Active or DisplayOrder. I might work those stuff in order to make them more reliable and keep the functionality of the default events.

Nice job, however.

I’ll probably get to adding the none-surface gui features at some point if you don’t beat me to it :slightly_smiling_face: (SurfaceGui mouse over would probably need a separate function and a lot more work)

1 Like

To clarify a bit more, if you’re referring to note 3, the mouse can move from one object to another in a single frame, and currently the code has no guarantee that the leave event will fire before the other objects enter event .

1 Like

This is really needed, too lazi to implement it now tho :stuck_out_tongue:

However I have some suggestions:

  1. Why not just return the MouseEnterLeaveEvent function?
    return MouseEnterLeaveEvent

  2. The function should return the Event signal instead of the Event object, so can connect directly:
    return EnterEvent.Event.Event, LeaveEvent.Event.Event

This would allow to do:

local MouseEnter, MouseLeave = MouseOverModule(GuiObject)
MouseEnter:connect(function()
   print("Mouse enter")
end
MouseLeave:connect(function()
   print("Mouse leave")
end

Which is slightly cleaner.

Alternativly allow include mouseenter/leave functions as parameters instead.
MouseOverModule(GuiObject, enterFunc, leaveFunc)

you are my everything.

This is so perfect

Updated with a new version:
-Fixed a bug causing the module to break if you call .MouseEnterLeaveEvent(GuiObject) on the same object more than once
-Within the same frame, mouse leave events now always fire before mouse enter events. Should help make the module overall easier to use.

2 Likes

This is a really great module and so perfect!
Btw I found a problem in your demonstration

It should be MouseEnter:connect(function() and MouseLeave:connect(function() instead.

I did. I finally did get fed up. Thank you.

In case anybody else wants to use this with roblox-ts, here’s a d.ts file for ya:

declare namespace MouseOver
{
function MouseEnterLeaveEvent( guiObject: GuiObject ) : [ RBXScriptSignal, RBXScriptSignal ]
}

export = MouseOver

3 Likes

Mouse Leave And Enter weren’t working, two seconds I installed this, it fixed all my problems. :smile: Thanks for this!

Finally got around to doing some restructuring. Updated a few things to follow better code practise (such as using an event to wait for local player instead of polling), and removed the really nasty spawn() statement, and slightly increased efficiency by making sure IsInFrame() is only called once per UI element, instead of twice.

After some consideration I switched from Mouse.Moved to RunService.RenderStepped, since this covers a lot of edge cases that would have bloated the code otherwise, while not really impacting efficiency much since the player will be moving their mouse almost every frame anyway.

There shouldn’t be any difference in how you use the module, unless you were somehow relying on there being a wait() between calling module.MouseEnterLeaveEvent, and MouseEnter firing if the player does not move their mouse when a button is spawned under it.

2 Likes

I believe this module still works for elements in a scrolling frame that aren’t currently being rendered on screen. Is there any way to get around this?

I recommend tying this to Heartbeat instead of RenderStepped.

Renderstepped should only really be used for camera or character code.

2 Likes