GuiManager2d v1.4 | Flexible Gui Module

Hello there :wave:

GuiManager2d is a module that helps making 2d Gui code less of a nightmare. Everything is based on scale instead of offset so it works on every screen size and shape!

Current Version [v1.4]:

v1.4 Summary
  • Added Destroying connection to clear GuiManager2d classes from gui that is destroyed externally

  • Added DisableGlobalEventParams, DisableLocalEventParams and DisableDestroyConnection settings

  • Added GuiBase2d check for GuiManager2d.new()

  • Couple of internal changes

)

Showcase and Samples

This video shows what GuiManager is currently capable of by just using some of it’s functions. The video also displays that everything is scaled when the window is made smaller, so it works just fine on any sized screen.

Local Script used in the video
local localPlayer = game:GetService("Players").LocalPlayer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local GuiManager2d = require(ReplicatedStorage:WaitForChild("GuiManager2d"))
local screenGui = Instance.new("ScreenGui", localPlayer.PlayerGui)

local ui = GuiManager2d.new("TextLabel", {
	Parent = screenGui,
	Name = "Click and Drag Me!",
	Position = UDim2.fromScale(0.9, 0.75),
	Size = UDim2.fromScale(0.15, 0.15),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Text = "Click and Drag Me!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui2 = GuiManager2d.new("TextLabel", {
	Parent = screenGui,
	Name = "Click and Rotate Me!",
	Position = UDim2.fromScale(0.9, 0.25),
	Size = UDim2.fromScale(0.15, 0.15),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Text = "Click and Rotate Me!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui3 = GuiManager2d.new("TextLabel", {
	Parent = screenGui,
	Name = "I'm Tweening!",
	Position = UDim2.fromScale(0.1, 0.25),
	Size = UDim2.fromScale(0.15, 0.15),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Text = "I'm Tweening!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui4 = GuiManager2d.new("TextLabel", {
	Parent = screenGui,
	Name = "I'm being Set!",
	Position = UDim2.fromScale(0.05, 0.75),
	Size = UDim2.fromScale(0.15, 0.15),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Text = "I'm being Set!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui5 = GuiManager2d.new("Frame", {
	Parent = screenGui,
	Name = "Background",
	Transparency = 0.5,
	Position = UDim2.fromScale(0.5, 0.5),
	Size = UDim2.fromScale(0.45, 0.45),
	AnchorPoint = Vector2.new(0.5, 0.5)
})

