ContextFrame - Prevents UI menu overlap and general tweening related issues

It can sometimes be a pain to make your UI’s tween nicely and without bugs. I have always found it kind of tedious to do. While working on one of my projects I made this module to handle all my frame tweening, and I decided to share it.

ContextFrame will handle all your menu tweening needs and prevent any pesky tweening related issues. It works by allowing you to create different context groups for frames. Within each context only one frame is allowed to be open.

When you add a frame to a context it will give you a few frame tweening methods to play with.

Source code:

--[[
	ContextFrame: A module to handle all your menu tweening needs! 
	Prevents menu overlap and general tweening related issues.
	--------------------------------------------------------------
	
	ContextFrame Methods:
	---------------------
	ContextFrame.NewContext(contextName) -> ContextObj | Creates and returns a new context object.
	
	ContextFrame:GetContext(contextName) -> ContextObj | Retrieves an existing context from it's name.
	
	ContextFrame:GetContextFromInstance(instance) -> ContextObj | Retrieves an existing context from an instance.
	
	ContextObj Methods:
	-------------------
	ContextObj:AddToContext(instance, openPosition, startPos) -> Frame | Adds instance to the context. 
	openPosition is a UDim2 value which will be the frame's position when it is opened. start pos can be "open" or "closed", it is closed by default.
	
	ContextObj:RemoveFromContext(instance) | Removes instance from the context.
	
	ContextObj:CloseAll(ignoreDefault) -> boolean | Closes all frames in ContextObj except those set as default. (set ignoreDefault to true to override).
	
	ContextObj:AddDefault(instance) -> boolean | Adds instance to the list of default frames (these frames will remain open when CloseAll() is called).
	
	ContextObj:RemoveDefault(instance) -> boolean | Removes instance from the list of default frames.
	
	ContextObj:GetOpenFrame() -> Frame | Returns the frame that is currently open in the ContextObj.
	
	Frame Methods:
	--------------
	Frame:SetStyle(length, easingStyle) | Adjusts the tween animation. length is the length of the tween. easing style is an enum.
	
	Frame:Open() | Tweens the frame to it's open position, any frame within it's context will be closed prior.
	
	Frame:Close() | Tweens the frame to it's closed position.
	
	Frame:Handle() | Tweens the frame to whatever position it isn't in. i.e if the frame is closed it will open it and vice versa.
	
	Frame:Lock() | Locks the frame, when this frame is opened no other frame may open until it is closed.
	
	Frame:Unlock() | Unlocks the frame.

]]

local tweenService = game:GetService("TweenService")
local UIHandler = {}
local contextFunc = {}
UIHandler.Context = {}
UIHandler.__index = UIHandler
contextFunc.__index = contextFunc

local framePositions = {
	"closed",
	"open",
	"opening",
	"closing",
}

function UIHandler.NewContext(name)
	if not UIHandler.Context[name] then
		local newEnv = {}
		newEnv.Frames = {}
		newEnv.DefaultOpen = {}
		setmetatable(newEnv, UIHandler)
		UIHandler.Context[name] = newEnv
		setmetatable(newEnv, UIHandler)
		return newEnv
	else
		warn("Context name is not original")
		return
	end
end

function UIHandler:GetContext(name)
	return UIHandler.Context[name]
end

function UIHandler:GetContextFromInstance(frame)
	for contextName, contextObj in pairs(UIHandler.Context) do
		if contextObj.Frames[frame] then
			return contextObj
		end
	end
	return
end

function UIHandler:GetContextFrameFromInstance(frame)
	for contextName, contextObj in pairs(UIHandler.Context) do
		local contextFrame = contextObj.Frames[frame]
		if contextFrame then
			return contextFrame
		end
	end
	return
end

function UIHandler:AddDefault(frame)
	local foundFrame = self.Frames[frame]
	if foundFrame then
		table.insert(self.DefaultOpen, foundFrame)
		return true
	end
	warn("Attempt to add frame out of context")
	return
end

