[V2] PaddingDrag - Proof of concept draggable gui objects

This is a module that uses a UIDragDetector and UIPadding to drag a gui object without actually changing it’s Position property.

It’s typechecked and works for all platforms.

Module:

--[[
	Handles dragging gui objects using UIPadding.
	Version 2.
	Made by BackspaceRGB.
	Devforum post: https://devforum.roblox.com/t/paddingdrag-proof-of-concept-draggable-gui-objects/2964821
]]

local PaddingDrag = {}
local DragFunctions = {}

PaddingDrag.__index = DragFunctions


export type PaddingDrag = typeof(DragFunctions) & {
	Frame: GuiObject,
	Holder: GuiObject,
	Padding: UIPadding,
	DragDetector: UIDragDetector,
	Enabled: boolean,
	Connections: {[string]: RBXScriptConnection},
}


-- Create draggable handler for frame
function PaddingDrag.new(frame: GuiObject, holder: GuiObject, initialState: boolean?): PaddingDrag
	local self = setmetatable({} :: any, PaddingDrag) :: PaddingDrag

	local holderParent = holder.Parent :: GuiBase2d

	local padding = Instance.new("UIPadding")
	padding.Name = "DragOffset"
	padding.Parent = holderParent

	local dragDetector = Instance.new("UIDragDetector")
	dragDetector.ResponseStyle = Enum.UIDragDetectorResponseStyle.CustomOffset
	dragDetector.Parent = frame

	local lastDragUDim2 = UDim2.new(0, 0, 0, 0)

	self.Frame = frame
	self.Holder = holder
	self.Padding = padding
	self.DragDetector = dragDetector
	self.Enabled = initialState or true
	self.Connections = {}

	self.DragDetector.Enabled = self.Enabled

	-- Set last drag UDim2 on drag started
	self.Connections.DragStart = self.DragDetector.DragStart:Connect(function()
		lastDragUDim2 = UDim2.fromOffset(self.Padding.PaddingLeft.Offset, self.Padding.PaddingTop.Offset)
	end)

	-- Set padding to last dragged UDim2 on drag continued
	self.Connections.DragContinue = self.DragDetector.DragContinue:Connect(function()
		local currentDragUDim2 = self.DragDetector.DragUDim2 - lastDragUDim2

		local left = self.Padding.PaddingLeft.Scale + (currentDragUDim2.X.Offset / holderParent.AbsoluteSize.X)
		local top = self.Padding.PaddingTop.Scale + (currentDragUDim2.Y.Offset / holderParent.AbsoluteSize.Y)

		self.Padding.PaddingLeft = UDim.new(left, 0)
		self.Padding.PaddingRight = UDim.new(-left, 0)

		self.Padding.PaddingTop = UDim.new(top, 0)
		self.Padding.PaddingBottom = UDim.new(-top, 0)

		lastDragUDim2 = self.DragDetector.DragUDim2
	end)

	return self
end


-- Set drag enabled state
function DragFunctions.SetEnabled(self: PaddingDrag, isEnabled: boolean): ()
	self.Enabled = isEnabled

	self.DragDetector.Enabled = self.Enabled
end


-- Destroy draggable handler
function DragFunctions.Destroy(self: PaddingDrag): ()
	for _, connection: RBXScriptConnection in self.Connections do
		connection:Disconnect()
	end

	self.Padding:Destroy()
	self.DragDetector:Destroy()

	setmetatable(self, nil)
end


return PaddingDrag

Be mindful if you have multiple frames in the same parent gui, as all of them will get dragged.
Also, the dragged position is scaled, meaning that changing your Roblox application window size would keep the gui in the same place as it was.

Example usage: (Exactly the same as old version)

local PaddingDrag = require(PathToPaddingDrag)

local WindowGui = game.Players.LocalPlayer.PlayerGui:WaitForChild("Window")

local DragHandler = PaddingDrag.new(
	WindowGui.Main.Titlebar, -- The gui element you need click and hold, like a window titlebar
	WindowGui.Main, -- The ancestor gui element that will get dragged, like the entire window
	true -- Initial enabled state
)

task.wait(5)
DragHandler:SetEnabled(false) -- Stop dragging

Result:

Old post

There is a pretty long story to this. I was looking for ways to make draggable ui that also worked for mobile and the gamepad virtual cursor. I found that using UIPadding for this actually makes sense. If you set the LeftPadding to something like 0, 50 and the opposite padding (RightPadding) to 0, -50, then your gui will NOT change in size. It seems like using UIPadding is more accurate than just adding the mouse delta to the gui position, and it does not actually change the Position property on the gui.

Here is the module:

