--[[
    A color picker containing a hue ring and saturation-value plane
    with color harmony guides
]]

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local Style = require(CommonModules.Style)

local CommonIncludes = Common.Includes
local Color = require(CommonIncludes.Color).Color
local Roact = require(CommonIncludes.RoactRodux.Roact)
local RoactRodux = require(CommonIncludes.RoactRodux.RoactRodux)

local CommonComponents = Common.Components
local ConnectTheme = require(CommonComponents.ConnectTheme)
local StandardUICorner = require(CommonComponents.StandardComponents.UICorner)

local Modules = root.Modules
local ColorEditorInputSignals = require(Modules.EditorInputSignals).ColorEditor
local Enums = require(Modules.Enums)

local Components = root.Components
local ButtonBar = require(Components.ButtonBar)

---

local EDITOR_KEY = Enums.EditorKey.ColorWheel
local ANALOGY_ANGLE = math.deg(math.pi / 6)

local harmonies = {
    {
        name = "None",
        image = Style.Images.NoHarmonyButtonIcon,
    },

    {
        name = "Complementary",
        image = Style.Images.ComplementaryHarmonyButtonIcon,
    },

    {
        name = "Analogous",
        image = Style.Images.AnalogousHarmonyButtonIcon,
    },

    {
        name = "Triadic",
        image = Style.Images.TriadicHarmonyButtonIcon,
    },

    {
        name = "SplitComplementary",
        image = Style.Images.SplitComplementaryButtonIcon,
    },

    {
        name = "Square",
        image = Style.Images.SquareHarmonyButtonIcon,
    },

    {
        name = "Tetradic",
        image = Style.Images.TetradicHarmonyButtonIcon,
    },

    {
        name = "Hexagon",
        image = Style.Images.HexagonalHarmonyButtonIcon,
    },
}

---

--[[
    props
        angle: number
        ringWidth: number
        wheelRadius: Binding<number>

        onActivated: () -> nil

    store props
        theme: StudioTheme
]]

local HueHarmonyMarker = Roact.PureComponent:extend("HueHarmonyMarker")

HueHarmonyMarker.render = function(self)
    local theme = self.props.theme

    local angle = self.props.angle
    local ringWidth = self.props.ringWidth

    return Roact.createElement("TextButton", {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Size = Style.UDim2.MarkerSize,

        Position = self.props.wheelRadius:map(function(wheelRadius)
            local rad = math.rad(angle)

            return UDim2.new(
                0.5, math.cos(rad) * (wheelRadius - (ringWidth / 2)),
                0.5, -math.sin(rad) * (wheelRadius - (ringWidth / 2))
            )
        end),

        BackgroundTransparency = 0,
        BorderSizePixel = 0,
        AutoButtonColor = false,
        ZIndex = 2,

        Text = "",
        TextTransparency = 1,

        BackgroundColor3 = Color.fromHSB(angle, 1, 1):bestContrastingColor(
            Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)),
            Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)):invert()
        ):toColor3(),

        [Roact.Event.Activated] = self.props.onActivated,
    })
end

---

--[[
    props

        ringWidth: number

    store props

        theme: StudioTheme
        color: Color
        editor: string
        harmony: number

        setColor: (Color) -> nil
        setHarmony: (number) -> nil
]]

local ColorWheel = Roact.PureComponent:extend("ColorWheel")

ColorWheel.init = function(self, initProps)
    local initH, initS, initB = initProps.color:toHSB()

    self.planeSize, self.updatePlaneSize = Roact.createBinding(Vector2.new(0, 0))
    self.wheelCenter, self.updateWheelCenter = Roact.createBinding(Vector2.new(0, 0))
    self.wheelRadius, self.updateWheelRadius = Roact.createBinding(0)

    self.updateH = function(cursorPosition)
        local distanceFromWheelCenter = self.wheelCenter:getValue() - cursorPosition
        local mouseAngle = math.atan2(distanceFromWheelCenter.Y, -distanceFromWheelCenter.X)

        local hueAngle
        local h

        -- calculate angle from [0, 2pi)
        if ((mouseAngle >= 0) and (mouseAngle <= math.pi)) then
            hueAngle = mouseAngle
        else
            hueAngle = mouseAngle + (2 * math.pi)
        end

        h = math.deg(hueAngle)

        self:setState({
            captureFocus = if (self.props.editor ~= EDITOR_KEY) then true else nil,
            h = h,
        })

        self.props.setColor(Color.fromHSB(h, self.state.s, self.state.b))
    end

    self.updateSB = function(cursorPosition)
        local distanceFromWheelCenter = cursorPosition - self.wheelCenter:getValue()
        local planeSize = self.planeSize:getValue()

        local s = math.clamp((distanceFromWheelCenter.X / planeSize) + 0.5, 0, 1)
        local b = 1 - math.clamp((distanceFromWheelCenter.Y / planeSize) + 0.5, 0, 1)

        self:setState({
            captureFocus = if (self.props.editor ~= EDITOR_KEY) then true else nil,
            s = s,
            b = b
        })

        self.props.setColor(Color.fromHSB(self.state.h, s, b))
    end

    self:setState({
        trackingH = false,
        trackingSB = false,

        h = if (initH ~= initH) then 0 else initH,
        s = initS,
        b = initB,
    })
end

ColorWheel.getDerivedStateFromProps = function(props, state)
    if (props.editor == EDITOR_KEY) then return end

    if (state.captureFocus) then
        return {
            captureFocus = Roact.None,
        }
    end

    local h, s, b = props.color:toHSB()
    if ((h == state.h) and (s == state.s) and (b == state.b)) then return end

    return {
        h = if (h ~= state.h) then
            (if (h ~= h) then 0 else h)
        else nil,

        s = if (s ~= state.s) then s else nil,
        b = if (b ~= state.b) then b else nil,
    }
end

ColorWheel.didMount = function(self)
    self.mousePositionChanged = ColorEditorInputSignals.MousePositionChanged.Event:subscribe(function(cursorPosition: Vector2)
        if (self.state.trackingH) then
            self.updateH(cursorPosition)
        end

        if (self.state.trackingSB) then
            self.updateSB(cursorPosition)
        end
    end)
end

ColorWheel.willUnmount = function(self)
    self.mousePositionChanged:unsubscribe()
end

ColorWheel.render = function(self)
    local theme = self.props.theme
    local editor = self.props.editor
    local harmonyIndex = self.props.harmony

    local h, s, b = self.state.h, self.state.s, self.state.b

    local color = Color.fromHSB(h, s, b)
    local pureHueColor = Color.fromHSB(h, 1, 1)

    local hRad = math.rad(h)

    local hueMarkers = {
        PrimaryMarker = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0.5),
            BackgroundTransparency = 0,
            BorderSizePixel = 0,
            ZIndex = 2,

            Size = UDim2.new(
                0, self.props.ringWidth + (if self.state.trackingH then 10 else -10),
                0, self.props.ringWidth + (if self.state.trackingH then 10 else -10)
            ),

            Position = self.wheelRadius:map(function(wheelRadius)
                return UDim2.new(
                    0.5, math.cos(hRad) * (wheelRadius - (self.props.ringWidth / 2)),
                    0.5, -math.sin(hRad) * (wheelRadius - (self.props.ringWidth / 2))
                )
            end),

            BackgroundColor3 = pureHueColor:bestContrastingColor(
                Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)),
                Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)):invert()
            ):toColor3(),
        }, {
            UICorner = Roact.createElement(StandardUICorner, { circular = true }),

            Indicator = if self.state.trackingH then
                Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0.5, 0.5),
                    Position = UDim2.new(0.5, 0, 0.5, 0),
                    Size = UDim2.new(1, -10, 1, -10),

                    BackgroundColor3 = pureHueColor:toColor3()
                }, {
                    UICorner = Roact.createElement(StandardUICorner, { circular = true }),
                })
            else nil
        })
    }

    if (harmonies[harmonyIndex]) then
        local harmony = harmonies[harmonyIndex]
        local harmonicColors

        if (harmony.name == "None") then
            harmonicColors = {}
        elseif (harmony.name == "Hexagon") then
            harmonicColors = {}

            for i = 1, 5 do
                table.insert(harmonicColors, Color.fromHSB((h + (360 / 6 * i) % 360), 1, 1))
            end
        else
            harmonicColors = pureHueColor:harmonies(harmony.name, ANALOGY_ANGLE)
        end

        for i = 1, #harmonicColors do
            local harmonicColor = harmonicColors[i]
            local harmonicH = harmonicColor:toHSB()
            harmonicH = if (harmonicH ~= harmonicH) then 0 else harmonicH

            hueMarkers[i] = Roact.createElement(HueHarmonyMarker, {
                angle = harmonicH,
                wheelRadius = self.wheelRadius,
                ringWidth = self.props.ringWidth,

                onActivated = function()
                    self:setState({
                        captureFocus = if (editor ~= EDITOR_KEY) then true else nil,
                        h = harmonicH,
                    })

                    self.props.setColor(Color.fromHSB(harmonicH, s, b))
                end,
            })
        end
    end

    return Roact.createFragment({
        HarmonyOptions = Roact.createElement(ButtonBar, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = Style.UDim2.ButtonBarSize,

            displayType = "image",
            selected = harmonyIndex,
            buttons = harmonies,
            onButtonActivated = self.props.setHarmony,
        }),

        WheelContainer = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0),
            Size = UDim2.new(1, 0, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.MajorElementPadding)),
            Position = UDim2.new(0.5, 0, 0, 0),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            ClipsDescendants = false,
        }, {
            -- h-wheel, sv-plane
            HueWheel = Roact.createElement("ImageLabel", {
                AnchorPoint = Vector2.new(0.5, 0.5),
                Size = UDim2.new(1, -0, 1, -0),
                Position = UDim2.new(0.5, 0, 0.5, 0),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
                ClipsDescendants = false,

                Image = Style.Images.HueWheel,

                [Roact.Event.InputBegan] = function(obj, input)
                    if (input.UserInputType ~= Enum.UserInputType.MouseButton1) then return end

                    local inputPosition = input.Position
                    local mousePosition = Vector2.new(inputPosition.X, inputPosition.Y)

                    local hueWheelSize = obj.AbsoluteSize
                    local upperRadius = hueWheelSize.X / 2
                    local lowerRadius =  upperRadius - self.props.ringWidth

                    local distanceFromWheelCenter = mousePosition - self.wheelCenter:getValue()

                    if ((distanceFromWheelCenter.Magnitude >= lowerRadius) and (distanceFromWheelCenter.Magnitude <= upperRadius)) then
                        self:setState({
                            trackingH = true,
                        })

                        self.updateH(Vector2.new(inputPosition.X, inputPosition.Y))
                    end
                end,

                [Roact.Event.InputEnded] = function(_, input)
                    if (input.UserInputType ~= Enum.UserInputType.MouseButton1) then return end
                    if (not self.state.trackingH) then return end

                    self:setState({
                        trackingH = false,
                    })
                end,

                [Roact.Change.AbsolutePosition] = function(obj)
                    local hueWheelPosition = obj.AbsolutePosition
                    local hueWheelSize = obj.AbsoluteSize

                    self.updateWheelCenter(hueWheelPosition + (hueWheelSize / 2))
                    self.updateWheelRadius(hueWheelSize.X / 2)
                end,

                [Roact.Change.AbsoluteSize] = function(obj)
                    local hueWheelPosition = obj.AbsolutePosition
                    local hueWheelSize = obj.AbsoluteSize

                    self.updateWheelCenter(hueWheelPosition + (hueWheelSize / 2))
                    self.updateWheelRadius(hueWheelSize.X / 2)
                end,
            }, {
                UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
                    AspectRatio = 1,
                    AspectType = Enum.AspectType.FitWithinMaxSize,
                    DominantAxis = Enum.DominantAxis.Width
                }),

                UICorner = Roact.createElement(StandardUICorner, { circular = true }),

                RingOverlay = Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0.5, 0.5),
                    Size = UDim2.new(1, -(self.props.ringWidth * 2), 1, -(self.props.ringWidth * 2)),
                    Position = UDim2.new(0.5, 0, 0.5, 0),
                    BackgroundTransparency = 0,
                    BorderSizePixel = 0,

                    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)
                }, {
                    UICorner = Roact.createElement(StandardUICorner, { circular = true }),
                }),

                Markers = Roact.createFragment(hueMarkers)
            }),

            SBPlaneContainer = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 0.5),
                Size = UDim2.new(1, -(self.props.ringWidth * 2) - 0, 1, -(self.props.ringWidth * 2) - 0),
                Position = UDim2.new(0.5, 0, 0.5, 0),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
                ClipsDescendants = false,
                ZIndex = 2,
            }, {
                UIAspectRatioConstraint = Roact.createElement("UIAspectRatioConstraint", {
                    AspectRatio = 1,
                    AspectType = Enum.AspectType.FitWithinMaxSize,
                    DominantAxis = Enum.DominantAxis.Width
                }),

                SBPlane = Roact.createElement("ImageLabel", {
                    AnchorPoint = Vector2.new(0.5, 0.5),
                    --[[
                        the side length of largest square that fits inside a circle can be found with
                        sqrt(d/2), where d is the diameter of the circle
                    ]]
                    Size = UDim2.new(math.sqrt(1/2), -8, math.sqrt(1/2), -8),
                    Position = UDim2.new(0.5, 0, 0.5, 0),
                    BackgroundTransparency = 0,
                    BorderSizePixel = 0,
                    ClipsDescendants = false,
                    Image = Style.Images.SBPlane,

                    BackgroundColor3 = Color3.new(1, 1, 1),
                    ImageColor3 = pureHueColor:toColor3(),

                    [Roact.Event.InputBegan] = function(_, input)
                        if (input.UserInputType ~= Enum.UserInputType.MouseButton1) then return end
                        if (self.state.trackingH) then return end

                        local inputPosition = input.Position

                        self:setState({
                            trackingSB = true,
                        })

                        self.updateSB(Vector2.new(inputPosition.X, inputPosition.Y))
                    end,

                    [Roact.Event.InputEnded] = function(_, input)
                        if (input.UserInputType ~= Enum.UserInputType.MouseButton1) then return end
                        if (not self.state.trackingSB) then return end

                        self:setState({
                            trackingSB = false,
                        })
                    end,

                    [Roact.Change.AbsoluteSize] = function(obj)
                        self.updatePlaneSize(obj.AbsoluteSize.X)
                    end,
                }, {
                    VGradient = Roact.createElement("UIGradient", {
                        Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.new(0, 0, 0)),
                        Offset = Vector2.new(0, 0),
                        Rotation = 90,
                        Transparency = NumberSequence.new(0),
                        Enabled = true,
                    }),

                    MarkerContainer = Roact.createElement("Frame", {
                        AnchorPoint = Vector2.new(0.5, 0.5),
                        Position = UDim2.new(s, 0, 1 - b, 0),
                        Size = UDim2.new(0, 10, 0, 10),
                        BackgroundTransparency = 0,
                        BorderSizePixel = 0,

                        BackgroundColor3 = color:bestContrastingColor(
                            Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)),
                            Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)):invert()
                        ):toColor3(),
                    }, {
                        UICorner = Roact.createElement(StandardUICorner, { circular = true }),

                        Indicator = Roact.createElement("Frame", {
                            AnchorPoint = Vector2.new(0.5, 0.5),
                            Size = UDim2.new(1, -2, 1, -2),
                            Position = UDim2.new(0.5, 0, 0.5, 0),
                            BackgroundTransparency = 0,
                            BorderSizePixel = 0,

                            BackgroundColor3 = color:toColor3(),
                        }, {
                            UICorner = Roact.createElement(StandardUICorner, { circular = true }),
                        })
                    })
                })
            })
        }),
    })
end

---

HueHarmonyMarker = ConnectTheme(HueHarmonyMarker)

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
        color = state.colorEditor.color,
        editor = state.colorEditor.authoritativeEditor,
        harmony = state.sessionData.lastHueHarmony,
    }
end, function(dispatch)
    return {
        setColor = function(newColor)
            dispatch({
                type = Enums.StoreActionType.ColorEditor_SetColor,
                color = newColor,
                editor = EDITOR_KEY
            })
        end,

        setHarmony = function(newHarmony)
            dispatch({
                type = Enums.StoreActionType.UpdateSessionData,
                slice = {
                    lastHueHarmony = newHarmony
                }
            })
        end
    }
end)(ColorWheel)


-- A page for exporting a palette

local HttpService = game:GetService("HttpService")
local Selection = game:GetService("Selection")
local ServerStorage = game:GetService("ServerStorage")

---

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)

local CommonIncludes = Common.Includes
local Roact = require(CommonIncludes.RoactRodux.Roact)
local RoactRodux = require(CommonIncludes.RoactRodux.RoactRodux)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local RadioButtonGroup = require(CommonComponents.RadioButtonGroup)

local StandardComponents = CommonComponents.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIListLayout = require(StandardComponents.UIListLayout)

local Modules = root.Modules
local Util = require(Modules.Util)

local Components = root.Components
local SelectionComponent = require(Components.Selection)

---

local exportTypes = { "ModuleScript", "StringValue" }

local uiTranslations = Translator.GenerateTranslationTable({
    "ExportScriptInjection_Warning",
    "ModuleScriptExport_Label",

    "Export_ButtonText",
    "Cancel_ButtonText",
})

---

--[[
    props
        paletteIndex: number?
        onPromptClosed: (boolean) -> nil

    store props
        theme: StudioTheme
        palettes: array<Palette>
]]

local ExportPalette = Roact.PureComponent:extend("ExportPalette")