local ui6 = GuiManager2d.new("TextLabel", {
	Parent = ui5.Ui,
	Name = "Drag me downwards!",
	Position = UDim2.fromScale(0, 0),
	Size = UDim2.fromScale(0.25, 0.25),
	AnchorPoint = Vector2.new(0.5, 0.5),
	BackgroundColor3 = Color3.fromRGB(65, 65, 65),
	TextColor3 = Color3.new(1, 1, 1),
	Text = "Drag me downwards!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui7 = GuiManager2d.new("TextLabel", {
	Parent = ui5.Ui,
	Name = "Drag me sideways, I snap in place!",
	Position = UDim2.fromScale(0, 1),
	Size = UDim2.fromScale(0.25, 0.25),
	AnchorPoint = Vector2.new(0.5, 0.5),
	BackgroundColor3 = Color3.fromRGB(65, 65, 65),
	TextColor3 = Color3.new(1, 1, 1),
	Text = "Drag me sideways, I snap in place!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui8 = GuiManager2d.new("TextLabel", {
	Parent = ui5.Ui,
	Name = "Drag me, I snap to a grid!",
	Position = UDim2.fromScale(0.35, 0.5),
	Size = UDim2.fromScale(0.25, 0.25),
	AnchorPoint = Vector2.new(0.5, 0.5),
	BackgroundColor3 = Color3.fromRGB(65, 65, 65),
	TextColor3 = Color3.new(1, 1, 1),
	Text = "Drag me, I snap to a grid!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui9 = GuiManager2d.new("TextLabel", {
	Parent = ui5.Ui,
	Name = "Rotate me, I snap in place and only go to 180 degrees!",
	Position = UDim2.fromScale(0.65, 0.5),
	Size = UDim2.fromScale(0.25, 0.25),
	AnchorPoint = Vector2.new(0.5, 0.5),
	BackgroundColor3 = Color3.fromRGB(65, 65, 65),
	TextColor3 = Color3.new(1, 1, 1),
	Text = "Rotate me, I snap in place and only go to 180 degrees!",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui10 = GuiManager2d.new("TextLabel", {
	Parent = screenGui,
	Name = "Currently not Overlapping",
	Position = UDim2.fromScale(0.35, 0.9),
	Size = UDim2.fromScale(0.15, 0.15),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Text = "Currently not Overlapping",
	Font = Enum.Font.Code,
	TextScaled = true
})

local ui11 = GuiManager2d.new("TextLabel", {
	Parent = screenGui,
	Name = "Currently not Overlapping",
	Position = UDim2.fromScale(0.65, 0.9),
	Size = UDim2.fromScale(0.15, 0.15),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Text = "Currently not Overlapping",
	Font = Enum.Font.Code,
	TextScaled = true
})

ui:Draggable()

ui.DraggingChanged:Connect(function(difference: UDim2)
	print("Position Difference:", difference)
	ui:Overlap(screenGui:GetDescendants())
end)

ui.DraggingEnded:Connect(function(difference: UDim2)
	warn("Final Position Difference:", difference)
end)

ui2:Rotatable()

ui2.RotatingChanged:Connect(function(difference: number)
	print("Rotation Difference:", difference)
end)

ui2.RotatingEnded:Connect(function(difference: number)
	warn("Final Rotation Difference:", difference)
end)

ui3:Tween({
	Position = UDim2.fromScale(0.1, 0.5),
	BackgroundColor3 = Color3.fromRGB(160, 160, 0),
	Rotation = 360
	
}, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.In, -1, true))

task.spawn(function()
	while task.wait() do
		ui4:Set({
			Position = UDim2.fromScale(0.05, 0.75),
			BackgroundColor3 = Color3.fromRGB(160, 0, 0),
			Size = UDim2.fromScale(0.05, 0.05)
		})
		
		task.wait(1)
		
		ui4:Set({
			Position = UDim2.fromScale(0.075, 0.75),
			BackgroundColor3 = Color3.fromRGB(0, 160, 0),
			Size = UDim2.fromScale(0.1, 0.1)
		})
		
		task.wait(1)
		
		ui4:Set({
			Position = UDim2.fromScale(0.1, 0.75),
			BackgroundColor3 = Color3.fromRGB(0, 0, 160),
			Size = UDim2.fromScale(0.15, 0.15)
		})
		
		task.wait(1)
	end
end)

ui6:Draggable({
	X = Vector2.new(0, 0),
	Y = Vector2.new(0, 1)
})

ui6.DraggingChanged:Connect(function(difference: UDim2)
	print("Position Difference:", difference)
end)

ui6.DraggingEnded:Connect(function(difference: UDim2)
	warn("Final Position Difference:", difference)
end)

ui7:Draggable({
	X = Vector2.new(0, 1),
	Y = Vector2.new(1, 1)
},
{
	X = 0.1
})

ui7.DraggingChanged:Connect(function(difference: UDim2)
	print("Position Difference:", difference)
end)

ui7.DraggingEnded:Connect(function(difference: UDim2)
	warn("Final Position Difference:", difference)
end)

ui8:Draggable({
	X = Vector2.new(0.15, 0.85),
	Y = Vector2.new(0.15, 0.85)
},
{
	X = 0.1,
	Y = 0.1
})

ui8.DraggingChanged:Connect(function(difference: UDim2)
	print("Position Difference:", difference)
end)

ui8.DraggingEnded:Connect(function(difference: UDim2)
	warn("Final Position Difference:", difference)
end)

ui9:Rotatable({
	X = Vector2.new(0, 180)
},
{
	X = 30
})

ui9.RotatingChanged:Connect(function(difference: number)
	print("Rotation Difference:", difference)
end)

ui9.RotatingEnded:Connect(function(difference: number)
	warn("Final Rotation Difference:", difference)
end)

task.spawn(function()
	while task.wait(1) do
		ui10:Set({
			Position = UDim2.fromScale(0.45, 0.9)
		})
		
		ui11:Set({
			Position = UDim2.fromScale(0.55, 0.9)
		})
		
		local overlapResult = ui10:Overlap({ui11.Ui})
		
		if overlapResult and table.find(overlapResult, ui11.Ui) then
			ui10:Set({
				Text = "We're Overlapping!"
			})

			ui11:Set({
				Text = "We're Overlapping!"
			})
		end
		
		task.wait(1)
		
		ui10:Set({
			Position = UDim2.fromScale(0.35, 0.9),
			Text = "Currently Not Overlapping"
		})

		ui11:Set({
			Position = UDim2.fromScale(0.65, 0.9),
			Text = "Currently Not Overlapping"
		})
	end
end)
  • gui.new() was used to make the ui elements. The first parameter is the type of ui element you want to create, the second parameter is a table of properties to apply (The parent property is always set last despite its position in the table). gui.setup() can be used on already existing ui elements.

  • gui:Tween() is a function that is used on the table returned by gui.new(), and is basically a wrapper for TweenService:Create(), but the first parameter is filled in by the ui element and automatically played. The first parameter of this function is the goal, and the second is the TweenInfo.

  • gui:Change() is a function that is also used on the table returned by gui.new(), and just sets the property to those passed in the first parameter as a table.

  • ui:Draggable() is once again a function that is used on the table returned by gui.new(), and is what enables the draggable functionality of the ui. The first parameter is a table containing two Vector2’s for the X and Y axis, these values act as clamps so things like sliders can be made. The second parameter is a table containing two numbers for the X and Y axis, these values determine what the values should be rounded to, allowing snapping and grids to be made. If OnlyOnEnded is set to true in either of these tables, the clamping / gridding will only be applied when the mouse button is lifted.

  • ui:Rotatable() as you guessed is a function that is used on the table returned by gui.new(), and is what enables the rotatability of the ui. The first parameter is a Vector2 value which acts as a clamp for rotation. The second parameter is a number and causes the ui to rotate in segments (e.g. only 30 degrees at a time). The third parameter is the OnlyOnEnded boolean and behaves the same as in ui:Draggable()

)

