diff --git a/slider.luau b/slider.luau new file mode 100644 index 0000000..9bc5674 --- /dev/null +++ b/slider.luau @@ -0,0 +1,189 @@ +-- vide/slider.luau +local UserInputService = game:GetService("UserInputService") + +local function clamp(x, a, b) + if x < a then return a end + if x > b then return b end + return x +end + +local function lerp(a, b, t) + return a + (b - a) * t +end + +local function invLerp(a, b, v) + if a == b then return 0 end + return (v - a) / (b - a) +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 + +-- Factory: inject Vide functions to avoid cyclic requires +return function(create, source, action, cleanup) + return function(props) + props = props or {} + + 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 theme = props.theme or {} + local format = props.format or function(v) return tostring(v) end + local onChanged = props.onChanged + + local trackRef = source(nil) + + local function setFromX(screenX) + local track = trackRef() + if not track then return end + + local absPosX = track.AbsolutePosition.X + local absSizeX = track.AbsoluteSize.X + if absSizeX <= 0 then return end + + local t = clamp((screenX - absPosX) / absSizeX, 0, 1) + local v = lerp(minV, maxV, t) + v = quantize(v, step) + + 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) + end + end) + + cleanup(function() + if trackRef() == track then trackRef(nil) end + trackBeganConn:Disconnect() + 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, + }, + + 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) }, + }, + + 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) + 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