ExportPalette.init = function(self, initProps)
    self:setState({
        dropdownExpanded = false,
        paletteIndex = initProps.paletteIndex or 1,
    })
end

ExportPalette.render = function(self)
    local theme = self.props.theme
    local palettes = self.props.palettes

    local dropdownExpanded = self.state.dropdownExpanded
    local exportType = self.state.exportType

    local paletteIndex = self.state.paletteIndex
    local paletteName = if paletteIndex then palettes[paletteIndex].name else nil

    local nameItems = {}

    for i = 1, #palettes do
        table.insert(nameItems, {
            name = palettes[i].name
        })
    end

    return Roact.createElement("Frame", {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Size = UDim2.new(1, 0, 1, 0),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        Selection = Roact.createElement(SelectionComponent, {
            selectedItem = {1, self.state.paletteIndex},

            itemSections = {
                {
                    name = "",
                    items = nameItems,
                }
            },

            onExpandedStateToggle = function(expanded)
                self:setState({
                    dropdownExpanded = expanded
                })
            end,

            onItemChanged = function(_, j)
                self:setState({
                    paletteIndex = j,
                })
            end,
        }),

        Dialog = if ((not dropdownExpanded) and paletteIndex) then
            Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, Style.Constants.LargeButtonHeight + Style.Constants.SpaciousElementPadding),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,

                Size = UDim2.new(
                    1, 0, 1, -(
                        Style.Constants.LargeButtonHeight +
                        Style.Constants.StandardButtonHeight +
                        Style.Constants.SpaciousElementPadding * 2
                    )
                ),
            }, {
                UIListLayout = Roact.createElement(StandardUIListLayout, {
                    Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),

                    preset = 1,
                }),

                ExportType = Roact.createElement(RadioButtonGroup, {
                    Size = UDim2.new(1, 0, 0, (Style.Constants.StandardInputHeight * 2) + Style.Constants.MinorElementPadding),
                    LayoutOrder = 2,

                    selected = table.find(exportTypes, exportType),
                    options = exportTypes,

                    onSelected = function(i)
                        self:setState({
                            exportType = exportTypes[i]
                        })
                    end,
                }),

                ScriptInjectionWarning = if (exportType == "ModuleScript") then
                    Roact.createElement(StandardTextLabel, {
                        Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
                        LayoutOrder = 3,

                        Text = uiTranslations["ExportScriptInjection_Warning"],
                        TextWrapped = true,

                        TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.WarningText),
                    })
                else nil,

                ExportLocation = if paletteName then
                    Roact.createElement(StandardTextLabel, {
                        Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize * 2),
                        LayoutOrder = 4,
                        Text = Translator.FormatByKey("ExportDestination_Message", { paletteName }),
                    })
                else nil,
            })
        else nil,

        Buttons = if (not dropdownExpanded) then
            Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(1, 1),
                Position = UDim2.new(1, 0, 1, 0),
                Size = UDim2.new(0, (Style.Constants.DialogButtonWidth * 2) + Style.Constants.SpaciousElementPadding, 0, Style.Constants.StandardButtonHeight),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
            }, {
                UIListLayout = Roact.createElement(StandardUIListLayout, {
                    Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),

                    preset = 2,
                }),

                CancelButton = Roact.createElement(Button, {
                    Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                    LayoutOrder = 0,

                    displayType = "text",
                    text = uiTranslations["Cancel_ButtonText"],

                    backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton),
                    borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                    hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton, Enum.StudioStyleGuideModifier.Hover),
                    displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonText),

                    onActivated = function()
                        self.props.onPromptClosed(false)
                    end
                }),

                ExportButton = Roact.createElement(Button, {
                    Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                    LayoutOrder = 1,

                    disabled = (not (paletteIndex and exportType)),
                    displayType = "text",
                    text = uiTranslations["Export_ButtonText"],

                    backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton),
                    borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                    hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Hover),
                    displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText),

                    onActivated = function()
                        local palette = Util.table.deepCopy(palettes[paletteIndex])
                        local colors = palette.colors

                        -- convert Color3s to tables
                        for i = 1, #colors do
                            local color = colors[i].color

                            colors[i].color = {color.R, color.G, color.B}
                        end

                        local instance
                        local jsonPalette = HttpService:JSONEncode(palette)

                        if (exportType == "ModuleScript") then
                            local highestLevelStringBrackets = -1

                            for lBracket, equals, rBracket in string.gmatch(jsonPalette, "([%[%]])(=*)([%[%]])") do
                                if ((lBracket == "[") and (rBracket == "[")) or ((lBracket == "]") and (rBracket == "]")) then
                                    local numEquals = string.len(equals)

                                    if (numEquals > highestLevelStringBrackets) then
                                        highestLevelStringBrackets = numEquals
                                    end
                                end
                            end

                            instance = Instance.new("ModuleScript")
                            instance.Source = "-- " .. uiTranslations["ModuleScriptExport_Label"] .. "\n" ..
                                "-- " .. paletteName .. "\n" ..
                                "-- " .. os.date("%Y-%m-%dT%H:%M:%S%z") .. "\n" .. --
                                "\n" ..
                                "return [" .. string.rep("=", highestLevelStringBrackets + 1) .. "[" ..
                                jsonPalette ..
                                "]" .. string.rep("=", highestLevelStringBrackets + 1) .. "]"
                        elseif (exportType == "StringValue") then
                            instance = Instance.new("StringValue")
                            instance.Value = jsonPalette
                        end

                        instance.Name = paletteName .. ".palette"

                        local success = pcall(function()
                            instance.Parent = ServerStorage
                        end)

                        if (not success) then
                            if (exportType == "ModuleScript") then
                                warn("[ColorPane] " .. Translator.FormatByKey("ModuleScriptExportFailure_Message", { paletteName }))
                            end
                        else
                            Selection:Set({instance})
                        end

                        self.props.onPromptClosed(true)
                    end
                }),
            })
        else nil
    })
end

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
        palettes = state.colorEditor.palettes,
    }
end)(ExportPalette)


-- An informational page shown when launching ColorPane for the first time

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)

local CommonIncludes = Common.Includes
local Roact = require(CommonIncludes.RoactRodux.Roact)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local ConnectTheme = require(CommonComponents.ConnectTheme)

local StandardComponents = CommonComponents.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIPadding = require(StandardComponents.UIPadding)

---

local uiTranslations = Translator.GenerateTranslationTable({
    "FirstTimeSetup_Prompt",
    "FirstTimeSetup_Confirm_ButtonText",
})

---

--[[
    props
        onConfirm: () -> nil

    store props
        theme: StudioTheme
]]

local FirstTimeSetup = Roact.PureComponent:extend("FirstTimeSetup")

FirstTimeSetup.render = function(self)
    local theme = self.props.theme

    return Roact.createElement("Frame", {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Size = UDim2.new(1, 0, 1, 0),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        BackgroundTransparency = 0,
        BorderSizePixel = 0,

        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        Notice = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0.5, 0),
            Size = UDim2.new(1, 0, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding)),
            Position = UDim2.new(0.5, 0, 0, 0),

            Text = uiTranslations["FirstTimeSetup_Prompt"],
            TextXAlignment = Enum.TextXAlignment.Center,
            TextYAlignment = Enum.TextYAlignment.Top,
            TextWrapped = true,
        }),

        ConfirmButton = Roact.createElement(Button, {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(0, 90, 0, Style.Constants.StandardButtonHeight),

            displayType = "text",
            text = uiTranslations["FirstTimeSetup_Confirm_ButtonText"],

            backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton),
            borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
            hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Hover),
            displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText),

            disabledBackgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Disabled),
            disabledDisplayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText, Enum.StudioStyleGuideModifier.Disabled),

            onActivated = self.props.onConfirm,
        }),
    })
end

return ConnectTheme(FirstTimeSetup)


-- The entire gradient editor interface

local TextService = game:GetService("TextService")

---

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local CommonEnums = require(CommonModules.Enums)
local Constants = require(CommonModules.Constants)
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)
local Window = require(CommonModules.Window)

local CommonIncludes = Common.Includes
local ColorLib = require(CommonIncludes.Color)
local Roact = require(CommonIncludes.RoactRodux.Roact)
local RoactRodux = require(CommonIncludes.RoactRodux.RoactRodux)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local TextInput = require(CommonComponents.TextInput)

local StandardComponents = CommonComponents.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUICorner = require(StandardComponents.UICorner)
local StandardUIListLayout = require(StandardComponents.UIListLayout)
local StandardUIPadding = require(StandardComponents.UIPadding)

local Components = root.Components
local GradientInfo = require(Components.GradientInfo)
local GradientPalette = require(Components.GradientPalette)

local Modules = root.Modules
local Enums = require(Modules.Enums)
local GradientEditorInputSignals = require(Modules.EditorInputSignals).GradientEditor
local Store = require(Modules.Store)
local Util = require(Modules.Util)
local WidgetInfo = require(Modules.WidgetInfo)

