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: