the cutting room mirror

Unused Scripts

This is a sub-page of Roblox Developer.

To do:
content/Scripts has alot of unused stuff.

Roblox developer has a bunch of scripts that are copied from the PC version, Most of them being unused and some dev comments.

VR#

In content/scripts/Module/VR and content/scripts/Module are unused scripts for VR.

VR Scripts

-- VirtualKeyboard.lua --
-- Written by Kip Turner, copyright ROBLOX 2016 --


local CoreGui = game:GetService('CoreGui')
local RunService = game:GetService('RunService')
local UserInputService = game:GetService('UserInputService')
local GuiService = game:GetService('GuiService')
local HttpService = game:GetService('HttpService')
local ContextActionService = game:GetService('ContextActionService')
local PlayersService = game:GetService('Players')
local TextService = game:GetService('TextService')

local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Util = require(RobloxGui.Modules.Settings.Utility)

local vrKeyboardSuccess, vrKeyboardFlagValue = pcall(function() return settings():GetFFlag("UseVRKeyboardInLua") end)
local useVRKeyboard = (vrKeyboardSuccess and vrKeyboardFlagValue == true)

local BACKGROUND_OPACITY = 0.3
local NORMAL_KEY_COLOR = Color3.new(49/255,49/255,49/255)
local HOVER_KEY_COLOR = Color3.new(49/255,49/255,49/255)
local PRESSED_KEY_COLOR = Color3.new(0,162/255,1)
local SET_KEY_COLOR = Color3.new(0,162/255,1)

local KEY_TEXT_COLOR = Color3.new(1,1,1)
---------------------------------------- KEYBOARD LAYOUT --------------------------------------
local MINIMAL_KEYBOARD_LAYOUT = HttpService:JSONDecode([==[
[
  [
    {
      "a": 7,
      "w": 0.8
    },
    "*",
    "Q",
    "W",
    "E",
    "R",
    "T",
    "Y",
    "U",
    "I",
    "O",
    "P",
    {
      "w": 1.8
    },
    "Delete"
  ],
  [
    {
      "w": 1.6
    },
    "Caps",
    "A",
    "S",
    "D",
    "F",
    "G",
    "H",
    "J",
    "K",
    "L",
    "?",
    {
      "h": 2,
      "w2": 2.4,
      "h2": 1,
      "x2": -1.4,
      "y2": 1
    },
    "Enter"
  ],
  [
    {
      "w": 2.2
    },
    "Shift",
    "Z",
    "X",
    "C",
    "V",
    "B",
    "N",
    "M",
    "."
  ],
  [
    {
      "w": 2.2
    },
    "123/sym",
    {
      "w": 8
    },
    "",
    {
      "w": 2.4
    },
    "<Speaker>"
  ]
]
]==])

local MINIMAL_KEYBOARD_LAYOUT_SYMBOLS = HttpService:JSONDecode([==[
[
  [
    {
      "a": 7,
      "w": 0.8
    },
    "*",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "0",
    {
      "w": 1.8
    },
    "Delete"
  ],
  [
    {
      "w": 1.6
    },
    "!",
    "@",
    "#",
    "$",
    "%",
    "^",
    "&",
    "(",
    ")",
    "=",
    "?",
    {
      "h": 2,
      "w2": 2.4,
      "h2": 1,
      "x2": -1.4,
      "y2": 1
    },
    "Enter"
  ],
  [
    {
      "w": 1.2
    },
    "/",
    "-",
    "+",
    "_",
    ":",
    ";",
    "'",
    "\"",
    ",",
    "."
  ],
  [
    {
      "w": 2.2
    },
    "abc",
    {
      "w": 8
    },
    "",
    {
      "w": 2.4
    },
    "<Speaker>"
  ]
]
]==])


---------------------------------------- END KEYBOARD LAYOUT --------------------------------------


local function tokenizeString(str, tokenChar)
    local words = {}
    for word in string.gmatch(str, '([^' .. tokenChar .. ']+)') do
        table.insert(words, word)
    end
    return words
end

local function ConvertFontSizeEnumToInt(fontSizeEnum)
    local result = string.match(fontSizeEnum.Name, '%d+')
    return (result and tostring(result)) or 12
end


-- RayPlaneIntersection

-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
local function RayPlaneIntersection(ray, planeNormal, pointOnPlane)
    planeNormal = planeNormal.unit
    ray = ray.Unit
    -- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane
    local Vd = planeNormal:Dot(ray.Direction)
    
    -- could fuzzy equals this a little bit to account for imprecision or very close angles to zero
    if Vd == 0 then -- parallel, no intersection
        return nil
    end

    local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
    local t = V0 / Vd

    if t < 0 then --plane is behind ray origin, and thus there is no intersection
        return nil
    end
    
    return ray.Origin + ray.Direction * t
end

function Clamp(low, high, input)
    return math.max(low, math.min(high, input))
end

-- No rotation as of yet
local function PointInGuiObject(object, x, y)
    local minPt = object.AbsolutePosition
    local maxPt = object.AbsolutePosition + object.AbsoluteSize
    if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then
        return true
    end
    return false
end


local function ExtendedInstance(instance)
    local this = {}
    do
        local mt =
        {
            __index = function (t, k)
                return instance[k]
            end;

            __newindex = function (t, k, v)
                instance[k] = v
            end;
        }
        setmetatable(this, mt)
    end
    return this
end

local function IsVoiceToTextEnabled()
    return false
end

local function CreateVRButton(instance)
    local newButton = ExtendedInstance(instance)

    rawset(newButton, "OnEnter", function(self)
    end)
    rawset(newButton, "OnLeave", function(self)
    end)
    rawset(newButton, "OnDown", function(self)
    end)
    rawset(newButton, "OnUp", function(self)
    end)
    rawset(newButton, "ContainsPoint", function(self, x, y)
        return PointInGuiObject(instance, x, y)
    end)
    rawset(newButton, "Update", function(self)
    end)

    return newButton
end

local selectionRing = Util:Create'ImageLabel'
{
    Name = 'SelectionRing';
    Size = UDim2.new(1, -6, 1, -6);
    Position = UDim2.new(0, 4, 0, 3);
    Image = 'rbxasset://textures/ui/menu/buttonHover.png';
    ScaleType = Enum.ScaleType.Slice;
    SliceCenter = Rect.new(94/2, 94/2, 94/2, 94/2);
    BackgroundTransparency = 1;
}

local KEY_ICONS =
{
    ["<Speaker>"] = {Asset = "rbxasset://textures/ui/Keyboard/mic_icon.png", AspectRatio = 0.615};
}