local Color, Gradient = ColorLib.Color, ColorLib.Gradient

---

local CURSOR_KEYPOINT_SNAP_VALUE = 0.01

local uiTranslations = Translator.GenerateTranslationTable({
    "SnapInput_Label",
    "Reset_ButtonText",
    "GradientInfo_WindowTitle",
    "GradientPalette_WindowTitle"
})

local getNearestKeypointIndex = function(keypoints, time: number): (number?, number?)
    if (time == 0) then
        return 1, 0
    elseif (time == 1) then
        return #keypoints, 0
    end

    for i = 1, (#keypoints - 1) do
        local thisKeypoint, nextKeypoint = keypoints[i], keypoints[i + 1]
        local thisKeypointTime, nextKeypointTime = thisKeypoint.Time, nextKeypoint.Time

        if ((time >= thisKeypointTime) and (time < nextKeypointTime)) then
            local thisTimeDiff = time - thisKeypointTime
            local nextTimeDiff = nextKeypointTime - time

            if (thisTimeDiff < nextTimeDiff) then
                return i, thisTimeDiff
            else
                return (i + 1), nextTimeDiff
            end
        end
    end

    return
end

local getColorSequenceCode = function(keypoints): string
    local code = "ColorSequence.new({"

    for i = 1, #keypoints do
        local keypoint = keypoints[i]

        code = code ..
            string.format("\n    ColorSequenceKeypoint.new(%f, Color3.new(%f, %f, %f))", keypoint.Time, keypoint.Color:components()) ..
            (if (i ~= #keypoints) then "," else "")
    end

    code = code .. "\n})"
    return code
end

---

--[[
    props
        originalKeypoints: array<GradientKeypoint>
        originalColorSpace: string
        originalHueAdjustment: string
        originalPrecision: number
        fireFinished: FireSignal<boolean>

        promptForColorEdit: (ColorPromptOptions?) -> Promise<Color>

    store props
        theme: StudioTheme
        state: table

        keypoints: array<Gradientkeypoint>
        colorSpace: string
        hueAdjustment: string?
        precision: number
        timeSnapValue: number

        resetState: () -> nil
        setKeypoints: (array<GradientKeypoint>?, number?) -> nil
        setSnapValue: (number) -> nil
]]

local GradientEditor = Roact.PureComponent:extend("GradientEditor")

GradientEditor.init = function(self)
    self.timelineStartPosition, self.updateTimelineStartPosition = Roact.createBinding(Vector2.new(0, 0))
    self.timelineWidth, self.updateTimelineWidth = Roact.createBinding(0)
    self.timelineProgress, self.updateTimelineProgress = Roact.createBinding(0)

    self.gradientInfoWindow = Window.new(WidgetInfo.GradientInfo.Id, WidgetInfo.GradientInfo.Info)
    self.gradientPaletteWindow = Window.new(WidgetInfo.GradientPalette.Id, WidgetInfo.GradientPalette.Info)

    self.markerTime = self.timelineProgress:map(function(timelineProgress)
        local keypoints = self.props.keypoints

        local nearestKeypoint, nearestKeypointTimeDiff = getNearestKeypointIndex(keypoints, timelineProgress)
        local isKeypointInSnapProximity = (nearestKeypointTimeDiff < CURSOR_KEYPOINT_SNAP_VALUE)

        return (if isKeypointInSnapProximity then keypoints[nearestKeypoint].Time else timelineProgress)
    end)

    self.getNewKeypointTime = function(time: number, snapValue: number?): number?
        local keypoints = self.props.keypoints
        local selectedKeypoint = self.props.selectedKeypoint
        local timeSnapValue = snapValue or self.props.timeSnapValue

        local newTime = Util.round(time, math.log10(timeSnapValue))
        local nearestKeypoint, nearestKeypointTimeDiff = getNearestKeypointIndex(keypoints, time)

        if (nearestKeypointTimeDiff < timeSnapValue) then
            if (nearestKeypoint == 1) then
                newTime = timeSnapValue
            elseif (nearestKeypoint == #keypoints) then
                newTime = 1 - timeSnapValue
            else
                local nearestKeypointTime = keypoints[nearestKeypoint].Time

                if (nearestKeypoint < selectedKeypoint) then
                    newTime = math.min(nearestKeypointTime + timeSnapValue, 1)
                elseif (nearestKeypoint > selectedKeypoint) then
                    newTime = math.max(nearestKeypointTime - timeSnapValue, 0)
                end
            end

            nearestKeypoint, nearestKeypointTimeDiff = getNearestKeypointIndex(keypoints, newTime)

            if (nearestKeypointTimeDiff < timeSnapValue) then
                newTime = nil
            end
        end

        return newTime
    end

    self.updateSelectedKeypointTime = function(cursorPosition)
        local selectedKeypointIndex = self.props.selectedKeypoint
        if ((selectedKeypointIndex == 1) or (selectedKeypointIndex == #self.props.keypoints)) then return end

        local keypoints = Util.table.deepCopy(self.props.keypoints)

        local distanceFromStart = cursorPosition - self.timelineStartPosition:getValue()
        local newTime = self.getNewKeypointTime(math.clamp(distanceFromStart.X / self.timelineWidth:getValue(), 0, 1))
        if (not newTime) then return end

        local newKeypoint = { Time = newTime, Color = keypoints[selectedKeypointIndex].Color }
        keypoints[selectedKeypointIndex] = newKeypoint

        table.sort(keypoints, function(keypointA, keypointB)
            return keypointA.Time < keypointB.Time
        end)

        local newKeypointIndex = table.find(keypoints, newKeypoint)

        self.props.setKeypoints(keypoints, newKeypointIndex)
    end

    self.removeKeypoint = function(index)
        local keypoints = Util.table.deepCopy(self.props.keypoints)

        table.remove(keypoints, index)
        self.props.setKeypoints(keypoints, -1)
    end

    self:setState({
        tracking = false,
        showTimelineMarker = false,
    })
end

GradientEditor.didMount = function(self)
    self.mousePositionChanged = GradientEditorInputSignals.MousePositionChanged.Event:subscribe(function(cursorPosition: Vector2)
        local distanceFromStart = cursorPosition - self.timelineStartPosition:getValue()
        self.updateTimelineProgress(math.clamp(distanceFromStart.X / self.timelineWidth:getValue(), 0, 1))

        if (self.state.tracking and self.props.selectedKeypoint) then
            self.updateSelectedKeypointTime(cursorPosition)
        end
    end)
end

GradientEditor.willUnmount = function(self)
    self.unmounting = true

    self.gradientInfoWindow:destroy()
    self.gradientPaletteWindow:destroy()
    self.mousePositionChanged:unsubscribe()

    if (self.state.colorEditPromise) then
        self.state.colorEditPromise:cancel()
    end
end

GradientEditor.render = function(self)
    local theme = self.props.theme

    local keypoints = Util.table.deepCopy(self.props.keypoints)
    local displayKeypoints = self.props.displayKeypoints
    local colorSpace = self.props.colorSpace
    local hueAdjustment = self.props.hueAdjustment
    local precision = self.props.precision

    local selectedKeypoint = self.props.selectedKeypoint
    local timeSnapValue = self.props.timeSnapValue

    local colorEditPromise = self.state.colorEditPromise
    local showCode = self.state.showCode
    local maxUserKeypoints = Util.getMaxUserKeypoints(Constants.MAX_COLORSEQUENCE_KEYPOINTS, precision)

    local gradient = Gradient.new(keypoints)
    local displayGradient = Gradient.new(displayKeypoints)

    local gradientElements = {}
    local colorSequenceCode
    local colorSequenceCodeTextSize

    if (showCode) then
        colorSequenceCode = getColorSequenceCode(displayKeypoints)

        colorSequenceCodeTextSize = TextService:GetTextSize(
            colorSequenceCode,
            Style.Constants.StandardTextSize,
            Enum.Font.Code,
            Vector2.new(math.huge, math.huge)
        )
    end

    for i = 1, #keypoints do
        local keypoint = keypoints[i]
        local isFixed = (i == 1) or (i == #keypoints)

        table.insert(gradientElements, Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0.5),
            Position = UDim2.new(keypoint.Time, 0, 0.5, 0),
            Size = Style.UDim2.MarkerSize,
            BorderSizePixel = 0,
            ZIndex = 2,

            BackgroundColor3 = keypoint.Color:bestContrastingColor(
                Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)),
                Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)):invert()
            ):toColor3(),
        }, {
            UICorner = if (not isFixed) then
                Roact.createElement(StandardUICorner)
            else nil,

            Inner = if (selectedKeypoint == i) then
                Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0.5, 0.5),
                    Position = UDim2.new(0.5, 0, 0.5, 0),
                    Size = UDim2.new(1, -2, 1, -2),
                    BorderSizePixel = 0,

                    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, Enum.StudioStyleGuideModifier.Selected),
                }, {
                    UICorner = if (not isFixed) then
                        Roact.createElement(StandardUICorner)
                    else nil,
                })
            else nil
        }))
    end

    gradientElements.TimelineMarker = if (self.state.showTimelineMarker) then
        Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0.5),
            Size = UDim2.new(0, 1, 1, 0),
            BackgroundTransparency = 0,
            BorderSizePixel = 0,

            Position = self.markerTime:map(function(markerTime)
                return UDim2.new(markerTime, 0, 0.5, 0)
            end),

            BackgroundColor3 = self.markerTime:map(function(markerTime)
                return displayGradient:color(markerTime):bestContrastingColor(
                    Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)),
                    Color.fromColor3(theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame)):invert()
                ):toColor3()
            end),
        })
    else nil

    gradientElements.UICorner = Roact.createElement(StandardUICorner)
    gradientElements.UIGradient = Roact.createElement("UIGradient", {
        Color = displayGradient:colorSequence()
    })

    return Roact.createElement("Frame", {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        Size = UDim2.new(1, 0, 1, 0),
        BackgroundTransparency = 0,
        BorderSizePixel = 0,

        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame),

        [Roact.Event.InputBegan] = function(_, input)
            if (input.UserInputType ~= Enum.UserInputType.Keyboard) then return end
            if (not selectedKeypoint) then return end

            local keyCode = input.KeyCode
            local nextSelected

            if (keyCode == Enum.KeyCode.Left) then
                nextSelected = selectedKeypoint - 1
            elseif (keyCode == Enum.KeyCode.Right) then
                nextSelected = selectedKeypoint + 1
            end

            if (not keypoints[nextSelected]) then return end

            self.props.setKeypoints(nil, nextSelected)

            self:setState({
                tracking = false,
            })
        end,
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        GradientEditor = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, 0),
            Size = UDim2.new(1, 0, 1, -60),
            BackgroundTransparency = 1,
        }, {
            Border = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 0.5),
                Position = UDim2.new(0.5, 0, 0.5, 0),
                Size = UDim2.new(1, 0, 1, 0),
                BackgroundTransparency = 0,

                BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
            }, {
                UICorner = Roact.createElement(StandardUICorner),

                Editor = if (not showCode) then
                    Roact.createElement("Frame", {
                        AnchorPoint = Vector2.new(0.5, 0.5),
                        Position = UDim2.new(0.5, 0, 0.5, 0),
                        Size = UDim2.new(1, -2, 1, -2),
                        BackgroundTransparency = 0,
                        ClipsDescendants = true,

                        BackgroundColor3 = Color3.new(1, 1, 1),

                        [Roact.Event.InputBegan] = function(_, input)
                            if (input.UserInputType == Enum.UserInputType.MouseMovement) then
                                self:setState({
                                    showTimelineMarker = true,
                                })
                            else
                                if (colorEditPromise) then return end

                                local timelineProgress = self.timelineProgress:getValue()
                                local nearestKeypoint, nearestKeypointTimeDiff = getNearestKeypointIndex(keypoints, timelineProgress)
                                local isKeypointInSnapProximity = (nearestKeypointTimeDiff < CURSOR_KEYPOINT_SNAP_VALUE)

                                if (input.UserInputType == Enum.UserInputType.MouseButton1) then
                                    if (isKeypointInSnapProximity) then
                                        self.props.setKeypoints(nil, nearestKeypoint)

                                        self:setState({
                                            tracking = true
                                        })
                                    else
                                        if (#keypoints < maxUserKeypoints) then
                                            local newKeypointTime = self.getNewKeypointTime(timelineProgress)
                                            if (not newKeypointTime) then return end

                                            local newKeypoint = { Time = newKeypointTime, Color = gradient:color(newKeypointTime, colorSpace, hueAdjustment) }
                                            table.insert(keypoints, newKeypoint)

                                            table.sort(keypoints, function(keypointA, keypointB)
                                                return keypointA.Time < keypointB.Time
                                            end)

                                            local newKeypointIndex = table.find(keypoints, newKeypoint)

                                            self.props.setKeypoints(keypoints, newKeypointIndex)

                                            --[[
                                                Defer tracking to prevent the currently-selected
                                                keypoint from being tracked instead of the
                                                newly-created one
                                            ]]
                                            task.defer(function()
                                                self:setState({
                                                    tracking = true,
                                                })
                                            end)
                                        end
                                    end
                                elseif (input.UserInputType == Enum.UserInputType.MouseButton2) then
                                    if (not isKeypointInSnapProximity) then return end
                                    if ((nearestKeypoint == 1) or (nearestKeypoint == #keypoints)) then return end

                                    self.removeKeypoint(nearestKeypoint)
                                end
                            end
                        end,

                        [Roact.Event.InputEnded] = function(_, input)
                            if (input.UserInputType == Enum.UserInputType.MouseMovement) then
                                self:setState({
                                    showTimelineMarker = false,
                                })
                            elseif ((input.UserInputType == Enum.UserInputType.MouseButton1) and self.state.tracking) then
                                self:setState({
                                    tracking = false,
                                })
                            end
                        end,

                        [Roact.Change.AbsolutePosition] = function(obj)
                            self.updateTimelineStartPosition(obj.AbsolutePosition)
                            self.updateTimelineWidth(obj.AbsoluteSize.X)
                        end,

                        [Roact.Change.AbsoluteSize] = function(obj)
                            self.updateTimelineStartPosition(obj.AbsolutePosition)
                            self.updateTimelineWidth(obj.AbsoluteSize.X)
                        end,
                    }, gradientElements)
                else nil,

                ColorSequenceCode = if (showCode) then
                    Roact.createElement("ScrollingFrame", {
                        AnchorPoint = Vector2.new(0.5, 0.5),
                        Position = UDim2.new(0.5, 0, 0.5, 0),
                        Size = UDim2.new(1, 0, 1, 0),
                        BackgroundTransparency = 1,
                        BorderSizePixel = 0,

                        CanvasSize = UDim2.new(
                            0, colorSequenceCodeTextSize.X + (Style.Constants.MinorElementPadding * 2),
                            0, colorSequenceCodeTextSize.Y + (Style.Constants.MinorElementPadding * 2)
                        ),

                        ClipsDescendants = true,
                        TopImage = Style.Images.ScrollbarImage,
                        MidImage = Style.Images.ScrollbarImage,
                        BottomImage = Style.Images.ScrollbarImage,
                        HorizontalScrollBarInset = Enum.ScrollBarInset.Always,
                        VerticalScrollBarInset = Enum.ScrollBarInset.Always,
                        VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right,
                        ScrollBarThickness = Style.Constants.ScrollbarThickness / 4,

                        ScrollBarImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar),
                    }, {
                        UIPadding = Roact.createElement(StandardUIPadding, {
                            paddings = {Style.Constants.MinorElementPadding}
                        }),

                        TextBox = Roact.createElement("TextBox", {
                            AnchorPoint = Vector2.new(0, 0),
                            Position = UDim2.new(0, 0, 0, 0),
                            Size = UDim2.new(1, 0, 1, 0),
                            BackgroundTransparency = 1,

                            Text = colorSequenceCode,
                            Font = Enum.Font.Code,
                            TextSize = Style.Constants.StandardTextSize,
                            TextXAlignment = Enum.TextXAlignment.Left,
                            TextYAlignment = Enum.TextYAlignment.Top,
                            ClearTextOnFocus = false,
                            TextEditable = false,

                            TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),

                            [Roact.Event.Focused] = function(obj)
                                obj.CursorPosition = string.len(obj.Text) + 1
                                obj.SelectionStart = 1
                            end,
                        })
                    })
                else nil,
            }),
        }),

        SelectedKeypointInfo = if selectedKeypoint then
            Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0, 1),
                Position = UDim2.new(0, 0, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding)),
                Size = UDim2.new(0.5, 0, 0, Style.Constants.StandardButtonHeight),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
            }, {
                ColorLabel = Roact.createElement(StandardTextLabel, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Position = UDim2.new(0, 0, 0.5, 0),
                    Size = UDim2.new(0, 28, 1, 0),
                    Text = "Color",
                }),

                EditColor = Roact.createElement(Button, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Size = UDim2.new(0, 50, 1, 0),
                    Position = UDim2.new(0, 28 + Style.Constants.MinorElementPadding, 0.5, 0),

                    displayType = "color",
                    color = keypoints[selectedKeypoint].Color:toColor3(),
                    disabled = if colorEditPromise then true else false,

                    onActivated = function()
                        if (colorEditPromise) then
                            colorEditPromise:cancel()
                        end

                        local originalColor = keypoints[selectedKeypoint].Color

                        local editPromise = self.props.promptForColorEdit({
                            PromptTitle = string.format("Gradient keypoint, %.3f%%", keypoints[selectedKeypoint].Time * 100),
                            ColorType = "Color",
                            InitialColor = originalColor,

                            OnColorChanged = function(color)
                                local newKeypoints = Util.table.deepCopy(self.props.keypoints)
                                local keypoint = newKeypoints[selectedKeypoint]

                                newKeypoints[selectedKeypoint] = { Time = keypoint.Time, Color = color }

                                self.props.setKeypoints(newKeypoints)
                            end,
                        })

                        editPromise:andThen(function(newColor)
                            local newKeypoints = Util.table.deepCopy(self.props.keypoints)
                            local keypoint = keypoints[selectedKeypoint]

                            newKeypoints[selectedKeypoint] = { Time = keypoint.Time, Color = newColor }

                            self.props.setKeypoints(newKeypoints, nil)
                        end, function(err)
                            if (err == Enums.PromptRejection.PromptCancelled) then
                                local keypoint = keypoints[selectedKeypoint]

                                local newKeypoints = Util.table.deepCopy(self.props.keypoints)
                                newKeypoints[selectedKeypoint] = { Time = keypoint.Time, Color = originalColor }

                                self.props.setKeypoints(newKeypoints, nil)
                            end
                        end):finally(function()
                            if (not self.unmounting) then
                                self:setState({
                                    colorEditPromise = Roact.None,
                                })
                            end
                        end)

                        self:setState({
                            colorEditPromise = editPromise,
                        })
                    end
                }),

                ProgressInput = Roact.createElement(TextInput, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Position = UDim2.new(0, 28 + 50 + Style.Constants.MinorElementPadding + Style.Constants.SpaciousElementPadding, 0.5, 0),
                    Size = UDim2.new(0, 50, 1, 0),
                    Text = string.format("%.3f", keypoints[selectedKeypoint].Time * 100),
                    TextXAlignment = Enum.TextXAlignment.Center,

                    isTextAValidValue = function(text)
                        local n = tonumber(text)
                        if (not n) then return false end

                        n = n / 100
                        return ((n > 0) and (n < 1))
                    end,

                    onSubmit = function(text)
                        local newTime = tonumber(text)
                        newTime = self.getNewKeypointTime(newTime / 100, Constants.MIN_SNAP_VALUE)
                        if (not newTime) then return end

                        local newKeypoint = { Time = newTime, Color = keypoints[selectedKeypoint].Color }
                        keypoints[selectedKeypoint] = newKeypoint

                        table.sort(keypoints, function(keypointA, keypointB)
                            return keypointA.Time < keypointB.Time
                        end)

                        local newKeypointIndex = table.find(keypoints, newKeypoint)

                        self.props.setKeypoints(keypoints, newKeypointIndex)
                    end,

                    disabled = (colorEditPromise or (selectedKeypoint == 1) or (selectedKeypoint == #keypoints)),
                }),

                ProgressLabel = Roact.createElement(StandardTextLabel, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Position = UDim2.new(0, 28 + 50 + 50 + (Style.Constants.MinorElementPadding * 2) + Style.Constants.SpaciousElementPadding, 0.5, 0),
                    Size = UDim2.new(0, 10, 1, 0),
                    Text = "%",
                }),
            })
        else nil,

        GradientInfo = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(1, 1),
            Position = UDim2.new(1, 0, 1, -(Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding)),
            Size = UDim2.new(0, 180 + Style.Constants.StandardButtonHeight, 0, Style.Constants.StandardButtonHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            UIListLayout = Roact.createElement(StandardUIListLayout, {
                Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),
                FillDirection = Enum.FillDirection.Horizontal,
                HorizontalAlignment = Enum.HorizontalAlignment.Right,
                VerticalAlignment = Enum.VerticalAlignment.Center,
            }),

            InfoLabel = Roact.createElement(StandardTextLabel, {
                Size = UDim2.new(0, 180, 0, Style.Constants.StandardTextSize),
                LayoutOrder = 0,

                Text = Translator.FormatByKey("KeypointReadout_Label", { #keypoints, maxUserKeypoints }),
                TextXAlignment = Enum.TextXAlignment.Right,
                TextYAlignment = Enum.TextYAlignment.Center,

                TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText)
            }),

            CodeButton = Roact.createElement(Button, {
                Size = Style.UDim2.StandardButtonSize,
                LayoutOrder = 1,

                displayType = "image",
                image = if showCode then Style.Images.HideCodeButtonIcon else Style.Images.ShowCodeButtonIcon,

                onActivated = function()
                    local newShowCode = (not showCode)

                    if (newShowCode) then
                        if (colorEditPromise) then
                            colorEditPromise:cancel()
                        end

                        self.props.setKeypoints(nil, -1)
                    end

                    self:setState({
                        showCode = newShowCode,
                        colorEditPromise = if newShowCode then Roact.None else nil,
                        tracking = false,
                    })
                end,
            }),

            GradientInfoButton = Roact.createElement(Button, {
                Size = Style.UDim2.StandardButtonSize,
                LayoutOrder = 2,

                displayType = "image",
                image = Style.Images.GradientInfoButtonIcon,

                onActivated = function()
                    if (self.gradientInfoWindow:isMounted()) then
                        self.gradientInfoWindow:unmount()
                    else
                        self.gradientInfoWindow:mount(
                            uiTranslations["GradientInfo_WindowTitle"],
                            Roact.createElement(GradientInfo),
                            Store
                        )
                    end
                end,
            })
        }),

        EditorActions = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0, 1),
            Position = UDim2.new(0, 0, 1, 0),
            Size = UDim2.new(0, 274, 0, Style.Constants.StandardButtonHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
        }, {
            UIListLayout = Roact.createElement(StandardUIListLayout, {
                Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),
                FillDirection = Enum.FillDirection.Horizontal,
                VerticalAlignment = Enum.VerticalAlignment.Center,
            }),

            -- keypoint must be selected
            -- t ~= 0, 1
            RemoveKeypointButton = Roact.createElement(Button, {
                LayoutOrder = 1,

                displayType = "image",
                image = Style.Images.DeleteButtonIcon,
                disabled = (colorEditPromise or (not selectedKeypoint) or (selectedKeypoint == 1) or (selectedKeypoint == #keypoints)),

                onActivated = function()
                    self.removeKeypoint(selectedKeypoint)
                end,
            }),

            -- keypoint must be selected
            -- t ~= 0
            SwapKeypointLeftButton = Roact.createElement(Button, {
                LayoutOrder = 2,

                displayType = "image",
                image = Style.Images.MoveLeftButtonIcon,
                disabled = (colorEditPromise or (not selectedKeypoint) or (selectedKeypoint == 1)),

                onActivated = function()
                    local thisKeypoint = keypoints[selectedKeypoint]
                    local prevKeypoint = keypoints[selectedKeypoint - 1]

                    keypoints[selectedKeypoint] = { Time = thisKeypoint.Time, Color = prevKeypoint.Color }
                    keypoints[selectedKeypoint - 1] = { Time = prevKeypoint.Time, Color = thisKeypoint.Color }

                    self.props.setKeypoints(keypoints, selectedKeypoint - 1)
                end,
            }),

            -- keypoint must be selected
            -- t ~= 1
            SwapKeypointRightButton = Roact.createElement(Button, {
                LayoutOrder = 3,

                displayType = "image",
                image = Style.Images.MoveRightButtonIcon,
                disabled = (colorEditPromise or (not selectedKeypoint) or (selectedKeypoint == #keypoints)),

                onActivated = function()
                    local thisKeypoint = keypoints[selectedKeypoint]
                    local nextKeypoint = keypoints[selectedKeypoint + 1]

                    keypoints[selectedKeypoint] = { Time = thisKeypoint.Time, Color = nextKeypoint.Color }
                    keypoints[selectedKeypoint + 1] = { Time = nextKeypoint.Time, Color = thisKeypoint.Color }

                    self.props.setKeypoints(keypoints, selectedKeypoint + 1)
                end,
            }),

            ReverseSequenceButton = Roact.createElement(Button, {
                LayoutOrder = 5,

                displayType = "image",
                image = Style.Images.ReverseGradientButtonIcon,
                disabled = if colorEditPromise then true else false,

                onActivated = function()
                    local reversedKeypoints = {}
                    local reversedSelectedKeypoint

                    for i = #keypoints, 1, -1 do
                        local keypoint = keypoints[i]
                        local reversedIndex = #reversedKeypoints + 1

                        if (selectedKeypoint == i) then
                            reversedSelectedKeypoint = reversedIndex
                        end

                        reversedKeypoints[reversedIndex] = { Time = 1 - keypoint.Time, Color = keypoint.Color }
                    end

                    self.props.setKeypoints(reversedKeypoints, reversedSelectedKeypoint)
                end,
            }),

            SnapContainer = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0, 1),
                Position = UDim2.new(0, 0, 1, -30),
                Size = UDim2.new(0, 94, 0, 22),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
                LayoutOrder = 6,
            }, {
                SnapLabel = Roact.createElement(StandardTextLabel, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Position = UDim2.new(0, 0, 0.5, 0),
                    Size = UDim2.new(0, 26, 1, 0),

                    Text = uiTranslations["SnapInput_Label"],
                    TextXAlignment = Enum.TextXAlignment.Right,
                    TextYAlignment = Enum.TextYAlignment.Center,
                }),

                SnapInput = Roact.createElement(TextInput, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Position = UDim2.new(0, 30, 0.5, 0),
                    Size = UDim2.new(0, 50, 1, 0),

                    Text = timeSnapValue * 100,
                    TextXAlignment = Enum.TextXAlignment.Center,

                    isTextAValidValue = function(text)
                        local n = tonumber(text)
                        if (not n) then return false end

                        n = n / 100
                        return ((n >= Constants.MIN_SNAP_VALUE) and (n <= Constants.MAX_SNAP_VALUE))
                    end,

                    onSubmit = function(text)
                        local n = tonumber(text)
                        n = math.clamp(n / 100, Constants.MIN_SNAP_VALUE, Constants.MAX_SNAP_VALUE)
                        n = Util.round(n, math.log10(Constants.MIN_SNAP_VALUE))

                        self.props.setSnapValue(n)
                    end,
                }),

                SnapUnitLabel = Roact.createElement(StandardTextLabel, {
                    AnchorPoint = Vector2.new(0, 0.5),
                    Position = UDim2.new(0, 84, 0.5, 0),
                    Size = UDim2.new(0, 10, 1, 0),

                    Text = "%",
                    TextXAlignment = Enum.TextXAlignment.Right,
                    TextYAlignment = Enum.TextYAlignment.Center,
                }),
            }),
        }),

        MainActions = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(1, 1),
            Position = UDim2.new(1, 0, 1, 0),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            Size = UDim2.new(
                0, (Style.Constants.StandardButtonHeight + (Style.Constants.DialogButtonWidth * 3) + (Style.Constants.SpaciousElementPadding * 3)),
                0, Style.Constants.StandardButtonHeight),
        }, {
            UIListLayout = Roact.createElement(StandardUIListLayout, {
                Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),
                FillDirection = Enum.FillDirection.Horizontal,
                HorizontalAlignment = Enum.HorizontalAlignment.Right,
                VerticalAlignment = Enum.VerticalAlignment.Center,
            }),

            PalettesButton = Roact.createElement(Button, {
                LayoutOrder = 1,

                displayType = "image",
                image = Style.Images.PaletteEditorButtonIcon,
                displayColor = Color3.new(1, 1, 1),

                onActivated = function()
                    if (self.gradientPaletteWindow:isMounted()) then
                        self.gradientPaletteWindow:unmount()
                    else
                        self.gradientPaletteWindow:mount(
                            uiTranslations["GradientPalette_WindowTitle"],

                            Roact.createElement(GradientPalette, {
                                beforeSetGradient = function()
                                    if (self.state.colorEditPromise) then
                                        self.state.colorEditPromise:cancel()
                                    end

                                    self.props.setKeypoints(nil, -1)

                                    self:setState({
                                        colorEditPromise = Roact.None,
                                    })
                                end,
                            }),

                            Store
                        )
                    end
                end
            }),

            ResetButton = Roact.createElement(Button, {
                Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                LayoutOrder = 2,

                displayType = "text",
                text = uiTranslations["Reset_ButtonText"],

                backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton),
                borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton, Enum.StudioStyleGuideModifier.Hover),
                displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonText),

                onActivated = function()
                    if (colorEditPromise) then
                        colorEditPromise:cancel()
                    end

                    self.props.setKeypoints(nil, -1)
                    self.props.setGradient(self.props.originalKeypoints, self.props.originalColorSpace, self.props.originalHueAdjustment, self.props.originalPrecision)

                    self:setState({
                        colorEditPromise = Roact.None,
                        tracking = false,
                    })
                end
            }),

            CancelButton = Roact.createElement(Button, {
                Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                LayoutOrder = 3,

                displayType = "text",
                text = "Cancel",

                backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton),
                borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton, Enum.StudioStyleGuideModifier.Hover),
                displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonText),

                onActivated = function()
                    self.props.fireFinished(false)
                end
            }),

            ConfirmButton = Roact.createElement(Button, {
                Size = UDim2.new(0, Style.Constants.DialogButtonWidth, 0, Style.Constants.StandardButtonHeight),
                LayoutOrder = 4,

                displayType = "text",
                text = "OK",

                backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton),
                borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
                hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, Enum.StudioStyleGuideModifier.Hover),
                displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText),

                onActivated = function()
                    self.props.fireFinished(true)
                end
            }),
        })
        or nil
    })
