Refactor draggable module and cleanup functions

Refactor draggable functionality to improve clarity and maintainability.
This commit is contained in:
sindri 2026-03-03 09:56:08 +01:00 committed by GitHub
parent 5362f7ecee
commit 7d89cb5a1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,198 +1,91 @@
-- src/draggable.luau
local UserInputService = game and game:GetService("UserInputService") or nil local UserInputService = game and game:GetService("UserInputService") or nil
local function clamp(x, a, b) -- Factory: inject vide.action and vide.cleanup to avoid cyclic requires
if x < a then return a end return function(action, cleanup)
if x > b then return b end return function(opts)
return x opts = opts or {}
end local axis = opts.axis or "both" -- "x" | "y" | "both"
local function lerp(a, b, t) return action(function(gui)
return a + (b - a) * t -- allow require in non-Roblox test environments
end if not UserInputService then
return
end
local function invLerp(a, b, v) if typeof(gui) ~= "Instance" or not gui:IsA("GuiObject") then
if a == b then return 0 end return
return (v - a) / (b - a) end
end
local function quantize(v, step) local dragging = false
if not step or step <= 0 then return v end local dragInput
return math.floor((v / step) + 0.5) * step local dragStart
end local startPos
-- Factory: inject Vide functions to avoid cyclic requires local function update(input)
return function(create, source, action, cleanup) local delta = input.Position - dragStart
return function(props)
props = props or {}
local minV = props.min or 0 local xOff = startPos.X.Offset
local maxV = props.max or 1 local yOff = startPos.Y.Offset
local step = props.step
local value = props.value
assert(value, "vide.slider: props.value (source<number>) is required")
local theme = props.theme or {} if axis == "x" or axis == "both" then
local format = props.format or function(v) return tostring(v) end xOff = startPos.X.Offset + delta.X
local onChanged = props.onChanged end
if axis == "y" or axis == "both" then
yOff = startPos.Y.Offset + delta.Y
end
local trackRef = source(nil) gui.Position = UDim2.new(startPos.X.Scale, xOff, startPos.Y.Scale, yOff)
local function setFromX(screenX) if opts.onDrag then
local track = trackRef() opts.onDrag(gui.Position)
if not track then return end end
end
local absPosX = track.AbsolutePosition.X local beganConn = gui.InputBegan:Connect(function(input)
local absSizeX = track.AbsoluteSize.X if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
if absSizeX <= 0 then return end dragging = true
dragStart = input.Position
startPos = gui.Position
local t = clamp((screenX - absPosX) / absSizeX, 0, 1) if opts.onDragStart then
local v = lerp(minV, maxV, t) opts.onDragStart(startPos)
v = quantize(v, step) end
value(v) local changedConn
if onChanged then onChanged(v) end changedConn = input.Changed:Connect(function()
end if input.UserInputState == Enum.UserInputState.End then
dragging = false
return create "Frame" { if changedConn then changedConn:Disconnect() end
Name = props.name or "Slider", if opts.onDragEnd then
Size = props.size or UDim2.fromOffset(516, 90), opts.onDragEnd(gui.Position)
Position = props.position, end
AnchorPoint = props.anchorPoint,
LayoutOrder = props.layoutOrder,
BackgroundTransparency = 1,
create "TextLabel" {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(516, 24),
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = props.fontFace,
TextSize = props.labelTextSize or 16,
TextColor3 = props.labelTextColor or Color3.fromRGB(235, 238, 245),
Text = props.label or "Slider",
},
create "TextLabel" {
BackgroundTransparency = 1,
Size = UDim2.fromOffset(516, 24),
TextXAlignment = Enum.TextXAlignment.Right,
FontFace = props.fontFace,
TextSize = props.valueTextSize or 16,
TextColor3 = props.valueTextColor or Color3.fromRGB(200, 206, 220),
Text = function()
return format(value())
end,
},
create "Frame" {
Name = "Track",
Position = UDim2.fromOffset(0, 38),
Size = UDim2.fromOffset(516, 16),
BackgroundColor3 = theme.trackColor or Color3.fromRGB(20, 22, 28),
BackgroundTransparency = theme.trackTransparency or 0.15,
action(function(track)
trackRef(track)
local trackBeganConn = track.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
setFromX(input.Position.X)
end end
end) end)
cleanup(function() cleanup(function()
if trackRef() == track then trackRef(nil) end if changedConn then changedConn:Disconnect() end
trackBeganConn:Disconnect()
end) end)
end), end
end)
create "UICorner" { CornerRadius = UDim.new(0, theme.cornerRadius or 8) }, local changedConn = gui.InputChanged:Connect(function(input)
create "UIStroke" { if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
Thickness = 1, dragInput = input
Color = theme.strokeColor or Color3.fromRGB(255, 255, 255), end
Transparency = theme.strokeTransparency or 0.8, end)
},
create "Frame" { local uisConn = UserInputService.InputChanged:Connect(function(input)
Name = "Fill", if input == dragInput and dragging then
Size = function() update(input)
local t = clamp(invLerp(minV, maxV, value()), 0, 1) end
return UDim2.fromScale(t, 1) end)
end,
BackgroundColor3 = theme.fillColor or Color3.fromRGB(80, 140, 255),
BackgroundTransparency = theme.fillTransparency or 0.15,
create "UICorner" { CornerRadius = UDim.new(0, theme.cornerRadius or 8) },
},
create "TextButton" { cleanup(function()
Name = "Knob", beganConn:Disconnect()
AutoButtonColor = false, changedConn:Disconnect()
Text = "", uisConn:Disconnect()
Size = UDim2.fromOffset(24, 24), end)
AnchorPoint = Vector2.new(0.5, 0.5), end)
Position = function()
local t = clamp(invLerp(minV, maxV, value()), 0, 1)
return UDim2.fromScale(t, 0.5)
end,
BackgroundColor3 = theme.knobColor or Color3.fromRGB(235, 238, 245),
BackgroundTransparency = theme.knobTransparency or 0,
create "UICorner" { CornerRadius = UDim.new(0, theme.knobRadius or 999) },
create "UIStroke" { Thickness = 1, Color = Color3.fromRGB(0, 0, 0), Transparency = 0.75 },
action(function(btn)
-- Non-Roblox / unit-test environment: allow requiring this module without crashing.
-- Slider still renders; dragging is disabled when UserInputService is unavailable.
if not UserInputService then
return
end
if typeof(btn) ~= "Instance" or not btn:IsA("GuiButton") then
return
end
local dragInput
local dragging = false
local beganConn
local changedConn
local uisChangedConn
local function update(input)
setFromX(input.Position.X)
end
beganConn = btn.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
update(input)
changedConn = input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then
dragging = false
end
end)
end
end)
btn.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
dragInput = input
end
end)
uisChangedConn = UserInputService.InputChanged:Connect(function(input)
if input == dragInput and dragging then
update(input)
end
end)
cleanup(function()
if beganConn then beganConn:Disconnect() end
if changedConn then changedConn:Disconnect() end
if uisChangedConn then uisChangedConn:Disconnect() end
end)
end),
},
},
}
end end
end end