Sources

Module Model: GuiManager2d - Roblox
Module Demo (Uncopylocked): GuiManager2d Demo - Roblox

Module Code
--!strict
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")

local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui

local GoodSignal = require(script:WaitForChild("GoodSignal"))

local defaultInfo = TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)

type setting = "DisableGlobalEvents"
	| "DisableGlobalEventParams"
	| "DisableLocalEvents"
	| "DisableLocalEventParams"
	| "DisableAttributes"
	| "DisableDestroyConnection"

type clamp1D = {
	X: Vector2?,
	OnlyOnEnded: boolean?
}

type grid1D = {
	X: number?,
	OnlyOnEnded: boolean?
}

type clamp2D = {
	X: Vector2?,
	Y: Vector2?,
	OnlyOnEnded: boolean?
}

type grid2D = {
	X: number?,
	Y: number?,
	OnlyOnEnded: boolean?
}

type array = {[number]: any?}
type dictionary = {[any]: any?}

local Settings = {
	DisableGlobalEvents = false,
	DisableGlobalEventParams = false,
	DisableLocalEvents = false,
	DisableLocalEventParams = false,
	DisableAttributes = false,
	DisableDestroyConnection = false
}

local GuiManager2d = {}
GuiManager2d.__index = GuiManager2d

GuiManager2d.DraggingBegan = GoodSignal.new()
GuiManager2d.DraggingChanged = GoodSignal.new()
GuiManager2d.DraggingEnded = GoodSignal.new()

GuiManager2d.RotatingBegan = GoodSignal.new()
GuiManager2d.RotatingChanged = GoodSignal.new()
GuiManager2d.RotatingEnded = GoodSignal.new()

local function round(number: number, bound: number): number	
	return math.round(number / bound) * bound
end