end

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,

        keypoints = state.gradientEditor.keypoints,
        displayKeypoints = state.gradientEditor.displayKeypoints,
        selectedKeypoint = state.gradientEditor.selectedKeypoint,

        colorSpace = state.gradientEditor.colorSpace,
        hueAdjustment = state.gradientEditor.hueAdjustment,
        precision = state.gradientEditor.precision,
        timeSnapValue = state.userData[CommonEnums.ColorPaneUserDataKey.SnapValue],
    }
end, function(dispatch)
    return {
        setKeypoints = function(keypoints, selectedKeypoint)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_SetKeypoints,

                keypoints = keypoints,
                selectedKeypoint = selectedKeypoint,
            })
        end,

        setGradient = function(keypoints, colorSpace, hueAdjustment, precision)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_SetGradient,

                keypoints = keypoints,
                colorSpace = colorSpace,
                hueAdjustment = hueAdjustment,
                precision = precision,
            })
        end,

        setSnapValue = function(snapValue)
            dispatch({
                type = Enums.StoreActionType.UpdateUserData,
                key = CommonEnums.ColorPaneUserDataKey.SnapValue,
                value = snapValue
            })
        end,

        resetState = function()
            dispatch({
                type = Enums.StoreActionType.GradientEditor_ResetState,
            })
        end,
    }