local function CreateKeyboardKey(keyboard, layoutData, keyData)
    local isSpecialShapeKey = layoutData['width2'] and layoutData['height2'] and layoutData['x2'] and layoutData['y2']

    local newKeyElement = Util:Create'ImageButton'
    {
        Name = keyData[1];
        Position = UDim2.new(layoutData['x'], 0, layoutData['y'], 0);
        Size = UDim2.new(layoutData['width'], 0, layoutData['height'], 0);
        BorderSizePixel = 0;
        Image = "";
        BackgroundTransparency = 1;
        ZIndex = 1;
    }
    local keyText = Util:Create'TextLabel'
    {
        Name = "KeyText";
        Text = keyData[#keyData];
        Position = UDim2.new(0, -10, 0, -10);
        Size = UDim2.new(1, 0, 1, 0);
        Font = Enum.Font.SourceSansBold;
        FontSize = Enum.FontSize.Size96;
        TextColor3 = KEY_TEXT_COLOR;
        BackgroundTransparency = 1;
        Selectable = true;
        ZIndex = 2;
        Parent = newKeyElement;
    }
    local backgroundImage = Util:Create'Frame'
    {
        Name = 'KeyBackground';
        Size = UDim2.new(1,-10,1,-10);
        Position = UDim2.new(0,-5,0,-5);
        BackgroundColor3 = NORMAL_KEY_COLOR;
        BackgroundTransparency = BACKGROUND_OPACITY;
        BorderSizePixel = 0;
        Parent = newKeyElement;
    }
    
    local selectionObject = Util:Create'ImageLabel'
    {
        Name = 'SelectionObject';
        Size = UDim2.new(1,0,1,0);
        BackgroundTransparency = 1;
        Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
        ImageTransparency = 0;
        ScaleType = Enum.ScaleType.Slice;
        SliceCenter = Rect.new(12,12,52,52);
        BorderSizePixel = 0;
    }

    newKeyElement.SelectionImageObject = Util:Create'ImageLabel'
    {
        Visible = false;
    }

    -- Special silly enter key nonsense
    local secondBackgroundImage = nil
    local specialSelectionObject, specialSelectionObject2, specialSelectionObject3 = nil, nil, nil
    if isSpecialShapeKey then
        secondBackgroundImage = Util:Create'ImageButton'
        {
            Name = 'KeyBackground';
            Position = UDim2.new(layoutData['x2'] / layoutData['width'], -5, layoutData['y2'] / layoutData['height'], -5);
            Size = UDim2.new(layoutData['width2'] / layoutData['width'], 0, layoutData['height2'] / layoutData['height'], -10);
            BackgroundColor3 = NORMAL_KEY_COLOR;
            BackgroundTransparency = BACKGROUND_OPACITY;
            BorderSizePixel = 0;
            AutoButtonColor = false;
            Parent = newKeyElement;
        }
        if layoutData['x2'] <= 0 then
            keyText.Size = secondBackgroundImage.Size - UDim2.new(0,10,0,0)
            keyText.Position = secondBackgroundImage.Position
            secondBackgroundImage.Size = secondBackgroundImage.Size - UDim2.new(1,0,0,0)
        end

        do
            specialSelectionObject = Util:Create'Frame'
            {
                Name = 'SpecialSelectionObject';
                Size = UDim2.new(1,0,0.5,0);
                Position = UDim2.new(0,0,0.5,0);
                BackgroundTransparency = 1;
                ClipsDescendants = true;
                Util:Create'ImageLabel'
                {
                    Name = 'Borders';
                    Position = UDim2.new(-1,0,-1,0);
                    Size = UDim2.new(2,0,2,0);
                    BackgroundTransparency = 1;
                    Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
                    ImageTransparency = 0;
                    ScaleType = Enum.ScaleType.Slice;
                    SliceCenter = Rect.new(12,12,52,52);
                };
            }
            specialSelectionObject2 = specialSelectionObject:Clone()
            specialSelectionObject2.Size = UDim2.new(1,0,0.5,5)
            specialSelectionObject2.Position = UDim2.new(0,0,0,0)
            specialSelectionObject2.Borders.Size = UDim2.new(1,0,1,30)
            specialSelectionObject2.Borders.Position = UDim2.new(0,0,0,0)

            specialSelectionObject3 = specialSelectionObject:Clone()
            specialSelectionObject3.Size = UDim2.new(1,5,1,0)
            specialSelectionObject3.Position = UDim2.new(0,0,0,0)
            specialSelectionObject3.Borders.Size = UDim2.new(1,30,1,0)
            specialSelectionObject3.Borders.Position = UDim2.new(0,0,0,0)
        end
        -- End of nonsense
    end

    local newKey = CreateVRButton(newKeyElement)

    local hovering = false
    local pressed = false
    local isAlpha = #keyData == 1 and type(keyData[1]) == 'string' and #keyData[1] == 1 and
                    string.byte(keyData[1]) >= string.byte("A") and string.byte(keyData[1]) <= string.byte("z")

    local icon = nil
    if keyData[1] and KEY_ICONS[keyData[1]] then
        keyText.Visible = false
        icon = Util:Create'ImageLabel'
        {
            Name = 'KeyIcon';
            Size = UDim2.new(KEY_ICONS[keyData[1]].AspectRatio, -20, 1, -20);
            SizeConstraint = Enum.SizeConstraint.RelativeYY;
            BackgroundTransparency = 1;
            Image = KEY_ICONS[keyData[1]].Asset;
            Parent = backgroundImage;
        }

        local function onChanged(prop)
            if prop == 'AbsoluteSize' then
                icon.Position = UDim2.new(0.5,-icon.AbsoluteSize.X/2,0.5,-icon.AbsoluteSize.Y/2);
            end
        end
        icon.Changed:connect(onChanged)
        onChanged('AbsoluteSize')
    end

    local function isEnabled()
        if newKey:GetCurrentKeyValue() == "<Speaker>" then
            return IsVoiceToTextEnabled()
        end
        return true
    end

    local function onClicked()
        local keyValue = nil
        local currentKeySetting = newKey:GetCurrentKeyValue()

        if currentKeySetting == 'Shift' then
            keyboard:SetShift(not keyboard:GetShift())
        elseif currentKeySetting == 'Caps' then
            keyboard:SetCaps(not keyboard:GetCaps())
        elseif currentKeySetting == 'Enter' then
            keyboard:SubmitText(true, true)
        elseif currentKeySetting == 'Delete' then
            keyboard:BackspaceAtCursor()
        elseif currentKeySetting == "123/sym" then
            keyboard:SetCurrentKeyset(2)
        elseif currentKeySetting == "abc" then
            keyboard:SetCurrentKeyset(1)
        elseif currentKeySetting == "<Speaker>" then
            keyboard:SetVoiceMode(true)
        elseif currentKeySetting == 'Tab' then
            keyValue = '\t'
        else
            keyValue = currentKeySetting
        end

        if keyValue ~= nil then
            keyboard:SubmitCharacter(keyValue, isAlpha)
        end
    end

    local function setKeyColor(newColor, hovering)
        backgroundImage.BackgroundColor3 = newColor
        if secondBackgroundImage then
            secondBackgroundImage.BackgroundColor3 = newColor
        end
        if isSpecialShapeKey then
            specialSelectionObject.Parent = hovering and backgroundImage or nil
            specialSelectionObject2.Parent = hovering and backgroundImage or nil
            specialSelectionObject3.Parent = hovering and secondBackgroundImage or nil
        else
            selectionObject.Parent = hovering and backgroundImage or nil
        end
    end

    local function update()
        local currentKey = newKey:GetCurrentKeyValue()

        if pressed then
            setKeyColor(PRESSED_KEY_COLOR, false)
        elseif hovering then
            setKeyColor(HOVER_KEY_COLOR, true)
        elseif currentKey == 'Caps' and keyboard:GetCaps() then
            setKeyColor(SET_KEY_COLOR, false)
        elseif currentKey == 'Shift' and keyboard:GetShift() then
            setKeyColor(SET_KEY_COLOR, false)
        elseif currentKey == 'abc' then
            setKeyColor(SET_KEY_COLOR, false)
        else
            setKeyColor(NORMAL_KEY_COLOR, false)
        end

        if icon then
            icon.ImageTransparency = isEnabled() and 0 or 0.5
        end

        keyText.Text = newKey:GetCurrentKeyValue()
    end

    local hoveringGuiElements = {}

    rawset(newKey, "OnEnter", function(self)
        hovering = true
        update()
    end)
    rawset(newKey, "OnLeave", function(self)
        if not next(hoveringGuiElements) then
            hovering = false
            pressed = false
            update()
        end
    end)
    rawset(newKey, "OnDown", function(self)
        pressed = true
        update()
    end)
    rawset(newKey, "OnUp", function(self)
        pressed = false
        update()
    end)
    rawset(newKey, "GetCurrentKeyValue", function(self)
        local shiftEnabled = keyboard:GetShift()
        local capsEnabled = keyboard:GetCaps()

        if isAlpha then
            if capsEnabled and shiftEnabled then
                return string.lower(keyData[#keyData])
            elseif capsEnabled or shiftEnabled then
                return keyData[1]
            else
                return string.lower(keyData[#keyData])
            end
        end

        if shiftEnabled then
            return keyData[1]
        end

        return keyData[#keyData]
    end)
    rawset(newKey, "ContainsPoint", function(self, x, y)
        return PointInGuiObject(backgroundImage, x, y) or
               (secondBackgroundImage and PointInGuiObject(secondBackgroundImage, x, y))
    end)
    rawset(newKey, "Update", function(self)
        update()
    end)
    rawset(newKey, "GetInstance", function(self)
        return newKeyElement
    end)

    newKeyElement.MouseButton1Down:connect(function() newKey:OnDown() end)
    newKeyElement.MouseButton1Up:connect(function() newKey:OnUp() end)
    newKeyElement.SelectionGained:connect(function() hoveringGuiElements[newKeyElement] = true newKey:OnEnter() end)
    newKeyElement.SelectionLost:connect(function() hoveringGuiElements[newKeyElement] = nil newKey:OnLeave() end)
    newKeyElement.MouseButton1Click:connect(function() onClicked() end)
    if secondBackgroundImage then
        secondBackgroundImage.MouseButton1Click:connect(onClicked)
        secondBackgroundImage.MouseButton1Down:connect(function() newKey:OnDown() end)
        secondBackgroundImage.MouseButton1Up:connect(function() newKey:OnUp() end)
        secondBackgroundImage.SelectionGained:connect(function()
            hoveringGuiElements[secondBackgroundImage] = true
            newKey:OnEnter()
        end)
        secondBackgroundImage.SelectionLost:connect(function()
            hoveringGuiElements[secondBackgroundImage] = nil
            newKey:OnLeave()
        end)
    end

    update()

    return newKey
end



local function ConstructKeyboardUI(keyboardLayoutDefinitions)
    local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
    local panel = Panel3D.Get("Keyboard")
    panel:SetVisible(false)

    -- DEBUG setting
    -- panel:GetGUI().Parent = workspace


    local buttons = {}

    local keyboardContainer = Util:Create'Frame'
    {
        Name = 'VirtualKeyboard';
        Size = UDim2.new(1, 0, 1, 0);
        Position = UDim2.new(0, 0, 0, 0);
        BackgroundTransparency = 1;
        Active = true;
        Visible = false;
    };

    local textEntryBackground = Util:Create'ImageLabel'
    {
        Name = 'TextEntryBackground';
        Size = UDim2.new(0.5,0,0.125,0);
        Position = UDim2.new(0.25,0,0,0);
        Image = "";
        BackgroundTransparency = 0.5;
        BackgroundColor3 = Color3.new(31/255,31/255,31/255);
        BorderSizePixel = 0;
        ClipsDescendants = true;
        Parent = keyboardContainer;
    }
        local textfieldBackground = Util:Create'Frame'
        {
            Name = 'TextfieldBackground';
            Position = UDim2.new(0,2,0,2);
            Size = UDim2.new(1, -4, 1, -4);
            BackgroundTransparency = 0;
            BackgroundColor3 = Color3.new(209/255,216/255,221/255);
            BorderSizePixel = 0;
            Visible = true;
            Parent = textEntryBackground;
        };
            local textEntryField = Util:Create'TextButton'
            {
                Name = "TextEntryField";
                Text = "";
                Position = UDim2.new(0,4,0,4);
                Size = UDim2.new(1, -8, 1, -8);
                Font = Enum.Font.SourceSans;
                FontSize = Enum.FontSize.Size60;
                TextXAlignment = Enum.TextXAlignment.Left;
                BackgroundTransparency = 1;
                BorderSizePixel = 0;
                Parent = textfieldBackground;
            }
                local textfieldCursor = Util:Create'Frame'
                {
                    Name = 'TextfieldCursor';
                    Size = UDim2.new(0, 5, 0.9, 0);
                    Position = UDim2.new(0, 0, 0.05, 0);
                    BackgroundTransparency = 0;
                    BackgroundColor3 = SET_KEY_COLOR;
                    BorderSizePixel = 0;
                    Visible = true;
                    ZIndex = 2;
                    Parent = textEntryField;
                };

    local closeButtonElement = Util:Create'ImageButton'
    {
        Name = 'CloseButton';
        Size = UDim2.new(0.075,-10,0.198,-10);
        Position = UDim2.new(0,-5,0,-35);
        Image = "rbxasset://textures/ui/Keyboard/close_button_background.png";
        BackgroundTransparency = 1;
        AutoButtonColor = false;
        Parent = keyboardContainer;
    }
    do
        closeButtonElement.SelectionImageObject = Util:Create'ImageLabel'
        {
            Name = 'Selection';
            Size = UDim2.new(0.9,0,0.9,0);
            Position = UDim2.new(0.05,0,0.05,0);
            Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png";
            BackgroundTransparency = 1;
        }
        Util:Create'ImageLabel'
        {
            Name = 'Icon';
            Size = UDim2.new(0.5,0,0.5,0);
            Position = UDim2.new(0.25,0,0.25,0);
            Image = "rbxasset://textures/ui/Keyboard/close_button_icon.png";
            BackgroundTransparency = 1;
            Parent = closeButtonElement;
        }
    end
    local closeButton = CreateVRButton(closeButtonElement)
    table.insert(buttons, closeButton)

    local voiceRecognitionContainer = Util:Create'Frame'
    {
        Name = 'VoiceRecognitionContainer';
        Size = UDim2.new(1, 0, 0.85, 0);
        Position = UDim2.new(0, 0, 0.15, 0);
        BackgroundTransparency = 1;
        Active = true;
        Visible = false;
        Parent = keyboardContainer;
    };
    do
        local voiceRecognitionBackground1 = Util:Create'Frame'
        {
            Name = 'voiceRecognitionBackground1';
            Size = UDim2.new(1, 0, 0.75, 0);
            Position = UDim2.new(0, 0, 0, 0);
            BackgroundColor3 = Color3.new(0,0,0);
            BackgroundTransparency = 0.5;
            BorderSizePixel = 0;
            Active = true;
            Parent = voiceRecognitionContainer;
        };
        local voiceRecognitionBackground2 = voiceRecognitionBackground1:Clone()
        voiceRecognitionBackground2.Size = UDim2.new(0.75, 0, 0.25, 0)
        voiceRecognitionBackground2.Position = UDim2.new(0, 0, 0.75, 0)
        voiceRecognitionBackground2.Parent = voiceRecognitionContainer
    end

    local voiceDoneButton = CreateVRButton(Util:Create'ImageButton'
    {
        Name = 'DoneButton';
        Size = UDim2.new(0.25,-5,0.25,-5);
        Position = UDim2.new(0.75,5,0.75,5);
        Image = "";
        BackgroundTransparency = 0;
        AutoButtonColor = false;
        BorderSizePixel = 0;
        Parent = voiceRecognitionContainer;
    })
    table.insert(buttons, voiceDoneButton)

    local newKeyboard = ExtendedInstance(keyboardContainer)

    local keyboardOptions = nil
    local keysets = {}

    local capsLockEnabled = false
    local shiftEnabled = false

    local textfieldCursorPosition = 0

    local closedEvent = Instance.new('BindableEvent')
    local opened = false

    local function SetTextFieldCursorPosition(newPosition)
        textfieldCursorPosition = Clamp(0, #textEntryField.Text, newPosition)
        if not textEntryField.TextFits then
            textfieldCursorPosition = #textEntryField.Text
        end

        local textSize = TextService:GetTextSize(
            string.sub(textEntryField.Text, 1, textfieldCursorPosition),
            ConvertFontSizeEnumToInt(textEntryField.FontSize),
            textEntryField.Font,
            textEntryField.AbsoluteSize)
        textfieldCursor.Position = UDim2.new(0, textSize.x, textfieldCursor.Position.Y.Scale, textfieldCursor.Position.Y.Offset)
    end

    local function UpdateTextEntryFieldText(newText)
        textEntryField.Text = newText
        SetTextFieldCursorPosition(textfieldCursorPosition)
    end

    local buffer = ""
    local function getBufferText()
        if keyboardOptions and keyboardOptions.TextBox then
            return keyboardOptions.TextBox.Text
        end
        return buffer
    end
    local function setBufferText(newBufferText)
        if keyboardOptions and keyboardOptions.TextBox then
            keyboardOptions.TextBox.Text = newBufferText
        elseif buffer ~= newBufferText then
            buffer = newBufferText
            UpdateTextEntryFieldText(buffer)
        end
    end

    local function calculateTextCursorPosition(x, y)
        x = x - textEntryField.AbsolutePosition.x
        y = y - textEntryField.AbsolutePosition.y

        for i = 1, #textEntryField.Text do
            local textSize = TextService:GetTextSize(
                string.sub(textEntryField.Text, 1, i),
                ConvertFontSizeEnumToInt(textEntryField.FontSize),
                textEntryField.Font,
                textEntryField.AbsoluteSize)
            if textSize.x > x then
                return i - 1
            end
        end

        return #textEntryField.Text
    end

    local currentKeyset = nil

    rawset(newKeyboard, "ClosedEvent",  closedEvent.Event)

    rawset(newKeyboard, "GetCurrentKeyset", function(self)
        return keysets[currentKeyset]
    end)

    rawset(newKeyboard, "SetCurrentKeyset", function(self, newKeyset)
        if newKeyset ~= currentKeyset and keysets[newKeyset] ~= nil then
            if keysets[currentKeyset] and keysets[currentKeyset].container then
                keysets[currentKeyset].container.Visible = false
            end

            currentKeyset = newKeyset

            if keysets[currentKeyset] and keysets[currentKeyset].container then
                keysets[currentKeyset].container.Visible = true
            end
        end
    end)

    rawset(newKeyboard, "SetVoiceMode", function(self, inVoiceMode)
        inVoiceMode = inVoiceMode and IsVoiceToTextEnabled()

        local currentKeysetObject = self:GetCurrentKeyset()
        if currentKeysetObject and currentKeysetObject.container then
            currentKeysetObject.container.Visible = not inVoiceMode
        end

        voiceRecognitionContainer.Visible = inVoiceMode
    end)

    rawset(newKeyboard, "GetCaps", function(self)
        return capsLockEnabled
    end)

    rawset(newKeyboard, "SetCaps", function(self, newCaps)
        capsLockEnabled = newCaps
        for _, key in pairs(self:GetCurrentKeyset().keys) do
            key:Update()
        end
    end)

    rawset(newKeyboard, "GetShift", function(self)
        return shiftEnabled
    end)

    rawset(newKeyboard, "SetShift", function(self, newShift)
        shiftEnabled = newShift
        for _, key in pairs(self:GetCurrentKeyset().keys) do
            key:Update()
        end
    end)

    local textChangedConn = nil
    local panelClosedConn = nil
    rawset(newKeyboard, "Open", function(self, options)
        if opened then return end
        opened = true

        keyboardOptions = options

        self:SetCurrentKeyset(1)
        self:SetVoiceMode(false)
        keyboardContainer.Visible = true

        panel:ResizeStuds(5.9, 2.25, 320)

        local localCF = CFrame.new()

        if textChangedConn then textChangedConn:disconnect() end
        textChangedConn = nil   
        if options.TextBox then
            textChangedConn = options.TextBox.Changed:connect(function(prop)
                if prop == 'Text' then
                    UpdateTextEntryFieldText(options.TextBox.Text)
                end
            end)
            options.TextBox.FocusLost:connect(function(submitted)
                self:Close(submitted)
            end)
            if options.TextBox.ClearTextOnFocus then
                setBufferText("")
            else
                UpdateTextEntryFieldText(options.TextBox.Text)
            end

            local textboxPanel = Panel3D.FindContainerOf(options.TextBox)
            if textboxPanel then
                panelClosedConn = Panel3D.OnPanelClosed.Event:connect(function(closedPanelName)
                    if closedPanelName == textboxPanel.name then
                        self:Close(false)
                    end
                end)

                --Attach to it if it's in the same space
                if textboxPanel.panelType == Panel3D.Type.Fixed then
                    local panelCF = textboxPanel.localCF
                    localCF = panelCF * CFrame.new(0, (-textboxPanel.height / 2) - 0.5, 0) * 
                                        CFrame.Angles(math.rad(30), 0, 0) * 
                                        CFrame.new(0, (-panel.height / 2) - 0.5, 0)
                else
                    --Otherwise, best-guess where it should go based on the user's head.
                    local headForwardCF = Panel3D.GetHeadLookXZ(true)
                    localCF = headForwardCF * CFrame.Angles(math.rad(22.5), 0, 0) * CFrame.new(0, -1, 5)
                end
            end
        else
            setBufferText("")
        end

        self.Parent = panel:GetGUI()

        panel:SetType(Panel3D.Type.Fixed, { CFrame = localCF })
        panel:SetCanFade(false)
        panel:SetVisible(true, true)
        panel:ForceShowUntilLookedAt()

        function panel:OnUpdate()
        end
    end)

    rawset(newKeyboard, "Close", function(self, submit)
        submit = (submit == true)

        if not opened then return end
        opened = false


        if textChangedConn then textChangedConn:disconnect() end
        textChangedConn = nil
        if panelClosedConn then panelClosedConn:disconnect() end
        panelClosedConn = nil
        -- Clean-up
        panel:OnMouseLeave()
        panel:SetVisible(false, true)
        keyboardContainer.Visible = false

        self:SubmitText(submit, false)
    end)

    rawset(newKeyboard, "SubmitText", function(self, submit, keepKeyboardOpen)
        local keyboardTextbox = keyboardOptions and keyboardOptions.TextBox
        if keyboardTextbox then
            if submit then
                keyboardTextbox.Text = getBufferText()
            end
            keyboardTextbox:ReleaseFocus(submit)
            if keepKeyboardOpen then
                keyboardTextbox:CaptureFocus()
            end
        end
    end)

    rawset(newKeyboard, "GetCurrentOptions", function(self)
        return keyboardOptions
    end)

    rawset(newKeyboard, "BackspaceAtCursor", function(self)
        if textfieldCursorPosition >= 1 then
            local bufferText = getBufferText()
            local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition - 1) .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText)
            local newCursorPosition = textfieldCursorPosition - 1
            setBufferText(newBufferText)
            SetTextFieldCursorPosition(newCursorPosition)
        end
    end)

    rawset(newKeyboard, "SubmitCharacter", function(self, character, isAnAlphaKey)
        setBufferText(getBufferText() .. character)
        SetTextFieldCursorPosition(textfieldCursorPosition + #character)

        if isAnAlphaKey and self:GetShift() then
            self:SetShift(false)
        end
    end)

    do -- Parse input definition
        for _, keyboardKeyset in pairs(keyboardLayoutDefinitions) do
            local keys = {}
            local keyboardSizeConstrainer = Util:Create'Frame'
            {
                Name = 'KeyboardSizeConstrainer';
                Size = UDim2.new(1, 0, 1, -20);
                Position = UDim2.new(0, 0, 0, 20);
                BackgroundTransparency = 1;
                Parent = keyboardContainer;
            };

            local maxWidth = 0
            local maxHeight = 0
            local y = 0
            for rowNum, rowData in pairs(keyboardKeyset) do
                local x = 0
                local width = 1
                local height = 1
                local width2, height2, x2, y2;
                for columnNum, columnData in pairs(rowData) do
                    if type(columnData) == 'table' then
                        if columnData['w'] then width = columnData['w'] end
                        if columnData['h'] then height = columnData['h'] end
                        if columnData['x'] then x = x + columnData['x'] end
                        if columnData['y'] then y = y + columnData['y'] end
                        if columnData['x2'] then x2 = columnData['x2'] end
                        if columnData['y2'] then y2 = columnData['y2'] end
                        if columnData['w2'] then width2 = columnData['w2'] end
                        if columnData['h2'] then height2 = columnData['h2'] end
                    elseif type(columnData) == 'string' then
                        if columnData == "" then
                            columnData = " "
                        end
                        -- put key
                        local key = CreateKeyboardKey(
                            newKeyboard,
                            {x = x, y = y, width = width, height = height, x2 = x2, y2 = y2, width2 = width2, height2 = height2},
                            tokenizeString(columnData, '\n'))
                        table.insert(keys, key)

                        x = x + width
                        maxWidth = math.max(maxWidth, x)
                        maxHeight = math.max(maxHeight, y + height)
                        -- reset for the next key
                        width = 1
                        height = 1
                        width2, height2, x2, y2 = nil, nil, nil, nil
                    end
                end
                y = y + 1
            end

            -- Fix the positions and sizes to fit in our KeyboardContainer
            for _, element in pairs(keys) do
                element.Position = UDim2.new(element.Position.X.Scale / maxWidth, 0, element.Position.Y.Scale / maxHeight, 0)
                element.Size = UDim2.new(element.Size.X.Scale / maxWidth, 0, element.Size.Y.Scale / maxHeight, 0)
                element.Parent = keyboardSizeConstrainer
            end

            keyboardSizeConstrainer.SizeConstraint = Enum.SizeConstraint.RelativeXX
            keyboardSizeConstrainer.Size = UDim2.new(1, 0, -maxHeight / maxWidth, 0)
            keyboardSizeConstrainer.Position = UDim2.new(0, 0, 1, 0)
            keyboardSizeConstrainer.Visible = false

            table.insert(keysets, {keys = keys, container = keyboardSizeConstrainer})
        end
        newKeyboard:SetCurrentKeyset(1)
    end

    textEntryField.MouseButton1Click:connect(function()
        SetTextFieldCursorPosition(calculateTextCursorPosition(panel.lookAtPixel.X, panel.lookAtPixel.Y))
    end)

    closeButton.MouseButton1Click:connect(function()
        newKeyboard:Close(false)
        closedEvent:Fire()
    end)

    voiceDoneButton.MouseButton1Click:connect(function()
        newKeyboard:SetVoiceMode(false)
    end)

    return newKeyboard
end


local Keyboard = nil;
local function GetKeyboard()
    if Keyboard == nil then
        Keyboard = ConstructKeyboardUI({MINIMAL_KEYBOARD_LAYOUT, MINIMAL_KEYBOARD_LAYOUT_SYMBOLS})
    end
    return Keyboard
end



local VirtualKeyboardClass = {}


function VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox)
    local keyboardOptions = {}

    keyboardOptions.TextBox = textbox

    return keyboardOptions
end

local VirtualKeyboardPlatform = false
do
    -- iOS, Android and Xbox already have platform specific keyboards
    local platform = UserInputService:GetPlatform()
    VirtualKeyboardPlatform = platform == Enum.Platform.Windows or
                              platform == Enum.Platform.OSX
end


function VirtualKeyboardClass:ShowVirtualKeyboard(virtualKeyboardOptions)
    if VirtualKeyboardPlatform and UserInputService.VREnabled then
        GetKeyboard():Open(virtualKeyboardOptions)
    end
end

function VirtualKeyboardClass:CloseVirtualKeyboard()
    if VirtualKeyboardPlatform and UserInputService.VREnabled then
        local currentKeyboard = GetKeyboard()
        currentKeyboard:Close(false)
    end
end

VirtualKeyboardClass.ClosedEvent = GetKeyboard().ClosedEvent


if VirtualKeyboardPlatform and useVRKeyboard then
    UserInputService.TextBoxFocused:connect(function(textbox)
        VirtualKeyboardClass:ShowVirtualKeyboard(VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox))
    end)

    UserInputService.TextBoxFocusReleased:connect(function(textbox)
        VirtualKeyboardClass:CloseVirtualKeyboard()
    end)
end


return VirtualKeyboardClass
--Panel3D: 3D GUI panels for VR
--written by 0xBAADF00D
--revised/refactored 5/11/16

local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local PlayersService = game.Players


--Util
local Util = {}
do
    function Util.Create(instanceType)
        return function(data)
            local obj = Instance.new(instanceType)
            for k, v in pairs(data) do
                if type(k) == 'number' then
                    v.Parent = obj
                else
                    obj[k] = v
                end
            end
            return obj
        end
    end

    -- RayPlaneIntersection (shortened)
    -- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
    function Util.RayPlaneIntersection(ray, planeNormal, pointOnPlane)
        planeNormal = planeNormal.unit
        ray = ray.Unit

        local Vd = planeNormal:Dot(ray.Direction)
        if Vd == 0 then -- parallel, no intersection
            return nil
        end

        local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
        local t = V0 / Vd
        if t < 0 then --plane is behind ray origin, and thus there is no intersection
            return nil
        end
        
        return ray.Origin + ray.Direction * t
    end
end
--End of Util


--Panel3D State variables
local renderStepName = "Panel3DRenderStep-" .. game:GetService("HttpService"):GenerateGUID()
local defaultPixelsPerStud = 64
local pointUpCF = CFrame.Angles(math.rad(-90), math.rad(180), 0)
local zeroVector = Vector3.new(0, 0, 0)
local zeroVector2 = Vector2.new(0, 0)
local turnAroundCF = CFrame.Angles(0, math.rad(180), 0)
local fullyOpaqueAtPixelsFromEdge = 10
local fullyTransparentAtPixelsFromEdge = 80
local partThickness = 0.2

local cursorHidden = false
local cursorHideTime = 2.5

local currentModal = nil
local lastModal = nil
local currentMaxDist = math.huge
local currentClosest = nil
local lastClosest = nil
local currentHeadScale = 1
local panels = {}
local floorRotation = CFrame.new()
local cursor = Util.Create "ImageLabel" {
    Image = "rbxasset://textures/Cursors/Gamepad/Pointer.png",
    Size = UDim2.new(0, 8, 0, 8),
    BackgroundTransparency = 1,
    ZIndex = 10
}
--End of Panel3D State variables


--Panel3D Declaration and enumerations
local Panel3D = {}
Panel3D.Type = {
    None = 0,
    Floor = 1,
    Fixed = 2,
    HorizontalFollow = 3,
    FixedToHead = 4
}

Panel3D.OnPanelClosed = Util.Create 'BindableEvent' {
    Name = 'OnPanelClosed'
}

function Panel3D.GetHeadLookXZ(withTranslation)
    local userHeadCF = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
    local headLook = userHeadCF.lookVector
    local headYaw = math.atan2(-headLook.Z, headLook.X) + math.rad(90)
    local cf = CFrame.Angles(0, headYaw, 0)

    if withTranslation then
        cf = cf + userHeadCF.p
    end
    return cf
end

function Panel3D.FindContainerOf(element)
    for i, v in pairs(panels) do
        if v.gui and v.gui:IsAncestorOf(element) then
            return v
        end
    end
    return nil
end

function Panel3D.SetModalPanel(panel)
    if currentModal == panel then
        return
    end
    if currentModal then
        currentModal:OnModalChanged(false)
    end
    if panel then
        panel:OnModalChanged(true)
    end
    lastModal = currentModal
    currentModal = panel
end
--End of Panel3D Declaration and enumerations


--Cursor autohiding methods
local cursorHidden = false
local hasTool = false
local lastMouseActivity = tick()
local lastMouseBehavior = Enum.MouseBehavior.Default

local function OnCharacterAdded(character)
    hasTool = false
    for i, v in ipairs(character:GetChildren()) do
        if v:IsA("Tool") then
            hasTool = true
        end
    end
    character.ChildAdded:connect(function(child)
        if child:IsA("Tool") then
            hasTool = true
            lastMouseActivity = tick() --kick the mouse when a tool is equipped
        end
    end)
    character.ChildRemoved:connect(function(child)
        if child:IsA("Tool") then
            hasTool = false
        end
    end)
end
spawn(function()
    while not PlayersService.LocalPlayer do wait() end
    PlayersService.LocalPlayer.CharacterAdded:connect(OnCharacterAdded)
    if PlayersService.LocalPlayer.Character then OnCharacterAdded(PlayersService.LocalPlayer.Character) end
end)
local function autoHideCursor(hide)
    if not UserInputService.VREnabled then
        cursorHidden = false
        return
    end
    if hide then
        --don't hide if there's a tool in the character
        local character = PlayersService.LocalPlayer.Character
        if character and hasTool then
            return
        end
        cursorHidden = true
        UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
    else
        cursorHidden = false
        UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
    end
end

local function isCursorVisible()
    --if ForceShow, the cursor is definitely visible at all times
    if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceShow then
        return true
    end
    --if ForceHide, the cursor is definitely NOT visible
    if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceHide then
        return false
    end
    --Otherwise, we need to check if the developer set MouseIconEnabled=false
    if UserInputService.MouseIconEnabled and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
        return true
    end
    return false
end

--End of cursor autohiding methods


--Panel class implementation
local Panel = {}
Panel.mt = { __index = Panel }
function Panel.new(name)
    local instance = {
        name = name,

        part = nil,
        gui = nil,

        width = 1,
        height = 1,

        isVisible = false,
        isEnabled = false,
        panelType = Panel3D.Type.None,
        pixelScale = 1,
        showCursor = true,
        canFade = true,
        shouldFindLookAtGuiElement = false,
        ignoreModal = false,

        linkedTo = nil,
        subpanels = {},

        transparency = 1,
        forceShowUntilLookedAt = false,
        isLookedAt = false,
        isOffscreen = true,
        lookAtPixel = Vector2.new(-1, -1),
        lookAtDistance = math.huge,
        lookAtGuiElement = nil,
        isClosest = true
    }

    if panels[name] then
        error("A panel by the name of " .. name .. " already exists.")
    end
    panels[name] = instance

    return setmetatable(instance, Panel.mt)
end

--Panel accessor methods
function Panel:GetPart()
    if not self.part then
        self.part = Util.Create "Part" {
            Name = self.name,
            Parent = nil,

            Transparency = 1,

            CanCollide = false,
            Anchored = true,
            Archivable = false,

            Size = Vector3.new(1, 1, partThickness)
        }
    end
    return self.part
end

function Panel:GetGUI()
    if not self.gui then
        local part = self:GetPart()
        self.gui = Util.Create "SurfaceGui" {
            Parent = CoreGui,
            Adornee = part,
            Active = true,
            ToolPunchThroughDistance = 1000,
            CanvasSize = Vector2.new(0, 0),
            Enabled = self.isEnabled,
            AlwaysOnTop = true
        }
    end
    return self.gui
end

function Panel:FindHoveredGuiElement(elements)
    local x, y = self.lookAtPixel.X, self.lookAtPixel.Y
    for i, v in pairs(elements) do
        local minPt = v.AbsolutePosition
        local maxPt = v.AbsolutePosition + v.AbsoluteSize
        if minPt.X <= x and maxPt.X >= x and
           minPt.Y <= y and maxPt.Y >= y then
            return v, i
        end
    end
end
--End of panel accessor methods


--Panel update methods
function Panel:SetPartCFrame(cframe)
    self:GetPart().CFrame = cframe * CFrame.new(0, 0, -0.5 * partThickness)
end

function Panel:SetEnabled(enabled)
    if self.isEnabled == enabled then
        return
    end

    self.isEnabled = enabled
    if enabled then
        self:GetPart().Parent = workspace.CurrentCamera --todo: Perhaps this can change soon.
        self:GetGUI().Enabled = true
        for i, v in pairs(self.subpanels) do
            v:GetPart().Parent = workspace.CurrentCamera
            v:GetGUI().Enabled = true
        end
    else
        self:GetPart().Parent = nil
        self:GetGUI().Enabled = false
        for i, v in pairs(self.subpanels) do
            v:GetPart().Parent = nil
            v:GetGUI().Enabled = false
        end
    end

    self:OnEnabled(enabled)
end

function Panel:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF)
    if self.panelType == Panel3D.Type.Floor then
        --Floor panels simply... go on the floor.
        --Panel will be in camera's local space (which is assumed to be horizontal),
        --and floorRotation is derived from the user's head rotation (yaw only)
        local floorCF = cameraCF * CFrame.new(0, -headHeightFromFloor * currentHeadScale, 0) * floorRotation
        self:SetPartCFrame(floorCF * pointUpCF)
    elseif self.panelType == Panel3D.Type.Fixed then
        --Places the panel in the camera's local space, but doesn't follow the user's head.
        --Useful if you know what you're doing. localCF can be updated in PreUpdate for animation.
        local cf = self.localCF - self.localCF.p
        cf = cf + (self.localCF.p * currentHeadScale)
        self:SetPartCFrame(cameraCF * cf)
    elseif self.panelType == Panel3D.Type.HorizontalFollow then
        local headLook = userHeadCF.lookVector
        local headYaw = math.atan2(-headLook.Z, headLook.X) + math.rad(90)
        local headForwardCF = CFrame.Angles(0, headYaw, 0) + userHeadCF.p
        local localCF = (headForwardCF * self.angleFromForward) * --Rotate about Y (left-right)
                        self.angleFromHorizon * --Rotate about X (up-down)
                        (currentHeadScale * self.distance) * --Move into scene
                        turnAroundCF --Turn around to face character
        self:SetPartCFrame(cameraCF * localCF)
    elseif self.panelType == Panel3D.Type.FixedToHead then
        --Places the panel in the user's head local space. localCF can be updated in PreUpdate for animation.
        local cf = self.localCF - self.localCF.p
        cf = cf + (self.localCF.p * currentHeadScale)
        self:SetPartCFrame(cameraRenderCF * cf)
    end
end

function Panel:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay)
    --reset distance data
    self.isClosest = false
    self.lookAtPixel = zeroVector2
    self.lookAtDistance = math.huge

    --Evaluate the lookRay versus this panel
    local planeCF = self:GetPart().CFrame
    local planeNormal = planeCF.lookVector
    local pointOnPlane = planeCF.p + (planeNormal * partThickness * 0.5) --Move the point out by half the thickness of the part

    local worldIntersectPoint = Util.RayPlaneIntersection(lookRay, planeNormal, pointOnPlane)
    if worldIntersectPoint then
        self.isOffscreen = false

        --transform worldIntersectPoint to gui space
        local gui = self:GetGUI()
        local guiWidth, guiHeight = gui.AbsoluteSize.X, gui.AbsoluteSize.Y
        local localIntersectPoint = (planeCF:pointToObjectSpace(worldIntersectPoint) / currentHeadScale) * Vector3.new(-1, 1, 1) + Vector3.new(self.width / 2, -self.height / 2, 0)
        self.lookAtPixel = Vector2.new((localIntersectPoint.X / self.width) * gui.AbsoluteSize.X, (localIntersectPoint.Y / self.height) * -gui.AbsoluteSize.Y)
        
        --fire mouse enter/leave events if necessary
        local guiX, guiY = self.lookAtPixel.X, self.lookAtPixel.Y
        if guiX >= 0 and guiX <= guiWidth and
           guiY >= 0 and guiY <= guiHeight then
            if not self.isLookedAt then
                self.isLookedAt = true
                self:OnMouseEnter(guiX, guiY)
                if self.forceShowUntilLookedAt then
                    self.forceShowUntilLookedAt = false
                end
            end
        else
            if self.isLookedAt then
                self.isLookedAt = false
                self:OnMouseLeave(guiX, guiY)
            end
        end

        --evaluate distance
        self.lookAtDistance = (worldIntersectPoint - cameraRenderCF.p).magnitude
        if self.isLookedAt and self.lookAtDistance < currentMaxDist and self.showCursor then
            currentMaxDist = self.lookAtDistance
            currentClosest = self
        end
    else
        self.isOffscreen = true

        --Not looking at the plane at all, so fire off mouseleave if necessary.
        if self.isLookedAt then
            self.isLookedAt = false
            self:OnMouseLeave(self.lookAtPixel.X, self.lookAtPixel.Y)
        end
    end
end

function Panel:EvaluateTransparency()
    --Early exit if force shown
    if self.forceShowUntilLookedAt then
        self.transparency = 0
        return
    end
    --Early exit if we're looking at the panel (no transparency!)
    if self.isLookedAt then
        self.transparency = 0
        return
    end
    --Similarly, exit if we can't possibly see the panel.
    if self.isOffscreen then
        self.transparency = 1
        return
    end
    --Otherwise, we'll want to calculate the transparency.
    self.transparency = self:CalculateTransparency()
end

function Panel:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay)
    if self.forceShowUntilLookedAt and not self.part then
        self:GetPart()
        self:GetGUI()
    end
    if not self.part then
        return
    end

    local isModal = (currentModal == self)
    if not isModal and self.linkedTo and self.linkedTo == currentModal then
        isModal = true
    end
    if currentModal and not isModal then
        self:SetEnabled(false)
        return
    end

    self:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay)
    if self.isVisible then
        self:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF)
        self:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay)

        self:EvaluateTransparency(cameraCF, cameraRenderCF)
    end
