PaddingDrag - Proof of concept draggable gui objects

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:

7 Likes