end)(GradientEditor)


-- A page showing the editing metadata for a gradient

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local Constants = require(CommonModules.Constants)
local Style = require(CommonModules.Style)
local Translator = require(CommonModules.Translator)

local CommonIncludes = Common.Includes
local Roact = require(CommonIncludes.RoactRodux.Roact)
local RoactRodux = require(CommonIncludes.RoactRodux.RoactRodux)

local CommonComponents = Common.Components
local Button = require(CommonComponents.Button)
local TextInput = require(CommonComponents.TextInput)

local StandardComponents = CommonComponents.StandardComponents
local StandardScrollingFrame = require(StandardComponents.ScrollingFrame)
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIListLayout = require(StandardComponents.UIListLayout)
local StandardUIPadding = require(StandardComponents.UIPadding)

local Modules = root.Modules
local Enums = require(Modules.Enums)
local Util = require(Modules.Util)

---

local HUE_COLOR_SPACES = {
    HSB = true,
    HWB = true,
    HSL = true,
    LChab = true,
    LChuv = true,
}

local COLOR_SPACE_BUTTONS_PER_ROW = 4
local HUE_ADJUSTMENT_BUTTONS_PER_ROW = 3

local MIN_PRECISION = 0
local MAX_PRECISION = 18

local uiTranslations = Translator.GenerateTranslationTable({
    "RGB_ColorType",
    "CMYK_ColorType",
    "HSB_ColorType",
    "HSL_ColorType",
    "HWB_ColorType",
    "Lab_ColorType",
    "Luv_ColorType",
    "LChab_ColorType",
    "LChuv_ColorType",
    "xyY_ColorType",
    "XYZ_ColorType",
    "HPLuv_ColorType",
    "HSLuv_ColorType",
    "Oklab_ColorType",

    "Shorter_HueInterpolation",
    "Longer_HueInterpolation",
    "Increasing_HueInterpolation",
    "Decreasing_HueInterpolation",
    "Specified_HueInterpolation",

    "ColorSpace_Label",
    "HueInterpolation_Label",
    "Precision_Label",
    "MaxKeypoints_ButtonText",
    "Reset_ButtonText",
})