end
--End of Panel update methods

--Panel virtual methods
function Panel:PreUpdate() --virtual: handle positioning here
end

function Panel:OnUpdate() --virtual: handle transparency here
end

function Panel:OnMouseEnter(x, y) --virtual
end

function Panel:OnMouseLeave(x, y) --virtual
end

function Panel:OnEnabled(enabled) --virtual
end

function Panel:OnModalChanged(isModal) --virtual
end

function Panel:OnVisibilityChanged(visible) --virtual
end

function Panel:CalculateTransparency() --virtual
    if not self.canFade then
        return 0
    end

    local guiWidth, guiHeight = self.gui.AbsoluteSize.X, self.gui.AbsoluteSize.Y
    local lookX, lookY = self.lookAtPixel.X, self.lookAtPixel.Y

    --Determine the distance from the edge; 
    --if x is negative it's on the left side, meaning the distance is just absolute value
    --if x is positive it's on the right side, meaning the distance is x minus the width
    local xEdgeDist = lookX < 0 and -lookX or (lookX - guiWidth)
    local yEdgeDist = lookY < 0 and -lookY or (lookY - guiHeight)
    if lookX > 0 and lookX < guiWidth then
        xEdgeDist = 0
    end
    if lookY > 0 and lookY < guiHeight then
        yEdgeDist = 0
    end
    local edgeDist = math.sqrt(xEdgeDist ^ 2 + yEdgeDist ^ 2)

    --since transparency is 0-1, we know how many pixels will give us 0 and how many will give us 1.
    local offset = fullyOpaqueAtPixelsFromEdge
    local interval = fullyTransparentAtPixelsFromEdge
    --then we just clamp between 0 and 1.
    return math.max(0, math.min(1, (edgeDist - offset) / interval))
end
--End of Panel virtual methods


--Panel configuration methods
function Panel:ResizeStuds(width, height, pixelsPerStud)
    pixelsPerStud = pixelsPerStud or defaultPixelsPerStud

    self.width = width
    self.height = height

    self.pixelScale = pixelsPerStud / defaultPixelsPerStud

    local part = self:GetPart()
    part.Size = Vector3.new(self.width * currentHeadScale, self.height * currentHeadScale, partThickness)

    local gui = self:GetGUI()
    gui.CanvasSize = Vector2.new(pixelsPerStud * self.width, pixelsPerStud * self.height)
end

function Panel:ResizePixels(width, height, pixelsPerStud)
    pixelsPerStud = pixelsPerStud or defaultPixelsPerStud

    local widthInStuds = width / pixelsPerStud
    local heightInStuds = height / pixelsPerStud
    self:ResizeStuds(widthInStuds, heightInStuds, pixelsPerStud)
end

function Panel:OnHeadScaleChanged(newHeadScale)
    local pixelsPerStud = self.pixelScale * defaultPixelsPerStud
    self:ResizeStuds(self.width, self.height, pixelsPerStud)
end

function Panel:SetType(panelType, config)
    self.panelType = panelType

    --clear out old type-specific members
    self.floorPos = nil

    self.localCF = nil

    self.angleFromHorizon = nil
    self.angleFromForward = nil
    self.distance = nil

    if not config then
        config = {}
    end

    if panelType == Panel3D.Type.None then
        --nothing to do
        return
    elseif panelType == Panel3D.Type.Floor then
        self.floorPos = config.FloorPosition or Vector3.new(0, 0, 0)
    elseif panelType == Panel3D.Type.Fixed then
        self.localCF = config.CFrame or CFrame.new()
    elseif panelType == Panel3D.Type.HorizontalFollow then
        self.angleFromHorizon = CFrame.Angles(config.angleFromHorizon or 0, 0, 0)
        self.angleFromForward = CFrame.Angles(0, config.angleFromForward or 0, 0)
        self.distance = CFrame.new(0, 0, config.distance or 5)
    elseif panelType == Panel3D.Type.FixedToHead then
        self.localCF = config.CFrame or CFrame.new()
    else
        error("Invalid Panel type")
    end
end

function Panel:SetVisible(visible, modal)
    if visible ~= self.isVisible then
        self:OnVisibilityChanged(visible)
        if not visible then
            Panel3D.OnPanelClosed:Fire(self.name)
        end
    end

    self.isVisible = visible
    self:SetEnabled(visible)
    if visible and modal then
        Panel3D.SetModalPanel(self)
    end
    if not visible and currentModal == self then
        if modal and not doNotRestore then
            --restore last modal panel
            Panel3D.SetModalPanel(lastModal)
        else
            Panel3D.SetModalPanel(nil)

            --if the coder explicitly wanted to hide this modal panel,
            --it follows that they don't want it to be restored when the next
            --modal panel is hidden.
            if lastModal == self then
                lastModal = nil
            end
        end
    end

    if not visible and self.forceShowUntilLookedAt then
        self.forceShowUntilLookedAt = false
    end
end

function Panel:LinkTo(panelName)
    if type(panelName) == "string" then
        self.linkedTo = Panel3D.Get(panelName)
    else
        self.linkedTo = panelName
    end
end

function Panel:ForceShowUntilLookedAt(makeModal)
    --ensure the part exists
    self:GetPart()
    self:GetGUI()

    self:SetVisible(true, makeModal)
    self.forceShowUntilLookedAt = true
end

function Panel:SetCanFade(canFade)
    self.canFade = canFade
end

--Child class, Subpanel
local Subpanel = {}
local Subpanel_mt = {}
function Subpanel.new(guiElement)
    local instance = {
        guiElement = guiElement
    }
    return setmetatable(instance, Subpanel_mt)
end

function Subpanel:GetPart()
    warn("Subpanel:GetPart() not yet implemented")
    return nil
end

function Subpanel:GetGUI()
    warn("Subpanel:GetGUI() not yet implemented")
    return nil
end

function Panel:AddSubpanel(guiElement)
    local subpanel = Subpanel.new(guiElement)
    self.subpanels[guiElement] = subpanel
end

function Panel:RemoveSubpanel(guiElement)
    self.subpanels[guiElement] = nil
end
--End of Panel configuration methods
--End of Panel class implementation


--Panel3D API
function Panel3D.Get(name)
    local panel = panels[name] 
    if not panels[name] then
        panels[name] = Panel.new(name)
        panel = panels[name]
    end
    return panel
end
--End of Panel3D API


--Panel3D Setup
local function onRenderStep()
    if not UserInputService.VREnabled then
        return
    end
    

    --reset distance info
    currentClosest = nil
    currentMaxDist = math.huge

    --figure out some useful stuff
    local camera = workspace.CurrentCamera
    local cameraCF = camera.CFrame
    local cameraRenderCF = camera:GetRenderCFrame()
    local userHeadCF = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
    local lookRay = Ray.new(cameraRenderCF.p, cameraRenderCF.lookVector)

    --allow all panels to run their own update code
    for i, v in pairs(panels) do
        v:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay)
    end

    --evaluate linked panels
    local processed = {}
    for i, v in pairs(panels) do
        if not processed[v] and v.linkedTo and v.isVisible and v.linkedTo.isVisible then
            processed[v] = true
            processed[v.linkedTo] = true

            local minTransparency = math.min(v.transparency, v.linkedTo.transparency)
            v.transparency = minTransparency
            v.linkedTo.transparency = minTransparency
        end
    end

    --run post update because the distance information hasn't been
    --finalized until now.
    for i, v in pairs(panels) do
        --If the part is fully transparent, we don't want to keep it around in the workspace.
        if v.part and v.gui then
            --check if this panel is the current modal panel
            local isModal = (currentModal == v)
            --but also check if this panel is linked to the current modal panel
            if not isModal and v.linkedTo and v.linkedTo == currentModal then
                isModal = true
            end

            local show = v.isVisible
            if not isModal and currentModal then
                show = false
            end
            if v.transparency >= 1 then
                show = false
            end

            if v.forceShowUntilLookedAt then
                show = true
            end
            if not v.canFade and v.isVisible then
                show = true
            end
            
            v:SetEnabled(show)
        end

        v:OnUpdate()
    end

    --place the cursor on the closest panel (for now)
    if not currentClosest and lastClosest then
        UserInputService.MouseBehavior = lastMouseBehavior
    elseif currentClosest and not lastClosest then
        lastMouseBehavior = UserInputService.MouseBehavior
    end

    if currentClosest then
        UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
        UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
        cursor.Parent = currentClosest:GetGUI()

        local x, y = currentClosest.lookAtPixel.X, currentClosest.lookAtPixel.Y
        cursor.Size = UDim2.new(0, 8 * currentClosest.pixelScale, 0, 8 * currentClosest.pixelScale)
        cursor.Position = UDim2.new(0, x - cursor.AbsoluteSize.x * 0.5, 0, y - cursor.AbsoluteSize.y * 0.5)
    else
        cursor.Parent = nil
    end
    lastClosest = currentClosest
end
RunService:BindToRenderStep(renderStepName, Enum.RenderPriority.Last.Value, onRenderStep)

--Implement cursor autohide functionality
UserInputService.InputChanged:connect(function(inputObj, processed)
    if inputObj.UserInputType == Enum.UserInputType.MouseMovement then
        lastMouseActivity = tick()
        autoHideCursor(false)
    end
end)
local function onHeartbeat()
    if isCursorVisible() then
        cursorHidden = false
    end
    if lastMouseActivity + cursorHideTime < tick() and not GuiService.MenuIsOpen and not cursorHidden then
        autoHideCursor(true)
    end
end
RunService.Heartbeat:connect(onHeartbeat)

--Handle camera changes
local cameraChangedConnection = nil
local function onCameraChanged(prop)
    if prop == "HeadScale" then
        pcall(function()
            currentHeadScale = workspace.CurrentCamera.HeadScale
        end)
        for i, v in pairs(panels) do
            v:OnHeadScaleChanged(currentHeadScale)
        end
    end
end

local function onWorkspaceChanged(prop)
    if prop == "CurrentCamera" then
        onCameraChanged("HeadScale")
        if cameraChangedConnection then
            cameraChangedConnection:disconnect()
        end
        cameraChangedConnection = workspace.CurrentCamera.Changed:connect(onCameraChanged)
    end
end
if workspace.CurrentCamera then
    onWorkspaceChanged("CurrentCamera")
end
workspace.Changed:connect(onWorkspaceChanged)

return Panel3D
--Panel3D: 3D GUI panels for VR
--written by 0xBAADF00D
local PIXELS_PER_STUD = 64
local SETTINGS_DISTANCE = 3.5

local CURSOR_HIDE_TIME = 2
local CURSOR_FADE_TIME = 0.125

local CoreGui = game:GetService('CoreGui')
local RobloxGui = CoreGui:WaitForChild("RobloxGui")

local Panel3D = {}

Panel3D.Panels = {
    Lower = 1,
    Hamburger = 2,
    Settings = 3,
    Keyboard = 4,
    Chat = 5
}

Panel3D.Orientation = {
    Horizontal = 1,
    Fixed = 2
}

Panel3D.Visibility = {
    BelowAngleThreshold = 1,
    Modal = 2,
    Normal = 3
}

local panelDefaultVectors = {
    [Panel3D.Panels.Lower] = CFrame.Angles(math.rad(-45), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5)),
    [Panel3D.Panels.Hamburger] = CFrame.Angles(math.rad(-55), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5)),
    [Panel3D.Panels.Settings] = Vector3.new(0, 0, -SETTINGS_DISTANCE),
    [Panel3D.Panels.Keyboard] = CFrame.Angles(math.rad(-22.5), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5)),
    [Panel3D.Panels.Chat] = CFrame.Angles(math.rad(5), 0, 0):vectorToWorldSpace(Vector3.new(0, 0, -5))
}
local DEFAULT_PANEL_LOCK_THRESHOLD = math.rad(-25)
local panelTransparencyBias = { --tuned values; raise the opacity value to this power
    [Panel3D.Panels.Lower] = 6.5,
    [Panel3D.Panels.Hamburger] = 8,
    [Panel3D.Panels.Settings] = 0,
    [Panel3D.Panels.Keyboard] = 1.5,
    [Panel3D.Panels.Chat] = 1.5
}
local panels = {}

local headScale = 1

local renderStepName = "Panel3D"

local cursor = Instance.new("ImageLabel")
cursor.Image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
cursor.Size = UDim2.new(0, 8, 0, 8)
cursor.BackgroundTransparency = 1
cursor.ZIndex = 10

local resetOrientationFlag = false
local currentModalPanel = nil

local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")

local cursorHidden = false
local hasTool = false
local lastMouseMove = tick()

local function OnCharacterAdded(character)
    hasTool = false
    for i, v in ipairs(character:GetChildren()) do
        if v:IsA("Tool") then
            hasTool = true
        end
    end
    character.ChildAdded:connect(function(child)
        if child:IsA("Tool") then
            hasTool = true
            lastMouseMove = tick() --kick the mouse when a tool is equipped
        end
    end)
    character.ChildRemoved:connect(function(child)
        if child:IsA("Tool") then
            hasTool = false
        end
    end)
end
spawn(function()
    while not game.Players.LocalPlayer do wait() end
    game.Players.LocalPlayer.CharacterAdded:connect(OnCharacterAdded)
    if game.Players.LocalPlayer.Character then OnCharacterAdded(game.Players.LocalPlayer.Character) end
end)
local function autoHideCursor(hide)
    if not UserInputService.VREnabled then
        cursorHidden = false
        return
    end
    if hide then
        --don't hide if there's a tool in the character
        local character = game.Players.LocalPlayer.Character
        if character and hasTool then
            return
        end
        cursorHidden = true
        UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
    else
        cursorHidden = false
        UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
    end
end

local function isCursorVisible()
    --if ForceShow, the cursor is definitely visible at all times
    if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceShow then
        return true
    end
    --if ForceHide, the cursor is definitely NOT visible
    if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceHide then
        return false
    end
    --Otherwise, we need to check if the developer set MouseIconEnabled=false
    if UserInputService.MouseIconEnabled and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
        return true
    end
    return false
end

UserInputService.InputChanged:connect(function(inputObj, processed)
    if inputObj.UserInputType == Enum.UserInputType.MouseMovement then
        lastMouseMove = tick()
        autoHideCursor(false)
    end
end)
game:GetService("RunService").Heartbeat:connect(function()
    if isCursorVisible() then
        cursorHidden = false
    end
    if lastMouseMove + CURSOR_HIDE_TIME < tick() and not GuiService.MenuIsOpen and not cursorHidden then
        autoHideCursor(true)
    end
end)

local function createPanel()
    local panelPart = Instance.new("Part")
    panelPart.Transparency = 1
    panelPart.CanCollide = false
    panelPart.Anchored = true
    panelPart.Archivable = false
    panelPart.Size = Vector3.new(1.5, 1.5, 1)
    panelPart.Parent = workspace.CurrentCamera
    local panelGUI = Instance.new("SurfaceGui", CoreGui)
    panelGUI.Name = "GUI"
    panelGUI.Adornee = panelPart
    panelGUI.ToolPunchThroughDistance = 1000
    panelGUI.Active = true
    pcall(function() --todo: remove this when api is live
        panelGUI.AlwaysOnTop = true
    end)
    return panelPart, panelGUI
end