function UIHandler:RemoveDefault(frame)
	local foundFrame = self.Frames[frame]
	if foundFrame then
		local foundDefault = table.find(self.DefaultOpen, foundFrame)
		if foundDefault then
			table.remove(self.DefaultOpen, foundDefault)
		else
			return false
		end
	else
		warn("Frame is not in context")
		return
	end
end

function UIHandler:CloseAll(ignoreDefault)
	for index, object in pairs(self.Frames) do
		if not table.find(self.DefaultOpen, object) or ignoreDefault then
			print("Closing", object.Object)
			object:Close()
		elseif not ignoreDefault and (self.FrameStatus ~= "open" or self.FrameStatus ~= "opening") then
			print("Open", object.Object)
			object:Open()
		end
	end
end

function UIHandler:AddToContext(frame, openPos, startPos)
	if not self.Frames[frame] then
		local proxy = {}
		
		if startPos and not table.find(framePositions, startPos) then
			warn("invalid start position, default has been set")
			startPos = "closed"
		end
		
		--Data set--
		proxy.FrameStatus = startPos or "closed"
		proxy.Context = self
		proxy.Object = frame
		proxy.OpenPos = openPos or UDim2.new(0.5, 0, 0.5, 0)
		proxy.ClosePos = proxy.OpenPos + UDim2.new(0, 0, 1, 0)
		proxy.Locked = false
		proxy.TweenInInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
		proxy.TweenOutInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
		proxy.CurrentTween = nil
		
		self.Frames[frame] = proxy
		setmetatable(proxy, contextFunc)
		return proxy
	else
		warn("Attempted to add duplicate frame to context")
		return self.Frames[frame]
	end
end

function UIHandler:RemoveFromContext(frame)
	if self.Frames[frame] then
		self:RemoveDefault(frame)
		self.Frames[frame] = nil
	end
end

function UIHandler:GetOpenFrame()
	for _, object in pairs(self.Frames) do
		if object.FrameStatus == "open" or object.FrameStatus == "opening" then
			return object
		end
	end
	return nil
end

---------------------------
--Context frame functions--
---------------------------

function contextFunc:SetStyle(length, easingStyle)
	self.tweenInInfo = TweenInfo.new(length, (easingStyle or Enum.EasingStyle.Quad), Enum.EasingDirection.In)
	self.tweenOutInfo = TweenInfo.new(length, (easingStyle or Enum.EasingStyle.Quad), Enum.EasingDirection.Out)
end

function contextFunc:Lock()
	self.Locked = true
end

function contextFunc:Unlock()
	self.Locked = false
end

function contextFunc:Close()
	if self.FrameStatus == "opening" then
		self.CurrentTween:Cancel()
	elseif self.FrameStatus ~= "closed" or self.FrameStatus ~= "closing" then
		local tween = tweenService:Create(self.Object, self.TweenInInfo, {Position = self.ClosePos})
		self.CurrentTween = tween
		self.FrameStatus = "closing"
		tween:Play()
		spawn(function()
			tween.Completed:Wait()
			tween:Destroy()
			self.FrameStatus = "closed"
		end)
	end
end

function contextFunc:Open()
	local openFrame = self.Context:GetOpenFrame()
	if openFrame ~= self then
		if openFrame then
			if openFrame.Locked then
				return
			else
				openFrame:Close()
			end
		end
		local tween = tweenService:Create(self.Object, self.TweenOutInfo, {Position = self.OpenPos})
		self.CurrentTween = tween
		self.FrameStatus = "opening"
		tween:Play()
		spawn(function()
			tween.Completed:Wait()
			tween:Destroy()
			self.FrameStatus = "open"
		end)
	end
end

function contextFunc:Handle()
	if self.Context:GetOpenFrame() == self then
		self:Close()
	else
		self:Open()
	end
end

return UIHandler
5 Likes

I’ve never had an issue tweening UI. Could you provide examples of how you’d use this module?

1 Like

Can you please show us a demonstration on how it works?
Thanks!