diff --git a/src/draggable.luau b/src/draggable.luau index 409a6ff..eef1f5a 100644 --- a/src/draggable.luau +++ b/src/draggable.luau @@ -1,198 +1,91 @@ +-- src/draggable.luau local UserInputService = game and game:GetService("UserInputService") or nil -local function clamp(x, a, b) - if x < a then return a end - if x > b then return b end - return x -end +-- Factory: inject vide.action and vide.cleanup to avoid cyclic requires +return function(action, cleanup) + return function(opts) + opts = opts or {} + local axis = opts.axis or "both" -- "x" | "y" | "both" -local function lerp(a, b, t) - return a + (b - a) * t -end + return action(function(gui) + -- allow require in non-Roblox test environments + if not UserInputService then + return + end -local function invLerp(a, b, v) - if a == b then return 0 end - return (v - a) / (b - a) -end + if typeof(gui) ~= "Instance" or not gui:IsA("GuiObject") then + return + end -local function quantize(v, step) - if not step or step <= 0 then return v end - return math.floor((v / step) + 0.5) * step -end + local dragging = false + local dragInput + local dragStart + local startPos --- Factory: inject Vide functions to avoid cyclic requires -return function(create, source, action, cleanup) - return function(props) - props = props or {} + local function update(input) + local delta = input.Position - dragStart - local minV = props.min or 0 - local maxV = props.max or 1 - local step = props.step - local value = props.value - assert(value, "vide.slider: props.value (source) is required") + local xOff = startPos.X.Offset + local yOff = startPos.Y.Offset - local theme = props.theme or {} - local format = props.format or function(v) return tostring(v) end - local onChanged = props.onChanged + if axis == "x" or axis == "both" then + xOff = startPos.X.Offset + delta.X + 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) - local track = trackRef() - if not track then return end + if opts.onDrag then + opts.onDrag(gui.Position) + end + end - local absPosX = track.AbsolutePosition.X - local absSizeX = track.AbsoluteSize.X - if absSizeX <= 0 then return end + local beganConn = gui.InputBegan:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then + dragging = true + dragStart = input.Position + startPos = gui.Position - local t = clamp((screenX - absPosX) / absSizeX, 0, 1) - local v = lerp(minV, maxV, t) - v = quantize(v, step) + if opts.onDragStart then + opts.onDragStart(startPos) + end - value(v) - if onChanged then onChanged(v) end - end - - return create "Frame" { - Name = props.name or "Slider", - Size = props.size or UDim2.fromOffset(516, 90), - Position = props.position, - 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) + local changedConn + changedConn = input.Changed:Connect(function() + if input.UserInputState == Enum.UserInputState.End then + dragging = false + if changedConn then changedConn:Disconnect() end + if opts.onDragEnd then + opts.onDragEnd(gui.Position) + end end end) cleanup(function() - if trackRef() == track then trackRef(nil) end - trackBeganConn:Disconnect() + if changedConn then changedConn:Disconnect() end end) - end), + end + end) - create "UICorner" { CornerRadius = UDim.new(0, theme.cornerRadius or 8) }, - create "UIStroke" { - Thickness = 1, - Color = theme.strokeColor or Color3.fromRGB(255, 255, 255), - Transparency = theme.strokeTransparency or 0.8, - }, + local changedConn = gui.InputChanged:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then + dragInput = input + end + end) - create "Frame" { - Name = "Fill", - Size = function() - local t = clamp(invLerp(minV, maxV, value()), 0, 1) - return UDim2.fromScale(t, 1) - 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) }, - }, + local uisConn = UserInputService.InputChanged:Connect(function(input) + if input == dragInput and dragging then + update(input) + end + end) - create "TextButton" { - Name = "Knob", - AutoButtonColor = false, - Text = "", - Size = UDim2.fromOffset(24, 24), - AnchorPoint = Vector2.new(0.5, 0.5), - 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), - }, - }, - } + cleanup(function() + beganConn:Disconnect() + changedConn:Disconnect() + uisConn:Disconnect() + end) + end) end end