function Panel3D.Get(panelId)
    local panel = panels[panelId]
    if not panel then
        local panelName
        for name, value in pairs(Panel3D.Panels) do
            if value == panelId then
                panelName = name
                break
            end
        end
        if not panelName then
            error("Tried to request an invalid 3D panel")
        end

        local part, gui = createPanel()

        panel = {}
        panel.part = part
        panel.part.Name = "GUI"--("%sPanel3D"):format(panelName)
        panel.gui = gui
        panel.gui.Name = ("%sPanelGUI"):format(panelName)

        panel.vector = panelDefaultVectors[panelId]
        panel.horizontalVector = Vector3.new(panel.vector.x, 0, panel.vector.z).unit
        panel.pitchAngle = math.asin(panel.vector.unit.y)
        panel.verticalRange = math.rad(5)
        panel.horizontalRange = math.rad(5)

        panel.transparencyCallbacks = {}

        panel.OnMouseEnter = nil
        panel.OnMouseLeave = nil

        panel.pixelScale = 1

        panel.visible = true
        panel.visibilityBehavior = Panel3D.Visibility.BelowAngleThreshold

        panel.orientationMode = Panel3D.Orientation.Horizontal
        panel.orientation = nil

        panel.cursorEnabled = true

        panel.width = 1
        panel.height = 1

        function panel:AddTransparencyCallback(callback)
            table.insert(panel.transparencyCallbacks, callback)
        end

        function panel:Resize(width, height, pixelsPerStud)
            panel.width = width
            panel.height = height

            pixelsPerStud = pixelsPerStud or PIXELS_PER_STUD
            panel.pixelScale = pixelsPerStud / PIXELS_PER_STUD
            panel.part.Size = Vector3.new(panel.width * headScale, panel.height * headScale, 1)
            panel.gui.CanvasSize = Vector2.new(pixelsPerStud * panel.width, pixelsPerStud * panel.height)

            local distance = panel.vector.magnitude
            panel.verticalRange = math.atan(panel.part.Size.Y / (2 * distance)) * 2
            panel.horizontalRange = math.atan(panel.part.Size.X / (2 * distance)) * 2
        end

        function panel:ResizePixels(width, height)
            local widthStuds = width / PIXELS_PER_STUD
            local heightStuds = height / PIXELS_PER_STUD
            panel:Resize(widthStuds, heightStuds)
        end

        function panel:SetModal()
            panel.visible = false
            panel.visibilityBehavior = Panel3D.Visibility.Modal

            if panel.visible then
                currentModalPanel = panel
                if panel.orientationMode == Panel3D.Orientation.Fixed then
                    local userHeadCFrame = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
                    panel.orientation = userHeadCFrame * CFrame.new(0, 0, -panel.vector.magnitude) * CFrame.Angles(0, math.pi, 0)
                end
            end
        end

        function panel:SetVisible(visible)
            panel.visible = visible

            if visible and panel.visibilityBehavior == Panel3D.Visibility.Modal then
                currentModalPanel = panel
                if panel.orientationMode == Panel3D.Orientation.Fixed then
                    local userHeadCFrame = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
                    panel.orientation = userHeadCFrame * CFrame.new(0, 0, -panel.vector.magnitude) * CFrame.Angles(0, math.pi, 0)
                end
            else
                if currentModalPanel == panel then
                    currentModalPanel = nil
                end
            end
        end

        panels[panelId] = panel
    end
    return panel
end

function Panel3D.ResetOrientation()
    resetOrientationFlag = true
end

function Panel3D.GetGUI(panel)
    local panelGUI = panelGUIs[panel]
    if not panelGUI then
        local part = Panel3D.GetPart(panel)
        panelGUI = panelGUIs[panel]
    end
    return panelGUI
end

local zeroVector = Vector3.new(0, 0, 0)
local headXZ = CFrame.new()
local headOffset = Vector3.new()
local currentHoverPanel = nil
local savedMouseBehavior = Enum.MouseBehavior.Default
function Panel3D.OnRenderStep()
    if not UserInputService.VREnabled then
        return
    end
    local cameraCFrame = workspace.CurrentCamera.CFrame
    local cameraRenderCFrame = workspace.CurrentCamera:GetRenderCFrame()
    local userHeadCFrame = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)

    local userHeadLook = userHeadCFrame.lookVector
    local userHeadHorizontalVector = Vector3.new(userHeadLook.X, 0, userHeadLook.Z).unit
    local userHeadPitch = math.asin(userHeadLook.Y)

    local panelLockThreshold = DEFAULT_PANEL_LOCK_THRESHOLD

    for panelId, panel in pairs(panels) do
        if panel.pitchLockThreshold and panel.visible then
            panelLockThreshold = math.max(panelLockThreshold, panel.pitchLockThreshold)
        end
    end

    local isAboveThreshold = false
    if userHeadPitch > panelLockThreshold or resetOrientationFlag then
        isAboveThreshold = true
        headXZ = CFrame.new(zeroVector, userHeadHorizontalVector)
        headOffset = userHeadCFrame.p
        resetOrientationFlag = false
    end

    local panelsOrigin = cameraCFrame * CFrame.new(headOffset) * headXZ
    for panelId, panel in pairs(panels) do
        if panel.visibilityBehavior == Panel3D.Visibility.BelowAngleThreshold then
            panel.visible = not (isAboveThreshold or currentModalPanel)
        end

        if not panel.visible then
            panel.part.Parent = nil
            panel.gui.Adornee = nil
        else
            panel.part.Parent = workspace.CurrentCamera --TODO: move to new 3D gui space
            panel.gui.Adornee = panel.part

            local panelCFrame;
            if panel.orientationMode == Panel3D.Orientation.Fixed and panel.orientation then
                local pos = panel.orientation.p * headScale
                panel.part.CFrame = workspace.CurrentCamera.CFrame * ((panel.orientation - panel.orientation.p) + pos)
                panelCFrame = panel.part.CFrame
            else
                local panelPosition = panelsOrigin:pointToWorldSpace(panel.vector * headScale)
                panelCFrame = CFrame.new(panelPosition, panelsOrigin.p)
                panel.part.CFrame = panelCFrame
            end

            local toPanel = (panelCFrame.p - cameraRenderCFrame.p).unit

            local transparency = panel.visible and 1 - (math.max(0, cameraRenderCFrame.lookVector:Dot(toPanel)) ^ panelTransparencyBias[panelId]) or 1
            for _, callback in pairs(panel.transparencyCallbacks) do
                callback(transparency)
            end
        end
    end

    --Render a cursor overlaid onto the panels
    local cframe = cameraRenderCFrame
    local ray = Ray.new(cframe.p, cframe.lookVector * 999)
    local ignoreList = { game.Players.LocalPlayer.Character }
    local part, endpoint = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
    local hitPanel = nil
    local hitPanelId = nil
    for panelId, panel in pairs(panels) do
        if part == panel.part then
            hitPanel = panel
            hitPanelId = panelId
        end
    end
    if hitPanel then
        if not currentHoverPanel then
            savedMouseBehavior = UserInputService.MouseBehavior
            UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
        else
            if currentHoverPanel ~= hitPanel then
                if currentHoverPanel.OnMouseLeave then
                    currentHoverPanel:OnMouseLeave()
                end
            end
        end
        if hitPanel.OnMouseEnter and currentHoverPanel ~= hitPanel then
            hitPanel:OnMouseEnter()
        end
        
        currentHoverPanel = hitPanel

        UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
        if hitPanel.cursorEnabled then
            cursor.Parent = hitPanel.gui
        else
            cursor.Parent = nil
        end

        local localEndpoint = part:GetRenderCFrame():pointToObjectSpace(endpoint)
        local x = (localEndpoint.X / part.Size.X) + 0.5
        local y = (localEndpoint.Y / part.Size.Y) + 0.5
        x = 1 - x
        y = 1 - y
        cursor.Size = UDim2.new(0, 8 * hitPanel.pixelScale, 0, 8 * hitPanel.pixelScale)
        cursor.Position = UDim2.new(x, -cursor.AbsoluteSize.x * 0.5, y, -cursor.AbsoluteSize.y * 0.5)

        if hitPanel.visible and not modalPanelIsOpen then
            hitPanel.part.Parent = workspace.CurrentCamera
            for _, callback in pairs(hitPanel.transparencyCallbacks) do
                callback(0)
            end
        end
    else
        if currentHoverPanel then
            UserInputService.MouseBehavior = savedMouseBehavior

            if currentHoverPanel.OnMouseLeave then
                currentHoverPanel:OnMouseLeave()
            end
        end
        currentHoverPanel = false
        cursor.Parent = nil
    end
end



-- RayPlaneIntersection

-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
local function RayPlaneIntersection(ray, planeNormal, pointOnPlane)
    planeNormal = planeNormal.unit
    ray = ray.Unit
    -- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane
    local Vd = planeNormal:Dot(ray.Direction)
    
    -- could fuzzy equals this a little bit to account for imprecision or very close angles to zero
    if Vd == 0 then -- parallel, no intersection
        return nil
    end

    local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
    local t = V0 / Vd

    if t < 0 then --plane is behind ray origin, and thus there is no intersection
        return nil
    end
    
    return ray.Origin + ray.Direction * t
end

function Panel3D.FindHoveredGuiElement(panel, elements)
    local cameraRenderCFrame = workspace.CurrentCamera and workspace.CurrentCamera:GetRenderCFrame()
    local panelPart = panel.part
    if cameraRenderCFrame and panelPart then
        local panelPartSize = panelPart.Size
        local panelSurfaceCFrame = panelPart.CFrame + panelPart.CFrame.lookVector * (panelPartSize.Z * 0.5)
        local intersectionPt = RayPlaneIntersection(Ray.new(cameraRenderCFrame.p, cameraRenderCFrame.lookVector), panelSurfaceCFrame.lookVector, panelSurfaceCFrame.p)
        if intersectionPt then
            local localPoint = panelSurfaceCFrame:pointToObjectSpace(intersectionPt) * Vector3.new(-1, 1, 1) + Vector3.new(panelPartSize.X/2, -panelPartSize.Y/2, 0)
            local guiPoint = Vector2.new((localPoint.X / panelPartSize.X) *  panel.gui.AbsoluteSize.X, (localPoint.Y / panelPartSize.Y) * -panel.gui.AbsoluteSize.Y)
            
            local x = guiPoint.x
            local y = guiPoint.y
            for _, item in pairs(elements) do
                local minPt = item.AbsolutePosition
                local maxPt = item.AbsolutePosition + item.AbsoluteSize
                if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then
                    return item
                end
            end
        end
    end
end





game:GetService("RunService"):BindToRenderStep(renderStepName, Enum.RenderPriority.Last.Value, Panel3D.OnRenderStep)

local function OnCameraChanged(prop)
    if prop == "HeadScale" then
        pcall(function()
            headScale = workspace.CurrentCamera.HeadScale
        end)
        for i, v in pairs(panels) do
            v:Resize(v.width, v.height, v.pixelScale * PIXELS_PER_STUD)
        end
    end
end
local cameraChangedConn = nil
workspace.Changed:connect(function(prop)
    if prop == "CurrentCamera" then
        OnCameraChanged("HeadScale")
        if cameraChangedConn then
            cameraChangedConn:disconnect()
        end
        cameraChangedConn = workspace.CurrentCamera.Changed:connect(OnCameraChanged)
    end
end)
if workspace.CurrentCamera then
    OnCameraChanged("HeadScale")
    cameraChangedConn = workspace.CurrentCamera.Changed:connect(OnCameraChanged)
end

return Panel3D

Developer Console#

In content/scripts/Modules is an unused developer console.

Developer Console

-- Made by Tomarty (talk to me if you have questions)

-- Quick optimizations
local Instance_new = Instance.new
local UDim2_new = UDim2.new
local Color3_new = Color3.new
local math_max = math.max
local tick = tick
local pairs = pairs
local os_time = os.time

local DEBUG = false

local ContextActionService = game:GetService("ContextActionService")

-- Eye candy uses RenderStepped
local EYECANDY_ENABLED = true

local ZINDEX = 6

local Style; do
    local function c3(r, g, b)
        return Color3_new(r / 255, g / 255, b / 255)
    end
    local frameColor = Color3_new(0.1, 0.1, 0.1)
    local textColor = Color3_new(1, 1, 1)
    local optionsFrameColor = Color3_new(1, 1, 1)
    
    pcall(function() -- Fun window colors for cool people
        local Players = game:GetService("Players")
        if not Players or not Players.LocalPlayer then
            return
        end
        local FunColors = {
            [56449]   = {c3(255, 63,  127)}; -- ReeseMcBlox
            [6949935] = {c3(255, 63,  127)}; -- NobleDragon
        }
        local funColor = FunColors[Players.LocalPlayer.userId]
        if funColor then
            frameColor = funColor[1] or frameColor
            textColor = funColor[2] or textColor
        end
    end)
    
    Style = {
        Font = Enum.Font.SourceSans;
        FontBold = Enum.Font.SourceSansBold;
        
        HandleHeight = 24; -- How tall the top window handle is, as well as the width of the scroll bar
        TabHeight = 28;
        GearSize = 24;
        BorderSize = 2;
        CommandLineHeight = 22;
        
        OptionAreaHeight = 56;
        
        FrameColor = frameColor; -- Applies to pretty much everything, including buttons
        FrameTransparency = 0.5;
        OptionsFrameColor = optionsFrameColor;
        
        TextColor = textColor;
        
        MessageColors = {
            [0] = Color3_new(1, 1, 1); -- Enum.MessageType.MessageOutput
            [1] = Color3_new(0.4, 0.5, 1); -- Enum.MessageType.MessageInfo
            [2] = Color3_new(1, 0.6, 0.4); -- Enum.MessageType.MessageWarning
            [3] = Color3_new(1, 0, 0); -- Enum.MessageType.MessageError
        };
        
        ScrollbarFrameColor = frameColor;
        ScrollbarBarColor = frameColor;
        
        ScriptButtonHeight = 32;
        ScriptButtonColor = Color3_new(0, 1/3, 2/3);
        ScriptButtonTransparency = 0.5;
        
        CheckboxSize = 24;
        
        ChartTitleHeight = 20;
        ChartGraphHeight = 64;
        ChartDataHeight = 24;
        ChartHeight = 0; -- This gets added up at end and set at end of block
        ChartWidth = 620;
        
        -- (-1) means right to left
        -- (1) means left to right
        ChartGraphDirection = 1; -- the direction the bars move
        
        
        GetButtonDownColor = function(normalColor)
            local r, g, b = normalColor.r, normalColor.g, normalColor.b
            return Color3_new(1 - 0.75 * (1 - r), 1 - 0.75 * (1 - g), 1 - 0.75 * (1 - b))
        end;
        GetButtonHoverColor = function(normalColor)
            local r, g, b = normalColor.r, normalColor.g, normalColor.b
            return Color3_new(1 - 0.875 * (1 - r), 1 - 0.875 * (1 - g), 1 - 0.875 * (1 - b))
        end;

    }
    
    Style.ChartHeight = Style.ChartTitleHeight + Style.ChartGraphHeight + Style.ChartDataHeight + Style.BorderSize

end

-- This provides an easy way to create GUI objects without writing insanely redundant code
local Primitives = {}; do
    local function new(className, parent, name)
        local n = Instance_new(className, parent)
        n.ZIndex = ZINDEX
        if name then
            n.Name = name
        end
        return n
    end
    local unitSize = UDim2_new(1, 0, 1, 0)
    
    local function setupFrame(n)
        n.BackgroundColor3 = Style.FrameColor
        n.BackgroundTransparency = Style.FrameTransparency
        n.BorderSizePixel = 0
    end
    local function setupText(n, text)
        n.Font = Style.Font
        n.TextColor3 = Style.TextColor
        n.Text = text or n.Text
    end
    
    function Primitives.Frame(parent, name)
        local n = new('Frame', parent, name)
        setupFrame(n)
        return n
    end
    function Primitives.TextLabel(parent, name, text)
        local n = new('TextLabel', parent, name)
        setupFrame(n)
        setupText(n, text)
        return n
    end
    function Primitives.TextBox(parent, name, text)
        local n = new('TextBox', parent, name)
        setupFrame(n)
        setupText(n, text)
        return n
    end
    function Primitives.TextButton(parent, name, text)
        local n = new('TextButton', parent, name)
        setupFrame(n)
        setupText(n, text)
        return n
    end
    function Primitives.Button(parent, name)
        local n = new('TextButton', parent, name)
        setupFrame(n)
        n.Text = ""
        return n
    end
    function Primitives.ImageButton(parent, name, image)
        local n = new('ImageButton', parent, name)
        setupFrame(n)
        n.Image = image or ""
        n.Size = unitSize
        return n
    end
    
    -- An invisible frame of size (1, 0, 1, 0)
    function Primitives.FolderFrame(parent, name) -- Should this be called InvisibleFrame? lol
        local n = new('Frame', parent, name)
        n.BackgroundTransparency = 1
        n.Size = unitSize
        return n
    end
    function Primitives.InvisibleTextLabel(parent, name, text)
        local n = new('TextLabel', parent, name)
        setupText(n, text)
        n.BackgroundTransparency = 1
        return n
    end
    function Primitives.InvisibleButton(parent, name, text)
        local n = new('TextButton', parent, name)
        n.BackgroundTransparency = 1
        n.Text = ""
        return n
    end
    function Primitives.InvisibleImageLabel(parent, name, image)
        local n = new('ImageLabel', parent, name)
        n.BackgroundTransparency = 1
        n.Image = image or ""
        n.Size = unitSize
        return n
    end
end

local CreateSignal = assert(LoadLibrary('RbxUtility')).CreateSignal

-- This is a Signal that only calls once, then forgets about the function. It also accepts event listeners as functions
local CreateDisconnectSignal; do
    local Methods = {}
    local Metatable = {__index = Methods}
    function Methods.fire(this, ...)
        return this.Signal:fire(...)
    end
    function Methods.wait(this, ...)
        return this.Signal:wait(...)
    end
    function Methods.connect(this, func)
        local t = type(func)
        if t == 'table' or t == 'userdata' then
            -- Got event listener
            local listener = func
            function func()
                listener:disconnect()
            end
        elseif t ~= 'function' then
            error('Invalid disconnect method type: ' .. t, 2)
        end

        local listener;
        listener = this.Signal:connect(function(...)
            if listener then
                listener:disconnect()
                listener = nil
                func(...)
            end
        end)
        return listener
    end
    function CreateDisconnectSignal()
        return setmetatable({
            Signal = CreateSignal();
        }, Metatable)
    end
end

-- Services
local UserInputService = game:GetService('UserInputService')
local RunService = game:GetService('RunService')
local TouchEnabled = UserInputService.TouchEnabled

local DeveloperConsole = {}

local Methods = {}
local Metatable = {__index = Methods}

----
-- Listener management --
----
function Methods.ConnectSetVisible(devConsole, func)
    -- This is used mainly for pausing rendering and stuff when the console isn't visible
    func(devConsole.Visible)
    return devConsole.VisibleChanged:connect(function(visible)
        func(visible)
    end)
end
function Methods.ConnectObjectSetVisible(devConsole, object, func)
    -- Same as above, but used for calling methods like object:SetVisible()
    func(object, devConsole.Visible)
    return devConsole.VisibleChanged:connect(function(visible)
        func(object, visible)
    end)
end

----
-- Frame/Window Dimensions --
----

local function connectPropertyChanged(object, property, callback)
    return object.Changed:connect(function(propertyChanged)
        if propertyChanged == property then
            callback(object[property])
        end
    end)
end

function Methods.ResetFrameDimensions(devConsole)
    devConsole.Frame.Size = UDim2_new(0.5, 20, 0.5, 20);
    devConsole.Frame.Position = UDim2_new(0.25, -10, 0.125, -10)
end
function Methods.BoundFrameSize(devConsole, x, y)
    -- Minimum frame size
    return math_max(x, 300), math_max(y, 200)
end
function Methods.SetFrameSize(devConsole, x, y)
    x, y = devConsole:BoundFrameSize(x, y)
    devConsole.Frame.Size = UDim2_new(0, x, 0, y)
end
function Methods.BoundFramePosition(devConsole, x, y)
    -- Make sure the frame doesn't go somewhere where the bar can't be clicked
    return x, math_max(y, 0)
end
function Methods.SetFramePosition(devConsole, x, y)
    x, y = devConsole:BoundFramePosition(x, y)
    devConsole.Frame.Position = UDim2_new(0, x, 0, y)
end

-- Open/Close the console
function Methods.SetVisible(devConsole, visible, animate)
    if devConsole.Visible == visible then
        return
    end
    devConsole.Visible = visible
    devConsole.VisibleChanged:fire(visible)
    if devConsole.Frame then
        devConsole.Frame.Visible = visible
    end 
    if visible then -- Open the console
        devConsole:ResetFrameDimensions()
    end
end