local function metaSetup(ui: GuiBase2d): any
	assert(typeof(ui) == "Instance" and ui:IsA("GuiBase2d"), "ui must be a GuiBase2d")
	
	return setmetatable({
		["Ui"] = ui,
		["Connections"] = {},

		["DraggingBegan"] =  GoodSignal.new(),
		["DraggingChanged"] = GoodSignal.new(),
		["DraggingEnded"] = GoodSignal.new(),

		["RotatingBegan"] = GoodSignal.new(),
		["RotatingChanged"] = GoodSignal.new(),
		["RotatingEnded"] = GoodSignal.new()
	}, GuiManager2d)
end

function GuiManager2d.new(name: string, properties: dictionary): dictionary	
	local ui = Instance.new(name)
	local parent = nil
	
	assert(ui:IsA("GuiBase2d"), "ui must be a GuiBase2d")
	
	for prop, value in pairs(properties) do
		if (prop == "Parent") then parent = value continue end
		ui[prop] = value
	end
	
	ui.Parent = parent
	
	local class = metaSetup(ui)
	
	if not Settings.DisableDestroyConnection then
		ui.Destroying:Once(function()
			class:Clear()
		end)
	end
	
	return class
end

function GuiManager2d.setup(ui: GuiBase2d): dictionary
	assert(typeof(ui) == "Instance" and ui:IsA("GuiBase2d"), "ui must be a GuiBase2d")
	
	local class = metaSetup(ui)

	if not Settings.DisableDestroyConnection then
		ui.Destroying:Once(function()
			class:Clear()
		end)
	end

	return class
end

function GuiManager2d.updateSettings(option: setting, value: any)
	Settings[option] = value
end

function GuiManager2d:Tween(goal: dictionary, tweenInfo: TweenInfo?)
	TweenService:Create(self.Ui, (tweenInfo or defaultInfo), goal):Play()
end

function GuiManager2d:TweenChildren(goal: dictionary, tweenInfo: TweenInfo?, recursive: boolean?)
	local uis = if recursive then self.Ui:GetDescendants() else self.Ui:GetChildren()
	table.insert(uis, self.Ui)
	
	for _, ui in ipairs(uis) do
		TweenService:Create(ui, (tweenInfo or defaultInfo), goal):Play()
	end
end

function GuiManager2d:Set(goal: dictionary)
	for prop, value in pairs(goal) do
		self.Ui[prop] = value
	end
end

function GuiManager2d:SetChildren(goal: dictionary, recursive: boolean?)
	local uis = if recursive then self.Ui:GetDescendants() else self.Ui:GetChildren()
	table.insert(uis, self.Ui)

	for _, ui in ipairs(uis) do
		for prop, value in pairs(goal) do
			ui[prop] = value
		end
	end
end

function GuiManager2d:Destroy()
	self.Ui:Destroy()
	
	for _, conn in pairs(self.Connections) do
		if conn.Connected then conn:Disconnect() end
	end
	
	for _, signal in pairs(self) do
		if (type(signal) ~= "table") or (signal["DisconnectAll"] == nil) then continue end
		signal:DisconnectAll()
	end
	
	table.clear(self)
end

function GuiManager2d:Clear(): Instance
	local ui = self.Ui
	
	for _, conn in pairs(self.Connections) do
		if conn.Connected then conn:Disconnect() end
	end
	
	for _, signal in pairs(self) do
		if (type(signal) ~= "table") or (signal["DisconnectAll"] == nil) then continue end
		signal:DisconnectAll()
	end
	
	table.clear(self)
	return ui
end