--[[
	Handles dragging gui objects using UIPadding.
	Made by BackspaceRGB.
	Devforum post: https://devforum.roblox.com/t/paddingdrag-proof-of-concept-draggable-gui-objects/2964821
]]

local PaddingDrag = {}
local DragFunctions = {}

PaddingDrag.__index = DragFunctions

local UserInputService = game:GetService("UserInputService")

local currentDragging: PaddingDrag
local mouseStartingPosition: Vector2

local isTouching = false

local paddingStartLeft: UDim
local paddingStartTop: UDim


export type PaddingDrag = typeof(DragFunctions) & {
	Frame: GuiObject,
	Holder: GuiObject,
	Padding: UIPadding,
	Enabled: boolean,
}


-- Create draggable handler for frame
function PaddingDrag.new(frame: GuiObject, holder: GuiObject, initialState: boolean?): PaddingDrag
	local self = setmetatable({} :: any, PaddingDrag) :: PaddingDrag

	local padding = Instance.new("UIPadding")
	padding.Name = "DragOffset"
	padding.Parent = holder.Parent

	self.Frame = frame
	self.Holder = holder
	self.Padding = padding
	self.Enabled = initialState or true

	-- Start dragging
	self.Frame.InputBegan:Connect(function(inputObject: InputObject)
		if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch then
			if currentDragging ~= nil or isTouching == true or self.Enabled == false then
				return
			end

			self.Holder.Interactable = false

			currentDragging = self

			paddingStartLeft = self.Padding.PaddingLeft
			paddingStartTop = self.Padding.PaddingTop

			mouseStartingPosition = UserInputService:GetMouseLocation()
		end
	end)

	return self
end


-- Set drag enabled state
function DragFunctions.SetEnabled(self: PaddingDrag, isEnabled: boolean): ()
	self.Enabled = isEnabled

	if isEnabled == false and currentDragging == self then -- Stop dragging
		currentDragging = nil
	end
end


-- Prevent draggable objects from "sticking" to your finger on mobile
UserInputService.InputBegan:Connect(function(inputObject: InputObject)
	if inputObject.UserInputType == Enum.UserInputType.Touch then

		-- Not deferring this would cause dragging to be impossible on mobile
		task.defer(function()
			isTouching = true
		end)
	end
end)


-- Drag input changed
UserInputService.InputChanged:Connect(function(inputObject: InputObject)
	if currentDragging ~= nil then
		if inputObject.UserInputType == Enum.UserInputType.MouseMovement or inputObject.UserInputType == Enum.UserInputType.Touch or inputObject.KeyCode == Enum.KeyCode.Thumbstick1 then
			local delta = UserInputService:GetMouseLocation() - mouseStartingPosition

			local left = paddingStartLeft.Scale + (delta.X / currentDragging.Padding.Parent.AbsoluteSize.X)
			local top = paddingStartTop.Scale + (delta.Y / currentDragging.Padding.Parent.AbsoluteSize.Y)

			currentDragging.Padding.PaddingLeft = UDim.new(left, 0)
			currentDragging.Padding.PaddingTop = UDim.new(top, 0)

			currentDragging.Padding.PaddingRight = UDim.new(-left, 0)
			currentDragging.Padding.PaddingBottom = UDim.new(-top, 0)
		end
	end
end)


-- Drag input ended
UserInputService.InputEnded:Connect(function(inputObject: InputObject)
	if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch or inputObject.KeyCode == Enum.KeyCode.ButtonA or inputObject.KeyCode == Enum.KeyCode.ButtonR2 then
		if currentDragging ~= nil then
			currentDragging.Holder.Interactable = true

			currentDragging = nil
		end
	end

	if inputObject.UserInputType == Enum.UserInputType.Touch then
		isTouching = false
	end
end)


return PaddingDrag

It’s typechecked, works for PC, mobile and gamepad (virtual cursor only)

Be mindful if you have multiple frames in the same parent gui, as all of them will get dragged.
Also, the dragged position is scaled, meaning that changing your Roblox application window size would keep the gui in the same place as it was.

Example usage:

local PaddingDrag = require(PathToPaddingDrag)

local WindowGui = game.Players.LocalPlayer.PlayerGui:WaitForChild("Window")

local DragHandler = PaddingDrag.new(
	WindowGui.Main.Titlebar, -- The gui element you need click and hold, like a window titlebar
	WindowGui.Main, -- The ancestor gui element that will get dragged, like the entire window
	true -- Initial enabled state
)

task.wait(5)
DragHandler:SetEnabled(false) -- Stop dragging

Result:

11 Likes

Thought I would update this module to work with the new UIDragDetectors. It also shortens the source code quite a bit, and adds a Destroy method.

If you used version 1, everything should be compatible with version 2 with no code changes needed on your end.

That is all.

1 Like