----
-- Constructor --
----
function DeveloperConsole.new(screenGui, permissions, messagesAndStats)

    local visibleChanged = CreateSignal()
    
    local devConsole = {
        ScreenGui = screenGui;
        Permissions = permissions;
        MessagesAndStats = messagesAndStats;
        Initialized = false;
        Visible = false;
        Tabs = {};
        VisibleChanged = visibleChanged; -- Created by :Initialize(); It's used to stop and disconnect things when the window is hidden
    }
    
    setmetatable(devConsole, Metatable)

    devConsole:EnableGUIMouse()
    
    -- It's a button so it catches mouse events
    local frame = Primitives.Button(screenGui, 'DeveloperConsole')
    frame.AutoButtonColor = false
    --frame.ClipsDescendants = true
    frame.Visible = devConsole.Visible
    devConsole.Frame = frame
    devConsole:ResetFrameDimensions()
    
    -- The bar at the top that you can drag around
    local handle = Primitives.Button(frame, 'Handle')
    handle.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize), 0, Style.HandleHeight)
    handle.Modal = true -- Unlocks mouse
    handle.AutoButtonColor = false
    
    
    do -- Title
        local title = Primitives.InvisibleTextLabel(handle, 'Title', "ROBLOX Developer Console")
        title.Size = UDim2_new(1, -5, 1, 0)
        title.Position = UDim2_new(0, 5, 0, 0)  
        title.FontSize = Enum.FontSize.Size18
        title.TextXAlignment = Enum.TextXAlignment.Left
    end
    
    local function setCornerButtonImageSize(buttonImage, buttonImageSize)
        buttonImage.Size = UDim2_new(buttonImageSize, 0, buttonImageSize, 0)
        buttonImage.Position = UDim2_new((1 - buttonImageSize) / 2, 0, (1 - buttonImageSize) / 2, 0)        
    end
    -- This is used for creating the square exit button and the square window resize button
    local function createCornerButton(name, x, y, image, buttonImageSize)
        -- Corners (x, y):
        -- (0, 0) (1, 0)
        -- (0, 1) (1, 1)
        
        local button = Primitives.Button(frame, name)
        button.Size = UDim2_new(0, Style.HandleHeight, 0, Style.HandleHeight)
        button.Position = UDim2_new(x, -x * Style.HandleHeight, y, -y * Style.HandleHeight)
        
        local buttonImage = Primitives.InvisibleImageLabel(button, 'Image', image)
        setCornerButtonImageSize(buttonImage, buttonImageSize)
        
        return button, buttonImage
    end
    
    do -- Create top right exit button
        local exitButton, exitButtonImage = createCornerButton('Exit', 1, 0, 'https://www.roblox.com/asset/?id=261878266', 2/3)
        exitButton.AutoButtonColor = false
        
        local buttonEffectFunction = devConsole:CreateButtonEffectFunction(exitButton)
        
        devConsole:ConnectButtonHover(exitButton, function(clicking, hovering)
            if hovering and not clicking then
                setCornerButtonImageSize(exitButtonImage, 3/4)
            else
                setCornerButtonImageSize(exitButtonImage, 2/3)
            end
            buttonEffectFunction(clicking, hovering)
        end)
        
        exitButton.MouseButton1Click:connect(function()
            devConsole:SetVisible(false, true)
        end)

        local closeDevConsole = function(name, inputState, input)
            ContextActionService:UnbindCoreAction("RBXDevConsoleCloseAction")
            devConsole:SetVisible(false, true)
        end

        ContextActionService:BindCoreAction("RBXDevConsoleCloseAction", closeDevConsole, false, Enum.KeyCode.ButtonB)

    end
    
    do -- Repositioning and Resizing
        
        do -- Create bottom right window resize button and activate resize dragging
            local resizeButton, resizeButtonImage = createCornerButton('Resize', 1, 1, 'https://www.roblox.com/asset/?id=261880743', 1)
            resizeButtonImage.Position = UDim2_new(0, 0, 0, 0)
            resizeButtonImage.Size = UDim2_new(1, 0, 1, 0)

            local dragging = false
            
            local buttonEffectFunction = devConsole:CreateButtonEffectFunction(resizeButton)
            
            devConsole:ConnectButtonDragging(resizeButton, function()
                local x0, y0 = frame.AbsoluteSize.X, frame.AbsoluteSize.Y
                return function(dx, dy)
                    devConsole:SetFrameSize(x0 + dx, y0 + dy)
                end
            end, function(clicking, hovering)
                dragging = clicking
                buttonEffectFunction(clicking, hovering)
            end)
            
        end
        
        do -- Activate top handle dragging
            local frame = devConsole.Frame
            local handle = frame.Handle
            
            local buttonEffectFunction = devConsole:CreateButtonEffectFunction(handle)
            
            devConsole:ConnectButtonDragging(handle, function()
                local x, y = frame.AbsolutePosition.X, frame.AbsolutePosition.Y
                return function(dx, dy)
                    devConsole:SetFramePosition(x + dx, y + dy)
                end
                --deltaCallback_Resize(-dx, -dy) -- Used if they are grabbing both at the same time
            end, buttonEffectFunction)
        end
    end
    
    -- interiorFrame contains tabContainer and window
    local interiorFrame = Primitives.FolderFrame(frame, 'Interior')
    interiorFrame.Position = UDim2_new(0, 0, 0, Style.HandleHeight)
    interiorFrame.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize * 2), 1, -(Style.HandleHeight + Style.BorderSize))
    
    local windowContainer = Primitives.FolderFrame(interiorFrame, 'WindowContainer')
    windowContainer.Size = UDim2_new(1, 0, 1, -(Style.TabHeight))
    windowContainer.Position = UDim2_new(0, Style.BorderSize, 0, Style.TabHeight)
    
    -- This is what applies ClipsDescendants to tab contents
    local window = Primitives.Frame(windowContainer, 'Window')
    window.Size = UDim2_new(1, 0, 1, 0) -- The tab open/close methods, and the consoles also set this
    window.Position = UDim2_new(0, 0, 0, 0)
    window.ClipsDescendants = true
    
    -- This is the frame that moves around with the scroll bar
    local body = Primitives.FolderFrame(window, 'Body')
    
    do -- Scrollbars
        local scrollbar = devConsole:CreateScrollbar()
        devConsole.WindowScrollbar = scrollbar
        local scrollbarFrame = scrollbar.Frame
        scrollbarFrame.Parent = frame
        scrollbarFrame.Size = UDim2_new(0, Style.HandleHeight, 1, -(Style.HandleHeight + Style.BorderSize) * 2)
        scrollbarFrame.Position = UDim2_new(1, -Style.HandleHeight, 0, Style.HandleHeight + Style.BorderSize)
        
        devConsole:ApplyScrollbarToFrame(scrollbar, window, body, frame)
    end
    
    local tabContainer = Primitives.FolderFrame(interiorFrame, 'Tabs') -- Shouldn't this be named 'tabFrame'?
    tabContainer.Size = UDim2_new(1, -(Style.GearSize + Style.BorderSize), 0, Style.TabHeight)
    tabContainer.Position = UDim2_new(0, 0, 0, 0)
    tabContainer.ClipsDescendants = true
    
    -- Options button
    local optionsButton = Primitives.InvisibleButton(frame, 'OptionsButton')
    
    local optionsClippingFrame = Primitives.FolderFrame(interiorFrame, 'OptionsClippingFrame')
    optionsClippingFrame.ClipsDescendants = true
    optionsClippingFrame.Position = UDim2_new(0, 0, 0, 0)
    optionsClippingFrame.Size = UDim2_new(1, 0, 0, 0)
    local optionsFrame = Primitives.FolderFrame(optionsClippingFrame, 'OptionsFrame')
    optionsFrame.Size = UDim2_new(1, 0, 0, Style.OptionAreaHeight)
    optionsFrame.Position = UDim2_new(0, 0, 0, Style.OptionAreaHeight)
    --optionsFrame.BackgroundColor3 = Style.OptionsFrameColor
    do -- Options animation
        
        local gearSize = Style.GearSize
        local tabHeight = Style.TabHeight
        local offset = (tabHeight - gearSize) / 2
        optionsButton.Size = UDim2_new(0, Style.GearSize, 0, Style.GearSize)
        optionsButton.Position = UDim2_new(1, -(Style.GearSize + offset + Style.HandleHeight), 0, Style.HandleHeight + offset)
        local gear = Primitives.InvisibleImageLabel(optionsButton, 'Image', 'https://www.roblox.com/asset/?id=261882463')
        --gear.ZIndex = ZINDEX + 1
        local animationToggle = devConsole:GenerateOptionButtonAnimationToggle(interiorFrame, optionsButton, gear, tabContainer, optionsClippingFrame, optionsFrame)
        local open = false
        optionsButton.MouseButton1Click:connect(function()
            open = not open
            animationToggle(open)
        end)
        
    end
    
    -- Console/Log and Stats options
    local setShownOptionTypes; -- Toggles what options to show: setOptionType({Log = true})
    
    local textFilter, scriptStatFilter;
    local textFilterChanged, scriptStatFilterChanged;
    
    local messageFilter;
    local messageFilterChanged, messageTextWrappedChanged;
    do -- Options contents/filters
        
        local function createCheckbox(color, callback)
            local this = {
                Value = true;
            }
            
            local frame = Primitives.FolderFrame(nil, 'Checkbox')
            this.Frame = frame
            frame.Size = UDim2_new(0, Style.CheckboxSize, 0, Style.CheckboxSize)
            frame.BackgroundColor3 = color
            
            local padding = 2
            
            local function f(xs, xp, yp) -- quick way to get an opaque border around a transparent center
                local ys = 1 - xs
                local f = Primitives.Frame(frame, 'Border')
                f.BackgroundColor3 = color
                f.BackgroundTransparency = 0
                f.Size = UDim2_new(xs, ys * padding, ys, xs * padding)
                f.Position = UDim2_new(xp, -xp * padding, yp, -yp * padding)
            end
            f(1, 0, 0)
            f(1, 0, 1)
            f(0, 0, 0)
            f(0, 1, 0)
            
            local button = Primitives.Button(frame, 'Button')
            button.Size = UDim2_new(1, -padding * 2, 1, -padding * 2)
            button.Position = UDim2_new(0, padding, 0, padding)
            
            local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button)
            
            local check = Primitives.Frame(button, 'Check')
            
            local padding = 4
            check.Size = UDim2_new(1, -padding * 2, 1, -padding * 2)
            check.Position = UDim2_new(0, padding, 0, padding)
            check.BackgroundColor3 = color
            check.BackgroundTransparency = 0
            
            devConsole:ConnectButtonHover(button, buttonEffectFunction)
            
            function this.SetValue(this, value)
                if value == this.Value then
                    return
                end
                this.Value = value
                check.Visible = value
                this.Value = value
                callback(value)
            end
            
            button.MouseButton1Click:connect(function()
                this:SetValue(not this.Value)
            end)
            
            return this
        end
        
        local string_find = string.find
        local containsString; -- the text typed into the search textBox, nil if equal to ""
        
        function textFilter(text)
            return not containsString or string_find(text:lower(), containsString)
        end
        
        local filterLookup = {} -- filterLookup[Enum.MessageType.x.Value] = true or false
        function messageFilter(message)
            return filterLookup[message.Type] and (not containsString or string_find(message.Message:lower(), containsString))
        end
        
        -- Events
        textFilterChanged = CreateSignal()
        scriptStatFilterChanged = CreateSignal()
        
        messageFilterChanged = CreateSignal()
        messageTextWrappedChanged = CreateSignal()
        
        local optionTypeContainers = {
            --[OptionType] = Frame
            --Log = Frame;
            --Scripts = Frame;
        }
        function setShownOptionTypes(shownOptionTypes)
            -- Example showOptionTypes:
            -- {Log = true}
            for optionType, container in pairs(optionTypeContainers) do
                container.Visible = shownOptionTypes[optionType] or false
            end
        end
        
        do -- Log options
            local container = Primitives.FolderFrame(optionsFrame, 'Log')
            container.Visible = false
            optionTypeContainers.Log = container

            local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Filters")
            label.FontSize = 'Size18'
            label.TextXAlignment = 'Left'
            label.Size = UDim2_new(0, 54, 0, Style.CheckboxSize)
            label.Position = UDim2_new(0, 4, 0, 2)
    
            do
                local x = label.Size.X.Offset
                local messageColors = Style.MessageColors
                for i = 0, #messageColors do -- 0, 3 initially
                    local checkbox = createCheckbox(messageColors[i], function(value)
                        filterLookup[i] = value
                        messageFilterChanged:fire()
                    end)
                    filterLookup[i] = checkbox.Value
                    checkbox.Frame.Parent = container
                    checkbox.Frame.Position = UDim2_new(0, x, 0, 4)
                    x = x + Style.CheckboxSize + 4
                end
                
                do -- Word wrap
                    x = x + 8
                    
                    local label = Primitives.InvisibleTextLabel(container, 'WrapLabel', "Word Wrap")
                    label.FontSize = 'Size18'
                    label.TextXAlignment = 'Left'
                    label.Size = UDim2_new(0, 54 + Style.CheckboxSize, 0, Style.CheckboxSize)
                    label.Position = UDim2_new(0, x + 4, 0, 2)
                    
                    local checkbox = createCheckbox(Color3.new(0.65, 0.65, 0.65), function(value)
                        messageTextWrappedChanged:fire(value) -- an event isn't ideal here
                    end)
                    checkbox:SetValue(false)
                    checkbox.Frame.Parent = container
                    checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4)
                end
            end
        end
        
        do -- Scripts options
            local container = Primitives.FolderFrame(optionsFrame, 'Stats')
            container.Visible = false
            optionTypeContainers.Scripts = container

            do
                local x = 0
                
                do -- Show inactive
                    x = x + 4
                    local label = Primitives.InvisibleTextLabel(container, 'FilterLabel', "Show inactive")
                    label.FontSize = 'Size18'
                    label.TextXAlignment = 'Left'
                    label.Size = UDim2_new(0, label.TextBounds.X + 6, 0, Style.CheckboxSize)
                    label.Position = UDim2_new(0, x, 0, 2)
                    x = x + label.Size.X.Offset
                    
                    local showInactive;
                    local function getScriptCurrentlyActive(chartStat)
                        local stats = chartStat.Stats
                        if stats then
                            local stat = stats[#stats]
                            if stat then
                                return stat[1] > 0.000001 or stat[2] > 0.000001
                            end
                        end
                        return false
                    end
                    function scriptStatFilter(chartStat)
                        return (showInactive or getScriptCurrentlyActive(chartStat))
                            and (not containsString or string_find(chartStat.Name:lower(), containsString))
                    end
                        
                    local checkbox = createCheckbox(Color3_new(1, 1, 1), function(value)
                        showInactive = value
                        scriptStatFilterChanged:fire()
                    end)
                    showInactive = checkbox.Value
                    checkbox.Frame.Parent = container
                    checkbox.Frame.Position = UDim2_new(0, x, 0, 4)
                    x = x + Style.CheckboxSize + 4
                
                end

                x = x + 8
                
                --[[
                local label = Primitives.InvisibleTextLabel(container, 'WrapLabel', "Word Wrap")
                label.FontSize = 'Size18'
                label.TextXAlignment = 'Left'
                label.Size = UDim2_new(0, 54 + Style.CheckboxSize, 0, Style.CheckboxSize)
                label.Position = UDim2_new(0, x + 4, 0, 2)
                
                local checkbox = createCheckbox(Color3.new(0.65, 0.65, 0.65), function(value)
                    messageTextWrappedChanged:fire(value)
                end)
                checkbox:SetValue(false)
                checkbox.Frame.Parent = container
                checkbox.Frame.Position = UDim2_new(0, x + label.Size.X.Offset, 0, 4)
                --]]
            end
        end
        
        do -- Search/filter/contains textbox
            
            local label = Primitives.InvisibleTextLabel(optionsFrame, 'FilterLabel', "Contains:")
            label.FontSize = 'Size18'
            label.TextXAlignment = 'Left'
            label.Size = UDim2_new(0, 60, 0, Style.CheckboxSize)
            label.Position = UDim2_new(0, 4, 0, 4 + Style.CheckboxSize + 4)
            
            local textBox = Primitives.TextBox(optionsFrame, 'ContainsFilter')
            textBox.ClearTextOnFocus = true
            textBox.FontSize = 'Size18'
            textBox.TextXAlignment = 'Left'
            textBox.Size = UDim2_new(0, 150, 0, Style.CheckboxSize)
            textBox.Position = UDim2_new(0, label.Position.X.Offset + label.Size.X.Offset + 4, 0, 4 + Style.CheckboxSize + 4)
            textBox.Text = ""
        
            local runningColor = Color3.new(0, 0.5, 0)
            local normalColor = textBox.BackgroundColor3
            
            connectPropertyChanged(textBox, 'Text', function(text)
                text = text:lower()
                if text == "" then
                    text = nil
                end
                if text == containsString then
                    return
                end
                textBox.BackgroundColor3 = text and runningColor or normalColor
                containsString = text
                messageFilterChanged:fire()
                textFilterChanged:fire()
            end)
            
            connectPropertyChanged(textBox, 'TextBounds', function(textBounds)
                textBox.Size = UDim2_new(0, math.max(textBounds.X, 150), 0, Style.CheckboxSize)
            end)
        end
    end
    
    ----------
    -- Tabs --
    ----------
    do -- Console/Log tabs
        
        -- Wrapper for :AddTab
        local function createConsoleTab(name, text, width, outputMessageSync, commandLineVisible, commandInputtedCallback, openCallback)
            local tabBody = Primitives.FolderFrame(body, name)
            local output, commandLine;
            local disconnector = CreateDisconnectSignal()
            
            local tab = devConsole:AddTab(text, width, tabBody, function(open)
                if commandLine then
                    commandLine.Frame.Visible = open
                end
                
                if open then
                    
                    setShownOptionTypes({
                        Log = true;
                    })
                    
                    if not output then
                        output = devConsole:CreateOutput(outputMessageSync:GetMessages(), messageFilter)
                        output.Frame.Parent = tabBody
                    end
                    
                    output:SetVisible(true)
                    
                    if commandLineVisible then
                        if open and not commandLine then
                            commandLine = devConsole:CreateCommandLine()
                            commandLine.Frame.Parent = frame
                            commandLine.Frame.Size = UDim2_new(1, -(Style.HandleHeight + Style.BorderSize * 2), 0, Style.CommandLineHeight)
                            commandLine.Frame.Position = UDim2_new(0, Style.BorderSize, 1, -(Style.CommandLineHeight + Style.BorderSize))
                            commandLine.CommandInputted:connect(commandInputtedCallback)
                        end
                    end

                    window.Size = commandLineVisible
                        and UDim2_new(1, 0, 1, -(Style.HandleHeight))
                        or  UDim2_new(1, 0, 1, 0)

                    
                    local messages = outputMessageSync:GetMessages()
                    
                    local height = output:RefreshMessages()
                    body.Size = UDim2_new(1, 0, 0, height)
                    
                    disconnector:connect(output.HeightChanged:connect(function(height)
                        body.Size = UDim2_new(1, 0, 0, height)
                    end))
                    body.Size = UDim2_new(1, 0, 0, output.Height)
                    
                    disconnector:connect(outputMessageSync.MessageAdded:connect(function(message)
                        output:RefreshMessages(#messages)
                    end))
                    
                    disconnector:connect(messageFilterChanged:connect(function()
                        output:RefreshMessages()
                    end))
                    disconnector:connect(messageTextWrappedChanged:connect(function(enabled)
                        output:SetTextWrappedEnabled(enabled)
                    end))
                else
                    if output then
                        output:SetVisible(false)
                    end
                    window.Size = UDim2_new(1, 0, 1, 0)
                    disconnector:fire()
                end
                if openCallback then
                    openCallback(open)
                end
            end)

            return tab
        end
        
        -- Local Log tab --
        if permissions.MayViewClientLog then
            local tab = createConsoleTab(
                'LocalLog', "Local Log", 60,
                devConsole.MessagesAndStats.OutputMessageSyncLocal,
                permissions.ClientCodeExecutionEnabled
            )
            tab:SetVisible(true)
            tab:SetOpen(true)
        end
        
        -- Server Log tab --
        if permissions.MayViewServerLog then
            local LogService = game:GetService('LogService')
            local tab = createConsoleTab(
                'ServerLog', "Server Log", 70,
                devConsole.MessagesAndStats.OutputMessageSyncServer,
                permissions.ServerCodeExecutionEnabled,
                function(text)
                    if #text <= 1 then
                        return
                    end
                    if permissions.ServerCodeExecutionEnabled then
                        -- print("Server Loadstring:", text)
                        LogService:ExecuteScript(text)
                    end
                end
            )
            tab:SetVisible(true)
        end
    end
    
    do -- Stats tabs
        
        local function generateGreenYellowRedColor(unit) -- 0 <= unit <= 1
            --[[
                0   -> 0,   223, 0
                0.5 -> 223, 233, 0
                1   -> 233, 0,   0
            --]]
            local brightness = 0.9
            if not unit then
                return Color3.new(0, 0, 0)
            elseif unit <= 0 then
                return Color3.new(1, 1, 1)
            elseif unit <= 0.5 then
                unit = unit * 2
                return Color3.new(unit * brightness, brightness, 0)
            elseif unit <= 1 then
                unit = unit * 2 - 1
                return Color3.new(brightness, (1 - unit) * brightness, 0)
            else
                return Color3.new(1, 0, 0)
            end
        end
        
        -- Wrapper for :AddTab
        local function createStatsTab(name, text, width, config, openCallback, filterStats, shownOptionTypes)
            local statsSyncServer = devConsole.MessagesAndStats.StatsSyncServer
            
            local open = false
            
            local statList = devConsole:CreateChartList(config)
            
            local tabBody = statList.Frame
            tabBody.Parent = body
            tabBody.Name = name
            tabBody.BackgroundTransparency = 1
            tabBody.Size = UDim2_new(1, 0, 1, 0)
            statList.SideMenu.Parent = windowContainer -- so the left side menu doesn't resize with the contents on right
            
            statsSyncServer:GetStats()
            statsSyncServer.StatsReceived:connect(function(stats)
                local statsFiltered = filterStats(stats)
                if statsFiltered then
                    statList:UpdateStats(statsFiltered)
                end
            end)
            
            local tab = devConsole:AddTab(text, width, tabBody, function(openNew)
                open = openNew
                if open then
                    devConsole.WindowScrollbar:SetValue(0)
                    setShownOptionTypes(shownOptionTypes)
                end
                statList:SetVisible(open)
                if openCallback then
                    openCallback(open)
                end
            end)
            tab:SetVisible(true)
            
            return tab, statList
        end
        
        -- Server Scripts --
        if permissions.MayViewServerScripts then
            
            local open = false
            
            local config = {
                GetNotifyColor = function(chartButton)
                    local chartStat = chartButton.ChartStat
                    local point;
                    local stat = chartStat.Stats[#chartStat.Stats]
                    if stat then
                        point = stat[1]
                        local freq = stat[2]
                        if point and freq then
                            point = (point < 0 and 0) or (point > 1 and 1) or point -- clamp between 0 and 1
                            point = math.max(freq > 0 and 0.000001 or 0, point ^ (1/4))
                        end
                    end
                    return generateGreenYellowRedColor(point)
                end;
                CreateChartPage = function(chartButton, statsBody)
                    local chartStat = chartButton.ChartStat
                    local chart1 = devConsole:CreateChart(chartStat.Stats, "Script Activity", 1, function(point)
                        return point and math.ceil(point * 100000 * 100) / 100000 .. "%" or ""
                    end)
                    local chart2 = devConsole:CreateChart(chartStat.Stats, "Script Rate", 2, function(point)
                        return point and (math.floor(point * 100000) / 100000) .. "/s" or ""
                    end)
                    
                    local y = 16
                    chart1.Frame.Parent = statsBody
                    chart1.Frame.Position = UDim2_new(0, 16, 0, y)
                    y = y + 16 + chart1.Frame.Size.Y.Offset
                    
                    chart2.Frame.Parent = statsBody
                    chart2.Frame.Position = UDim2_new(0, 16, 0, y)
                    y = y + 16 + chart2.Frame.Size.Y.Offset
                    
                    local this = {}
                    function this.OnPointAdded(this)
                        chart1:OnPointAdded()
                        chart2:OnPointAdded()
                    end
                    function this.SetVisible(this, visible)
                        chart1:SetVisible(visible)
                        chart2:SetVisible(visible)
                        body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0)
                    end
                    function this.Dispose(this)
                        this:SetVisible(false)
                    end
                    return this
                end;
                FilterButton = function(chartButton)
                    return scriptStatFilter(chartButton.ChartStat)
                end;
            }

            local function filterStats(stats)
                -- return stats.Scripts
                if stats.Scripts then
                    local statsFiltered = {}
                    for k, v in pairs(stats.Scripts) do
                        statsFiltered[k] = {v[1]/100, v[2]}
                    end
                    return statsFiltered
                end
            end

            local function openCallback(openNew)
                open = openNew
            end
            
            local tab, statList = createStatsTab('ServerScripts', "Server Scripts", 80, config, openCallback, filterStats, {Scripts = true})

            textFilterChanged:connect(function()
                statList:Refresh()
            end)
            scriptStatFilterChanged:connect(function()
                statList:Refresh()
            end)

            tab:SetVisible(true)
        end
        
        -- Server Stats --
        if permissions.MayViewServerStats then

            local open = false
            
            local config = {
                GetNotifyColor = function(chartButton)
                    return Color3.new(0.5, 0.5, 0.5)
                end;
                CreateChartPage = function(chartButton, statsBody)
                    local chartStat = chartButton.ChartStat
                    local chart1 = devConsole:CreateChart(chartStat.Stats, chartStat.Name, 1)
                    
                    local y = 16
                    chart1.Frame.Parent = statsBody
                    chart1.Frame.Position = UDim2_new(0, 16, 0, y)
                    y = y + 16 + chart1.Frame.Size.Y.Offset

                    local this = {}
                    function this.OnPointAdded(this)
                        chart1:OnPointAdded()
                    end
                    function this.SetVisible(this, visible)
                        chart1:SetVisible(visible)
                        body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0)
                    end
                    function this.Dispose(this)
                        this:SetVisible(false)
                    end
                    return this
                end;
                FilterButton = function(chartButton)
                    return textFilter(chartButton.ChartStat.Name)
                end;
            }
            
            local function filterStats(stats)
                local statsFiltered = {}
                for k, v in pairs(stats) do
                    if type(v) == 'number' then
                        statsFiltered[k] = {v}
                    end
                end
                return statsFiltered
            end
            
            local function openCallback(openNew)
                open = openNew
            end

            local tab, statList = createStatsTab('ServerStats', "Server Stats", 70, config, openCallback, filterStats, {Stats = true})
            
            textFilterChanged:connect(function()
                statList:Refresh()
            end)
                        
            tab:SetVisible(true)
        end 

        -- Server Jobs --
        if permissions.MayViewServerJobs then

            local open = false
            
            local config = {
                GetNotifyColor = function(chartButton)
                    return Color3.new(0.5, 0.5, 0.5)
                end;
                CreateChartPage = function(chartButton, statsBody)
                    local chartStat = chartButton.ChartStat
                    local chart1 = devConsole:CreateChart(chartStat.Stats, "Duty Cycle", 1, function(point)
                        return point and math.floor(point * 10000000 + 0.5) / 100000 .. "%" or ""
                    end)
                    local chart2 = devConsole:CreateChart(chartStat.Stats, "Steps Per Sec", 2, function(point)
                        return point and (math.floor(point * 10000 + 0.5) / 10000) .. "/s" or ""
                    end)
                    local chart3 = devConsole:CreateChart(chartStat.Stats, "Step Time", 3, function(point)
                        return point and (math.floor(point * 10000000 + 0.5) / 10000) .. "ms" or ""
                    end)
                    
                    local y = 16
                    chart1.Frame.Parent = statsBody
                    chart1.Frame.Position = UDim2_new(0, 16, 0, y)
                    y = y + 16 + chart1.Frame.Size.Y.Offset
                    
                    chart2.Frame.Parent = statsBody
                    chart2.Frame.Position = UDim2_new(0, 16, 0, y)
                    y = y + 16 + chart2.Frame.Size.Y.Offset
                    
                    chart3.Frame.Parent = statsBody
                    chart3.Frame.Position = UDim2_new(0, 16, 0, y)
                    y = y + 16 + chart3.Frame.Size.Y.Offset
                    
                    local this = {}
                    function this.OnPointAdded(this)
                        chart1:OnPointAdded()
                        chart2:OnPointAdded()
                        chart3:OnPointAdded()
                    end
                    function this.SetVisible(this, visible)
                        chart1:SetVisible(visible)
                        chart2:SetVisible(visible)
                        chart3:SetVisible(visible)
                        body.Size = open and UDim2_new(1, 0, 0, y) or UDim2_new(1, 0, 1, 0)
                    end
                    function this.Dispose(this)
                        this:SetVisible(false)
                    end
                    return this
                end;
                FilterButton = function(chartButton)
                    return textFilter(chartButton.ChartStat.Name)
                end;
            }       
            
            local function filterStats(stats)
                return stats.Jobs
            end

            local function openCallback(openNew)
                open = openNew
            end
            
            local tab, statList = createStatsTab('ServerJobs', "Server Jobs", 70, config, openCallback, filterStats, {Stats = true})
            
            textFilterChanged:connect(function()
                statList:Refresh()
            end)
            
            tab:SetVisible(true)
        end
    end
    
    --[[
    do -- Sample tab
        local tabBody = Primitives.FolderFrame(body, 'TabName')
        
        -- 80 is the tab width
        local tab = devConsole:AddTab("Tab Name", 80, tabBody)
        tab:SetVisible(true)
        --tab:SetOpen(true)
    end
    --]]
    
    return devConsole
    
end

----
-- Backup GUI Mouse --
----
do -- This doesn't support multiple windows very well
    function Methods.EnableGUIMouse(devConsole)
        local label = Instance.new("ImageLabel")
        label.BackgroundTransparency = 1
        label.BorderSizePixel = 0
        label.Size = UDim2.new(0, 64, 0, 64)
        label.Image = "rbxasset://Textures/ArrowFarCursor.png"
        label.Name = "BackupMouse"
        label.ZIndex = ZINDEX + 2
        
        local disconnector = CreateDisconnectSignal()
        
        local enabled = false
        
        local mouse = game:GetService("Players").LocalPlayer:GetMouse()
        
        local function Refresh()
            local enabledNew = devConsole.Visible and not UserInputService.MouseIconEnabled
            if enabledNew == enabled then
                return
            end
            enabled = enabledNew
            label.Visible = enabled
            label.Parent = enabled and devConsole.ScreenGui or nil
            disconnector:fire()
            if enabled then
                label.Position = UDim2.new(0, mouse.X - 32, 0, mouse.Y - 32)
                disconnector:connect(UserInputService.InputChanged:connect(function(input)
                    if input.UserInputType == Enum.UserInputType.MouseMovement then
                        --local p = input.Position
                        --if p then
                        label.Position = UDim2.new(0, mouse.X - 32, 0, mouse.Y - 32)
                        --end
                    end
                end))
            end
        end
        
        Refresh()
        local userInputServiceListener;
        devConsole.VisibleChanged:connect(function(visible)
            if userInputServiceListener then
                userInputServiceListener:disconnect()
                userInputServiceListener = nil
            end
            
            userInputServiceListener = UserInputService.Changed:connect(Refresh)
            
            Refresh()
        end)
        
    end
end


----
-- Charts and Stats --
----


do -- Script performance/Chart list
    
    --[[
    local chartStatExample = {
        Name = "RoundScript";
        Stats = {
            -- {Activity, InvocationCount}
            {0, 0};
            {0, 0};
        }
    }
    --]]
    
    -- this manages the button and the chartPage
    local function createChartButton(devConsole, chartList, chartStat, config)
        
        local this = {
            ChartList = chartList;
            ChartStat = chartStat;
            Open = false;
        }
        
        local button = Primitives.Button(nil, 'Button')
        this.Frame = button
        this.Button = button
        
        button.AutoButtonColor = false
        local size0 = UDim2_new(1, -12 - chartList.ScrollingFrame.ScrollBarThickness, 0, 16) -- Size when script is closed
        local size1 = UDim2_new(1, -2  - chartList.ScrollingFrame.ScrollBarThickness, 0, 16) -- Size when script is open
        button.Size = size0
        button.Name = (chartStat.Name or "[no name]")
        if not chartStat.Name then
            button.TextColor3 = Color3.new(1, 0.5, 0.5)
        end
        button.BackgroundColor3 = Style.ScriptButtonColor
        button.BackgroundTransparency = Style.ScriptBackgroundTransparency
        
        local notifyFrame = Primitives.Frame(button, 'NotifyFrame')
        notifyFrame.BackgroundTransparency = 0
        notifyFrame.Size = UDim2.new(0, 8, 1, 0)
        notifyFrame.BackgroundColor3 = Color3.new(0, 0.75, 0)
        
        local label = Primitives.InvisibleTextLabel(button, 'Label', chartStat.Name)
        label.Size = UDim2_new(1, -notifyFrame.Size.X.Offset - 4 - 1, 1, 0)
        label.Position = UDim2_new(0, notifyFrame.Size.X.Offset + 4, 0, 0)
        label.FontSize = 'Size14'
        label.TextXAlignment = 'Left' -- Enum.TextXAlignment.Left
        --label.TextWrap = true
        
        local buttonEffectFunction = devConsole:CreateButtonEffectFunction(button)
        devConsole:ConnectButtonHover(button, function(clicking, hovering)
            buttonEffectFunction(clicking, hovering)
        end)

        button.MouseButton1Down:connect(function()
            -- not ideal
            for i, that in pairs(chartList.ChartButtons) do
                if this ~= that and that.Open then
                    that:SetOpen(false)
                end
            end
            this:SetOpen(true)
        end)
        
        -- This fires when the button opens/closes
        local disconnector = CreateDisconnectSignal()
        -- This fires when the button disposes
        local disconnector2 = CreateDisconnectSignal() -- (Best variable name ever)
        
        local function refreshNotifyFrame()
            notifyFrame.BackgroundColor3 = config.GetNotifyColor(this)
        end
        refreshNotifyFrame()
        disconnector2:connect(chartList.OnStatUpdate:connect(refreshNotifyFrame))
                
        function this.SetOpen(this, open)
            if this.Open == open then
                return
            end
            this.Open = open
            
            button:TweenSize(open and size1 or size0, "Out", "Sine", 0.25, true)
            
            disconnector:fire()
            
            if open then
                
                -- The chart page is initialized directly from the button, (this is not ideal, but it works)
                local statsBody = Primitives.FolderFrame(chartList.Body, 'StatsBody') -- Button container
                
                local chartPage = config.CreateChartPage(this, statsBody)
                
                chartPage:SetVisible(true)
                
                disconnector:connect(chartList.OnStatUpdate:connect(function()
                    chartPage:OnPointAdded()
                end))
                
                disconnector:connect(function()
                    chartPage:Dispose()
                    statsBody:Destroy()
                end)
                
            end
            
        end
        
        function this.Dispose(this)
            button:Destroy()
            disconnector:fire()
            disconnector2:fire()
        end
        
        return this
    end
    
    
    local function defaultSorter(a, b) -- this sorts chartButtons
        return a.ChartStat.Name < b.ChartStat.Name
    end

    function Methods.CreateChartList(devConsole, config)
        
        local this = {
            Config = config;
            Visible = false;
            OnStatUpdate = CreateSignal();
            ChartButtons = {}; -- usage: chartButtons[position] = scriptButton
            ChartStats = {}; -- usage: chartStats[chartStat.Name] = chartStat
        }
        
        local frame = Primitives.FolderFrame(nil, 'ScriptList')
        this.Frame = frame
        frame.Visible = false
        
        local sideMenu = Primitives.Frame(frame, 'SideMenu') -- not necessarily parented to frame!
        sideMenu.Size = UDim2_new(0, 196, 1, 0)
        this.SideMenu = sideMenu
        sideMenu.Visible = false
        
        local body = Primitives.FolderFrame(frame, 'Body')
        this.Body = body
        body.Size = UDim2_new(1, -sideMenu.Size.X.Offset, 1, 0)
        body.Position = UDim2_new(0, sideMenu.Size.X.Offset, 0, 0)
        
        local scrollingFrame = Instance.new("ScrollingFrame", sideMenu)
        scrollingFrame.BorderSizePixel = 0
        scrollingFrame.ZIndex = ZINDEX
        scrollingFrame.ScrollBarThickness = 12
        this.ScrollingFrame = scrollingFrame
        do
            local y = 1 -- if we want to add a label above it later
            scrollingFrame.Size = UDim2_new(1, 0, 1, -y)
            scrollingFrame.Position = UDim2_new(0, 0, 0, y)
            scrollingFrame.BackgroundTransparency = 1
        end
        
        local chartButtons = this.ChartButtons
        local chartStats = this.ChartStats

        local sorter = defaultSorter
        
        function this.SetChartButtonSorter(this, sorterNew)
            if sorter == sorterNew then
                return
            end
            sorter = sorterNew
            table.sort(chartButtons, sorter)
            this:Refresh()
        end
        
        function this.GetChartButton(this, name) -- not used?
            for i = #chartButtons, 1, -1 do
                local chartButton = chartButtons[i]
                if chartButton.ChartStat.Name == name then
                    return chartButton, i
                end
            end
        end
        
        function this.RemoveChart(this, name)
            chartStats[name] = nil
            for i = #chartButtons, 1, -1 do
                local chartButton = chartButtons[i]
                if chartButton.ChartStat.Name == name then
                    chartButton:Dispose()
                    table.remove(chartButtons, i)
                    return
                end
            end
        end
        
        function this.UpdateStats(this, newStats)
            
            local timeStamp = os_time() -- Should it use tick instead?
            
            local scriptAddedOrRemoved = false
            
            for name, stat in pairs(chartStats) do
                if not newStats[name] then
                    scriptAddedOrRemoved = true
                    this:RemoveChart(name)
                end
            end
            
            for name, newStat in pairs(newStats) do
                local chartStat = chartStats[name]
                if not chartStats[name] then
                    chartStat = {
                        Name = name;
                        Stats = {}; -- this could be loaded
                    }
                    chartStats[name] = chartStat
                    if this.Visible then
                        local chartButton = createChartButton(devConsole, this, chartStat, config)
                        chartButton.Frame.Parent = scrollingFrame
                        chartButtons[#chartButtons + 1] = chartButton
                    end
                end
                local stats = chartStat.Stats
                stats[#stats + 1] = newStat
            end
            
            table.sort(chartButtons, sorter)
            
            this:Refresh()
            
            this.OnStatUpdate:fire()
            
        end
        
        function this.Refresh(this)
            
            if not this.Visible then
                for i = #chartButtons, 1, -1 do
                    chartButtons[i]:Dispose()
                    chartButtons[i] = nil
                end
                return
            end
            
            table.sort(chartButtons, sorter)
            
            local y = 0
            for i = 1, #chartButtons do
                local chartButton = chartButtons[i]
                local visible = config.FilterButton(chartButton)
                local button = chartButton.Button
                button.Visible = visible
                if visible then
                    button.Position = UDim2_new(0, 1, 0, y) -- Should it lerp to position if animating?
                    y = y + button.AbsoluteSize.Y + 1
                end
            end
            
            scrollingFrame.CanvasSize = UDim2_new(0, 0, 0, y)
            
        end
        this:Refresh()
        
        function this.SetVisible(this, visible)
            if visible == this.Visible then
                return
            end
            this.Visible = visible
            
            frame.Visible = visible
            sideMenu.Visible = visible
            
            if visible then
                for name, chartStat in pairs(chartStats) do
                    local chartButton = createChartButton(devConsole, this, chartStat, config)
                    chartButton.Button.Parent = scrollingFrame
                    chartButtons[#chartButtons + 1] = chartButton
                end
                this:Refresh()
            else
                for i = #chartButtons, 1, -1 do
                    chartButtons[i]:Dispose()
                    chartButtons[i] = nil
                end
            end
        end
        
        return this
    end
end

do -- Chart
    
    local barWidth = 4
    local numBars = math.ceil((Style.ChartWidth - Style.BorderSize * 2) / (barWidth + 1))
    
    local function round(x)
        return math.floor(x * 1000 + 0.5) / 1000
    end
    
    local function CreateBar()
        local bar = Primitives.Frame()
        bar.BackgroundTransparency = 0
        bar.BackgroundColor3 = Color3_new(0, 0.5, 1)
        return bar
    end
    
    local function CreateGraph(points, statIndex, autoScale)
        -- point = points[i][statIndex]
        
        local this = {}
        
        local direction = Style.ChartGraphDirection -- -1 means coming from right, 1 means coming from left
        
        local frame = Primitives.Frame(nil, 'Graph')
        this.Frame = frame
        frame.ClipsDescendants = true
        
        local scaleFrame = Primitives.FolderFrame(frame, 'ScaleFrame')
        
        local body = Primitives.FolderFrame(scaleFrame, 'Body')
        body.Size = UDim2_new(0, barWidth, 1, 0)
        
        local bars = {}
        local barHeights = {}
        local barPositions = {}
        
        do -- reference notches
            local function getReferenceHeight(position)
                if position % 60 == 0 then
                    return 24
                elseif position % 30 == 0 then
                    return 12
                elseif position % 15 == 0 then
                    return 4
                elseif position % 5 == 0 then
                    return 1
                else
                    return 0
                end
            end
            for position = 0, numBars do
                local height = getReferenceHeight(position)
                if height ~= 0 then
                    local notch = Instance_new('Frame', frame)
                    notch.ZIndex = ZINDEX
                    notch.BorderSizePixel = 0
                    notch.BackgroundColor3 = Color3_new(1, 1, 1)
                    notch.Size = UDim2_new(0, 1, 0, height)
                    notch.Position = UDim2_new(0, position * (barWidth + 1), 1, -height)
                end
            end
        end
        local scale = 1
        
        local position = 0
        
        local visible = false
        
        local function generateSizeAndPosition(height, position)
            height = height * scale
            return
                UDim2_new(0, barWidth, height, 0),
                UDim2_new(0, (barWidth + 1) * position * -direction + 1, 1 - height, 0)
        end
        
        local function RefreshScale(animate)
            if not autoScale then
                return
            end
            local heightMax;
            for i = math_max(#points - numBars + 1, 1), #points do
                local height = points[i][statIndex]
                if not heightMax or height > heightMax then
                    heightMax = height
                end
            end
            
            if not heightMax or heightMax <= 0 then
                local size, position = UDim2_new(1, 0, 1, 0), UDim2_new(0, 0, 0, 0)
                if animate then
                    scaleFrame:TweenSizeAndPosition(size, position, 'Out', 'Sine', 0.25, true)
                else
                    scaleFrame.Size, scaleFrame.Position = UDim2_new(1, 0, 1, 0), UDim2_new(0, 0, 0, 0)
                end
                return
            end
            
            local scaleNew = 1 / heightMax * 0.95
            if math.abs(scale - scaleNew) < 0.0000001 then
                return
            end
            -- Possible performance boost Todo: if the scale isn't significantly different (within like 0.5-4), just adjust scaleFrame's size
            scale = scaleNew
            
            for i = 1, #bars do
                local bar = bars[i]
                local height = barHeights[i]
                local barSize, barPosition = generateSizeAndPosition(height, barPositions[i])
                if animate then
                    bar:TweenSizeAndPosition(barSize, barPosition, 'Out', 'Sine', 0.25, true)
                else
                    bar.Size, bar.Position = barSize, barPosition
                end
            end
            
            --local scale = 1 / heightMax * 0.95
            --scaleFrame:TweenSizeAndPosition(UDim2_new(1, 0, scale, 0), UDim2_new(0, 0, 1 - scale, 0), 'Out', 'Sine', 0.25, true)

        end
        
        function this.OnPointAdded(this)
            if not visible then
                return
            end
            local bar;
            
            -- possible game crasher
            while #bars > numBars do
                if bar then
                    bar:Destroy()
                end
                bar = bars[1]
                table.remove(bars, 1)
                table.remove(barHeights, 1)
                table.remove(barPositions, 1)
            end 
            
            local point = points[#points] and points[#points][statIndex]
            assert(point)
            if not bar then
                bar = CreateBar()
                bar.Parent = body
            end
            
            local height = point
            
            bars[#bars + 1] = bar
            barHeights[#barHeights + 1] = height
            barPositions[#barPositions + 1] = position
            
            bar.Size, bar.Position = generateSizeAndPosition(height, position)
            
            body:TweenPosition(UDim2_new(1 - (direction * 0.5 + 0.5), (barWidth + 1) * position * direction, 0, 0), 'Out', 'Sine', 0.25, true)
            
            position = position + 1
            
            RefreshScale(true)
        end

        function this.SetVisible(this, visibleNew)

            body.Position = UDim2_new(1 - (direction * 0.5 + 0.5), 0, 0, 0)
            if visibleNew == visible then
                return
            end
            visible = visibleNew
            if not visible then
                for i = #bars, 1, -1 do
                    bars[i]:Destroy()
                    bars[i] = nil
                end
                return
            end

            position = 0
            for i = math_max(#points - numBars + 1, 1), #points do
                local bar = bars[position + 1]
                if not bar then
                    bar = CreateBar()
                    bar.Parent = body
                    bars[position + 1] = bar
                end

                local point = points[i][statIndex]
                local height = point
                
                barHeights[position + 1] = height
                barPositions[position + 1] = position
                
                bar.Size, bar.Position = generateSizeAndPosition(height, position)
                
                position = position + 1
            end
            
            body.Position = UDim2_new(1 - (direction * 0.5 + 0.5), (barWidth + 1) * (position - 1) * direction, 0, 0)
            
            RefreshScale(false)
            
        end
        
        return this
    end
    
    local function createLabel(...)
        local n = Primitives.InvisibleTextLabel(...)
        n.TextXAlignment = 'Left'
        n.FontSize = 'Size14'
        return n
    end
    
    function Methods.CreateChart(devConsole, points, title, statIndex, pointToString)
    
        pointToString = pointToString or function(point)
            if point then
                local precision = 10000
                local v = point * precision
                if v < 1 and v > 0 then
                    return "<" .. (1 / precision)
                else
                    return math.floor(v + 0.5) / precision .. ""
                end
            else
                return ""
            end
        end
        
        local chart = {}
        
        local frame = Primitives.Frame(nil, 'Chart')
        chart.Frame = frame
        frame.Size = UDim2_new(0, Style.ChartWidth, 0, Style.ChartHeight)
        
        local labelCurrent = createLabel(frame, 'Current', "Current: " .. pointToString(points[#points] and points[#points][statIndex]))
        labelCurrent.Size = UDim2_new(0, 0.5, 0, Style.ChartTitleHeight)
        labelCurrent.Position = UDim2_new(0, 4, 0, Style.ChartTitleHeight + 1)
        
        local graph = CreateGraph(points, statIndex, true)
        graph.Frame.Parent = frame
        graph.Frame.Size = UDim2_new(0, Style.ChartWidth - Style.BorderSize * 2, 0, Style.ChartGraphHeight)
        graph.Frame.Position = UDim2_new(0, Style.BorderSize, 0, Style.ChartTitleHeight + Style.ChartDataHeight)
        
        do
            local bar = Primitives.Frame(frame, 'Bar')
            bar.Size = UDim2_new(1, 0, 0, Style.ChartTitleHeight)
            
            local label = Primitives.InvisibleTextLabel(bar, 'Title', title)
            label.TextXAlignment = 'Left' -- Enum.TextXAlignment.Left
            label.Size = UDim2_new(1, -4, 1, 0)
            label.Position = UDim2_new(0, 4, 0, 0)
            label.FontSize = 'Size18'
        end
        
        local visible = false
        function chart.SetVisible(chart, visibleNew)
            if visibleNew == visible then
                return
            end
            visible = visibleNew
            graph:SetVisible(visible)
        end
        
        function chart.OnPointAdded(chart)
            local point = points[#points]
            
            if not point then
                return
            end
            
            labelCurrent.Text = "Current: " .. pointToString(point and point[statIndex])
            
            graph:OnPointAdded()
        end
        
        return chart
    end
end

----
-- Output console --
----
do
    function Methods.CreateCommandLine(devConsole)
        local this = {
            CommandInputted = CreateSignal();
        }
        
        local frame = Primitives.FolderFrame(nil, 'CommandLine')
        this.Frame = frame
        frame.Size = UDim2_new(1, 0, 0, Style.CommandLineHeight)
        
        local textBoxFrame = Primitives.Frame(frame, 'TextBoxFrame')
        textBoxFrame.Size = UDim2_new(1, 0, 0, Style.CommandLineHeight)
        textBoxFrame.Position = UDim2_new(0, 0, 0, 0)
        textBoxFrame.ClipsDescendants = true
        
        local label = Primitives.InvisibleTextLabel(textBoxFrame, 'Label', ">")
        label.Position = UDim2_new(0, 4, 0, 0)
        label.Size = UDim2_new(0, 12, 1, -1)
        label.FontSize = 'Size14'
        
        local textBox = Primitives.TextBox(textBoxFrame, 'TextBox')
        --textBox.TextWrapped = true -- This needs to auto-resize
        textBox.BackgroundTransparency = 1
        textBox.Text = "Type command here"
        local padding = 2
        textBox.Size = UDim2_new(1, -(padding * 2) - 4 - 12, 0, 500)
        textBox.Position = UDim2_new(0, 4 + 12 + padding, 0, 0)
        textBox.TextXAlignment = 'Left'
        textBox.TextYAlignment = 'Top'
        textBox.FontSize = 'Size18'
        textBox.TextWrapped = true
        
        do
            local defaultSize = UDim2_new(1, 0, 0, Style.CommandLineHeight)
            local first = true
            
            textBox.Changed:connect(function(property)
                if property == 'TextBounds' or property == 'AbsoluteSize' then
                    if first then -- There's a glitch that only occurs on the first change
                        first = false
                        return
                    end
                    local textBounds = textBox.TextBounds
                    if textBounds.Y > Style.CommandLineHeight then
                        textBoxFrame.Size = UDim2_new(1, 0, 0, textBounds.Y + 2)
                    else
                        textBoxFrame.Size = defaultSize
                    end
                end
            end)
        end
        
        local disconnector = CreateDisconnectSignal()
        
        local backtrackPosition = 0
        local inputtedText = {}
        local isLastWeak = false
        local function addInputtedText(text, weak)
            -- weak means it gets overwritten by the next text that's inputted
            if isLastWeak then
                table.remove(inputtedText, 1)
            end
            if inputtedText[1] == text then
                isLastWeak = isLastWeak and weak
                return
            end
            isLastWeak = weak
            if not weak then
                for i = #inputtedText, 1, -1 do
                    if inputtedText[i] == text then
                        table.remove(inputtedText, i)
                    end
                end
            end
            table.insert(inputtedText, 1, text)
        end
        local function backtrack(direction)
            backtrackPosition = backtrackPosition + direction
            if backtrackPosition < 1 then
                backtrackPosition = 1
            elseif backtrackPosition > #inputtedText then
                backtrackPosition = #inputtedText
            end
            if inputtedText[backtrackPosition] then
                -- Setting the text doesn't always work, especially after losing focus without pressing enter, then clicking back
                textBox.Text = inputtedText[backtrackPosition]
            end
        end
        
        local focusLostWithoutEnter = false
        
        textBox.Focused:connect(function()
            disconnector:fire()
            backtrackPosition = 0
            disconnector:connect(UserInputService.InputBegan:connect(function(input)
                if input.KeyCode == Enum.KeyCode.Up then
                    if backtrackPosition == 0 and not focusLostWithoutEnter then
                        -- They typed something, then pressed up. They might want what they typed back, so we store it
                        --  after they input the next thing, we know they meant to discard this, which is why it's "weak" (second arg is true)
                        addInputtedText(textBox.Text, true)
                        backtrackPosition = 1
                    end
                    backtrack(1)
                elseif input.KeyCode == Enum.KeyCode.Down then
                    backtrack(-1)
                end
            end))
        end)
        
        textBox.FocusLost:connect(function(enterPressed)
            disconnector:fire()
            if enterPressed then
                focusLostWithoutEnter = false
                
                local text = textBox.Text
                addInputtedText(text, false)
                this.CommandInputted:fire(text)
                textBox.Text = ""
                textBox:CaptureFocus()
            else
                backtrackPosition = 0
                focusLostWithoutEnter = true
                addInputtedText(textBox.Text, true)
            end
        end)

        return this
    end
end
do
    local padding = 5
    local LabelSize = UDim2_new(1, -padding, 0, 2048)
    
    local TextColors = Style.MessageColors
    local TextColorUnknown = Color3_new(0.5, 0, 1)
    
    local function isHidden(message)
        return false
    end

    function Methods.CreateOutput(devConsole, messages, messageFilter)

        -- AKA 'Log'
        local heightChanged = CreateSignal()
        local output = {
            Visible = false;
            Height = 0;
            HeightChanged = heightChanged;
        }
        
        local function setHeight(height)
            height = height + 4
            output.Height = height
            heightChanged:fire(height)
        end

        -- The label container
        local frame = Primitives.FolderFrame(nil, 'Output')
        frame.ClipsDescendants = true
        output.Frame = frame

        local textWrappedEnabled = false

        do
            local lastX = 0
            connectPropertyChanged(frame, 'AbsoluteSize', function(size)
                local currentX = size.X
                --currentY = currentY - currentY
                if currentX ~= lastX then
                    lastX = currentX
                    output:RefreshMessages()
                end
            end)
        end

        local labels = {}
        local labelPositions = {}

        local function RefreshTextWrapped()
            if not output.Visible then
                return
            end
            local y = 1
            for i = 1, #labels do
                local label = labels[i]
                label.TextWrapped = textWrappedEnabled
                local height = label.TextBounds.Y
                label.Size = LabelSize -- UDim2_new(1, 0, 0, height)
                label.Position = UDim2_new(0, padding, 0, y)
                y = y + height
                if height > 16 then
                    y = y + 4
                end
            end
            setHeight(y)
        end
        local MAX_LINES = 2048

        local function RefreshMessagesForReal(messageStartPosition)
            if not output.Visible then
                return
            end

            local y = 1
            local labelPosition = 0 -- position of last used label

            -- Failed optimization:
            messageStartPosition = nil
            if messageStartPosition then
                local labelPositionLast;
                for i = messageStartPosition, math_max(1, #messages - MAX_LINES), -1 do
                    if labelPositions[i] then
                        labelPositionLast = labelPositions[i]
                        break
                    end
                end
                if labels[labelPositionLast] then
                    labelPosition = labelPositionLast
                    local label = labels[labelPositionLast]
                    y = label.Position.Y.Offset + label.Size.Y.Offset
                else
                    messageStartPosition = nil
                end
            end

            for i = messageStartPosition or math_max(1, #messages - MAX_LINES), #messages do
                local message = messages[i]
                if messageFilter(message) then
                    labelPosition = labelPosition + 1
                    labelPositions[i] = labelPosition
                    local label = labels[labelPosition]
                    if not label then
                        label = Instance_new('TextLabel', frame)
                        label.ZIndex = ZINDEX
                        label.BackgroundTransparency = 1
                        --label.Font = Style.Font
                        label.FontSize = 'Size10'
                        label.TextXAlignment = 'Left'
                        label.TextYAlignment = 'Top'
                        labels[labelPosition] = label
                    end
                    label.TextWrapped = textWrappedEnabled
                    label.Size = LabelSize
                    label.TextColor3 = TextColors[message.Type] or TextColorUnknown
                    label.Text = message.Time .. " -- " .. message.Message
                    
                    local height = label.TextBounds.Y
                    label.Size = LabelSize -- UDim2_new(1, -padding, 0, height)
                    label.Position = UDim2_new(0, padding, 0, y)

                    y = y + height
                    
                    if height > 16 then
                        y = y + 4
                    end
                else
                    labelPositions[i] = false
                end
            end

            -- Destroy extra labels
            for i = #labels, labelPosition + 1, -1 do
                labels[i]:Destroy()
                labels[i] = nil
            end
            
            setHeight(y)
        end
        
        local refreshHandle;
        function output.RefreshMessages(output, messageStartPosition)
            if not output.Visible then
                return
            end
            if not refreshHandle then
                refreshHandle = true
                coroutine.wrap(function() -- Not ideal
                    wait()
                    refreshHandle = false
                    RefreshMessagesForReal()
                end)()
            end
        end
        
        function output.SetTextWrappedEnabled(output, textWrappedEnabledNew)
            if textWrappedEnabledNew == textWrappedEnabled then
                return
            end
            textWrappedEnabled = textWrappedEnabledNew
            RefreshTextWrapped()
        end
        
        function output.SetVisible(output, visible)
            if visible == output.Visible then
                return
            end
            output.Visible = visible
            if visible then
                RefreshMessagesForReal()
            else
                for i = #labels, 1, -1 do
                    labels[i]:Destroy()
                    labels[i] = nil
                end
            end
        end

        return output
    end
end

----
-- Tabs --
----
function Methods.RefreshTabs(devConsole)
    -- Go through and reposition them
    local x = Style.BorderSize
    local tabs = devConsole.Tabs
    for i = 1, #tabs do
        local tab = tabs[i]
        if tab.ButtonFrame.Visible then
            x = x + 3
            tab.ButtonFrame.Position = UDim2_new(0, x, 0, 0)
            x = x + tab.ButtonFrame.AbsoluteSize.X + 3
        end
    end
end

function Methods.AddTab(devConsole, text, width, body, openCallback, visibleCallback)
    -- Body is a frame that contains the tab contents
    body.Visible = false
    
    local tab = {
        Open = false; -- If the tab is open
        Visible = false; -- If the tab is shown
        OpenCallback = openCallback;
        VisibleCallback = visibleCallback;
        Body = body;
    }
    
    local buttonFrame = Primitives.InvisibleButton(devConsole.Frame.Interior.Tabs, 'Tab_' .. text)
    tab.ButtonFrame = buttonFrame
    buttonFrame.Size = UDim2_new(0, width, 0, Style.TabHeight)
    buttonFrame.Visible = false
    
    local textLabel = Primitives.TextLabel(buttonFrame, 'Label', text)
    textLabel.FontSize = Enum.FontSize.Size14
    --textLabel.TextYAlignment = Enum.TextYAlignment.Top
    
    devConsole:ConnectButtonHover(buttonFrame, devConsole:CreateButtonEffectFunction(textLabel))

    -- These are the dimensions when the tab is closed
    local size0 = UDim2_new(1, 0, 1, -7)
    local position0 = UDim2_new(0, 0, 0, 4)
    -- There are the dimensions when the tab is open
    local size1 = UDim2_new(1, 0, 1, -4)
    local position1 = UDim2_new(0, 0, 0, 4)
    -- It starts closed
    textLabel.Size = size0
    textLabel.Position = position0
    
    function tab.SetVisible(tab, visible)
        if visible == tab.Visible then
            return
        end
        tab.Visible = visible
        tab:SetOpen(false)
        if tab.VisibleCallback then
            tab.VisibleCallback(visible)
        end
        buttonFrame.Visible = visible
        devConsole:RefreshTabs()
        if not visible then
            tab.SetOpen(false)
        end
    end

    function tab.SetOpen(tab, open)
        if open == tab.Open then
            return
        end
        tab.Open = open

        if open then
            if tab.SavedScrollbarValue then
                devConsole.WindowScrollbar:SetValue(tab.SavedScrollbarValue)  -- This doesn't load correctly?
            end
            local tabs = devConsole.Tabs
            for i = 1, #tabs do
                if tabs[i] ~= tab then
                    tabs[i]:SetOpen(false)
                end
            end
            if body then
                body.Visible = true
            end
            devConsole:RefreshTabs()
            -- Set dimensions for folder effect
            textLabel.Size = size1
            textLabel.Position = position1
        else
            tab.SavedScrollbarValue = devConsole.WindowScrollbar:GetValue() -- This doesn't save correctly

            if body then
                body.Visible = false
                -- todo: (not essential) these 2 lines should instead exist during open (above block) after going through tabs
                devConsole.Frame.Interior.WindowContainer.Window.Body.Size = UDim2_new(1, 0, 1, 0) 
                devConsole.Frame.Interior.WindowContainer.Window.Body.Position = UDim2_new(0, 0, 0, 0)
            end
            
            -- Set dimensions for folder effect
            textLabel.Size = size0
            textLabel.Position = position0
        end
        
        if tab.OpenCallback then
            tab.OpenCallback(open)
        end
        
    end
    
    buttonFrame.MouseButton1Click:connect(function()
        if tab.Visible then
            tab:SetOpen(true)
        end
    end)
    
    table.insert(devConsole.Tabs, tab)
    
    return tab
    
end

----
-- Scroll bar --
----
function Methods.ApplyScrollbarToFrame(devConsole, scrollbar, window, body, frame)
    local windowHeight, bodyHeight
    local height = scrollbar:GetHeight()
    local value = scrollbar:GetValue()
    local function getHeights()
        return window.AbsoluteSize.Y, body.AbsoluteSize.Y
    end
    local function refreshDimension()
        local windowHeightNew, bodyHeightNew = getHeights()

        if bodyHeight ~= bodyHeightNew or windowHeight ~= windowHeightNew then
            bodyHeight, windowHeight = bodyHeightNew, windowHeightNew
            height = windowHeight / bodyHeight
            scrollbar:SetHeight(height)
            
            local yOffset = (bodyHeight - windowHeight) * value
            local x = body.Position.X
            local y = body.Position.Y
            body.Position = UDim2_new(x.Scale, x.Offset, y.Scale, -math.floor(yOffset))
        end

    end
    
    local function setValue(valueNew)
        value = valueNew
        refreshDimension()
        local yOffset = (bodyHeight - windowHeight) * value
        local x = body.Position.X
        local y = body.Position.Y
        body.Position = UDim2_new(x.Scale, x.Offset, y.Scale, -math.floor(yOffset))
    end
    scrollbar.ValueChanged:connect(setValue)
    setValue(scrollbar:GetValue())

    local scrollDistance = 120

    scrollbar.ButtonUp.MouseButton1Click:connect(function()
        scrollbar:Scroll(-scrollDistance, getHeights())
    end)
    scrollbar.ButtonDown.MouseButton1Click:connect(function()
        scrollbar:Scroll(scrollDistance, getHeights())
    end)

    connectPropertyChanged(window, 'AbsoluteSize', refreshDimension)
    connectPropertyChanged(body, 'AbsoluteSize', function()
        local windowHeight, bodyHeight = getHeights()
        local value = scrollbar:GetValue()
        if value ~= 1 and value ~= 0 then
            local value = -body.Position.Y.Offset / (bodyHeight - windowHeight)
            scrollbar:SetValue(value)
        end
        refreshDimension()
    end)

    window.MouseWheelForward:connect(function()
        scrollbar:Scroll(-scrollDistance, getHeights())
    end)
    window.MouseWheelBackward:connect(function()
        scrollbar:Scroll(scrollDistance, getHeights())
    end)

end

function Methods.CreateScrollbar(devConsole, rotation)
    local scrollbar = {}

    local main = Primitives.FolderFrame(main, 'Scrollbar')
    scrollbar.Frame = main

    local frame = Primitives.Button(main, 'Frame')
    frame.AutoButtonColor = false
    frame.Size = UDim2_new(1, 0, 1, -(Style.HandleHeight) * 2 - 2)
    frame.Position = UDim2_new(0, 0, 0, Style.HandleHeight + 1)
    -- frame.BackgroundTransparency = 0.75
    
    -- This replaces the scrollbar when it's not being used
    local frame2 = Primitives.Frame(main, 'Frame')
    frame2.Size = UDim2_new(1, 0, 1, 0)
    frame2.Position = UDim2_new(0, 0, 0, 0)
    
    function scrollbar.SetVisible(scrollbar, visible)
        frame.Visible = visible
        frame2.Visible = not visible
    end
    
    local buttonUp = Primitives.ImageButton(frame, 'Up', 'https://www.roblox.com/asset/?id=261880783')
    scrollbar.ButtonUp = buttonUp
    buttonUp.Size = UDim2_new(1, 0, 0, Style.HandleHeight)
    buttonUp.Position = UDim2_new(0, 0, 0, -Style.HandleHeight - 1)
    buttonUp.AutoButtonColor = false
    devConsole:ConnectButtonHover(buttonUp, devConsole:CreateButtonEffectFunction(buttonUp))

    local buttonDown = Primitives.ImageButton(frame, 'Down', 'https://www.roblox.com/asset/?id=261880783')
    scrollbar.ButtonDown = buttonDown
    buttonDown.Size = UDim2_new(1, 0, 0, Style.HandleHeight)
    buttonDown.Position = UDim2_new(0, 0, 1, 1)
    buttonDown.Rotation = 180
    buttonDown.AutoButtonColor = false
    devConsole:ConnectButtonHover(buttonDown, devConsole:CreateButtonEffectFunction(buttonDown))

    local bar = Primitives.Button(frame, 'Bar')
    bar.Size = UDim2_new(1, 0, 0.5, 0)
    bar.Position = UDim2_new(0, 0, 0.25, 0)
    
    bar.AutoButtonColor = false
    
    local grip = Primitives.InvisibleImageLabel(bar, 'Image', 'https://www.roblox.com/asset/?id=261904959')
    grip.Size = UDim2_new(0, 16, 0, 16)
    grip.Position = UDim2_new(0.5, -8, 0.5, -8)

    local buttonEffectFunction = devConsole:CreateButtonEffectFunction(bar, nil, bar.BackgroundColor3, bar.BackgroundColor3)
    
    -- Inertial scrolling would be added around here
    
    local value = 1
    local valueChanged = CreateSignal()
    scrollbar.ValueChanged = valueChanged
    -- value = 0: at very top
    -- value = 1: at very bottom
    
    local height = 0.25
    local heightChanged = CreateSignal()
    scrollbar.HeightChanged = heightChanged
    -- height = 0: infinite page size
    -- height = 1: bar fills frame completely, no need to scroll
    
    local function getValueAtPosition(pos)
        return ((pos - main.AbsolutePosition.Y) / main.AbsoluteSize.Y) / (1 - height)
    end
    
    -- Refreshes the position and size of the scrollbar
    local function refresh()
        local y = height
        bar.Size = UDim2_new(1, 0, y, 0)
        bar.Position = UDim2_new(0, 0, value * (1 - y), 0)
    end
    refresh()
    
    function scrollbar.SetValue(scrollbar, valueNew)
        if valueNew < 0 then
            valueNew = 0
        elseif valueNew > 1 then
            valueNew = 1
        end
        if valueNew ~= value then
            value = valueNew
            refresh()
            valueChanged:fire(valueNew)
        end
    end
    function scrollbar.GetValue(scrollbar)
        return value
    end
    
    function scrollbar.Scroll(scrollbar, direction, windowHeight, bodyHeight)
        scrollbar:SetValue(value + direction / bodyHeight) -- needs to be adjusted
    end
    
    function scrollbar.SetHeight(scrollbar, heightNew)
        if heightNew < 0 then
            heightNew = 0 -- this is still an awkward case of divide-by-zero that shouldn't happen
        elseif heightNew > 1 then
            heightNew = 1
        end
        heightNew = math.max(heightNew, 0.1) -- Minimum scroll bar size, from that point on it is not the actual ratio
        if heightNew ~= height then
            height = heightNew
            scrollbar:SetVisible(heightNew < 1)
            refresh()
            heightChanged:fire(heightNew)
        end
    end
    function scrollbar.GetHeight(scrollbar)
        return height
    end

    devConsole:ConnectButtonDragging(bar, function()
        local value0 = value -- starting value
        return function(dx, dy)
            local dposition = dy -- net position change relative to the bar's axis (could support rotated scroll bars)
            local dvalue = (dposition / frame.AbsoluteSize.Y) / (1 - height) -- net value change
            scrollbar:SetValue(value0 + dvalue)
        end
    end, buttonEffectFunction)
    
    return scrollbar
end

----
-- Fancy color lerp --
----
local RenderLerpAnimation; do
    local math_cos = math.cos
    local math_pi = math.pi
    function RenderLerpAnimation(disconnectSignal, length, callback)
        disconnectSignal:fire()
        local timeStamp = tick()
        local listener = RunService.RenderStepped:connect(function()
            local t = (tick() - timeStamp) / length
            if t >= 1 then
                t = 1
                disconnectSignal:fire()
            else
                t = (1 - math_cos(t * math_pi)) / 2 -- cosine interpolation aka 'Sine' in :TweenSizeAndPosition
            end
            callback(t)
        end)
        disconnectSignal:connect(listener)
        return listener
    end
end

if EYECANDY_ENABLED then
    -- This is the pretty version
    function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor)
        normalColor = normalColor or button.BackgroundColor3
        clickingColor = clickingColor or Style.GetButtonDownColor(normalColor)
        hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor)
        local disconnectSignal = CreateDisconnectSignal()
        return function(clicking, hovering)
            local color0 = button.BackgroundColor3
            local color1 = clicking and clickingColor or (hovering and hoveringColor or normalColor)
            local r0, g0, b0 = color0.r, color0.g, color0.b
            local r1, g1, b1 = color1.r, color1.g, color1.b
            local r2, g2, b2 = r1 - r0, g1 - g0, b1 - b0
            RenderLerpAnimation(disconnectSignal, clicking and 0.125 or 0.25, function(t)
                button.BackgroundColor3 = Color3_new(r0 + r2 * t, g0 + g2 * t, b0 + b2 * t)
            end)
        end
    end
else
    -- This is the simple version
    function Methods.CreateButtonEffectFunction(devConsole, button, normalColor, clickingColor, hoveringColor)
        normalColor = normalColor or button.BackgroundColor3
        clickingColor = clickingColor or Style.GetButtonDownColor(normalColor)
        hoveringColor = hoveringColor or Style.GetButtonHoverColor(normalColor)
        return function(clicking, hovering)
            button.BackgroundColor3 = clicking and clickingColor or (hovering and hoveringColor or normalColor)
        end
    end
end

function Methods.GenerateOptionButtonAnimationToggle(devConsole, interior, button, gear, tabContainer, optionsClippingFrame, optionsFrame)
    
    local tabContainerSize0 = tabContainer.Size
    local tabContainerSize1 = UDim2_new(
        tabContainerSize0.X.Scale, tabContainerSize0.X.Offset + (Style.GearSize + 2) + Style.BorderSize,
        tabContainerSize0.Y.Scale, tabContainerSize0.Y.Offset)
        
    local gearRotation0 = gear.Rotation
    local gearRotation1 = gear.Rotation - 90
    local interiorSize0 = interior.Size
    local interiorSize1 = UDim2_new(interiorSize0.X.Scale, interiorSize0.X.Offset, interiorSize0.Y.Scale, interiorSize0.Y.Offset - Style.OptionAreaHeight)
    local interiorPosition0 = interior.Position
    local interiorPosition1 = UDim2_new(interiorPosition0.X.Scale, interiorPosition0.X.Offset, interiorPosition0.Y.Scale, interiorPosition0.Y.Offset + Style.OptionAreaHeight)
    
    local length = 0.5
    local disconnector = CreateDisconnectSignal()
    return function(open)
        if open then
            interior:TweenSizeAndPosition(interiorSize1, interiorPosition1, 'Out', 'Sine', length, true)
            tabContainer:TweenSize(tabContainerSize1, 'Out', 'Sine', length, true)
            optionsClippingFrame:TweenSizeAndPosition(
                UDim2_new(1, 0, 0, Style.OptionAreaHeight),
                UDim2_new(0, 0, 0, -Style.OptionAreaHeight),
                'Out', 'Sine', length, true
            )
            optionsFrame:TweenPosition(
                UDim2_new(0, 0, 0, 0),-- -Style.OptionAreaHeight),
                'Out', 'Sine', length, true
            )
            local gearRotation = gear.Rotation
            RenderLerpAnimation(disconnector, length, function(t)
                gear.Rotation = gearRotation1 * t + gearRotation * (1 - t)
            end)
        else
            interior:TweenSizeAndPosition(interiorSize0, interiorPosition0, 'Out', 'Sine', length, true)
            tabContainer:TweenSize(tabContainerSize0, 'Out', 'Sine', length, true)
            optionsClippingFrame:TweenSizeAndPosition(
                UDim2_new(1, 0, 0, 0),
                UDim2_new(0, 0, 0, 0),
                'Out', 'Sine', length, true
            )
            optionsFrame:TweenPosition(
                UDim2_new(0, 0, 0, Style.OptionAreaHeight),
                'Out', 'Sine', length, true
            )
            local gearRotation = gear.Rotation
            RenderLerpAnimation(disconnector, length, function(t)
                gear.Rotation = gearRotation0 * t + gearRotation * (1 - t)
            end)
        end 
    end
end

----
-- Events for color effects --
----
do
    local globalInteractEvent = CreateSignal()
    function Methods.ConnectButtonHover(devConsole, button, mouseInteractCallback)
        -- void mouseInteractCallback(bool clicking, bool hovering)
        
        local this = {}
        
        local clicking = false
        local hovering = false
        local function set(clickingNew, hoveringNew)
            if hoveringNew and TouchEnabled then
                hoveringNew = false -- Touch screens don't hover
            end
            if clickingNew ~= clicking or hoveringNew ~= hovering then
                clicking, hovering = clickingNew, hoveringNew
                mouseInteractCallback(clicking, hovering)
            end
        end
        
        button.MouseButton1Down:connect(function()
            set(true, true)
        end)
        button.MouseButton1Up:connect(function()
            set(false, true)
        end)
        button.MouseEnter:connect(function()
            set(clicking, true)
        end)
        button.MouseLeave:connect(function()
            set(false, false)
        end)
        --[[ these might cause memory leakes (when creating temporary buttons)
        -- This solves the case in which the user presses F9 while hovering over a button
        devConsole.VisibleChanged:connect(function()
            set(false, false)
        end)
        
        globalInteractEvent:connect(function()
            set(false, false)
        end)
        --]]
    end
end

----
-- Events for draggers -- (for the window's top handle, the resize button, and scrollbars)
----
function Methods.ConnectButtonDragging(devConsole, button, dragCallback, mouseInteractCallback)
    
    -- How dragCallback is called: local deltaCallback = dragCallback(xPositionAtMouseDown, yPositionAtMouseDown)
    -- How deltaCallback is called: deltaCallback(netChangeInAbsoluteXPositionSinceMouseDown, netChangeInAbsoluteYPositionSinceMouseDown)
    
    local dragging = false -- AKA 'clicking'
    local hovering = false
    
    local listeners = {}
    
    local disconnectCallback;
    
    local function stopDragging()
        if not dragging then
            return
        end
        dragging = false
        mouseInteractCallback(dragging, hovering)
        for i = #listeners, 1, -1 do
            listeners[i]:disconnect()
            listeners[i] = nil
        end
    end
    
    local ButtonUserInputTypes = {
        [Enum.UserInputType.MouseButton1] = true;
        [Enum.UserInputType.Touch] = true; -- I'm not sure if touch actually works here
    }
    
    local mouse = game:GetService("Players").LocalPlayer:GetMouse()

    local function startDragging()
        if dragging then
            return
        end
        dragging = true
        
        mouseInteractCallback(dragging, hovering)
        local deltaCallback;
        
        local x0, y0 = mouse.X, mouse.Y
        --[[
        listeners[#listeners + 1] = UserInputService.InputBegan:connect(function(input)
            if ButtonUserInputTypes[input.UserInputType] then
                local position = input.Position
                if position and not x0 then
                    x0, y0 = position.X, position.Y -- The same click
                end
            end
        end)
        --]]
        listeners[#listeners + 1] = UserInputService.InputEnded:connect(function(input)
            if ButtonUserInputTypes[input.UserInputType] then
                stopDragging()
            end
        end)
        listeners[#listeners + 1] = UserInputService.InputChanged:connect(function(input)
            if input.UserInputType ~= Enum.UserInputType.MouseMovement then
                return
            end
            local p1 = input.Position
            if not p1 then
                return
            end
            local x1, y1 = mouse.X, mouse.Y --p1.X, p1.Y
            if not deltaCallback then
                deltaCallback, disconnectCallback = dragCallback(x0 or x1, y0 or y1)
            end
            if x0 then
                deltaCallback(x1 - x0, y1 - y0)
            end
        end)
    end
    
    button.MouseButton1Down:connect(startDragging)
    button.MouseButton1Up:connect(stopDragging)
    button.MouseEnter:connect(function()
        if not hovering then
            hovering = true
            mouseInteractCallback(dragging, hovering)
        end
    end)
    button.MouseLeave:connect(function()
        if hovering then
            hovering = false
            mouseInteractCallback(dragging, hovering)
        end
    end)
    
    devConsole.VisibleChanged:connect(stopDragging)
end

----
-- Permissions --
----
do
    local permissions;
    function DeveloperConsole.GetPermissions()
        if permissions then
            return permissions
        end
        permissions = {}
        
        pcall(function()
            permissions.CreatorFlagValue = settings():GetFFlag("UseCanManageApiToDetermineConsoleAccess")
        end)
    
        pcall(function()
            -- This might not support group games, I'll leave it up to "UseCanManageApiToDetermineConsoleAccess"
            permissions.IsCreator = permissions.CreatorFlagValue or game:GetService("Players").LocalPlayer.userId == game.CreatorId
        end)
        
        if permissions.CreatorFlagValue then -- Use the new API
            permissions.IsCreator = false
            local success, result = pcall(function()
                local url = string.format("/users/%d/canmanage/%d", game:GetService("Players").LocalPlayer.userId, game.PlaceId)
                return game:GetService('HttpRbxApiService'):GetAsync(url, false, Enum.ThrottlingPriority.Default)
            end)
            if success and type(result) == "string" then
                -- API returns: {"Success":BOOLEAN,"CanManage":BOOLEAN}
                -- Convert from JSON to a table
                -- pcall in case of invalid JSON
                success, result = pcall(function()
                    return game:GetService('HttpService'):JSONDecode(result)
                end)
                if success and result.CanManage == true then
                    permissions.IsCreator = result.CanManage
                end
            end
        end
        
        permissions.ClientCodeExecutionEnabled = false
        pcall(function()
            permissions.ServerCodeExecutionEnabled = permissions.IsCreator and settings():GetFFlag("ConsoleCodeExecutionEnabled")
        end)
        
        if DEBUG then
            permissions.IsCreator = true
            permissions.ServerCodeExecutionEnabled = true
        end
        
        permissions.MayViewServerLog = permissions.IsCreator
        permissions.MayViewClientLog = true
        
        permissions.MayViewServerStats = permissions.IsCreator
        permissions.MayViewServerScripts = permissions.IsCreator
        permissions.MayViewServerJobs = permissions.IsCreator
        
        return permissions
        
    end
end

----
-- Output interface --
----
do
    local messagesAndStats;
    function DeveloperConsole.GetMessagesAndStats(permissions)
        
        if messagesAndStats then
            return messagesAndStats
        end
    
        local function NewOutputMessageSync(getMessages)
            local this;
            this = {
                Messages = nil; -- Private member, DeveloperConsole should use :GetMessages()
                MessageAdded = CreateSignal();
                GetMessages = function()
                    local messages = this.Messages
                    if not messages then
                        -- If it errors while getting messages, it skip it next time
                        if this.Attempted then
                            messages = {}
                        else
                            this.Attempted = true
                            messages = getMessages(this)
                            this.Messages = messages
                        end
        
                    end
                    return messages
                end;
            }
            return this
        end
        
        local ConvertTimeStamp; do
            -- Easy, fast, and working nicely
            local function numberWithZero(num)
                return (num < 10 and "0" or "") .. num
            end
            local string_format = string.format -- optimization
            function ConvertTimeStamp(timeStamp)
                local localTime = timeStamp - os_time() + math.floor(tick())
                local dayTime = localTime % 86400
                        
                local hour = math.floor(dayTime/3600)
                
                dayTime = dayTime - (hour * 3600)
                local minute = math.floor(dayTime/60)
                
                dayTime = dayTime - (minute * 60)
                local second = dayTime
    
                local h = numberWithZero(hour)
                local m = numberWithZero(minute)
                local s = numberWithZero(dayTime)
        
                return string_format("%s:%s:%s", h, m, s)
            end
        end
        
        local warningsToFilter = {"ClassDescriptor failed to learn", "EventDescriptor failed to learn", "Type failed to learn"}
        
        -- Filter "ClassDescriptor failed to learn" errors
        local function filterMessageOnAdd(message)
            if message.Type ~= Enum.MessageType.MessageWarning.Value then
                return false
            end
            local found = false
            for _, filterString in ipairs(warningsToFilter) do
                if string.find(message.Message, filterString) ~= nil then
                    found = true
                    break
                end
            end
            return found
        end
    
        local outputMessageSyncLocal;
        if permissions.MayViewClientLog then
            outputMessageSyncLocal = NewOutputMessageSync(function(this)
                local messages = {}
                
                local LogService = game:GetService("LogService")
                do -- This do block keeps history from sticking around in memory
                    local history = LogService:GetLogHistory()
                    for i = 1, #history do
                        local msg = history[i]
                        local message = {
                            Message = msg.message or "[DevConsole Error 1]";
                            Time = ConvertTimeStamp(msg.timestamp);
                            Type = msg.messageType.Value;
                        }
                        if not filterMessageOnAdd(message) then
                            messages[#messages + 1] = message
                        end
                    end
                end
                
                LogService.MessageOut:connect(function(text, messageType)
                    local message = {
                        Message = text or "[DevConsole Error 2]";
                        Time = ConvertTimeStamp(os_time());
                        Type = messageType.Value;
                    }
                    if not filterMessageOnAdd(message) then
                        messages[#messages + 1] = message
                        this.MessageAdded:fire(message)
                    end
                end)
            
                return messages
            end)
        end
        
        local outputMessageSyncServer;
        if permissions.MayViewServerLog then
            outputMessageSyncServer = NewOutputMessageSync(function(this)
                local messages = {}
                
                local LogService = game:GetService("LogService")
                
                LogService.ServerMessageOut:connect(function(text, messageType)
                    local message = {
                        Message = text or "[DevConsole Error 3]";
                        Time = ConvertTimeStamp(os_time());
                        Type = messageType.Value;
                    }
                    if not filterMessageOnAdd(message) then
                        messages[#messages + 1] = message
                        this.MessageAdded:fire(message)
                    end
                end)
                LogService:RequestServerOutput()
                
                return messages
            end)
        end
    
        local statsSyncServer;
        if permissions.MayViewServerStats or permissions.MayViewServerScripts then
            statsSyncServer = {
                Stats = nil; -- Private member, use GetStats instead
                StatsReceived = CreateSignal();
            }
            local statsListenerConnection;
            function statsSyncServer.GetStats(statsSyncServer)
                local stats = statsSyncServer.Stats
                if not stats then
                    stats = {}
                    pcall(function()
                        local clientReplicator = game:FindService("NetworkClient"):GetChildren()[1]
                            if clientReplicator then
                            statsListenerConnection = clientReplicator.StatsReceived:connect(function(stat)
                                statsSyncServer.StatsReceived:fire(stat)
                            end)
                            clientReplicator:RequestServerStats(true)
                        end
                    end)
                    statsSyncServer.Stats = stats
                end
                return stats
            end
            
        end
        --]]
        
        messagesAndStats = {
            OutputMessageSyncLocal = outputMessageSyncLocal;
            OutputMessageSyncServer = outputMessageSyncServer;
            StatsSyncServer = statsSyncServer;
        }
        
        return messagesAndStats
    end
end

return DeveloperConsole

Unused Code#

In content/scripts/Module/LoadingScreen3D.lua is code that goes unreferenced.

GuiService.ErrorMessageChanged:connect(function()
    -- TODO
end)

GuiService.UiMessageChanged:connect(function(type, newMessage)
    -- TODO
end)