local getMaxKeypointPrecision = function(numKeypoints): number
    if (numKeypoints == 2) then
        return MAX_PRECISION
    else
        return math.floor((Constants.MAX_COLORSEQUENCE_KEYPOINTS - 1) / (numKeypoints - 1)) - 1
    end
end

---

--[[
    store props
        theme: StudioTheme
        colorSpace: string?
        hueAdjustment: string?
        precision: number?

        setColorSpace: (string) -> nil
        setHueAdjustment: (string) -> nil
        setPrecision: (number) -> nil
]]

local GradientInfo = Roact.PureComponent:extend("GradientInfo")

GradientInfo.init = function(self)
    self.pageLength, self.updatePageLength = Roact.createBinding(0)
end

GradientInfo.render = function(self)
    local theme = self.props.theme

    local colorSpace = self.props.colorSpace
    local hueAdjustment = self.props.hueAdjustment
    local precision = self.props.precision

    local numKeypoints = #self.props.keypoints
    local maxPrecision = getMaxKeypointPrecision(numKeypoints)

    local colorSpaceListElements = {}
    local hueAdjustmentListElements = {}

    for i = 1, #Constants.VALID_GRADIENT_COLOR_SPACES do
        local listColorSpace = Constants.VALID_GRADIENT_COLOR_SPACES[i]
        local selected = (listColorSpace == colorSpace)

        colorSpaceListElements[listColorSpace] = Roact.createElement(Button, {
            LayoutOrder = i,

            displayType = "text",
            text = uiTranslations[listColorSpace .. "_ColorType"],

            hoverColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            borderColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.ButtonBorder, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            backgroundColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            displayColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            onActivated = function()
                self.props.setColorSpace(listColorSpace)
            end,
        })
    end

    for i = 1, #Constants.VALID_HUE_ADJUSTMENTS do
        local listHueAdjustment = Constants.VALID_HUE_ADJUSTMENTS[i]
        local selected = (listHueAdjustment == hueAdjustment)

        hueAdjustmentListElements[listHueAdjustment] = Roact.createElement(Button, {
            LayoutOrder = i,

            disabled = (not HUE_COLOR_SPACES[colorSpace]),
            displayType = "text",
            text = uiTranslations[listHueAdjustment .. "_HueInterpolation"],

            hoverColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            borderColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.ButtonBorder, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            backgroundColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            displayColor = if selected then
                theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Selected)
            else nil,

            onActivated = function()
                self.props.setHueAdjustment(listHueAdjustment)
            end,
        })
    end

    colorSpaceListElements.UIGridLayout = Roact.createElement("UIGridLayout", {
        CellPadding = Style.UDim2.MinorElementPaddingSize,
        SortOrder = Enum.SortOrder.LayoutOrder,

        CellSize = UDim2.new(
            1 / COLOR_SPACE_BUTTONS_PER_ROW, -math.ceil(Style.Constants.MinorElementPadding + (Style.Constants.MinorElementPadding / COLOR_SPACE_BUTTONS_PER_ROW)),
            0, Style.Constants.StandardButtonHeight
        ),
    })

    hueAdjustmentListElements.UIGridLayout = Roact.createElement("UIGridLayout", {
        CellPadding = Style.UDim2.MinorElementPaddingSize,
        SortOrder = Enum.SortOrder.LayoutOrder,

        CellSize = UDim2.new(
            1 / HUE_ADJUSTMENT_BUTTONS_PER_ROW, -math.ceil(Style.Constants.MinorElementPadding + (Style.Constants.MinorElementPadding / HUE_ADJUSTMENT_BUTTONS_PER_ROW)),
            0, Style.Constants.StandardButtonHeight
        ),
    })

    return Roact.createElement(StandardScrollingFrame, {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        Size = UDim2.new(1, 0, 1, 0),
        BorderSizePixel = 0,

        CanvasSize = self.pageLength:map(function(length)
            return UDim2.new(0, 0, 0, length)
        end),
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        UIListLayout = Roact.createElement(StandardUIListLayout, {
            Padding = UDim.new(0, Style.Constants.SpaciousElementPadding),

            [Roact.Change.AbsoluteContentSize] = function(obj)
                self.updatePageLength(obj.AbsoluteContentSize.Y + (Style.Constants.PagePadding * 2))
            end,
        }),

        ColorSpaceSection = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize + (Style.Constants.StandardButtonHeight * 3) + (Style.Constants.MinorElementPadding * 2)),
            BackgroundTransparency = 1,
            LayoutOrder = 0,
        }, {
            Header = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),

                Text = uiTranslations["ColorSpace_Label"],
            }),

            ColorSpaceList = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 1),
                Position = UDim2.new(0.5, 0, 1, 0),
                Size = UDim2.new(1, 0, 0, (Style.Constants.StandardButtonHeight * 3) + (Style.Constants.MinorElementPadding * 2)),
                BackgroundTransparency = 1,
            }, colorSpaceListElements),
        }),

        HueAdjustmentSection = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize + (Style.Constants.StandardButtonHeight * 2) + (Style.Constants.MinorElementPadding * 2)),
            BackgroundTransparency = 1,
            LayoutOrder = 1,
        }, {
            Header = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),

                Text = uiTranslations["HueInterpolation_Label"],
            }),

            HueAdjustmentList = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 1),
                Position = UDim2.new(0.5, 0, 1, 0),
                Size = UDim2.new(1, 0, 0, (Style.Constants.StandardButtonHeight * 2) + Style.Constants.MinorElementPadding),
                BackgroundTransparency = 1,
            }, hueAdjustmentListElements),
        }),

        PrecisionSection = Roact.createElement("Frame", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize + Style.Constants.StandardInputHeight + Style.Constants.MinorElementPadding),
            BackgroundTransparency = 1,
            LayoutOrder = 2,
        }, {
            Header = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(0.5, 0),
                Position = UDim2.new(0.5, 0, 0, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardTextSize),

                Text = uiTranslations["Precision_Label"],
            }),

            PrecisionInputs = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 1),
                Position = UDim2.new(0.5, 0, 1, 0),
                Size = UDim2.new(1, 0, 0, Style.Constants.StandardInputHeight),
                BackgroundTransparency = 1,
            }, {
                UIListLayout = Roact.createElement(StandardUIListLayout, {
                    Padding = UDim.new(0, Style.Constants.MinorElementPadding),
                    FillDirection = Enum.FillDirection.Horizontal,
                }),

                SubtractPrecisionButton = Roact.createElement(Button, {
                    Size = Style.UDim2.StandardButtonSize,
                    LayoutOrder = 0,

                    displayType = "image",
                    image = Style.Images.SubtractButtonIcon,
                    disabled = (precision <= MIN_PRECISION),

                    onActivated = function()
                        local newPrecision = precision - 1
                        if (newPrecision < MIN_PRECISION) then return end

                        self.props.setPrecision(newPrecision)
                    end
                }),

                AddPrecisionButton = Roact.createElement(Button, {
                    Size = Style.UDim2.StandardButtonSize,
                    LayoutOrder = 2,

                    displayType = "image",
                    image = Style.Images.AddButtonIcon,
                    disabled = (precision >= MAX_PRECISION) or (Util.getUtilisedKeypoints(numKeypoints, precision + 1) > Constants.MAX_COLORSEQUENCE_KEYPOINTS),

                    onActivated = function()
                        local newPrecision = precision + 1
                        if (newPrecision > MAX_PRECISION) then return end

                        self.props.setPrecision(newPrecision)
                    end
                }),

                MaxPrecisionButton = Roact.createElement(Button, {
                    Size = UDim2.new(0, 40, 0, Style.Constants.StandardButtonHeight),
                    LayoutOrder = 3,

                    displayType = "text",
                    text = uiTranslations["MaxKeypoints_ButtonText"],
                    disabled = (precision >= MAX_PRECISION) or (precision >= maxPrecision),

                    onActivated = function()
                        self.props.setPrecision(maxPrecision)
                    end
                }),

                ResetPrecisionButton = Roact.createElement(Button, {
                    Size = UDim2.new(0, 40, 0, Style.Constants.StandardButtonHeight),
                    LayoutOrder = 4,

                    displayType = "text",
                    text = uiTranslations["Reset_ButtonText"],
                    disabled = (precision == 0),

                    onActivated = function()
                        self.props.setPrecision(0)
                    end
                }),

                PrecisionInput = Roact.createElement(TextInput, {
                    Size = UDim2.new(0, 40, 0, Style.Constants.StandardInputHeight),
                    LayoutOrder = 1,

                    Text = precision,
                    TextXAlignment = Enum.TextXAlignment.Center,

                    isTextAValidValue = function(text)
                        local n = tonumber(text)
                        if (not n) then return false end
                        if (math.floor(n) ~= n) then return false end

                        return (n >= MIN_PRECISION) and
                            (n <= MAX_PRECISION) and
                            (Util.getUtilisedKeypoints(numKeypoints, n) <= Constants.MAX_COLORSEQUENCE_KEYPOINTS)
                    end,

                    onSubmit = function(text)
                        local n = tonumber(text)

                        self.props.setPrecision(n)
                    end,

                    canSubmitEmptyString = false,
                })
            })
        }),
    })
end

---

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,

        keypoints = state.gradientEditor.keypoints,
        colorSpace = state.gradientEditor.colorSpace,
        hueAdjustment = state.gradientEditor.hueAdjustment,
        precision = state.gradientEditor.precision,
    }
end, function(dispatch)
    return {
        setColorSpace = function(colorSpace: string)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_SetGradient,
                colorSpace = colorSpace
            })
        end,

        setHueAdjustment = function(hueAdjustment: string)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_SetGradient,
                hueAdjustment = hueAdjustment
            })
        end,

        setPrecision = function(precision: number)
            dispatch({
                type = Enums.StoreActionType.GradientEditor_SetGradient,
                precision = precision
            })
        end,
    }
end)(GradientInfo)