function GuiManager2d:Draggable(clamp: clamp2D?, grid: grid2D?)	
	assert(typeof(self.Ui.Parent) == "Instance" and self.Ui.Parent:IsA("GuiBase2d"), "The Ui's parent must be a GuiBase2d")
	assert(self.Connections.RotatingChanged == nil, "Rotation must be disabled before Dragging can be enabled")
	
	local draggingConn = self.Connections.DraggingChanged
	
	if draggingConn then
		if (self.Connections.DraggingBegan and self.Connections.DraggingBegan.Connected) then self.Connections.DraggingBegan:Disconnect() end
		if (self.Connections.DraggingChanged and self.Connections.DraggingChanged.Connected) then self.Connections.DraggingChanged:Disconnect() end
		if (self.Connections.DraggingEnded and self.Connections.DraggingEnded.Connected) then self.Connections.DraggingEnded:Disconnect() end
		
		self.Connections.DraggingBegan = nil
		self.Connections.DraggingChanged = nil
		self.Connections.DraggingEnded = nil
	else
		local dragging = false
		local startingPosition: UDim2
		
		self.Connections.DraggingBegan = self.Ui.InputBegan:Connect(function(input: InputObject)
			if (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
				dragging = true
				startingPosition = self.Ui.Position
				
				local globalSelf = if not Settings.DisableGlobalEventParams then self else nil
				
				if not Settings.DisableGlobalEvents then GuiManager2d.DraggingBegan:Fire(globalSelf) end
				if not Settings.DisableLocalEvents then self.DraggingBegan:Fire() end
				if not Settings.DisableAttributes then self.Ui:SetAttribute("Dragging", true) end
			end
		end)
		
		self.Connections.DraggingChanged = UserInputService.InputChanged:Connect(function(input: InputObject, gp: boolean)
			if dragging and not gp and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then

				local absolutePos = self.Ui.Parent.AbsolutePosition
				local mousePos = Vector2.new(input.Position.X - absolutePos.X, input.Position.Y - absolutePos.Y)

				local unit = (mousePos / self.Ui.Parent.AbsoluteSize)
				local scale = UDim2.fromScale(math.clamp(unit.X, 0, 1), math.clamp(unit.Y, 0, 1))

				local roundX = if (grid and grid.X) then round(scale.X.Scale, grid.X) else scale.X.Scale
				local roundY = if (grid and grid.Y) then round(scale.Y.Scale, grid.Y) else scale.Y.Scale
				scale = if (grid and not grid.OnlyOnEnded) then UDim2.fromScale(roundX, roundY) else scale

				local clampX = if (clamp and clamp.X) then math.clamp(scale.X.Scale, clamp.X.X, clamp.X.Y) else scale.X.Scale
				local clampY = if (clamp and clamp.Y) then math.clamp(scale.Y.Scale, clamp.Y.X, clamp.Y.Y) else scale.Y.Scale
				scale = if (clamp and not clamp.OnlyOnEnded) then UDim2.fromScale(clampX, clampY) else scale

				local posDifference = UDim2.fromScale(
					(self.Ui.Position.X.Scale - scale.X.Scale),
					(self.Ui.Position.Y.Scale - scale.Y.Scale)
				)

				self.Ui.Position = scale
				
				local globalSelf = if not Settings.DisableGlobalEventParams then self else nil
				local globalDiff = if not Settings.DisableGlobalEventParams then posDifference else nil
				local localDiff = if not Settings.DisableLocalEventParams then posDifference else nil

				if not Settings.DisableGlobalEvents then GuiManager2d.DraggingChanged:Fire(globalSelf, globalDiff) end
				if not Settings.DisableLocalEvents then self.DraggingChanged:Fire(localDiff) end
			end
		end)
		
		self.Connections.DraggingEnded = self.Ui.InputEnded:Connect(function(input: InputObject)
			if (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
				dragging = false
				
				local absolutePos = self.Ui.Parent.AbsolutePosition
				local mousePos = Vector2.new(input.Position.X - absolutePos.X, input.Position.Y - absolutePos.Y)
				
				local unit = (mousePos / self.Ui.Parent.AbsoluteSize)
				local scale = UDim2.fromScale(math.clamp(unit.X, 0, 1), math.clamp(unit.Y, 0, 1))
				
				local roundX = if (grid and grid.X) then round(scale.X.Scale, grid.X) else scale.X.Scale
				local roundY = if (grid and grid.Y) then round(scale.Y.Scale, grid.Y) else scale.Y.Scale
				scale = if grid then UDim2.fromScale(roundX, roundY) else scale

				local clampX = if (clamp and clamp.X) then math.clamp(scale.X.Scale, clamp.X.X, clamp.X.Y) else scale.X.Scale
				local clampY = if (clamp and clamp.Y) then math.clamp(scale.Y.Scale, clamp.Y.X, clamp.Y.Y) else scale.Y.Scale
				scale = if clamp then UDim2.fromScale(clampX, clampY) else scale
				
				local posDifference = UDim2.fromScale(
					(self.Ui.Position.X.Scale - startingPosition.X.Scale),
					(self.Ui.Position.Y.Scale - startingPosition.Y.Scale)
				)
				
				self.Ui.Position = scale
				
				local globalSelf = if not Settings.DisableGlobalEventParams then self else nil
				local globalDiff = if not Settings.DisableGlobalEventParams then posDifference else nil
				local localDiff = if not Settings.DisableLocalEventParams then posDifference else nil

				if not Settings.DisableGlobalEvents then GuiManager2d.DraggingEnded:Fire(globalSelf, globalDiff) end
				if not Settings.DisableLocalEvents then self.DraggingEnded:Fire(localDiff) end
				if not Settings.DisableAttributes then self.Ui:SetAttribute("Dragging", false) end
			end
		end)
	end
end

function GuiManager2d:Rotatable(clamp: clamp1D?, grid: grid1D?)
	assert(typeof(self.Ui.Parent) == "Instance" and self.Ui.Parent:IsA("GuiBase2d"), "The Ui's parent must be a GuiBase2d")
	assert(self.Connections.DraggingChanged == nil, "Dragging must be disabled before Rotation can be enabled")
	
	local screenGui = self.Ui:FindFirstAncestorWhichIsA("ScreenGui")
	assert(screenGui, "The Ui must have a ScreenGui as an ancestor")
	
	local rotatingConn = self.Connections.RotatingChanged
	
	if rotatingConn then
		if (self.Connections.RotatingBegan and self.Connections.RotatingBegan.Connected) then self.Connections.RotatingBegan:Disconnect() end
		if (self.Connections.RotatingChanged and self.Connections.RotatingChanged.Connected) then self.Connections.RotatingChanged:Disconnect() end
		if (self.Connections.RotatingEnded and self.Connections.RotatingEnded.Connected) then self.Connections.RotatingEnded:Disconnect() end

		self.Connections.RotatingBegan = nil
		self.Connections.RotatingChanged = nil
		self.Connections.RotatingEnded = nil
	else
		local rotating = false
		local startingRotation: number
		
		self.Connections.RotatingBegan = self.Ui.InputBegan:Connect(function(input: InputObject)
			if (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
				rotating = true
				startingRotation = self.Ui.Rotation
				
				local globalSelf = if not Settings.DisableGlobalEventParams then self else nil
				
				if not Settings.DisableGlobalEvents then GuiManager2d.RotatingBegan:Fire(globalSelf) end
				if not Settings.DisableLocalEvents then self.RotatingBegan:Fire() end
				if not Settings.DisableAttributes then self.Ui:SetAttribute("Rotating", true) end
			end
		end)
		
		self.Connections.RotatingChanged = UserInputService.InputChanged:Connect(function(input: InputObject, gp: boolean)
			if rotating and not gp and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then

				local absolutePos = screenGui.AbsolutePosition
				local mousePos = Vector2.new(input.Position.X - absolutePos.X, input.Position.Y - absolutePos.Y)

				local center = self.Ui.AbsolutePosition + (self.Ui.AbsoluteSize / 2)
				local rotation = math.deg(math.atan2(mousePos.Y - center.Y, mousePos.X - center.X))
				
				rotation = if (grid and grid.X and not grid.OnlyOnEnded) then round(rotation, grid.X) else rotation
				rotation = if (clamp and clamp.X and not clamp.OnlyOnEnded) then math.clamp(rotation, clamp.X.X, clamp.X.Y) else rotation
				
				local rotateDifference = (self.Ui.Rotation - rotation)
				
				self.Ui.Rotation = rotation
				
				local globalSelf = if not Settings.DisableGlobalEventParams then self else nil
				local globalDiff = if not Settings.DisableGlobalEventParams then rotateDifference else nil
				local localDiff = if not Settings.DisableLocalEventParams then rotateDifference else nil
				
				if not Settings.DisableGlobalEvents then GuiManager2d.RotatingChanged:Fire(globalSelf, globalDiff) end
				if not Settings.DisableLocalEvents then self.RotatingChanged:Fire(localDiff) end
			end
		end)
		
		self.Connections.RotatingEnded = self.Ui.InputEnded:Connect(function(input: InputObject)
			if (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
				rotating = false
				
				local absolutePos = screenGui.AbsolutePosition
				local mousePos = Vector2.new(input.Position.X - absolutePos.X, input.Position.Y - absolutePos.Y)
				
				local center = self.Ui.AbsolutePosition + (self.Ui.AbsoluteSize / 2)
				local rotation = math.deg(math.atan2(mousePos.Y - center.Y, mousePos.X - center.X))
				
				rotation = if (grid and grid.X) then round(rotation, grid.X) else rotation
				rotation = if (clamp and clamp.X) then math.clamp(rotation, clamp.X.X, clamp.X.Y) else rotation
				
				local rotateDifference = (self.Ui.Rotation - startingRotation)
				
				self.Ui.Rotation = rotation
				
				local globalSelf = if not Settings.DisableGlobalEventParams then self else nil
				local globalDiff = if not Settings.DisableGlobalEventParams then rotateDifference else nil
				local localDiff = if not Settings.DisableLocalEventParams then rotateDifference else nil
				
				if not Settings.DisableGlobalEvents then GuiManager2d.RotatingEnded:Fire(globalSelf, globalDiff) end
				if not Settings.DisableLocalEvents then self.RotatingEnded:Fire(localDiff) end
				if not Settings.DisableAttributes then self.Ui:SetAttribute("Rotating", false) end
			end
		end)
	end
end

function GuiManager2d:Overlap(toSearch: {Instance}, filterList: {GuiBase2d}?, filterType: ("Blacklist" | "Whitelist")?): {GuiBase2d}
	local overlapResult: {GuiBase2d} = {}
	
	local selfTopLeftCorner = self.Ui.AbsolutePosition
	local selfBottomRightCorner = (self.Ui.AbsolutePosition + self.Ui.AbsoluteSize)
	
	for _, object in ipairs(toSearch) do
		if (object ~= self.Ui) and object:IsA("GuiBase2d") then
			if (filterList ~= nil and filterType == "Blacklist") and table.find(filterList, object) then continue end
			if (filterList ~= nil and filterType == "Whitelist") and not table.find(filterList, object) then continue end
			
			local objectTopLeftCorner = object.AbsolutePosition
			local objectBottomRightCorner = (object.AbsolutePosition + object.AbsoluteSize)
			
			if (selfTopLeftCorner.X > objectBottomRightCorner.X) or (objectTopLeftCorner.X > selfBottomRightCorner.X) then continue end
			if (selfTopLeftCorner.Y > objectBottomRightCorner.Y) or (objectTopLeftCorner.Y > selfBottomRightCorner.Y) then continue end
			
			table.insert(overlapResult, object)
		end
	end
	
	return overlapResult
end

return GuiManager2d

--[[
Made by M_dgettMann (@shadowflame63)
GuiManager [v1.4]

How to use each basic function:
	GuiManager2d.new(name, properties) -- Creates the named ui, sets it properties (parents last) and sets up the metatable
	GuiManager2d.setup(ui) -- Sets up a metatable so all these functions can be used on the passed ui Instance
	GuiManager2d.updateSettings(setting, value) -- Sets the passed setting to the passed value

	GuiManager2d:Tween(goal, tweenInfo) -- Tween the values of the given properties inside goal
	GuiManager2d:TweenChildren(goal, tweenInfo, recursive) -- Same as Tween, but includes children (or descendants if recursive is true)
	GuiManager2d:Set(goal) -- Sets the values of the given properties inside goal, without tweening
	GuiManager2d:SetChildren(goal, recursive) -- Same as Set, but includes children (or descendants if recursive is true)
	
	GuiManager2d:Destroy() -- Destroys the ui and clears its associated table
	GuiManager2d:Clear() -- Clears the ui's associated table without destroying the ui itself, returns ui Instance
	
How to use each advanced function:
	GuiManager2d:Draggable(clamp -> { -- Allows ui to be draggable, clamps and grids can be setup to make things like sliders and inventories
		X = Vector2.new(min, max),
		Y = Vector2.new(min, max),
		OnlyOnEnded = boolean
	},
	grid -> {
		X = number,
		Y = number,
		OnlyOnEnded = boolean
	})
	
	GuiManager2d:Rotatable(clamp -> { -- Allows ui to be rotated via dragging, a clamp and grid can be setup
		X = Vector2.new(min, max),
		OnlyOnEnded = boolean
	},
	grid -> {
		X = number,
		OnlyOnEnded = boolean
	})

	GuiManager2d:Overlap(toSearch -> {Instance}, -- Returns an array of ui overlapping the original one, if they are in the 'toSearch' table
		filterList -> {GuiBase2d},
		filterType -> "Blacklist" or "Whitelist"
	)
	
How to use the global events:
	GuiManager2d.DraggingBegan:Connect(function(ui, difference) -- All ui fire this event, useful for knowing everything that is active in one go
		print(ui) -- Prints the table that contains the Ui, it's active Connections and the ScreenGui it's under
		print(difference) -- Prints the difference in Position of the Ui, as a Udim2 value
	end)
	
	> This example applies for DraggingBegan, DraggingChanged, DraggingEnded, RotatingBegan, RotatingChanged and RotatingEnded
	> These events can be disabled by setting 'Gui.DisableGlobalEvents' to true
	
How to use the local events:
	ui.DraggingBegan:Connect(function(difference) -- Only the ui that was used to call the event fires it, so the only parameter is difference
		print(difference) -- Prints the difference in Position of the Ui, as a Udim2 value
	end)
	
	> This example applies for DraggingBegan, DraggingChanged, DraggingEnded, RotatingBegan, RotatingChanged and RotatingEnded
	> These events can be disabled by setting 'Gui.DisableLocalEvents' to true
	
How Settings work:
	GuiManager2d.updateSettings(setting, value) -- Changes the passed setting to the passed value
	
	> DisableGlobalEvents -> When true, all global bindable events associated with gui won't fire
	> DisableGlobalEventParams -> When true, all global events won't pass any parameters along
	> DisableLocalEvents -> When true, all local bindable events associated with gui won't fire
	> DisableGlobalEventParams -> When true, all local events won't pass any parameters along
	> DisableAttributes -> When true, all attributes made by this module won't be set on gui
	> DisableDestroyingConnection -> When true, the Destroying connection won't be made (Only enable this if you're certain Gui won't be destroyed externally)
]]

Thanks for checking this post out and I hope you enjoy this resource! I plan to update it within the foreseeable future, I also plan to make GuiManager3d, GuiUtilities, GuiEffects and GuiAnimator as separate modules at some point.

If you have any ideas or bug reports, reply to this post or message me! :grinning_face_with_smiling_eyes:

31 Likes

This one is your github? shadowflame63

do you think you could insert some code samples or videos of it? it’d be easier for people instead of having to check it themselves in studio beforehand

1 Like

I’ll upload a video showcasing a demo, I’ll also make some code samples along with it to show what’s going on. Thanks for the suggestion. :smiley:

1 Like

sounds good to me, nice module by the way!

1 Like

I’ve added the ‘Showcase and Samples’ section which includes a short video, the code used for the demo and a brief explanation of it. Hope you like it! :grinning_face_with_smiling_eyes:

1 Like

Does the draggable function keep the UI in the said window? Usually you can just drag it too far until it disappears off the screen.

1 Like

It’ll only go as far as the cursor does on-screen, you can also set a clamp so the whole UI stays on-screen.

I wish I found this before I finished making my gui’s :smiling_face_with_tear:

1 Like

The Model and Showcase place have been updated to v1.2. (The Showcase place now also includes all previous versions in ServerStorage) :wink:

It’s been a little while, but v1.3 is out! The Model, Showcase place and Module code have been updated. :grin:

It’d be nice if you added an :attach() function so it can attach to guis thats already been made, no need to do .new()… or does this function already exists and i just missed it in the post?

1 Like

You can use .setup(GuiObject) to apply the class to existing guis, and :Clear() to remove it. :+1:

1 Like

Just a small update this time, v1.4 contains a couple (but useful) additions. Hoping to push a big update for v1.5, may take a while though. Once again, the Model, Showcase place and Module code have been updated. :upside_down_face: