[
  {
    "path": ".gitattributes",
    "content": "*.html linguist-detectable=false\n"
  },
  {
    "path": ".github/workflows/BuildRelease.yml",
    "content": "name: Build and Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  build:\n    name: Create release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - uses: ok-nick/setup-aftman@v0.4.2\n\n      - name: Build asset\n        run: rojo build --output Icon.rbxm withLink.project.json\n\n      - name: Git Release\n        uses: anton-yurchenko/git-release@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          args: |\n            ./Icon.rbxm"
  },
  {
    "path": ".gitignore",
    "content": "# Project place file\n/Icon.rbxm\n\n# macOS\n.DS_Store\n\n# Rojo\nsourcemap.json\n\n# Built documentation\n/site\n\n# Roblox Studio lock files\n/*.rbxlx.lock\n/*.rbxl.lock"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"johnnymorganz.luau-lsp\",\n        \"evaera.vscode-rojo\",\n        \"kampfkarren.selene-vscode\",\n        \"johnnymorganz.stylua\"\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"robloxLsp.diagnostics.disable\": [\n        \"undefined-global\"\n    ]\n}"
  },
  {
    "path": "LICENSE",
    "content": "TopbarPlus Credit\n==================================\nBy using TopbarPlus in your experience or application, you agree to either:\n\t1. Keep Attribute unchanged, or\n\t2. To credit TopbarPlus in your experience's description, or in a devforum\n\t   post linked from your experience's description.\n\n\nMozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "PackageLink.model.json",
    "content": "{\n \"ClassName\": \"PackageLink\"\n}"
  },
  {
    "path": "README.md",
    "content": "https://devforum.roblox.com/t/topbarplus/1017485"
  },
  {
    "path": "aftman.toml",
    "content": "# This file lists tools managed by Aftman, a cross-platform toolchain manager.\n# For more information, see https://github.com/LPGhatguy/aftman\n\n# To add a new tool, add an entry to this table.\n[tools]\nrojo = \"rojo-rbx/rojo@7.4.1\"\nselene = \"Kampfkarren/selene@0.26.1\"\nstylua = \"JohnnyMorganz/StyLua@0.20.0\"\nwally = \"UpliftGames/Wally@0.3.2\""
  },
  {
    "path": "default.project.json",
    "content": "{\n    \"name\": \"topbarplus\",\n    \"tree\": {\n      \"$path\": \"src\"\n    }\n  }"
  },
  {
    "path": "docs/api.md",
    "content": "[themes]: https://1foreverhd.github.io/TopbarPlus/features/#modify-theme\n[alignments]: https://1foreverhd.github.io/TopbarPlus/features/#alignments\n[font family]: https://create.roblox.com/docs/reference/engine/datatypes/Font/#fromEnum\n[toggle keys]: https://1foreverhd.github.io/TopbarPlus/features/#toggle-keys\n[captions]: https://1foreverhd.github.io/TopbarPlus/features/#captions\n[icon event]: https://1foreverhd.github.io/TopbarPlus/api/#events\n[menus]: https://1foreverhd.github.io/TopbarPlus/features/#menus\n[dropdowns]: https://1foreverhd.github.io/TopbarPlus/features/#dropdowns\n[numberSpinner]: https://devforum.roblox.com/t/numberspinner-module/1105961\n\n## Functions\n\n#### getIcons\n```lua\nlocal icons = Icon.getIcons()\n```\nReturns a dictionary of icons where the key is the icon's UID and value the icon.\n\n----\n#### getIcon\n```lua\nlocal icon = Icon.getIcon(nameOrUID)\n```\nReturns an icon of the given name or UID.\n\n----\n#### setTopbarEnabled\n```lua\nIcon.setTopbarEnabled(bool)\n```\nWhen set to ``false`` all TopbarPlus ScreenGuis are hidden. This does not impact Roblox's Topbar.\n\n----\n#### modifyBaseTheme\n```lua\nIcon.modifyBaseTheme(modifications)\n```\nUpdates the appearance of *all* icons. See [themes] for more details.\n\n----\n#### setDisplayOrder\n```lua\nIcon.setDisplayOrder(integer)\n```\nSets the base DisplayOrder of all TopbarPlus ScreenGuis.\n\n----\n\n\n\n## Constructors\n\n#### new\n```lua\nlocal icon = Icon.new()\n```\nConstructs an empty ``32x32`` icon on the topbar.\n\n----\n\n\n\n## Methods\n\n#### setName\n{chainable}\n```lua\nicon:setName(name)\n```\nSets the name of the Widget instance. This can be used in conjunction with ``Icon.getIcon(name)``.\n\n----\n#### getInstance\n```lua\nlocal instance = icon:getInstance(instanceName)\n```\nReturns the first descendant found within the widget of name ``instanceName``.\n\n----\n#### modifyTheme\n{chainable}\n```lua\nicon:modifyTheme(modifications)\n```\nUpdates the appearance of the icon. See [themes] for more details.\n\n----\n#### modifyChildTheme\n{chainable}\n```lua\nicon:modifyChildTheme(modifications)\n```\nUpdates the appearance of all icons that are parented to this icon (for example when a menu or dropdown). See [themes] for more details.\n\n----\n#### setEnabled\n{chainable}\n```lua\nicon:setEnabled(bool)\n```\nWhen set to ``false`` the icon will be disabled and hidden.\n\n----\n#### select\n{chainable}\n```lua\nicon:select()\n```\nSelects the icon (as if it were clicked once).\n\n----\n#### deselect\n{chainable}\n```lua\nicon:deselect()\n```\nDeselects the icon (as if it were clicked, then clicked again).\n\n----\n#### notify\n{chainable}\n```lua\nicon:notify(clearNoticeEvent)\n```\nPrompts a notice bubble which accumulates the further it is prompted. If the icon belongs to a dropdown or menu, then the notice will appear on the parent icon when the parent icon is deselected.\n\n----\n#### clearNotices\n{chainable}\n```lua\nicon:clearNotices()\n```\n\n----\n#### disableOverlay\n{chainable}\n```lua\nicon:disableStateOverlay(bool)\n```\nWhen set to ``true``, disables the shade effect which appears when the icon is pressed and released.\n\n----\n#### setImage\n{chainable} {toggleable}\n```lua\nicon:setImage(imageId, iconState)\n```\nApplies an image to the icon based on the given ``imageId``. ``imageId`` can be an assetId or a complete asset string.\n\n----\n#### setLabel\n{chainable} {toggleable}\n```lua\nicon:setLabel(text, iconState)\n```\n\n----\n#### setOrder\n{chainable} {toggleable}\n```lua\nicon:setOrder(order, iconState)\n```\n\n----\n#### setCornerRadius\n{chainable} {toggleable}\n```lua\nicon:setCornerRadius(scale, offset, iconState)\n```\n\n----\n#### align\n{chainable}\n```lua\nicon:align(alignment)\n```\nThis enables you to set the icon to the ``\"Left\"`` (default), ``\"Center\"`` or ``\"Right\"`` side of the screen. See [alignments] for more details.\n\n----\n#### setWidth\n{chainable} {toggleable}\n```lua\nicon:setWidth(minimumSize, iconState)\n```\nThis sets the minimum width the icon can be (it can be larger for instance when setting a long label). The default width is ``44``.\n\n----\n#### setImageScale\n{chainable} {toggleable}\n```lua\nicon:setImageScale(number, iconState)\n```\nHow large the image is relative to the icon. The default value is ``0.5``.\n\n----\n#### setImageRatio\n{chainable} {toggleable}\n```lua\nicon:setImageRatio(number, iconState)\n```\nHow stretched the image will appear. The default value is ``1`` (a perfect square).\n\n----\n#### setTextSize\n{chainable} {toggleable}\n```lua\nicon:setTextSize(number, iconState)\n```\nThe size of the icon labels' text. The default value is ``16``.\n\n----\n#### setTextColor\n{chainable} {toggleable}\n```lua\nicon:setTextColor(color, iconState)\n```\nThe color of the icon labels' text.\n\n----\n#### setTextFont\n{chainable} {toggleable}\n```lua\nicon:setTextFont(font, fontWeight, fontStyle, iconState)\n```\nSets the labels FontFace. ``font`` can be a [font family] name (such as `\"Creepster\"`), a font enum (such as `Enum.Font.Bangers`), a font ID (such as `12187370928`) or [font family] link (such as `\"rbxasset://fonts/families/Sarpanch.json\"`).\n\n----\n#### bindToggleItem\n{chainable}\n```lua\nicon:bindToggleItem(guiObjectOrLayerCollector)\n```\nBinds a GuiObject or LayerCollector to appear and disappeared when the icon is toggled.\n\n----\n#### unbindToggleItem\n{chainable}\n```lua\nicon:unbindToggleItem(guiObjectOrLayerCollector)\n```\nUnbinds the given GuiObject or LayerCollector from the toggle.\n\n----\n#### bindEvent\n{chainable}\n```lua\nicon:bindEvent(iconEventName, callback)\n```\nConnects to an [icon event] with ``iconEventName``. It's important to remember all event names are in camelCase. ``callback`` is called with arguments ``(self, ...)`` when the event is triggered.\n\n----\n#### unbindEvent\n{chainable}\n```lua\nicon:unbindEvent(iconEventName)\n```\nUnbinds the connection of the associated ``iconEventName``.\n\n----\n#### bindToggleKey\n{chainable}\n```lua\nicon:bindToggleKey(keyCodeEnum)\n```\nBinds a [keycode](https://developer.roblox.com/en-us/api-reference/enum/KeyCode) which toggles the icon when pressed. See [toggle keys] for more details.\n\n----\n#### unbindToggleKey\n{chainable}\n```lua\nicon:unbindToggleKey(keyCodeEnum)\n```\nUnbinds the given keycode.\n\n----\n#### call\n{chainable}\n```lua\nicon:call(func)\n```\nCalls the function immediately via ``task.spawn``. The first argument passed is the icon itself. This is useful when needing to extend the behaviour of an icon while remaining in the chain.\n\n----\n#### addToJanitor\n{chainable}\n```lua\nicon:addToJanitor(userdata)\n```\nPasses the given userdata to the icons janitor to be destroyed/disconnected on the icons destruction. If a function is passed, it will be called when the icon is destroyed.\n\n----\n#### lock\n{chainable}\n```lua\nicon:lock()\n```\nPrevents the icon being toggled by user-input (such as clicking) however the icon can still be toggled via localscript using methods such as ``icon:select()``.\n\n----\n#### unlock\n{chainable}\n```lua\nicon:unlock()\n```\nRe-enables user-input to toggle the icon again.\n\n----\n#### debounce\n{chainable} {yields}\n```lua\nicon:debounce(seconds)\n```\nLocks the icon, yields for the given time, then unlocks the icon, effectively shorthand for ``icon:lock() task.wait(seconds) icon:unlock()``. This is useful for applying cooldowns (to prevent an icon from being pressed again) after an icon has been selected or deselected. \n\n----\n#### autoDeselect\n{chainable}\n```lua\nicon:autoDeselect(true)\n```\nWhen set to ``true`` (the default) the icon is deselected when another icon (with autoDeselect enabled) is pressed. Set to ``false`` to prevent the icon being deselected when another icon is selected (a useful behaviour in dropdowns).\n\n----\n#### oneClick\n{chainable}\n```lua\nicon:oneClick(bool)\n```\nWhen set to true the icon will automatically deselect when selected. This creates the effect of a single click button.\n\n----\n#### setCaption\n{chainable}\n```lua\nicon:setCaption(text)\n```\nSets a caption. To remove, pass ``nil`` as ``text``. See [captions] for more details.\n\n----\n#### setCaptionHint\n{chainable}\n```lua\nicon:setCaptionHint(keyCodeEnum)\n```\nThis customizes the appearance of the caption's hint without having to use ``icon:bindToggleKey``. \n\n----\n#### setDropdown\n{chainable}\n```lua\nicon:setDropdown(arrayOfIcons)\n```\nCreates a vertical dropdown based upon the given ``table array`` of ``icons``. Pass an empty table ``{}`` to remove the dropdown. See [dropdowns] for more details.\n\n----\n#### joinDropdown\n{chainable}\n```lua\nicon:joinDropdown(parentIcon)\n```\nJoins the dropdown of `parentIcon`. This is what ``icon:setDropdown`` calls internally on the icons within its array.\n\n----\n#### setMenu\n{chainable}\n```lua\nicon:setMenu(arrayOfIcons)\n```\nCreates a horizontal menu based upon the given array of icons. Pass an empty table ``{}`` to remove the menu. See [menus] for more details.\n\n----\n#### setFixedMenu\n{chainable}\n```lua\nicon:setFixedMenu(arrayOfIcons)\n```\nCreates a menu that is always selected and has its close button hidden. Pass an empty table ``{}`` to remove the menu. See [menus] for more details.\n\n----\n#### joinMenu\n{chainable}\n```lua\nicon:joinMenu(parentIcon)\n```\nJoins the menu of `parentIcon`. This is what ``icon:setMenu`` calls internally on the icons within its array.\n\n----\n#### leave\n{chainable}\n```lua\nicon:leave()\n```\nUnparents an icon from a parentIcon if it belongs to a dropdown or menu.\n\n----\n#### convertLabelToNumberSpinner\n{chainable}\n```lua\nicon:convertLabelToNumberSpinner(numberSpinner, readyCallback)\n```\nAccepts a [numberSpinner] and converts the icon's label into that spinner. For example:\n```lua\nIcon.new()\n\t:align(\"Right\")\n\t:setLabel(\"Points\")\n\t:setWidth(80)\n\t:call(function(pointsIcon)\n\t\tlocal NumberSpinner = require(ReplicatedStorage.NumberSpinner)\n\t\tlocal numberSpinner = NumberSpinner.new()\n\t\tpointsIcon:convertLabelToNumberSpinner(numberSpinner, function()\n\t\t\tnumberSpinner.Name = \"LabelSpinner\"\n\t\t\tnumberSpinner.Prefix = \"$\"\n\t\t\tnumberSpinner.Commas = true\n\t\t\tnumberSpinner.Decimals = 0\n\t\t\tnumberSpinner.Duration = 0.25\n\t\t\twhile true do\n\t\t\t\tnumberSpinner.Value = math.random(1,1000)\n\t\t\t\ttask.wait(1)\n\t\t\tend\n\t\tend)\n\tend)\n```\n\n!!! warning\n\tAny changes to the NumberSpinner must be made within ``readyCallback`` otherwise you risk breaking the icon's appearance\n\n\n----\n#### destroy\n{chainable}\n```lua\nicon:destroy()\n```\nClears all connections and destroys all instances associated with the icon.\n\n----\n\n\n\n## Events\n#### selected \n```lua\nicon.selected:Connect(function(fromSource)\n    -- fromSource can be useful for checking if the behaviour was triggered by a user (such as clicking)\n    -- fromSource values include \"User\", \"OneClick\", \"AutoDeselect\", \"HideParentFeature\", \"Overflow\"\n    local sourceName = fromSource or \"Unknown\"\n    print(\"The icon was selected by the \"..sourceName)\nend)\n```\n\n----\n#### deselected \n```lua\nicon.deselected:Connect(function(fromSource)\n    local sourceName = fromSource or \"Unknown\"\n    print(\"The icon was deselected by the \"..sourceName)\nend)\n```\n\n----\n#### toggled \n```lua\nicon.toggled:Connect(function(isSelected, fromSource)\n    local stateName = (isSelected and \"selected\") or \"deselected\"\n    print(`The icon was {stateName}!`)\nend)\n```\n\n----\n#### viewingStarted \n```lua\nicon.viewingStarted:Connect(function()\n    print(\"A mouse, long-pressed finger or gamepad selection is hovering over the icon\")\nend)\n```\n\n----\n#### viewingEnded \n```lua\nicon.viewingEnded:Connect(function()\n    print(\"The input is no longer viewing (hovering over) the icon\")\nend)\n```\n\n----\n#### notified \n```lua\nicon.notified:Connect(function()\n    print(\"New notice\")\nend)\n```\n\n----\n\n\n\n## Properties\n#### name\n{read-only}\n```lua\nlocal string = icon.name --[default: \"Widget\"]\n```\n\n----\n#### isSelected\n{read-only}\n```lua\nlocal bool = icon.isSelected\n```\n\n----\n#### isEnabled\n{read-only}\n```lua\nlocal bool = icon.isEnabled\n```\n\n----\n#### totalNotices\n{read-only}\n```lua\nlocal int = icon.totalNotices\n```\n\n----\n#### locked\n{read-only}\n```lua\nlocal bool = icon.locked\n```\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "[discussion thread]: https://devforum.roblox.com/t/topbarplus-v2-construct-dynamic-and-intuitive-topbar-icons/1017485\n[Python]: https://www.python.org/\n[Material for MKDocs]: https://squidfunk.github.io/mkdocs-material/\n[ForeverHD on the devforum]: https://devforum.roblox.com/u/ForeverHD/summary\n[TopbarPlus repository]: https://github.com/1ForeverHD/TopbarPlus\n[open an issue]: https://github.com/1ForeverHD/TopbarPlus/issues\n\n## Bug Reports\n- To submit a bug report, [open an issue] or create a response at the [discussion thread].\n- Ensure your report includes a detailed explanation of the problem with any relavent images, videos, etc (such as console errors).\n- Aim to include a link to a stipped-down uncopylocked Roblox place which reproduces the bug.\n\n## Questions and Feedback\n- Be sure to first check out the documentation before asking a question.\n- We recommend asking all questions and posting feedback to the [discussion thread].\n\n## Submitting a resource (video tutorial, port, etc)\n- Fancy making a tutorial or resource for TopbarPlus? Feel free to get in touch and we can provide tips, best practices, etc.\n- We'll feature approved resources on the [discussion thread].\n- To submit a resource, [open an issue], or reach out on the [discussion thread] or to [ForeverHD on the devforum].\n\n## Suggestions and Code\n- TopbarPlus is completely free and open source; any suggestions and code contributions are greatly appreciated!\n- To make a suggestion, [open an issue] or create a response at the [discussion thread].\n- For large contributions (like a new feature) please open an issue before beginning a code contribution to ensure it's discussed through fully (we wouldn't want to waste your time!).\n- For smaller contributions (a few lines of code, fixing typos, etc) feel free to send a pull request right away.\n- Make sure to merge your pull requests into the #development branch.\n- Some tools you'll find useful when working on this project:\n    - [Rojo](https://rojo.space/docs/)\n    - [Material for MKDocs]\n    - [Roblox LSP](https://devforum.roblox.com/t/roblox-lsp-full-intellisense-for-roblox-and-luau/717745)\n\n## Documentation\n- If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.\n- To test documentation:\n    1. Install [Python] (which comes with pip).\n    2. Install [Material for MKDocs].\n    3. Visit the [TopbarPlus repository].\n    4. Click *Fork* in the top right corner.\n    5. Clone this fork into your local repository.\n    6. Change directory to this clone ``cd TopbarPlus``.\n    7. Swap to the development branch ``git checkout development``.\n    8. Call ``mkdocs serve`` within your terminal.\n    9. Open your local website (it will look something like ``http://0.0.0.0:8000``)\n    10. Any changes to ``mkdocs.yml`` or the files within ``docs`` will now update live to this local site.\n   \n!!! important\n    All pull requests must be made to the ***development*** branch.\n"
  },
  {
    "path": "docs/features.md",
    "content": "[icon states]: https://1foreverhd.github.io/TopbarPlus/#states\n[v3 Playground]: https://www.roblox.com/games/117501901079852/TopbarPlus\n\n\n### Images\n```lua\nIcon.new:setImage(shopImageId)\n```\n\n<a><img src=\"https://i.imgur.com/IEJfUye.png\" width=\"50%\"/></a>\n\n------------------------------\n\n### Labels\n```lua\nicon:setLabel(\"Shop\")\n```\n\n<a><img src=\"https://i.imgur.com/d0nVAc6.png\" width=\"50%\"/></a>\n\n```lua\nicon:setImage(shopImageId)\nicon:setLabel(\"Shop\")\n```\n\n<a><img src=\"https://i.imgur.com/vJHvJWI.png\" width=\"50%\"/></a>\n\n------------------------------\n\n### Alignments\n```lua\n-- Aligns the icon to the left bounds of the screen\n-- This is the default behaviour so you do not need to do anything\n-- This was formerly called :setLeft()\nicon:align(\"Left\")\n```\n\n```lua\n-- Aligns the icon in the middle of the screen\n-- This was formerly called :setMid()\nicon:align(\"Center\")\n```\n\n```lua\n-- Aligns the icon to the right bounds of the screen\n-- This was formerly called :setRight()\nicon:align(\"Right\")\n```\n\n------------------------------\n\n### Notices\n```lua\nicon:notify()\n```\n\n<a><img src=\"https://i.imgur.com/xFBbVoA.png\" width=\"50%\"/></a>\n\n------------------------------\n\n### Captions\n```lua\nicon:setCaption(\"Open Shop\")\n```\n\n<a><img src=\"https://i.imgur.com/QpecT2Y.gif\" width=\"50%\"/></a>\n\n------------------------------\n\n### Dropdowns\nDropdowns are vertical navigation frames that contain an array of icons:\n\n```lua\nIcon.new()\n\t:setLabel(\"Example\")\n\t:modifyTheme({\"Dropdown\", \"MaxIcons\", 3})\n\t:modifyChildTheme({\"Widget\", \"MinimumWidth\", 158})\n\t:setDropdown({\n\t\tIcon.new()\n\t\t\t:setLabel(\"Category 1\")\n\t\t,\n\t\tIcon.new()\n\t\t\t:setLabel(\"Category 2\")\n\t\t,\n\t\tIcon.new()\n\t\t\t:setLabel(\"Category 3\")\n\t\t,\n\t\tIcon.new()\n\t\t\t:setLabel(\"Category 4\")\n\t\t,\n\t})\n```\n\n<a><img src=\"https://i.imgur.com/ZMt6bhr.gif\" width=\"50%\"/></a>\n\n!!! warning\n\tIcons containing a dropdown can join other menus but not dropdowns.\n\n------------------------------\n\n### Menus\nMenus are horizontal navigation frames that contain an array of icons:\n\n```lua\nIcon.new()\n\t:setLabel(\"Example\")\n\t:modifyTheme({\"Menu\", \"MaxIcons\", 2})\n\t:setMenu({\n\t\tIcon.new()\n\t\t\t:setLabel(\"Item 1\")\n\t\t,\n\t\tIcon.new()\n\t\t\t:setLabel(\"Item 2\")\n\t\t,\n\t\tIcon.new()\n\t\t\t:setLabel(\"Item 3\")\n\t\t,\n\t\tIcon.new()\n\t\t\t:setLabel(\"Item 4\")\n\t\t,\n\t})\n```\n\n<a><img src=\"https://i.imgur.com/tXLrD8t.gif\" width=\"50%\"/></a>\n\n------------------------------\n\n### Fixed Menus\nFixed Menus are the same as normal menus, except forcefully opened (selected), with their close button hidden:\n\n```lua\nIcon.new()\n\t:modifyTheme({\"Menu\", \"MaxIcons\", 3})\n\t:setFixedMenu({\n\t\tIcon.new()\n\t\t:setLabel(\"Item 1\")\n\t\t,\n\t\tIcon.new()\n\t\t:setLabel(\"Item 2\")\n\t\t,\n\t\tIcon.new()\n\t\t:setLabel(\"Item 3\")\n\t\t,\n\t\tIcon.new()\n\t\t:setLabel(\"Item 4\")\n\t\t,\n\t\tIcon.new()\n\t\t:setLabel(\"Item 5\")\n\t\t,\n\t})\n```\n\n<a><img src=\"https://i.imgur.com/LgJCj4X.gif\" width=\"50%\"/></a>\n\n------------------------------\n\n### Modify Theme\nYou can modify the appearance of an icon doing:\n```lua\nicon:modifyTheme(modifications)\n```\n\nYou can modify the appearance of *all* icons doing:\n```lua\nIcon.modifyBaseTheme(modifications)\n```\n\n``modifications`` can be either a single array describing a change, or a *colllection* of these arrays. For example, both the following are valid:\n```lua\n-- Single array\nicon:modifyTheme({\"IconLabel\", \"TextSize\", 16})\n\n-- Collection of arrays\nicon:modifyTheme({\n\t{\"Widget\", \"MinimumWidth\", 290},\n\t{\"IconCorners\", \"CornerRadius\", UDim.new(0, 0)}\n})\n```\n\nA modification array has 4 components:\n```lua\n{name, property, value, iconState}\n```\n\n> **1. `name`** {required}\n\nThis can be:\n\n- \"Widget\" (which is the icon container frame)\n- The name of an instance within the widget such as ``IconGradient``, ``IconSpot``, ``Menu``, etc\n- A 'collective' - the value of an attribute called 'Collective' applied to some instances. This enables them to be acted upon all at once. For example, 'IconCorners'.\n\n\n> **2. `property`** {required}\n\nThis can be either:\n\n- A property from the instance (Name, BackgroundColor3, Text, etc)\n- Or if the property doesn't exist, an attribute of that property name will be set\n\n> **3. `value`** {required}\n\nThe value you want the property to become (``\"Hello\"``, ``Color3.fromRGB(255, 100, 50)``, etc)\n\n> **4. `iconState`** {optional}\n\nThis determines *when* the modification is applied. See [icon states] for more details.\n\nYou can find example arrays under the 'Default' module:\n\n<a><img src=\"https://i.imgur.com/idH1SRi.png\" width=\"100%\"/></a>\n\n------------------------------\n\n### One Click Icons\nYou can convert icons into single click icons (icons which instantly\ndeselect when selected) by doing:\n```lua\nicon:oneClick()\n```\n\nFor example:\n```lua\nIcon.new()\n\t:setImage(shopImageId)\n\t:setLabel(\"Shop\")\n\t:bindEvent(\"deselected\", function()\n\t\tshop.Visible = not shop.Visible\n\tend)\n\t:oneClick()\n```\n\n<a><img src=\"https://i.imgur.com/Ma2mpjB.gif\" width=\"50%\"/></a>\n\n------------------------------\n\n### Toggle Items\nBinds a GuiObject (such as a frame) to appear or disappear when the icon is toggled\n```lua\nicon:bindToggleItem(shopFrame)\n```\n\nIt is equivalent to doing:\n```lua\nicon.deselected:Connect(function()\n    shopFrame.Visible = false\nend)\nicon.selected:Connect(function()\n    shopFrame.Visible = true\nend)\n```\n\n------------------------------\n\n### Toggle Keys\nBinds a [keycode](https://developer.roblox.com/en-us/api-reference/enum/KeyCode) which toggles the icon when pressed. Also creates a caption hint of that keycode binding.\n```lua\nIcon.new()\n\t:setLabel(\"Shop\")\n\t:bindToggleKey(Enum.KeyCode.V)\n\t:setCaption(\"Open Shop\")\n```\n\n<a><img src=\"https://i.imgur.com/GsdNfXr.gif\" width=\"50%\"/></a>\n\n------------------------------\n\n### Gamepad & Console Support\n\nTopbarPlus comes with inbuilt support for gamepads (such as Xbox and PlayStation\ncontrollers) and console screens:\n\n<a><img src=\"https://i.imgur.com/N0n2Zau.gif\" width=\"100%\"/></a>\n\nTo highlight the last-selected icon (or left-most if none have been selected yet) users simply press DPadUp or navigate to the topbar via the virtual cursor.\nTo change the default trigger keycode (from DPadUp) do:\n```lua\nIcon.highlightKey = Enum.KeyCode.NEW_KEYCODE\n```\n\n------------------------------\n\n### Overflows\nWhen accounting for device types and screen sizes, icons may occasionally overlap. This is especially common for phones when they enter portrait mode. TopbarPlus solves this with overflows:\n\n<a><img src=\"https://i.imgur.com/9jrHBaJ.gif\" width=\"100%\"/></a>\n\nOverflows will appear when left-set or right-set icons exceed the boundary of the closest opposite-aligned icon or viewport.\n\nIf a center-aligned icon exceeds the bounds of another icon, its alignment will be set to the alignment of the icon it exceeded:\n\n<a><img src=\"https://i.imgur.com/fAds4Ph.gif\" width=\"100%\"/></a>\n\n------------------------------\n\nThese examples and more can be tested, viewed and edited at the [v3 Playground].\n"
  },
  {
    "path": "docs/index.md",
    "content": "[icon:setOrder]: https://1foreverhd.github.io/TopbarPlus/api/#setorder\n[Feature Guide]: https://1foreverhd.github.io/TopbarPlus/features\n[Icon API]: https://1foreverhd.github.io/TopbarPlus/api\n[TopbarPlus DevForum Thread]: https://devforum.roblox.com/t/topbarplus/1017485\n\n### About\nTopbarPlus is a module enabling the construction of dynamic topbar icons. These icons can be enhanced with features and methods like themes, dropdowns and menus to expand upon their appearance and behaviour.\n\nTopbarPlus fully supports PC, Mobile, Tablet and Gamepads (Consoles), and comes with internal features such as 'overflows' to ensure icons remain within suitable bounds.\n\n----------\n\n### Construction\nCreating an icon is as simple as:\n\n``` lua\n-- Within a LocalScript in StarterPlayerScripts and assuming the Icon package is placed in ReplicatedStorage\nlocal Icon = require(game:GetService(\"ReplicatedStorage\").Icon)\nlocal icon = Icon.new()\n```\n\nThis constructs an empty ``32x32`` icon on the topbar.\n\n!!! info\n    The order icons appear are determined by their construction sequence. Icons constructed first will have a smaller order therefore will appear left of icons with a higher order. You can modify this behaviour using [icon:setOrder]. Icon orders by default are ``1+(totalCreatedIcons*0.01)``, so 1.01, 1.02, 1.03, etc.\n\nTo add an image and label, do:\n```lua\nicon:setImage(imageId)\nicon:setLabel(\"Label\")\n```\n\n----------\n\n### Chaining\nThese methods are 'chainable' therefore can alternatively be called doing:\n```lua\nIcon.new()\n    :setImage(imageId)\n    :setLabel(\"Label\")\n```\n\nYou may want to act upon nested icons. You can achieve this using ``:call``\nwhich returns the icon as the first argument within the function you pass:\n```lua\nIcon.new()\n    :setName(\"TestIcon\")\n    :call(function(icon)\n        print(icon.name)\n        -- This will print 'TestIcon'!\n    end)\n```\n\n!!! info\n    Chainable methods have a ``chainable`` tag next to their name within the API Icon docs.\n\n----------\n\n### States\nSometimes you'll want an item to appear only when *deselected* and similarily only when *selected*. You can achieve this by specifying a string value within the ``iconState`` parameter of methods containing the ``toggleable`` tag. These are:\n\n```lua\n\"Deselected\" -- Applies the value when the icon is deselected (i.e. not pressed)\n\"Selected\" -- Applies the value when the icon is selected (i.e. pressed)\n\"Viewing\" -- Formerly known as Hovering, applies the value when a cursor is hovering above, a controller highlighting, or touchpad (mobile) long-pressing (but before releasing) an icon\n```\n\n!!! info\n    If no ``iconState`` is specified (i.e. is nil) the value will be applied to all states.\n\n```lua\n-- It doesn't matter if you do \"deselected\", \"Deselected\" or \"dEsElEcTeD\"; iconStates are not case sensitive\nIcon.new()\n\t:setImage(4882429582)\n\t:setLabel(\"Closed\", \"Deselected\")\n\t:setLabel(\"Open\", \"Selected\")\n\t:setLabel(\"Viewing\", \"Viewing\")\n```\n\n<a><img src=\"https://i.imgur.com/0QrDmi6.gif\" width=\"50%\"/></a>\n\n----------\n\n### Additional\nBy default icons will deselect when another icon is selected. You can disable this behaviour doing:\n```lua\nicon:autoDeselect(false)\n```\n\nYou can enhance icons further with features like modifyTheme, dropdowns and menus, or by binding GuiObjects and KeyCodes to their toggle. This and much more can be achieved by exploring the [Feature Guide] and [Icon API].\n\nHave a question or issue? Feel free to reach out at the [TopbarPlus DevForum Thread]."
  },
  {
    "path": "docs/installation.md",
    "content": "#### Take the model\n{recommended}\n\n1. Take the [TopbarPlus model](https://create.roblox.com/store/asset/92368439343389/TopbarPlus).\n2. Open the toolbox and navigate to Inventory -> My Models.\n3. Click TopbarPlus to insert into your game and place anywhere within ``ReplicatedStorage`` or ``Workspace``. \n4. TopbarPlus is a package so you can update it instantly (instead of re-adding) by right-clicking the Icon module and selecting an option such as 'Get Latest Package':\n\n    <a><img src=\"https://i.imgur.com/kIZdT83.png\" width=\"50%\"/></a>\n\n5. You can receive automatic updates by enabling 'AutoUpdate' within the PackageLink:\n\n    <a><img src=\"https://i.imgur.com/2hGbjTS.png\" width=\"50%\"/></a>\n\n!!! info\n    All v3 updates will be backwards compatible so you don't need to worry about updates interfering with your code.\n\n!!! warning\n    Try not to modify any code within the Icon package otherwise it will break the package link.\n\n!!! important\n    As of 7th June 2025 public packages haven't been rolled out by Roblox. Only after their full release will you be able to benefit from easily installable updates. For the time being, attempting to use 'Get Latest Package' and other package features will throw an error.\n\n-------------------------------------\n\n#### Download from Releases\n1. Visit the [latest release](https://github.com/1ForeverHD/TopbarPlus/releases/latest).\n2. Under *Assets*, download ``TopbarPlus.rbxm``.\n3. Within studio, navigate to MODEL -> Model and insert the file anywhere within ``ReplicatedStorage``. \n\n-------------------------------------\n\n#### With Rojo\n1. Setup with [Rojo](https://rojo.space/).\n2. Visit the [TopbarPlus repository](https://github.com/1ForeverHD/TopbarPlus).\n3. Click *Fork* in the top right corner.\n4. Clone this fork into your local repository.\n5. Modify the ``serve.project.json`` file to your desired location (by default TopbarPlus is built directly into ``Workspace``).\n6. Call ``rojo serve`` (terminal or VSC plugin) and connect to the rojo studio plugin.\n\n-------------------------------------\n\n#### With Wally\nTopbarPlus is now on Wally! You can find it [here](https://wally.run/package/1foreverhd/topbarplus)."
  },
  {
    "path": "docs/javascripts/tags.js",
    "content": "const style = `.tag {\n    color: #ffffff;\n    line-height: .8rem;\n    padding: 5px;\n    margin-left: 7px !important;\n    margin: 0 !important; \n    background-clip: padding-box;\n    border-radius: 3px;\n    display: inline-block;\n    font-size: .7rem;\n    font-family: \"Roboto\";\n    font-weight: normal;\n}\n.static {\n    background-color: rgb(38, 70, 83);\n}\n.read-only {\n    background-color: rgb(42, 157, 143);\n}\n.client-only {\n    background-color: rgb(89, 140, 206);\n}\n.server-only {\n    background-color: rgb(89, 140, 206);\n}\n.toggleable {\n    background-color: rgb(178, 92, 162);\n}\n.chainable {\n    background-color: rgb(122, 103, 231);\n}\n.recommended {\n    background-color: rgb(126, 194, 136);\n}\n.required {\n    background-color: rgb(231, 101, 104);\n}\n.optional {\n    background-color: rgb(188, 176, 116);\n}\n.unstable {\n    background-color: rgb(204, 134, 80);\n}\n.deprecated {\n    background-color: rgb(227, 87, 75);\n}\n.yields {\n    background-color: rgb(163, 149, 79);\n}\n.critical {\n    background-color: rgb(255, 0, 0);\n}\nh4 {\n    display: inline;\n}`\n\nvar replaceStuff = [\n    [\"{read-only}\", '<p class=\"tag read-only\">read-only</p>'],\n    [\"{static}\", '<p class=\"tag static\">static</p>'],\n    [\"{server-only}\", '<p class=\"tag server-only\">server-only</p>'],\n    [\"{client-only}\", '<p class=\"tag client-only\">client-only</p>'],\n    [\"{deprecated}\", '<p class=\"tag deprecated\">deprecated</p>'],\n    [\"{yields}\", '<p class=\"tag yields\">yields</p>'],\n    [\"{critical}\", '<p class=\"tag critical\">critical</p>'],\n    [\"{chainable}\", '<p class=\"tag chainable\">chainable</p>'],\n    [\"{required}\", '<p class=\"tag required\">required</p>'],\n    [\"{optional}\", '<p class=\"tag optional\">optional</p>'],\n    [\"{recommended}\", '<p class=\"tag recommended\">recommended</p>'],\n    [\"{unstable}\", '<p class=\"tag unstable\">unstable</p>'],\n    [\"{toggleable}\", '<p class=\"tag toggleable\">toggleable</p>'],\n];\n\nfunction replace(element) {\n    for (var i = 0; i < replaceStuff.length; i++) {\n        var from = replaceStuff[i][0]\n        var to = replaceStuff[i][1]\n        if ((element.innerHTML && element.innerHTML.includes(from))) {\n            element.innerHTML = element.innerHTML.replace(from, to)\n            element.style.display = \"inline\"\n        }\n    }\n}\n\nconst styleElement = document.createElement(\"style\")\nstyleElement.innerHTML = style\n\ndocument.head.appendChild(styleElement)\n\nwindow.onload = function WindowLoad(event) {\n    var elems = document.body.getElementsByTagName(\"p\")\n    for (var i = 0; i < elems.length; i++) {\n        replace(elems.item(i))\n    }\n}\n"
  },
  {
    "path": "docs/third_parties.md",
    "content": "TopbarPlus supports the use of multiple Icon packages within a single experience assuming all required packages are ``v3.0.0`` or above.\n\nWhen a package is required it will 'check' to see if a TopbarPlus package has already been required within the experience. If one has, it cancels loading itself and will instead refer to the already initialized package.\n\nThis prevents weird quirks from occuring and means third party applications, libraries etc that use TopbarPlus can be used safely without interferring with your own implementation of TopbarPlus.\n\nYou don't have to do anything to support multiple packages. Simply use TopbarPlus as normal."
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: TopbarPlus v3\nsite_description: Documentation for TopbarPlus v3\nsite_author: Ben Horton\nsite_url: https://1ForeverHD.github.io/TopbarPlus/\n\nrepo_name: 1ForeverHD/TopbarPlus\nrepo_url: https://github.com/1ForeverHD/TopbarPlus\nedit_uri: \"\"\n\ntheme:\n  logo: https://user-images.githubusercontent.com/51117782/104590568-71724f80-5663-11eb-9bc1-344fc2a4193c.png\n  favicon: https://user-images.githubusercontent.com/51117782/113474423-cefa8900-9467-11eb-8678-d69cbb0b3966.png\n  name: material\n  features:\n    - navigation.tabs\n    #- navigation.instant\n    #- navigation.sections\n  palette:\n    # Light mode\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      primary: blue\n      accent: blue\n      toggle:\n        icon: material/weather-sunny\n        name: Switch to dark mode\n    # Dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      primary: blue\n      accent: blue\n      toggle:\n        icon: material/weather-night\n        name: Switch to light mode\n  highlightjs: true\n  hljs_languages:\n    - lua\n\nextra_javascript:\n  - javascripts/tags.js\n\nextra:\n  social:\n    - icon: fontawesome/brands/github-alt\n      link: https://github.com/1ForeverHD/\n    - icon: fontawesome/brands/twitter\n      link: https://twitter.com/ForeverHD_\n    - icon: fontawesome/brands/youtube\n      link: https://www.youtube.com/channel/UCj9QhyYCvhAwiBHA5B88pYg\n\nmarkdown_extensions:\n  - admonition\n  - codehilite:\n      guess_lang: false\n  - toc:\n      permalink: true\n  - pymdownx.superfences\n\nnav:\n  - Home:\n    - Introduction: index.md\n    - Features: features.md\n    - Installation: installation.md\n    - Third Parties: third_parties.md\n    - API: api.md\n  - Contributing: contributing.md\n"
  },
  {
    "path": "rotriever.toml",
    "content": "[package]\nname = \"TopbarPlus\"\nversion = \"3.0.0\"\nlicense = \"MPL2\"\nauthors = [\"1ForeverHD\"]\ncontent_root = \"src\"\n\n[dependencies]"
  },
  {
    "path": "selene.toml",
    "content": "std = \"roblox\""
  },
  {
    "path": "serve.project.json",
    "content": "{\n    \"name\": \"topbarplus\",\n    \"tree\": {\n        \"$className\": \"DataModel\",\n\n        \"Workspace\": {\n            \"$className\": \"Workspace\",\n\n            \"TopbarPlus\": {\n                \"$className\": \"Folder\",\n\n                \"Icon\": {\n                    \"$path\": \"src\",\n\t\t\t\t\t\"PackageLink\": {\n                        \"$path\": \"PackageLink.model.json\"\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Attribute.lua",
    "content": "--[[\n\n\tTopbarPlus was developed by ForeverHD and is possible thanks to HD Admin.\n\n\tBy using TopbarPlus in your experience or application, you agree to either:\n\t\t1. Keep Attribute unchanged, or\n\t\t2. If an experience, to credit TopbarPlus in your description, or in a\n\t\t   devforum post linked from your experience's description.\n\n\tv3 has involved over 350 hours of work to develop, so please consider supporting\n\tits development by reporting any issues or feedback you have at its repository:\n\thttps://github.com/1ForeverHD/TopbarPlus\n\n\tYou can get in touch with me on Discord via the social link here:\n\thttps://create.roblox.com/store/asset/92368439343389/TopbarPlus\n\n\tMany thanks! ~Ben, June 10th 2025\n\t\n]]\n\ntask.defer(function()\n\tlocal RunService = game:GetService(\"RunService\")\n\tlocal VERSION = require(script.Parent.VERSION)\n\tlocal appVersion = VERSION.getAppVersion()\n\tlocal latestVersion = VERSION.getLatestVersion()\n\tlocal isOutdated = not VERSION.isUpToDate()\n\tif not RunService:IsStudio() then\n\t\tprint(`🍍 Running TopbarPlus {appVersion} by @ForeverHD & HD Admin`)\n\tend\n\tif isOutdated then\n\t\twarn(`A new version of TopbarPlus ({latestVersion}) is available: https://devforum.roblox.com/t/topbarplus/1017485`)\n\tend\nend)\n\nreturn {}"
  },
  {
    "path": "src/Elements/Caption.lua",
    "content": "local CAPTION_COLOR = Color3.fromRGB(39, 41, 48)\nlocal TEXT_SIZE = 15\nreturn function(icon)\n\n\t-- Credit to lolmansReturn and Canary Software for\n\t-- retrieving these values\n\tlocal clickRegion = icon:getInstance(\"ClickRegion\")\n\tlocal caption = Instance.new(\"CanvasGroup\")\n\tcaption.Name = \"Caption\"\n\tcaption.AnchorPoint = Vector2.new(0.5, 0)\n\tcaption.BackgroundTransparency = 1\n\tcaption.BorderSizePixel = 0\n\tcaption.GroupTransparency = 1\n\tcaption.Position = UDim2.fromOffset(0, 0)\n\tcaption.Visible = true\n\tcaption.ZIndex = 30\n\tcaption.Parent = clickRegion\n\n\tlocal box = Instance.new(\"Frame\")\n\tbox.Name = \"Box\"\n\tbox.AutomaticSize = Enum.AutomaticSize.XY\n\tbox.BackgroundColor3 = CAPTION_COLOR\n\tbox.Position = UDim2.fromOffset(4, 7)\n\tbox.ZIndex = 12\n\tbox.Parent = caption\n\n\tlocal header = Instance.new(\"TextLabel\")\n\theader.Name = \"Header\"\n\theader.FontFace = Font.new(\n\t\t\"rbxasset://fonts/families/BuilderSans.json\",\n\t\tEnum.FontWeight.Medium,\n\t\tEnum.FontStyle.Normal\n\t)\n\theader.Text = \"Caption\"\n\theader.TextColor3 = Color3.fromRGB(255, 255, 255)\n\theader.TextSize = TEXT_SIZE\n\theader.TextTruncate = Enum.TextTruncate.None\n\theader.TextWrapped = false\n\theader.TextXAlignment = Enum.TextXAlignment.Left\n\theader.AutomaticSize = Enum.AutomaticSize.X\n\theader.BackgroundTransparency = 1\n\theader.LayoutOrder = 1\n\theader.Size = UDim2.fromOffset(0, 16)\n\theader.ZIndex = 18\n\theader.Parent = box\n\n\tlocal layout = Instance.new(\"UIListLayout\")\n\tlayout.Name = \"Layout\"\n\tlayout.Padding = UDim.new(0, 8)\n\tlayout.SortOrder = Enum.SortOrder.LayoutOrder\n\tlayout.Parent = box\n\n\tlocal UICorner = Instance.new(\"UICorner\")\n\tUICorner.Name = \"CaptionCorner\"\n\tUICorner.Parent = box\n\n\tlocal padding = Instance.new(\"UIPadding\")\n\tpadding.Name = \"Padding\"\n\tpadding.PaddingBottom = UDim.new(0, 12)\n\tpadding.PaddingLeft = UDim.new(0, 12)\n\tpadding.PaddingRight = UDim.new(0, 12)\n\tpadding.PaddingTop = UDim.new(0, 12)\n\tpadding.Parent = box\n\n\tlocal hotkeys = Instance.new(\"Frame\")\n\thotkeys.Name = \"Hotkeys\"\n\thotkeys.AutomaticSize = Enum.AutomaticSize.Y\n\thotkeys.BackgroundTransparency = 1\n\thotkeys.LayoutOrder = 3\n\thotkeys.Size = UDim2.fromScale(1, 0)\n\thotkeys.Visible = false\n\thotkeys.Parent = box\n\n\tlocal layout1 = Instance.new(\"UIListLayout\")\n\tlayout1.Name = \"Layout1\"\n\tlayout1.Padding = UDim.new(0, 6)\n\tlayout1.FillDirection = Enum.FillDirection.Vertical\n\tlayout1.HorizontalAlignment = Enum.HorizontalAlignment.Center\n\tlayout1.HorizontalFlex = Enum.UIFlexAlignment.None\n\tlayout1.ItemLineAlignment = Enum.ItemLineAlignment.Automatic\n\tlayout1.VerticalFlex = Enum.UIFlexAlignment.None\n\tlayout1.SortOrder = Enum.SortOrder.LayoutOrder\n\tlayout1.Parent = hotkeys\n\n\tlocal keyTag1 = Instance.new(\"ImageLabel\")\n\tkeyTag1.Name = \"Key1\"\n\tkeyTag1.Image = \"rbxasset://textures/ui/Controls/key_single.png\"\n\tkeyTag1.ImageTransparency = 0.7\n\tkeyTag1.ScaleType = Enum.ScaleType.Slice\n\tkeyTag1.SliceCenter = Rect.new(5, 5, 23, 24)\n\tkeyTag1.AutomaticSize = Enum.AutomaticSize.X\n\tkeyTag1.BackgroundTransparency = 1\n\tkeyTag1.LayoutOrder = 1\n\tkeyTag1.Size = UDim2.fromOffset(0, 30)\n\tkeyTag1.ZIndex = 15\n\tkeyTag1.Parent = hotkeys\n\n\tlocal inset = Instance.new(\"UIPadding\")\n\tinset.Name = \"Inset\"\n\tinset.PaddingLeft = UDim.new(0, 8)\n\tinset.PaddingRight = UDim.new(0, 8)\n\tinset.Parent = keyTag1\n\n\tlocal labelContent = Instance.new(\"TextLabel\")\n\tlabelContent.AutoLocalize = false\n\tlabelContent.Name = \"LabelContent\"\n\tlabelContent.FontFace = Font.new(\n\t\t\"rbxasset://fonts/families/GothamSSm.json\",\n\t\tEnum.FontWeight.Medium,\n\t\tEnum.FontStyle.Normal\n\t)\n\tlabelContent.Text = \"\"\n\tlabelContent.TextColor3 = Color3.fromRGB(189, 190, 190)\n\tlabelContent.TextSize = TEXT_SIZE\n\tlabelContent.AutomaticSize = Enum.AutomaticSize.X\n\tlabelContent.BackgroundTransparency = 1\n\tlabelContent.Position = UDim2.fromOffset(0, -1)\n\tlabelContent.Size = UDim2.fromScale(1, 1)\n\tlabelContent.ZIndex = 16\n\tlabelContent.Parent = keyTag1\n\t\n\tlocal caret = Instance.new(\"ImageLabel\")\n\tcaret.Name = \"Caret\"\n\tcaret.Image = \"rbxassetid://101906294438076\"\n\tcaret.ImageColor3 = CAPTION_COLOR\n\tcaret.AnchorPoint = Vector2.new(0, 0.5)\n\tcaret.BackgroundTransparency = 1\n\tcaret.Position = UDim2.new(0, 0, 0, 4)\n\tcaret.Size = UDim2.fromOffset(16, 8)\n\tcaret.ZIndex = 12\n\tcaret.Parent = caption\n\n\tlocal dropShadow = Instance.new(\"ImageLabel\")\n\tdropShadow.Visible = true\n\tdropShadow.Name = \"DropShadow\"\n\tdropShadow.Image = \"rbxassetid://124920646932671\"\n\tdropShadow.ImageColor3 = Color3.fromRGB(0, 0, 0)\n\tdropShadow.ImageTransparency = 0.45\n\tdropShadow.ScaleType = Enum.ScaleType.Slice\n\tdropShadow.SliceCenter = Rect.new(12, 12, 13, 13)\n\tdropShadow.BackgroundTransparency = 1\n\tdropShadow.Position = UDim2.fromOffset(0, 5)\n\tdropShadow.Size = UDim2.new(1, 0, 0, 48)\n\tdropShadow.Parent = caption\n\tbox:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(function()\n\t\tdropShadow.Size = UDim2.new(1, 0, 0, box.AbsoluteSize.Y + 8)\n\tend)\n\t\n\t-- It's important we match the sizes as this is not\n\t-- handles within clipOutside (as it assumes the sizes\n\t-- are already the same)\n\tlocal captionJanitor = icon.captionJanitor\n\tlocal _, captionClone = icon:clipOutside(caption)\n\tcaptionClone.AutomaticSize = Enum.AutomaticSize.None\n\tlocal function matchSize()\n\t\tlocal absolute = caption.AbsoluteSize\n\t\tcaptionClone.Size = UDim2.fromOffset(absolute.X, absolute.Y)\n\tend\n\tcaptionJanitor:add(caption:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(matchSize))\n\tmatchSize()\n\t\n\t\n\t\n\t-- This handles the appearing/disappearing/positioning of the caption\n\tlocal isCompletelyEnabled = false\n\tlocal captionHeader = caption.Box.Header\n\tlocal UserInputService = game:GetService(\"UserInputService\")\n\tlocal function updateHotkey(keyCodeEnum)\n\t\tlocal hasKeyboard = UserInputService.KeyboardEnabled\n\t\tlocal text = caption:GetAttribute(\"CaptionText\") or \"\"\n\t\tlocal hideHeader = text == \"_hotkey_\"\n\t\tif not hasKeyboard and hideHeader then\n\t\t\ticon:setCaption()\n\t\t\treturn\n\t\tend\n\t\tcaptionHeader.Text = text\n\t\tcaptionHeader.Visible = not hideHeader\n\t\tif keyCodeEnum then\n\t\t\tlabelContent.Text = keyCodeEnum.Name\n\t\t\thotkeys.Visible = true\n\t\tend\n\t\tif not hasKeyboard then\n\t\t\thotkeys.Visible = false\n\t\tend\n\tend\n\tcaption:GetAttributeChangedSignal(\"CaptionText\"):Connect(updateHotkey)\n\n\tlocal EASING_STYLE = Enum.EasingStyle.Quad\n\tlocal TWEEN_SPEED = 0.2\n\tlocal TWEEN_INFO_IN = TweenInfo.new(TWEEN_SPEED, EASING_STYLE, Enum.EasingDirection.In)\n\tlocal TWEEN_INFO_OUT = TweenInfo.new(TWEEN_SPEED, EASING_STYLE, Enum.EasingDirection.Out)\n\tlocal TweenService = game:GetService(\"TweenService\")\n\tlocal RunService = game:GetService(\"RunService\")\n\tlocal function getCaptionPosition(customEnabled)\n\t\tlocal enabled = if customEnabled ~= nil then customEnabled else isCompletelyEnabled\n\t\tlocal yOut = 2\n\t\tlocal yIn = yOut + 8\n\t\tlocal yOffset = if enabled then yIn else yOut\n\t\treturn UDim2.new(0.5, 0, 1, yOffset)\n\tend\n\tlocal function updatePosition(forcedEnabled)\n\t\t\n\t\t-- Ignore changes if not enabled to reduce redundant calls\n\t\tif not isCompletelyEnabled then\n\t\t\treturn\n\t\tend\n\t\t\n\t\t-- Currently the one thing which isn't accounted for are the bounds of the screen\n\t\t-- This would be an issue if someone sets a long caption text for the left or\n\t\t-- right most icon\n\t\tlocal enabled = if forcedEnabled ~= nil then forcedEnabled else isCompletelyEnabled\n\t\tlocal startPosition = getCaptionPosition(not enabled)\n\t\tlocal endPosition = getCaptionPosition(enabled)\n\t\t\n\t\t-- It's essential we reset the carets position to prevent the x sizing bounds\n\t\t-- of the caption from infinitely scaling up\n\t\tif enabled then\n\t\t\tlocal caretY = caret.Position.Y.Offset\n\t\t\tcaret.Position = UDim2.fromOffset(0, caretY)\n\t\t\tcaption.AutomaticSize = Enum.AutomaticSize.XY\n\t\t\tcaption.Size = UDim2.fromOffset(32, 53)\n\t\telse\n\t\t\tlocal absolute = caption.AbsoluteSize\n\t\t\tcaption.AutomaticSize = Enum.AutomaticSize.Y\n\t\t\tcaption.Size = UDim2.fromOffset(absolute.X, absolute.Y)\n\t\tend\n\t\t\n\t\t-- We initially default to the opposite state\n\t\tlocal previousCaretX\n\t\tlocal function updateCaret()\n\t\t\tlocal caretX = clickRegion.AbsolutePosition.X - caption.AbsolutePosition.X + clickRegion.AbsoluteSize.X/2 - caret.AbsoluteSize.X/2\n\t\t\tlocal caretY = caret.Position.Y.Offset\n\t\t\tlocal newCaretPosition = UDim2.fromOffset(caretX, caretY)\n\t\t\tif previousCaretX ~= caretX then\n\t\t\t\t-- Again, it's essential we reset the caret if\n\t\t\t\t-- a difference in X position is detected otherwise\n\t\t\t\t-- a slight quirk with AutomaticCanvas can cause\n\t\t\t\t-- the caption to infinitely scale\n\t\t\t\tpreviousCaretX = caretX\n\t\t\t\tcaret.Position = UDim2.fromOffset(0, caretY)\n\t\t\t\ttask.wait()\n\t\t\tend\n\t\t\tcaret.Position = newCaretPosition\n\t\tend\n\t\tcaptionClone.Position = startPosition\n\t\tupdateCaret()\n\t\t\n\t\t-- Now we tween into the new state\n\t\tlocal tweenInfo = (enabled and TWEEN_INFO_IN) or TWEEN_INFO_OUT\n\t\tlocal tween = TweenService:Create(captionClone, tweenInfo, {Position = endPosition})\n\t\tlocal updateCaretConnection = RunService.Heartbeat:Connect(updateCaret)\n\t\ttween:Play()\n\t\ttween.Completed:Once(function()\n\t\t\tupdateCaretConnection:Disconnect()\n\t\tend)\n\t\t\n\tend\n\tcaptionJanitor:add(clickRegion:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(function()\n\t\tupdatePosition()\n\tend))\n\tupdatePosition(false)\n\t\n\tcaptionJanitor:add(icon.toggleKeyAdded:Connect(updateHotkey))\n\tfor keyCodeEnum, _ in pairs(icon.bindedToggleKeys) do\n\t\tupdateHotkey(keyCodeEnum)\n\t\tbreak\n\tend\n\tcaptionJanitor:add(icon.fakeToggleKeyChanged:Connect(updateHotkey))\n\tlocal fakeToggleKey = icon.fakeToggleKey\n\tif fakeToggleKey then\n\t\tupdateHotkey(fakeToggleKey)\n\tend\n\n\tlocal function setCaptionEnabled(enabled)\n\t\tif isCompletelyEnabled == enabled then\n\t\t\treturn\n\t\tend\n\t\tlocal joinedFrame = icon.joinedFrame\n\t\tif joinedFrame and string.match(joinedFrame.Name, \"Dropdown\") then\n\t\t\tenabled = false\n\t\tend\n\t\tisCompletelyEnabled = enabled\n\t\tlocal newTransparency = (enabled and 0) or 1\n\t\tlocal tweenInfo = (enabled and TWEEN_INFO_IN) or TWEEN_INFO_OUT\n\t\tlocal tweenTransparency = TweenService:Create(caption, tweenInfo, {\n\t\t\tGroupTransparency = newTransparency\n\t\t})\n\t\ttweenTransparency:Play()\n\t\tif enabled then\n\t\t\tcaptionClone:SetAttribute(\"ForceUpdate\", true)\n\t\tend\n\t\tupdatePosition()\n\t\tupdateHotkey()\n\tend\n\t\n\tlocal WAIT_DURATION = 0.5\n\tlocal RECOVER_PERIOD = 0.3\n\tlocal Icon = require(icon.iconModule)\n\tcaptionJanitor:add(icon.stateChanged:Connect(function(stateName)\n\t\tif stateName == \"Viewing\" then\n\t\t\tlocal lastClock = Icon.captionLastClosedClock\n\t\t\tlocal clockDifference = (lastClock and os.clock() - lastClock) or 999\n\t\t\tlocal waitDuration = (clockDifference < RECOVER_PERIOD and 0) or WAIT_DURATION\n\t\t\ttask.delay(waitDuration, function()\n\t\t\t\tif icon.activeState == \"Viewing\" then\n\t\t\t\t\tsetCaptionEnabled(true)\n\t\t\t\tend\n\t\t\tend)\n\t\telse\n\t\t\tIcon.captionLastClosedClock = os.clock()\n\t\t\tsetCaptionEnabled(false)\n\t\tend\n\tend))\n\t\n\treturn caption\nend"
  },
  {
    "path": "src/Elements/Container.lua",
    "content": "local hasBecomeOldTheme = false\nlocal previousInsetHeight = 0\nreturn function(Icon)\n\t\n\t-- Has to be included for the time being due to this bug mentioned here:\n\t-- https://devforum.roblox.com/t/bug/2973508/7\n\tlocal GuiService = game:GetService(\"GuiService\")\n\tlocal Players =  game:GetService(\"Players\")\n\tlocal UserInputService = game:GetService(\"UserInputService\")\n\tlocal container = {}\n\tlocal Signal = require(script.Parent.Parent.Packages.GoodSignal)\n\tlocal insetChanged = Signal.new()\n\tlocal guiInset = GuiService:GetGuiInset()\n\tlocal startInset = 0\n\tlocal yDownOffset = 0\n\tlocal ySizeOffset = 0\n\tlocal checkCount = 0\n\tlocal isConsoleScreen = false\n\tlocal isUsingVR = false\n\tlocal function checkInset(status)\n\t\tlocal currentHeight = GuiService.TopbarInset.Height\n\t\tlocal isOldTopbar = currentHeight <= 36\n\t\t\n\n\t\t-- These additional checks are needed to ensure *it is actually* the old topbar\n\t\t-- and not a client which takes a really long time to load\n\t\t-- There's unfortunately no APIs to do this a prettier way\n\t\tisConsoleScreen = GuiService:IsTenFootInterface()\n\t\tisUsingVR = UserInputService.VREnabled\n\t\tIcon.isOldTopbar = isOldTopbar\n\t\tcheckCount += 1\n\t\tif currentHeight == 0 and status == nil then\n\t\t\ttask.defer(function()\n\t\t\t\ttask.wait(8)\n\t\t\t\tcheckInset(\"ForceConvertToOld\")\n\t\t\tend)\n\t\telseif checkCount == 1 then\n\t\t\ttask.delay(5, function()\n\t\t\t\tlocal localPlayer = Players.LocalPlayer\n\t\t\t\tlocalPlayer:WaitForChild(\"PlayerGui\")\n\t\t\t\tif checkCount == 1 then\n\t\t\t\t\tcheckInset()\n\t\t\t\tend\n\t\t\tend)\n\t\tend\n\n\t\t-- Conver to old theme if verified\n\t\tif Icon.isOldTopbar and not isConsoleScreen and not isUsingVR and hasBecomeOldTheme == false and (currentHeight ~= 0 or status == \"ForceConvertToOld\") then\n\t\t\thasBecomeOldTheme = true\n\t\t\ttask.defer(function()\n\t\t\t\t-- If oldtopbar, apply the Classic theme\n\t\t\t\tlocal themes = script.Parent.Parent.Features.Themes\n\t\t\t\tlocal Classic = require(themes.Classic)\n\t\t\t\tIcon.modifyBaseTheme(Classic)\n\n\t\t\t\t-- Also configure the oldtopbar correctly\n\t\t\t\tlocal function decideToHideTopbar()\n\t\t\t\t\tif GuiService.MenuIsOpen then\n\t\t\t\t\t\tIcon.setTopbarEnabled(false, true)\n\t\t\t\t\telse\n\t\t\t\t\t\tIcon.setTopbarEnabled()\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tGuiService:GetPropertyChangedSignal(\"MenuIsOpen\"):Connect(decideToHideTopbar)\n\t\t\t\tdecideToHideTopbar()\n\t\t\tend)\n\t\tend\n\n\t\t-- Modify the offsets slightly depending on device type\n\t\tguiInset = GuiService:GetGuiInset()\n\t\tstartInset = if isOldTopbar then 12 else guiInset.Y - 50\n\t\tyDownOffset = if isOldTopbar then 2 else 0 --if isOldTopbar then 2 else 0 \n\t\tySizeOffset = -2\n\t\tif isConsoleScreen then\n\t\t\tstartInset = 10\n\t\t\tyDownOffset = 0 ---9\n\t\tend\n\t\tif GuiService.TopbarInset.Height == 0 and not hasBecomeOldTheme then\n\t\t\tyDownOffset += 13\n\t\t\tySizeOffset = 50\n\t\tend\n\n\t\t-- Now inform other areas of the change\n\t\tinsetChanged:Fire(guiInset)\n\t\tlocal insetHeight = guiInset.Y\n\t\tif insetHeight ~= previousInsetHeight then\n\t\t\tpreviousInsetHeight = insetHeight\n\t\t\ttask.defer(function()\n\t\t\t\tIcon.insetHeightChanged:Fire(insetHeight)\n\t\t\tend)\n\t\tend\n\t\t\n\tend\n\tGuiService:GetPropertyChangedSignal(\"TopbarInset\"):Connect(checkInset)\n\tcheckInset(\"FirstTime\")\n\n\tlocal screenGui = Instance.new(\"ScreenGui\")\n\tinsetChanged:Connect(function()\n\t\tscreenGui:SetAttribute(\"StartInset\", startInset)\n\tend)\n\tscreenGui.Name = \"TopbarStandard\"\n\tscreenGui.Enabled = true\n\tscreenGui.DisplayOrder = Icon.baseDisplayOrder\n\tscreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling\n\tscreenGui.IgnoreGuiInset = true\n\tscreenGui.ResetOnSpawn = false\n\tscreenGui.ScreenInsets = Enum.ScreenInsets.TopbarSafeInsets\n\tcontainer[screenGui.Name] = screenGui\n\tIcon.baseDisplayOrderChanged:Connect(function()\n\t\tscreenGui.DisplayOrder = Icon.baseDisplayOrder\n\tend)\n\n\tlocal holders = Instance.new(\"Frame\")\n\tholders.Name = \"Holders\"\n\tholders.BackgroundTransparency = 1\n\tinsetChanged:Connect(function()\n\t\tlocal holderY = if isUsingVR then 36 else 56\n\t\tlocal holderSize = if isConsoleScreen then UDim2.new(1, 0, 0, holderY) else UDim2.new(1, 0, 1, ySizeOffset)\n\t\tholders.Position = UDim2.new(0, 0, 0, yDownOffset)\n\t\tholders.Size = holderSize\n\tend)\n\tholders.Visible = true\n\tholders.ZIndex = 1\n\tholders.Parent = screenGui\n\t\n\tlocal screenGuiCenter = screenGui:Clone()\n\tlocal holdersCenter = screenGuiCenter.Holders\n\tlocal function updateCenteredHoldersHeight()\n\t\tholdersCenter.Size = UDim2.new(1, 0, 0, GuiService.TopbarInset.Height+ySizeOffset)\n\tend\n\tscreenGuiCenter.Name = \"TopbarCentered\"\n\tscreenGuiCenter.DisplayOrder = Icon.baseDisplayOrder\n\tscreenGuiCenter.ScreenInsets = Enum.ScreenInsets.None\n\tIcon.baseDisplayOrderChanged:Connect(function()\n\t\tscreenGuiCenter.DisplayOrder = Icon.baseDisplayOrder\n\tend)\n\tcontainer[screenGuiCenter.Name] = screenGuiCenter\n\t\n\tinsetChanged:Connect(updateCenteredHoldersHeight)\n\tupdateCenteredHoldersHeight()\n\t\n\tlocal screenGuiClipped = screenGui:Clone()\n\tscreenGuiClipped.Name = screenGuiClipped.Name..\"Clipped\"\n\tscreenGuiClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)\n\tIcon.baseDisplayOrderChanged:Connect(function()\n\t\tscreenGuiClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)\n\tend)\n\tcontainer[screenGuiClipped.Name] = screenGuiClipped\n\t\n\tlocal screenGuiCenterClipped = screenGuiCenter:Clone()\n\tscreenGuiCenterClipped.Name = screenGuiCenterClipped.Name..\"Clipped\"\n\tscreenGuiCenterClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)\n\tIcon.baseDisplayOrderChanged:Connect(function()\n\t\tscreenGuiCenterClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)\n\tend)\n\tcontainer[screenGuiCenterClipped.Name] = screenGuiCenterClipped\n\t\n\tlocal holderReduction = -24\n\tlocal left = Instance.new(\"ScrollingFrame\")\n\tleft:SetAttribute(\"IsAHolder\", true)\n\tleft.Name = \"Left\"\n\tinsetChanged:Connect(function()\n\t\tleft.Position = UDim2.fromOffset(startInset, 0)\n\tend)\n\tleft.Size = UDim2.new(1, holderReduction, 1, 0)\n\tleft.BackgroundTransparency = 1\n\tleft.Visible = true\n\tleft.ZIndex = 1\n\tleft.Active = false\n\tleft.ClipsDescendants = true\n\tleft.HorizontalScrollBarInset = Enum.ScrollBarInset.None\n\tleft.CanvasSize = UDim2.new(0, 0, 1, -1) -- This -1 prevents a dropdown scrolling appearance bug\n\tleft.AutomaticCanvasSize = Enum.AutomaticSize.X\n\tleft.ScrollingDirection = Enum.ScrollingDirection.X\n\tleft.ScrollBarThickness = 0\n\tleft.BorderSizePixel = 0\n\tleft.Selectable = false\n\tleft.ScrollingEnabled = false--true\n\tleft.ElasticBehavior = Enum.ElasticBehavior.Never\n\tleft.Parent = holders\n\t\n\tlocal UIListLayout = Instance.new(\"UIListLayout\")\n\tinsetChanged:Connect(function()\n\t\tUIListLayout.Padding = UDim.new(0, startInset)\n\tend)\n\tUIListLayout.FillDirection = Enum.FillDirection.Horizontal\n\tUIListLayout.SortOrder = Enum.SortOrder.LayoutOrder\n\tUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom\n\tUIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left\n\tUIListLayout.Parent = left\n\t\n\tlocal center = left:Clone()\n\tinsetChanged:Connect(function()\n\t\tcenter.UIListLayout.Padding = UDim.new(0, startInset)\n\tend)\n\tcenter.ScrollingEnabled = false\n\tcenter.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center\n\tcenter.Name = \"Center\"\n\tcenter.Parent = holdersCenter\n\t\n\tlocal right = left:Clone()\n\tinsetChanged:Connect(function()\n\t\tright.UIListLayout.Padding = UDim.new(0, startInset)\n\tend)\n\tright.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right\n\tright.Name = \"Right\"\n\tright.AnchorPoint = Vector2.new(1, 0)\n\tright.Position = UDim2.new(1, -12, 0, 0)\n\tright.Parent = holders\n\n\t-- This is important so that all elements update instantly\n\tinsetChanged:Fire(guiInset)\n\n\treturn container\nend"
  },
  {
    "path": "src/Elements/Dropdown.lua",
    "content": "local TweenService = game:GetService(\"TweenService\")\nlocal RunService = game:GetService(\"RunService\")\nlocal Themes = require(script.Parent.Parent.Features.Themes)\nlocal PADDING = 0 -- used to be 8\nreturn function(icon)\n\t\n\tlocal dropdown = Instance.new(\"Frame\") -- Instance.new(\"CanvasGroup\")\n\tdropdown.Name = \"Dropdown\"\n\tdropdown.AutomaticSize = Enum.AutomaticSize.X\n\tdropdown.BackgroundTransparency = 1\n\tdropdown.BorderSizePixel = 0\n\tdropdown.AnchorPoint = Vector2.new(0.5, 0)\n\tdropdown.Position = UDim2.new(0.5, 0, 1, 10)\n\tdropdown.ZIndex = -2\n\tdropdown.ClipsDescendants = true\n\tdropdown.Parent = icon.widget\n\n\t-- Account for PreferredTransparency which can be set by every player\n\tlocal GuiService = game:GetService(\"GuiService\")\n\ticon:setBehaviour(\"Dropdown\", \"BackgroundTransparency\", function(value)\n\t\tlocal preference = GuiService.PreferredTransparency\n\t\tlocal newValue = value * preference\n\t\tif value == 1 then\n\t\t\treturn value\n\t\tend\n\t\treturn newValue\n\tend)\n\ticon.janitor:add(GuiService:GetPropertyChangedSignal(\"PreferredTransparency\"):Connect(function()\n\t\ticon:refreshAppearance(dropdown, \"BackgroundTransparency\")\n\tend))\n\n\tlocal UICorner = Instance.new(\"UICorner\")\n\tUICorner.Name = \"DropdownCorner\"\n\tUICorner.CornerRadius = UDim.new(0, 10)\n\tUICorner.Parent = dropdown\n\n\tlocal dropdownScroller = Instance.new(\"ScrollingFrame\")\n\tdropdownScroller.Name = \"DropdownScroller\"\n\tdropdownScroller.AutomaticSize = Enum.AutomaticSize.X\n\tdropdownScroller.BackgroundTransparency = 1\n\tdropdownScroller.BorderSizePixel = 0\n\tdropdownScroller.AnchorPoint = Vector2.new(0, 0)\n\tdropdownScroller.Position = UDim2.new(0, 0, 0, 0)\n\tdropdownScroller.ZIndex = -1\n\tdropdownScroller.ClipsDescendants = true\n\tdropdownScroller.Visible = true\n\tdropdownScroller.VerticalScrollBarInset = Enum.ScrollBarInset.None --ScrollBar\n\tdropdownScroller.VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right\n\tdropdownScroller.Active = false\n\tdropdownScroller.ScrollingEnabled = true\n\tdropdownScroller.AutomaticCanvasSize = Enum.AutomaticSize.Y\n\tdropdownScroller.ScrollBarThickness = 5\n\tdropdownScroller.ScrollBarImageColor3 = Color3.fromRGB(255, 255, 255)\n\tdropdownScroller.ScrollBarImageTransparency = 0.8\n\tdropdownScroller.CanvasSize = UDim2.new(0, 0, 0, 0)\n\tdropdownScroller.Selectable = false\n\tdropdownScroller.Active = true\n\tdropdownScroller.Parent = dropdown\n\n\tlocal TweenDuration = Instance.new(\"NumberValue\") -- this helps to change the speed to open / close in modifyTheme()\n\tTweenDuration.Name = \"DropdownSpeed\"\n\tTweenDuration.Value = 0.07\n\tTweenDuration.Parent = dropdown\n\n\tlocal dropdownPadding = Instance.new(\"UIPadding\")\n\tdropdownPadding.Name = \"DropdownPadding\"\n\tdropdownPadding.PaddingTop = UDim.new(0, PADDING)\n\tdropdownPadding.PaddingBottom = UDim.new(0, PADDING)\n\tdropdownPadding.Parent = dropdownScroller\n\n\tlocal dropdownList = Instance.new(\"UIListLayout\")\n\tdropdownList.Name = \"DropdownList\"\n\tdropdownList.FillDirection = Enum.FillDirection.Vertical\n\tdropdownList.SortOrder = Enum.SortOrder.LayoutOrder\n\tdropdownList.HorizontalAlignment = Enum.HorizontalAlignment.Center\n\tdropdownList.HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly\n\tdropdownList.Parent = dropdownScroller\n\n\tlocal dropdownJanitor = icon.dropdownJanitor\n\tlocal Icon = require(icon.iconModule)\n\ticon.dropdownChildAdded:Connect(function(childIcon)\n\t\tlocal _, modificationUID = childIcon:modifyTheme({\n\t\t\t{\"Widget\", \"BorderSize\", 0},\n\t\t\t{\"IconCorners\", \"CornerRadius\", UDim.new(0, 10)},\n\t\t\t{\"Widget\", \"MinimumWidth\", 190},\n\t\t\t{\"Widget\", \"MinimumHeight\", 58},\n\t\t\t{\"IconLabel\", \"TextSize\", 20},\n\t\t\t{\"IconOverlay\", \"Size\", UDim2.new(1, 0, 1, 0)},\n\t\t\t{\"PaddingLeft\", \"Size\", UDim2.fromOffset(25, 0)},\n\t\t\t{\"Notice\", \"Position\", UDim2.new(1, -24, 0, 5)},\n\t\t\t{\"ContentsList\", \"HorizontalAlignment\", Enum.HorizontalAlignment.Left},\n\t\t\t{\"Selection\", \"Size\", UDim2.new(1, -PADDING, 1, -PADDING)},\n\t\t\t{\"Selection\", \"Position\", UDim2.new(0, PADDING/2, 0, PADDING/2)},\n\t\t})\n\t\ttask.defer(function()\n\t\t\tchildIcon.joinJanitor:add(function()\n\t\t\t\tchildIcon:removeModification(modificationUID)\n\t\t\tend)\n\t\tend)\n\tend)\n\ticon.dropdownSet:Connect(function(arrayOfIcons)\n\t\tfor i, otherIconUID in pairs(icon.dropdownIcons) do\n\t\t\tlocal otherIcon = Icon.getIconByUID(otherIconUID)\n\t\t\totherIcon:destroy()\n\t\tend\n\t\tif type(arrayOfIcons) == \"table\" then\n\t\t\tfor i, otherIcon in pairs(arrayOfIcons) do\n\t\t\t\totherIcon:joinDropdown(icon)\n\t\t\tend\n\t\tend\n\tend)\n\n\tlocal function updateMaxIcons()\n\t\t--icon:modifyTheme({\"Dropdown\", \"Visible\", icon.isSelected})\n\t\tlocal maxIcons = dropdown:GetAttribute(\"MaxIcons\")\n\t\tif not maxIcons then return 0 end\n\t\tlocal children = {}\n\t\tfor _, child in pairs(dropdownScroller:GetChildren()) do\n\t\t\tif child:IsA(\"GuiObject\") and child.Visible then\n\t\t\t\ttable.insert(children, child)\n\t\t\tend\n\t\tend\n\n\t\ttable.sort(children, function(a, b) return a.AbsolutePosition.Y < b.AbsolutePosition.Y end)\n\t\tlocal totalHeight = 0\n\t\tlocal maxIconsRoundedUp = math.ceil(maxIcons)\n\t\tfor i = 1, maxIconsRoundedUp do\n\t\t\tlocal child = children[i]\n\t\t\tif not child then break end\n\t\t\tlocal height = child.AbsoluteSize.Y\n\t\t\tlocal isReduced = i == maxIconsRoundedUp and maxIconsRoundedUp ~= maxIcons\n\t\t\tif isReduced then\n\t\t\t\theight *= (maxIcons - maxIconsRoundedUp + 1)\n\t\t\tend\n\t\t\ttotalHeight += height\n\t\tend\n\t\ttotalHeight += dropdownPadding.PaddingTop.Offset + dropdownPadding.PaddingBottom.Offset\n\t\treturn totalHeight\n\tend\n\t\n\tlocal openTween = nil\n\tlocal closeTween = nil\n\tlocal currentSpeedMultiplier = nil\n\tlocal currentTweenInfo = nil\n\tlocal function getTweenInfo()\n\t\tlocal speedMultiplier = Themes.getInstanceValue(dropdown, \"MaxIcons\") or 1\n\t\tif currentSpeedMultiplier and currentSpeedMultiplier == speedMultiplier and currentTweenInfo then\n\t\t\treturn currentTweenInfo\n\t\tend\n\t\tlocal newTweenInfo = TweenInfo.new(\n\t\t\tTweenDuration.Value * speedMultiplier,\n\t\t\tEnum.EasingStyle.Exponential,\n\t\t\tEnum.EasingDirection.Out\n\t\t)\n\t\tcurrentTweenInfo = newTweenInfo\n\t\tcurrentSpeedMultiplier = speedMultiplier\n\t\treturn newTweenInfo\n\tend\n\tlocal function updateVisibility()\n\t\t-- Update visibiliy of dropdown using tween transition\n\t\tlocal tweenInfo = getTweenInfo()\n\t\t\n\t\tif openTween then\n\t\t\topenTween:Cancel()\n\t\t\topenTween = nil\n\t\tend\n\t\tif closeTween then\n\t\t\tcloseTween:Cancel()\n\t\t\tcloseTween = nil\n\t\tend\n\n\t\tif icon.isSelected then\n\t\t\tlocal height = updateMaxIcons()\n\t\t\tdropdown.Visible = true\n\t\t\tdropdown.BackgroundTransparency = 0 -- no transparency so it looks solid\n\t\t\tdropdown.Size = UDim2.new(0, dropdown.Size.X.Offset, 0, 0) -- reset height to 0 before tween\n\n\t\t\topenTween = TweenService:Create(dropdown, tweenInfo, {Size = UDim2.new(0, dropdown.Size.X.Offset, 0, height)})\n\t\t\topenTween:Play()\n\t\t\topenTween.Completed:Connect(function()\n\t\t\t\topenTween = nil\n\t\t\tend)\n\t\telse\n\t\t\tlocal closeTweenInfo = TweenInfo.new(0)\n\t\t\tcloseTween = TweenService:Create(dropdown, closeTweenInfo, {Size = UDim2.new(0, dropdown.Size.X.Offset, 0, 0)})\n\t\t\tcloseTween:Play()\n\t\t\tcloseTween.Completed:Connect(function()\n\t\t\t\tcloseTween = nil\n\t\t\tend)\n\t\tend\n\tend\n\n\tdropdownJanitor:add(icon.toggled:Connect(updateVisibility))\n\tupdateVisibility()\n\t--task.delay(0.2, updateVisibility)\n\n\tlocal function updateChildSize()\n\t\tlocal tweenInfo = getTweenInfo()\n\t\tif not icon.isSelected then return end\n\t\tif openTween then\n\t\t\topenTween:Cancel()\n\t\t\topenTween = nil\n\t\tend\n\t\tif closeTween then\n\t\t\tcloseTween:Cancel()\n\t\t\tcloseTween = nil\n\t\tend\n\t\t\n\t\tRunService.Heartbeat:Wait()\n\t\t\n\t\tlocal height = updateMaxIcons()\n\n\t\topenTween = TweenService:Create(dropdown, tweenInfo, {Size = UDim2.new(0, dropdown.Size.X.Offset, 0, height)})\n\t\topenTween:Play()\n\t\topenTween.Completed:Connect(function()\t\n\t\t\topenTween = nil\n\t\tend)\n\tend\n\n\tdropdownJanitor:add(icon.toggled:Connect(updateVisibility))\n\n\t-- Ensures canvas and size stay synced (original updateMaxIcons logic)\n\tlocal updateCount = 0\n\tlocal isUpdating = false\n\n\t-- This updates the scrolling frame to only display a scroll\n\t-- length equal to the distance produced by its MaxIcons\n\tlocal function updateMaxIconsListener()\n\t\tupdateCount += 1\n\t\tif isUpdating then return end\n\t\tlocal myUpdateCount = updateCount\n\t\tisUpdating = true\n\t\ttask.defer(function()\n\t\t\tisUpdating = false\n\t\t\tif updateCount ~= myUpdateCount then\n\t\t\t\tupdateMaxIconsListener()\n\t\t\tend\n\t\tend)\n\t\tlocal maxIcons = dropdown:GetAttribute(\"MaxIcons\")\n\t\tif not maxIcons then return end\n\n\t\tlocal orderedInstances = {}\n\t\tfor _, child in pairs(dropdownScroller:GetChildren()) do\n\t\t\tif child:IsA(\"GuiObject\") and child.Visible then\n\t\t\t\ttable.insert(orderedInstances, {child, child.AbsolutePosition.Y})\n\t\t\tend\n\t\tend\n\t\ttable.sort(orderedInstances, function(a, b) return a[2] < b[2] end)\n\n\t\tlocal totalHeight = 0\n\t\tlocal hasSetNextSelection = false\n\t\tlocal maxIconsRoundedUp = math.ceil(maxIcons)\n\t\tfor i = 1, maxIconsRoundedUp do\n\t\t\tlocal group = orderedInstances[i]\n\t\t\tif not group then break end\n\t\t\tlocal child = group[1]\n\t\t\tlocal height = child.AbsoluteSize.Y\n\t\t\tlocal isReduced = i == maxIconsRoundedUp and maxIconsRoundedUp ~= maxIcons\n\t\t\tif isReduced then\n\t\t\t\theight = height * (maxIcons - maxIconsRoundedUp + 1)\n\t\t\tend\n\t\t\ttotalHeight += height\n\t\t\tif isReduced then\n\t\t\t\tcontinue\n\t\t\tend\n\t\t\tlocal iconUID = child:GetAttribute(\"WidgetUID\")\n\t\t\tlocal childIcon = iconUID and Icon.getIconByUID(iconUID)\n\t\t\tif childIcon then\n\t\t\t\tlocal nextSelection = nil\n\t\t\t\tif not hasSetNextSelection then\n\t\t\t\t\thasSetNextSelection = true\n\t\t\t\t\tnextSelection = icon:getInstance(\"ClickRegion\")\n\t\t\t\tend\n\t\t\t\tchildIcon:getInstance(\"ClickRegion\").NextSelectionUp = nextSelection\n\t\t\tend\n\t\tend\n\t\ttotalHeight += dropdownPadding.PaddingTop.Offset + dropdownPadding.PaddingBottom.Offset\n\n\t\tdropdownScroller.Size = UDim2.fromOffset(0, totalHeight)\n\n\tend\n\n\tdropdownJanitor:add(dropdownScroller:GetPropertyChangedSignal(\"AbsoluteCanvasSize\"):Connect(updateMaxIconsListener))\n\tdropdownJanitor:add(dropdownScroller.ChildAdded:Connect(updateMaxIconsListener))\n\tdropdownJanitor:add(dropdownScroller.ChildRemoved:Connect(updateChildSize)) -- rezise the dropdown when icon delects or adds\n\tdropdownJanitor:add(dropdownScroller.ChildRemoved:Connect(updateMaxIconsListener))\n\tdropdownJanitor:add(dropdown:GetAttributeChangedSignal(\"MaxIcons\"):Connect(updateMaxIconsListener))\n\tdropdownJanitor:add(dropdown:GetAttributeChangedSignal(\"MaxIcons\"):Connect(updateChildSize))\n\tdropdownJanitor:add(icon.childThemeModified:Connect(updateMaxIconsListener))\n\tupdateMaxIconsListener()\n\n\t-- Ensures each child listens to visibility changes\n\tlocal function connectVisibilityListeners(child)\n\t\tif child:IsA(\"GuiObject\") then\n\t\t\tchild:GetPropertyChangedSignal(\"Visible\"):Connect(updateChildSize)\n\t\t\tchild:GetPropertyChangedSignal(\"Size\"):Connect(updateChildSize) -- -- update max icons when child size changes\n\t\tend\n\tend\n\t\n\t-- For existing children\n\tfor _, child in pairs(dropdownScroller:GetChildren()) do\n\t\tconnectVisibilityListeners(child)\n\tend\n\t-- For new children\n\tdropdownScroller.ChildAdded:Connect(function(child)\n\t\tRunService.Heartbeat:Wait()\n\t\tconnectVisibilityListeners(child)\n\t\tupdateChildSize()\n\tend)\n\n\t-- On start, hide dropdown (prevent it showing as opened)\n\tdropdown.Visible = false\n\n\treturn dropdown\nend"
  },
  {
    "path": "src/Elements/Indicator.lua",
    "content": "return function(icon, Icon)\n\n\tlocal widget = icon.widget\n\tlocal contents = icon:getInstance(\"Contents\")\n\tlocal indicator = Instance.new(\"Frame\")\n\tindicator.Name = \"Indicator\"\n\tindicator.LayoutOrder = 9999999\n\tindicator.ZIndex = 6\n\tindicator.Size = UDim2.new(0, 42, 0, 42)\n\tindicator.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tindicator.BackgroundTransparency = 1\n\tindicator.Position = UDim2.new(1, 0, 0.5, 0)\n\tindicator.BorderSizePixel = 0\n\tindicator.BackgroundColor3 = Color3.fromRGB(0, 0, 0)\n\tindicator.Parent = contents\n\n\tlocal indicatorButton = Instance.new(\"Frame\")\n\tindicatorButton.Name = \"IndicatorButton\"\n\tindicatorButton.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tindicatorButton.AnchorPoint = Vector2.new(0.5, 0.5)\n\tindicatorButton.BorderSizePixel = 0\n\tindicatorButton.BackgroundColor3 = Color3.fromRGB(0, 0, 0)\n\tindicatorButton.Parent = indicator\n\t\n\tlocal GuiService = game:GetService(\"GuiService\")\n\tlocal GamepadService = game:GetService(\"GamepadService\")\n\tlocal ourClickRegion = icon:getInstance(\"ClickRegion\")\n\tlocal function selectionChanged()\n\t\tlocal selectedClickRegion = GuiService.SelectedObject\n\t\tif selectedClickRegion == ourClickRegion then\n\t\t\tindicatorButton.BackgroundTransparency = 1\n\t\t\tindicatorButton.Position = UDim2.new(0.5, -2, 0.5, 0)\n\t\t\tindicatorButton.Size = UDim2.fromScale(1.2, 1.2)\n\t\telse\n\t\t\tindicatorButton.BackgroundTransparency = 0.75\n\t\t\tindicatorButton.Position = UDim2.new(0.5, 2, 0.5, 0)\n\t\t\tindicatorButton.Size = UDim2.fromScale(1, 1)\n\t\tend\n\tend\n\ticon.janitor:add(GuiService:GetPropertyChangedSignal(\"SelectedObject\"):Connect(selectionChanged))\n\tselectionChanged()\n\n\tlocal imageLabel = Instance.new(\"ImageLabel\")\n\timageLabel.LayoutOrder = 2\n\timageLabel.ZIndex = 15\n\timageLabel.AnchorPoint = Vector2.new(0.5, 0.5)\n\timageLabel.Size = UDim2.new(0.5, 0, 0.5, 0)\n\timageLabel.BackgroundTransparency = 1\n\timageLabel.Position = UDim2.new(0.5, 0, 0.5, 0)\n\timageLabel.Image = \"rbxasset://textures/ui/Controls/XboxController/DPadUp@2x.png\"\n\timageLabel.Parent = indicatorButton\n\n\tlocal UICorner = Instance.new(\"UICorner\")\n\tUICorner.CornerRadius = UDim.new(1, 0)\n\tUICorner.Parent = indicatorButton\n\n\tlocal UserInputService = game:GetService(\"UserInputService\")\n\tlocal function setIndicatorVisible(visibility)\n\t\tif visibility == nil then\n\t\t\tvisibility = indicator.Visible\n\t\tend\n\t\tif GamepadService.GamepadCursorEnabled then\n\t\t\tvisibility = false\n\t\tend\n\t\tif visibility then\n\t\t\ticon:modifyTheme({\"PaddingRight\", \"Size\", UDim2.new(0, 0, 1, 0)}, \"IndicatorPadding\")\n\t\telseif indicator.Visible then\n\t\t\ticon:removeModification(\"IndicatorPadding\")\n\t\tend\n\t\ticon:modifyTheme({\"Indicator\", \"Visible\", visibility})\n\t\ticon.updateSize:Fire()\n\tend\n\ticon.janitor:add(GamepadService:GetPropertyChangedSignal(\"GamepadCursorEnabled\"):Connect(setIndicatorVisible))\n\ticon.indicatorSet:Connect(function(keyCode)\n\t\tlocal visibility = false\n\t\tif keyCode then\n\t\t\timageLabel.Image = UserInputService:GetImageForKeyCode(keyCode)\n\t\t\tvisibility = true\n\t\tend\n\t\tsetIndicatorVisible(visibility)\n\tend)\n\n\tlocal function updateSize()\n\t\tlocal ySize = widget.AbsoluteSize.Y*0.96\n\t\tindicator.Size = UDim2.new(0, ySize, 0, ySize)\n\tend\n\twidget:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(updateSize)\n\tupdateSize()\n\n\treturn indicator\nend"
  },
  {
    "path": "src/Elements/Menu.lua",
    "content": "return function(icon)\n\n\tlocal menu = Instance.new(\"ScrollingFrame\")\n\tmenu.Name = \"Menu\"\n\tmenu.BackgroundTransparency = 1\n\tmenu.Visible = true\n\tmenu.ZIndex = 1\n\tmenu.Size = UDim2.fromScale(1, 1)\n\tmenu.ClipsDescendants = true\n\tmenu.TopImage = \"\"\n\tmenu.BottomImage = \"\"\n\tmenu.HorizontalScrollBarInset = Enum.ScrollBarInset.Always\n\tmenu.CanvasSize = UDim2.new(0, 0, 1, -1) -- This -1 prevents a dropdown scrolling appearance bug\n\tmenu.ScrollingEnabled = true\n\tmenu.ScrollingDirection = Enum.ScrollingDirection.X\n\tmenu.ZIndex = 20\n\tmenu.ScrollBarThickness = 3\n\tmenu.ScrollBarImageColor3 = Color3.fromRGB(255, 255, 255)\n\tmenu.ScrollBarImageTransparency = 0.8\n\tmenu.BorderSizePixel = 0\n\tmenu.Selectable = false\n\t\n\tlocal Icon = require(icon.iconModule)\n\tlocal menuUIListLayout = Icon.container.TopbarStandard:FindFirstChild(\"UIListLayout\", true):Clone()\n\tmenuUIListLayout.Name = \"MenuUIListLayout\"\n\tmenuUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center\n\tmenuUIListLayout.Parent = menu\n\n\tlocal menuGap = Instance.new(\"Frame\")\n\tmenuGap.Name = \"MenuGap\"\n\tmenuGap.BackgroundTransparency = 1\n\tmenuGap.Visible = false\n\tmenuGap.AnchorPoint = Vector2.new(0, 0.5)\n\tmenuGap.ZIndex = 5\n\tmenuGap.Parent = menu\n\t\n\tlocal hasStartedMenu = false\n\tlocal Themes = require(script.Parent.Parent.Features.Themes)\n\tlocal function totalChildrenChanged()\n\t\t\n\t\tlocal menuJanitor = icon.menuJanitor\n\t\tlocal totalIcons = #icon.menuIcons\n\t\tif hasStartedMenu then\n\t\t\tif totalIcons <= 0 then\n\t\t\t\tmenuJanitor:clean()\n\t\t\t\thasStartedMenu = false\n\t\t\tend\n\t\t\treturn\n\t\tend\n\t\thasStartedMenu = true\n\t\t\n\t\t-- Listen for changes\n\t\tmenuJanitor:add(icon.toggled:Connect(function()\n\t\t\tif #icon.menuIcons > 0 then\n\t\t\t\ticon.updateSize:Fire()\n\t\t\tend\n\t\tend))\n\t\t\n\t\t-- Modify appearance of menu icon when joined\n\t\tlocal _, modificationUID = icon:modifyTheme({\n\t\t\t{\"Menu\", \"Active\", true},\n\t\t})\n\t\ttask.defer(function()\n\t\t\tmenuJanitor:add(function()\n\t\t\t\ticon:removeModification(modificationUID)\n\t\t\tend)\n\t\tend)\n\t\t\n\t\t-- For right-aligned icons, this ensures their menus\n\t\t-- close button appear instantly when selected (instead\n\t\t-- of partially hidden from view)\n\t\tlocal previousCanvasX = menu.AbsoluteCanvasSize.X\n\t\tlocal function rightAlignCanvas()\n\t\t\tif icon.alignment == \"Right\" then\n\t\t\t\tlocal newCanvasX = menu.AbsoluteCanvasSize.X\n\t\t\t\tlocal difference = previousCanvasX - newCanvasX\n\t\t\t\tpreviousCanvasX = newCanvasX\n\t\t\t\tmenu.CanvasPosition = Vector2.new(menu.CanvasPosition.X - difference, 0)\n\t\t\tend\n\t\tend\n\t\tmenuJanitor:add(icon.selected:Connect(rightAlignCanvas))\n\t\tmenuJanitor:add(menu:GetPropertyChangedSignal(\"AbsoluteCanvasSize\"):Connect(rightAlignCanvas))\n\t\t\n\t\t-- Apply a close selected image if the user hasn't applied thier own\n\t\tlocal stateGroup = icon:getStateGroup()\n\t\tlocal imageDeselected = Themes.getThemeValue(stateGroup, \"IconImage\", \"Image\", \"Deselected\")\n\t\tlocal imageSelected = Themes.getThemeValue(stateGroup, \"IconImage\", \"Image\", \"Selected\")\n\t\tif imageDeselected == imageSelected then\n\t\t\tlocal fontLink = \"rbxasset://fonts/families/FredokaOne.json\"\n\t\t\tlocal fontFace = Font.new(fontLink, Enum.FontWeight.Light, Enum.FontStyle.Normal)\n\t\t\ticon:removeModificationWith(\"IconLabel\", \"Text\", \"Viewing\")\n\t\t\ticon:removeModificationWith(\"IconLabel\", \"Image\", \"Viewing\")\n\t\t\ticon:modifyTheme({\n\t\t\t\t{\"IconLabel\", \"FontFace\", fontFace, \"Selected\"},\n\t\t\t\t{\"IconLabel\", \"Text\", \"X\", \"Selected\"},\n\t\t\t\t{\"IconLabel\", \"TextSize\", 20, \"Selected\"},\n\t\t\t\t{\"IconLabel\", \"TextStrokeTransparency\", 0.8, \"Selected\"},\n\t\t\t\t{\"IconImage\", \"Image\", \"\", \"Selected\"},\n\t\t\t})\n\t\tend\n\n\t\t-- Change order of spot when alignment changes\n\t\tlocal menuGap = icon:getInstance(\"MenuGap\")\n\t\tlocal function updateAlignent()\n\t\t\tlocal alignment = icon.alignment\n\t\t\tlocal spotIndex = -99999\n\t\t\tlocal gapIndex = -99998\n\t\t\tif alignment == \"Right\" then\n\t\t\t\tspotIndex = 99999\n\t\t\t\tgapIndex = 99998\n\t\t\tend\n\t\t\ticon:modifyTheme({\"IconSpot\", \"LayoutOrder\", spotIndex})\n\t\t\tmenuGap.LayoutOrder = gapIndex\n\t\tend\n\t\tmenuJanitor:add(icon.alignmentChanged:Connect(updateAlignent))\n\t\tupdateAlignent()\n\t\t\n\t\t-- This updates the scrolling frame to only display a scroll\n\t\t-- length equal to the distance produced by its MaxIcons\n\t\tmenu:GetAttributeChangedSignal(\"MenuCanvasWidth\"):Connect(function()\n\t\t\tlocal canvasWidth = menu:GetAttribute(\"MenuCanvasWidth\")\n\t\t\tlocal canvasY = menu.CanvasSize.Y\n\t\t\tmenu.CanvasSize = UDim2.new(0, canvasWidth, canvasY.Scale, canvasY.Offset)\n\t\tend)\n\t\tmenuJanitor:add(icon.updateMenu:Connect(function()\n\t\t\tlocal maxIcons = menu:GetAttribute(\"MaxIcons\")\n\t\t\tif not maxIcons then\n\t\t\t\treturn\n\t\t\tend\n\t\t\tlocal orderedInstances = {}\n\t\t\tfor _, child in pairs(menu:GetChildren()) do\n\t\t\t\tlocal widgetUID = child:GetAttribute(\"WidgetUID\")\n\t\t\t\tif widgetUID and child.Visible then\n\t\t\t\t\ttable.insert(orderedInstances, {child, child.AbsolutePosition.X})\n\t\t\t\tend\n\t\t\tend\n\t\t\ttable.sort(orderedInstances, function(groupA, groupB)\n\t\t\t\treturn groupA[2] < groupB[2]\n\t\t\tend)\n\t\t\tlocal totalWidth = 0\n\t\t\tfor i = 1, maxIcons do\n\t\t\t\tlocal group = orderedInstances[i]\n\t\t\t\tif not group then\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\t\tlocal child = group[1]\n\t\t\t\tlocal width = child.AbsoluteSize.X + menuUIListLayout.Padding.Offset\n\t\t\t\ttotalWidth += width\n\t\t\tend\n\t\t\tmenu:SetAttribute(\"MenuWidth\", totalWidth)\n\t\tend))\n\t\tlocal function startMenuUpdate()\n\t\t\ttask.delay(0.1, function()\n\t\t\t\ticon.startMenuUpdate:Fire()\n\t\t\tend)\n\t\tend\n\t\tmenuJanitor:add(menu.ChildAdded:Connect(startMenuUpdate))\n\t\tmenuJanitor:add(menu.ChildRemoved:Connect(startMenuUpdate))\n\t\tmenuJanitor:add(menu:GetAttributeChangedSignal(\"MaxIcons\"):Connect(startMenuUpdate))\n\t\tmenuJanitor:add(menu:GetAttributeChangedSignal(\"MaxWidth\"):Connect(startMenuUpdate))\n\t\tstartMenuUpdate()\n\tend\n\t\n\ticon.menuChildAdded:Connect(totalChildrenChanged)\n\ticon.menuSet:Connect(function(arrayOfIcons)\n\t\t-- Reset any previous icons\n\t\tfor i, otherIconUID in pairs(icon.menuIcons) do\n\t\t\tlocal otherIcon = Icon.getIconByUID(otherIconUID)\n\t\t\totherIcon:destroy()\n\t\tend\n\t\t-- Apply new icons\n\t\tif type(arrayOfIcons) == \"table\" then\n\t\t\tfor i, otherIcon in pairs(arrayOfIcons) do\n\t\t\t\totherIcon:joinMenu(icon)\n\t\t\tend\n\t\tend\n\tend)\n\t\n\treturn menu\nend"
  },
  {
    "path": "src/Elements/Notice.lua",
    "content": "return function(icon, Icon)\n\n\tlocal notice = Instance.new(\"Frame\")\n\tnotice.Name = \"Notice\"\n\tnotice.ZIndex = 25\n\tnotice.AutomaticSize = Enum.AutomaticSize.X\n\tnotice.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tnotice.BorderSizePixel = 0\n\tnotice.BackgroundTransparency = 0.1\n\tnotice.BackgroundColor3 = Color3.fromRGB(255, 255, 255)\n\tnotice.Visible = false\n\tnotice.Parent = icon.widget\n\n\tlocal UICorner = Instance.new(\"UICorner\")\n\tUICorner.CornerRadius = UDim.new(1, 0)\n\tUICorner.Parent = notice\n\n\tlocal UIStroke = Instance.new(\"UIStroke\")\n\tUIStroke.Parent = notice\n\n\tlocal noticeLabel = Instance.new(\"TextLabel\")\n\tnoticeLabel.Name = \"NoticeLabel\"\n\tnoticeLabel.ZIndex = 26\n\tnoticeLabel.AnchorPoint = Vector2.new(0.5, 0.5)\n\tnoticeLabel.AutomaticSize = Enum.AutomaticSize.X\n\tnoticeLabel.Size = UDim2.new(1, 0, 1, 0)\n\tnoticeLabel.BackgroundTransparency = 1\n\tnoticeLabel.Position = UDim2.new(0.5, 0, 0.515, 0)\n\tnoticeLabel.BackgroundColor3 = Color3.fromRGB(0, 0, 0)\n\tnoticeLabel.FontSize = Enum.FontSize.Size14\n\tnoticeLabel.TextColor3 = Color3.fromRGB(0, 0, 0)\n\tnoticeLabel.Text = \"1\"\n\tnoticeLabel.TextWrapped = true\n\tnoticeLabel.TextWrap = true\n\tnoticeLabel.Font = Enum.Font.Arial\n\tnoticeLabel.Parent = notice\n\t\n\tlocal iconModule = script.Parent.Parent\n\tlocal packages = iconModule.Packages\n\tlocal Janitor = require(packages.Janitor)\n\tlocal Signal = require(packages.GoodSignal)\n\tlocal Utility = require(iconModule.Utility)\n\ticon.noticeChanged:Connect(function(totalNotices)\n\n\t\t-- Notice amount\n\t\tif not totalNotices then\n\t\t\treturn\n\t\tend\n\t\tlocal exceeded99 = totalNotices > 99\n\t\tlocal noticeDisplay = (exceeded99 and \"99+\") or totalNotices\n\t\tnoticeLabel.Text = noticeDisplay\n\t\tif exceeded99 then\n\t\t\tnoticeLabel.TextSize = 11\n\t\tend\n\n\t\t-- Should enable\n\t\tlocal enabled = true\n\t\tif totalNotices < 1 then\n\t\t\tenabled = false\n\t\tend\n\t\tlocal parentIcon = Icon.getIconByUID(icon.parentIconUID)\n\t\tlocal dropdownOrMenuActive = #icon.dropdownIcons > 0 or #icon.menuIcons > 0\n\t\tif icon.isSelected and dropdownOrMenuActive then\n\t\t\tenabled = false\n\t\telseif parentIcon and not parentIcon.isSelected then\n\t\t\tenabled = false\n\t\tend\n\t\tUtility.setVisible(notice, enabled, \"NoticeHandler\")\n\n\tend)\n\ticon.noticeStarted:Connect(function(customClearSignal, noticeId)\n\t\n\t\tif not customClearSignal then\n\t\t\tcustomClearSignal = icon.deselected\n\t\tend\n\t\tlocal parentIcon = Icon.getIconByUID(icon.parentIconUID)\n\t\tif parentIcon then\n\t\t\tparentIcon:notify(customClearSignal)\n\t\tend\n\t\t\n\t\tlocal noticeJanitor = icon.janitor:add(Janitor.new())\n\t\tlocal noticeComplete = noticeJanitor:add(Signal.new())\n\t\tnoticeJanitor:add(icon.endNotices:Connect(function()\n\t\t\tnoticeComplete:Fire()\n\t\tend))\n\t\tnoticeJanitor:add(customClearSignal:Connect(function()\n\t\t\tnoticeComplete:Fire()\n\t\tend))\n\t\tnoticeId = noticeId or Utility.generateUID()\n\t\ticon.notices[noticeId] = {\n\t\t\tcompleteSignal = noticeComplete,\n\t\t\tclearNoticeEvent = customClearSignal,\n\t\t}\n\t\tlocal function updateNotice()\n\t\t\ticon.noticeChanged:Fire(icon.totalNotices)\n\t\tend\n\t\ticon.notified:Fire(noticeId)\n\t\ticon.totalNotices += 1\n\t\tupdateNotice()\n\t\tnoticeComplete:Once(function()\n\t\t\tnoticeJanitor:destroy()\n\t\t\ticon.totalNotices -= 1\n\t\t\ticon.notices[noticeId] = nil\n\t\t\tupdateNotice()\n\t\tend)\n\tend)\n\t\n\t-- Establish the notice\n\tnotice:SetAttribute(\"ClipToJoinedParent\", true)\n\ticon:clipOutside(notice)\n\t\n\treturn notice\nend"
  },
  {
    "path": "src/Elements/Selection.lua",
    "content": "return function(Icon)\n\n\t-- Credit to lolmansReturn and Canary Software for\n\t-- retrieving these values\n\tlocal selectionContainer = Instance.new(\"Frame\")\n\tselectionContainer.Name = \"SelectionContainer\"\n\tselectionContainer.Visible = false\n\t\n\tlocal selection = Instance.new(\"Frame\")\n\tselection.Name = \"Selection\"\n\tselection.BackgroundColor3 = Color3.fromRGB(255, 255, 255)\n\tselection.BackgroundTransparency = 1\n\tselection.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tselection.BorderSizePixel = 0\n\tselection.Parent = selectionContainer\n\n\tlocal UIStroke = Instance.new(\"UIStroke\")\n\tUIStroke.Name = \"UIStroke\"\n\tUIStroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border\n\tUIStroke.Color = Color3.fromRGB(255, 255, 255)\n\tUIStroke.Thickness = 3\n\tUIStroke.Parent = selection\n\n\tlocal selectionGradient = Instance.new(\"UIGradient\")\n\tselectionGradient.Name = \"SelectionGradient\"\n\tselectionGradient.Parent = UIStroke\n\n\tlocal UICorner = Instance.new(\"UICorner\")\n\tUICorner:SetAttribute(\"Collective\", \"IconCorners\")\n\tUICorner.Name = \"UICorner\"\n\tUICorner.CornerRadius = UDim.new(1, 0)\n\tUICorner.Parent = selection\n\t\n\tlocal RunService = game:GetService(\"RunService\")\n\tlocal GuiService = game:GetService(\"GuiService\")\n\tlocal rotationSpeed = 1\n\tselection:GetAttributeChangedSignal(\"RotationSpeed\"):Connect(function()\n\t\trotationSpeed = selection:GetAttribute(\"RotationSpeed\")\n\tend)\n\tRunService.Heartbeat:Connect(function()\n\t\tif not GuiService.SelectedObject then\n\t\t\treturn\n\t\tend\n\t\tselectionGradient.Rotation = (os.clock() * rotationSpeed * 100) % 360\n\tend)\n\n\treturn selectionContainer\n\t\nend"
  },
  {
    "path": "src/Elements/Widget.lua",
    "content": "-- I named this 'Widget' instead of 'Icon' to make a clear difference between the icon *object* and\n-- the icon (aka Widget) instance.\n-- This contains the core components of the icon such as the button, image, label and notice. It's\n-- also responsible for handling the automatic resizing of the widget (based upon image visibility and text length)\n\nreturn function(icon, Icon)\n\n\tlocal widget = Instance.new(\"Frame\")\n\twidget:SetAttribute(\"WidgetUID\", icon.UID)\n\twidget.Name = \"Widget\"\n\twidget.BackgroundTransparency = 1\n\twidget.Visible = true\n\twidget.ZIndex = 20\n\twidget.Active = false\n\twidget.ClipsDescendants = true\n\n\tlocal button = Instance.new(\"Frame\")\n\tbutton.Name = \"IconButton\"\n\tbutton.Visible = true\n\tbutton.ZIndex = 2\n\tbutton.BorderSizePixel = 0\n\tbutton.Parent = widget\n\tbutton.ClipsDescendants = true\n\tbutton.Active = false -- This is essential for mobile scrollers to work when dragging\n\ticon.deselected:Connect(function()\n\t\tbutton.ClipsDescendants = true\n\t\ttask.delay(0.2, function()\n\t\t\tif icon.isSelected then\n\t\t\t\tbutton.ClipsDescendants = false\n\t\t\tend\n\t\tend)\n\tend)\n\n\t-- Account for PreferredTransparency which can be set by every player\n\tlocal GuiService = game:GetService(\"GuiService\")\n\ticon:setBehaviour(\"IconButton\", \"BackgroundTransparency\", function(value)\n\t\tlocal preference = GuiService.PreferredTransparency\n\t\tlocal newValue = value * preference\n\t\tif value == 1 then\n\t\t\treturn value\n\t\tend\n\t\treturn newValue\n\tend)\n\ticon.janitor:add(GuiService:GetPropertyChangedSignal(\"PreferredTransparency\"):Connect(function()\n\t\ticon:refreshAppearance(button, \"BackgroundTransparency\")\n\tend))\n\n\tlocal iconCorner = Instance.new(\"UICorner\")\n\ticonCorner:SetAttribute(\"Collective\", \"IconCorners\")\n\ticonCorner.Name = \"UICorner\"\n\ticonCorner.Parent = button\n\n\tlocal menu = require(script.Parent.Menu)(icon)\n\tlocal menuUIListLayout = menu.MenuUIListLayout\n\tlocal menuGap = menu.MenuGap\n\tmenu.Parent = button\n\n\tlocal iconSpot = Instance.new(\"Frame\")\n\ticonSpot.Name = \"IconSpot\"\n\ticonSpot.BackgroundColor3 = Color3.fromRGB(225, 225, 225)\n\ticonSpot.BackgroundTransparency = 0.9\n\ticonSpot.Visible = true\n\ticonSpot.AnchorPoint = Vector2.new(0, 0.5)\n\ticonSpot.ZIndex = 5\n\ticonSpot.Parent = menu\n\n\tlocal iconSpotCorner = iconCorner:Clone()\n\ticonSpotCorner.Parent = iconSpot\n\n\tlocal overlay = iconSpot:Clone()\n\toverlay.UICorner.Name = \"OverlayUICorner\"\n\toverlay.Name = \"IconOverlay\"\n\toverlay.BackgroundColor3 = Color3.fromRGB(255, 255, 255)\n\toverlay.ZIndex = iconSpot.ZIndex + 1\n\toverlay.Size = UDim2.new(1, 0, 1, 0)\n\toverlay.Position = UDim2.new(0, 0, 0, 0)\n\toverlay.AnchorPoint = Vector2.new(0, 0)\n\toverlay.Visible = false\n\toverlay.Parent = iconSpot\n\n\tlocal clickRegion = Instance.new(\"TextButton\")\n\tclickRegion:SetAttribute(\"CorrespondingIconUID\", icon.UID)\n\tclickRegion.Name = \"ClickRegion\"\n\tclickRegion.BackgroundTransparency = 1\n\tclickRegion.Visible = true\n\tclickRegion.Text = \"\"\n\tclickRegion.ZIndex = 20\n\tclickRegion.Selectable = true\n\tclickRegion.SelectionGroup = true\n\tclickRegion.Parent = iconSpot\n\t\n\tlocal Gamepad = require(script.Parent.Parent.Features.Gamepad)\n\tGamepad.registerButton(clickRegion)\n\n\tlocal clickRegionCorner = iconCorner:Clone()\n\tclickRegionCorner.Parent = clickRegion\n\n\tlocal contents = Instance.new(\"Frame\")\n\tcontents.Name = \"Contents\"\n\tcontents.BackgroundTransparency = 1\n\tcontents.Size = UDim2.fromScale(1, 1)\n\tcontents.Parent = iconSpot\n\n\tlocal contentsList = Instance.new(\"UIListLayout\")\n\tcontentsList.Name = \"ContentsList\"\n\tcontentsList.FillDirection = Enum.FillDirection.Horizontal\n\tcontentsList.VerticalAlignment = Enum.VerticalAlignment.Center\n\tcontentsList.SortOrder = Enum.SortOrder.LayoutOrder\n\tcontentsList.VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly\n\tcontentsList.Padding = UDim.new(0, 3)\n\tcontentsList.Parent = contents\n\n\tlocal paddingLeft = Instance.new(\"Frame\")\n\tpaddingLeft.Name = \"PaddingLeft\"\n\tpaddingLeft.LayoutOrder = 1\n\tpaddingLeft.ZIndex = 5\n\tpaddingLeft.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tpaddingLeft.BackgroundTransparency = 1\n\tpaddingLeft.BorderSizePixel = 0\n\tpaddingLeft.BackgroundColor3 = Color3.fromRGB(255, 255, 255)\n\tpaddingLeft.Parent = contents\n\n\tlocal paddingCenter = Instance.new(\"Frame\")\n\tpaddingCenter.Name = \"PaddingCenter\"\n\tpaddingCenter.LayoutOrder = 3\n\tpaddingCenter.ZIndex = 5\n\tpaddingCenter.Size = UDim2.new(0, 0, 1, 0)\n\tpaddingCenter.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tpaddingCenter.BackgroundTransparency = 1\n\tpaddingCenter.BorderSizePixel = 0\n\tpaddingCenter.BackgroundColor3 = Color3.fromRGB(255, 255, 255)\n\tpaddingCenter.Parent = contents\n\n\tlocal paddingRight = Instance.new(\"Frame\")\n\tpaddingRight.Name = \"PaddingRight\"\n\tpaddingRight.LayoutOrder = 5\n\tpaddingRight.ZIndex = 5\n\tpaddingRight.BorderColor3 = Color3.fromRGB(0, 0, 0)\n\tpaddingRight.BackgroundTransparency = 1\n\tpaddingRight.BorderSizePixel = 0\n\tpaddingRight.BackgroundColor3 = Color3.fromRGB(255, 255, 255)\n\tpaddingRight.Parent = contents\n\n\tlocal iconLabelContainer = Instance.new(\"Frame\")\n\ticonLabelContainer.Name = \"IconLabelContainer\"\n\ticonLabelContainer.LayoutOrder = 4\n\ticonLabelContainer.ZIndex = 3\n\ticonLabelContainer.AnchorPoint = Vector2.new(0, 0.5)\n\ticonLabelContainer.Size = UDim2.new(0, 0, 0.5, 0)\n\ticonLabelContainer.BackgroundTransparency = 1\n\ticonLabelContainer.Position = UDim2.new(0.5, 0, 0.5, 0)\n\ticonLabelContainer.Parent = contents\n\n\tlocal iconLabel = Instance.new(\"TextLabel\")\n\tlocal viewportX = workspace.CurrentCamera.ViewportSize.X+200\n\ticonLabel.Name = \"IconLabel\"\n\ticonLabel.LayoutOrder = 4\n\ticonLabel.ZIndex = 15\n\ticonLabel.AnchorPoint = Vector2.new(0, 0)\n\ticonLabel.Size = UDim2.new(0, viewportX, 1, 0)\n\ticonLabel.ClipsDescendants = false\n\ticonLabel.BackgroundTransparency = 1\n\ticonLabel.Position = UDim2.fromScale(0, 0)\n\ticonLabel.RichText = true\n\ticonLabel.TextColor3 = Color3.fromRGB(255, 255, 255)\n\ticonLabel.TextXAlignment = Enum.TextXAlignment.Left\n\ticonLabel.Text = \"\"\n\ticonLabel.TextWrapped = true\n\ticonLabel.TextWrap = true\n\ticonLabel.TextScaled = false\n\ticonLabel.Active = false\n\ticonLabel.AutoLocalize = true\n\ticonLabel.Parent = iconLabelContainer\n\n\tlocal iconImage = Instance.new(\"ImageLabel\")\n\ticonImage.Name = \"IconImage\"\n\ticonImage.LayoutOrder = 2\n\ticonImage.ZIndex = 15\n\ticonImage.AnchorPoint = Vector2.new(0, 0.5)\n\ticonImage.Size = UDim2.new(0, 0, 0.5, 0)\n\ticonImage.BackgroundTransparency = 1\n\ticonImage.Position = UDim2.new(0, 11, 0.5, 0)\n\ticonImage.ScaleType = Enum.ScaleType.Stretch\n\ticonImage.Active = false\n\ticonImage.Parent = contents\n\n\tlocal iconImageCorner = iconCorner:Clone()\n\ticonImageCorner:SetAttribute(\"Collective\", nil)\n\ticonImageCorner.CornerRadius = UDim.new(0, 0)\n\ticonImageCorner.Name = \"IconImageCorner\"\n\ticonImageCorner.Parent = iconImage\n\n\tlocal TweenService = game:GetService(\"TweenService\")\n\tlocal resizingCount = 0\n\tlocal function handleLabelAndImageChangesUnstaggered(forceUpdateString)\n\n\t\t-- We defer changes by a frame to eliminate all but 1 requests which\n\t\t-- could otherwise stack up to 20+ requests in a single frame\n\t\t-- We then repeat again once to account for any final changes\n\t\t-- Deferring is also essential because properties are set immediately\n\t\t-- afterwards (therefore calculations will use the correct values)\n\t\ttask.defer(function()\n\t\t\tlocal indicator = icon.indicator\n\t\t\tlocal usingIndicator = indicator and indicator.Visible\n\t\t\tlocal usingText = usingIndicator or iconLabel.Text ~= \"\"\n\t\t\tlocal usingImage = iconImage.Image ~= \"\" and iconImage.Image ~= nil\n\t\t\tlocal _alignment = Enum.HorizontalAlignment.Center\n\t\t\tlocal NORMAL_BUTTON_SIZE = UDim2.fromScale(1, 1)\n\t\t\tlocal buttonSize = NORMAL_BUTTON_SIZE\n\t\t\tif usingImage and not usingText then\n\t\t\t\ticonLabelContainer.Visible = false\n\t\t\t\ticonImage.Visible = true\n\t\t\t\tpaddingLeft.Visible = false\n\t\t\t\tpaddingCenter.Visible = false\n\t\t\t\tpaddingRight.Visible = false\n\t\t\telseif not usingImage and usingText then\n\t\t\t\ticonLabelContainer.Visible = true\n\t\t\t\ticonImage.Visible = false\n\t\t\t\tpaddingLeft.Visible = true\n\t\t\t\tpaddingCenter.Visible = false\n\t\t\t\tpaddingRight.Visible = true\n\t\t\telseif usingImage and usingText then\n\t\t\t\ticonLabelContainer.Visible = true\n\t\t\t\ticonImage.Visible = true\n\t\t\t\tpaddingLeft.Visible = true\n\t\t\t\tpaddingCenter.Visible = not usingIndicator\n\t\t\t\tpaddingRight.Visible = not usingIndicator\n\t\t\t\t_alignment = Enum.HorizontalAlignment.Left\n\t\t\tend\n\t\t\tbutton.Size = buttonSize\n\n\t\t\tlocal function getItemWidth(item)\n\t\t\t\tlocal targetWidth = item:GetAttribute(\"TargetWidth\") or item.AbsoluteSize.X\n\t\t\t\treturn targetWidth\n\t\t\tend\n\t\t\tlocal contentsPadding = contentsList.Padding.Offset\n\t\t\tlocal initialWidgetWidth = contentsPadding --0\n\t\t\tlocal textWidth = iconLabel.TextBounds.X\n\t\t\ticonLabelContainer.Size = UDim2.new(0, textWidth, iconLabel.Size.Y.Scale, 0)\n\t\t\tfor _, child in pairs(contents:GetChildren()) do\n\t\t\t\tif child:IsA(\"GuiObject\") and child.Visible == true then\n\t\t\t\t\tlocal itemWidth = getItemWidth(child)\n\t\t\t\t\tinitialWidgetWidth += itemWidth + contentsPadding\n\t\t\t\tend\n\t\t\tend\n\t\t\tlocal widgetMinimumWidth = widget:GetAttribute(\"MinimumWidth\")\n\t\t\tlocal widgetMinimumHeight = widget:GetAttribute(\"MinimumHeight\")\n\t\t\tlocal widgetBorderSize = widget:GetAttribute(\"BorderSize\")\n\t\t\tlocal widgetWidth = math.clamp(initialWidgetWidth, widgetMinimumWidth, viewportX)\n\t\t\tlocal menuIcons = icon.menuIcons\n\t\t\tlocal additionalWidth = 0\n\t\t\tlocal hasMenu = #menuIcons > 0\n\t\t\tlocal showMenu = hasMenu and icon.isSelected\n\t\t\tif showMenu then\n\t\t\t\tfor _, frame in pairs(menu:GetChildren()) do\n\t\t\t\t\tif frame ~= iconSpot and frame:IsA(\"GuiObject\") and frame.Visible then\n\t\t\t\t\t\tadditionalWidth += getItemWidth(frame) + menuUIListLayout.Padding.Offset\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif not iconSpot.Visible then\n\t\t\t\t\twidgetWidth -= (getItemWidth(iconSpot) + menuUIListLayout.Padding.Offset*2 + widgetBorderSize)\n\t\t\t\tend\n\t\t\t\tadditionalWidth -= (widgetBorderSize*0.5)\n\t\t\t\twidgetWidth += additionalWidth - (widgetBorderSize*0.75)\n\t\t\tend\n\t\t\tmenuGap.Visible = showMenu and iconSpot.Visible\n\t\t\tlocal desiredWidth = widget:GetAttribute(\"DesiredWidth\")\n\t\t\tif desiredWidth and widgetWidth < desiredWidth then\n\t\t\t\twidgetWidth = desiredWidth\n\t\t\tend\n\n\t\t\ticon.updateMenu:Fire()\n\t\t\tlocal preWidth = math.max(widgetWidth-additionalWidth, widgetMinimumWidth)\n\t\t\tlocal spotWidth = preWidth-(widgetBorderSize*2)\n\t\t\tlocal menuWidth = menu:GetAttribute(\"MenuWidth\")\n\t\t\tlocal totalMenuWidth = menuWidth and menuWidth + spotWidth + menuUIListLayout.Padding.Offset + 10\n\t\t\tif totalMenuWidth then\n\t\t\t\tlocal maxWidth = menu:GetAttribute(\"MaxWidth\")\n\t\t\t\tif maxWidth then\n\t\t\t\t\ttotalMenuWidth = math.max(maxWidth, widgetMinimumWidth)\n\t\t\t\tend\n\t\t\t\tmenu:SetAttribute(\"MenuCanvasWidth\", widgetWidth)\n\t\t\t\tif totalMenuWidth < widgetWidth then\n\t\t\t\t\twidgetWidth = totalMenuWidth\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tlocal style = Enum.EasingStyle.Quint\n\t\t\tlocal direction = Enum.EasingDirection.Out\n\t\t\tlocal spotWidthMax = math.max(spotWidth, getItemWidth(iconSpot), iconSpot.AbsoluteSize.X)\n\t\t\tlocal widgetWidthMax = math.max(widgetWidth, getItemWidth(widget), widget.AbsoluteSize.X)\n\t\t\tlocal SPEED = 750\n\t\t\tlocal spotTweenInfo = TweenInfo.new(spotWidthMax/SPEED, style, direction)\n\t\t\tlocal widgetTweenInfo = TweenInfo.new(widgetWidthMax/SPEED, style, direction)\n\t\t\tTweenService:Create(iconSpot, spotTweenInfo, {\n\t\t\t\tPosition = UDim2.new(0, widgetBorderSize, 0.5, 0),\n\t\t\t\tSize = UDim2.new(0, spotWidth, 1, -widgetBorderSize*2),\n\t\t\t}):Play()\n\t\t\tTweenService:Create(clickRegion, spotTweenInfo, {\n\t\t\t\tSize = UDim2.new(0, spotWidth, 1, 0),\n\t\t\t}):Play()\n\t\t\tlocal newWidgetSize = UDim2.fromOffset(widgetWidth, widgetMinimumHeight)\n\t\t\tlocal updateInstantly = widget.Size.Y.Offset ~= widgetMinimumHeight\n\t\t\tif updateInstantly then\n\t\t\t\twidget.Size = newWidgetSize\n\t\t\tend\n\t\t\twidget:SetAttribute(\"TargetWidth\", newWidgetSize.X.Offset)\n\t\t\tlocal movingTween = TweenService:Create(widget, widgetTweenInfo, {\n\t\t\t\tSize = newWidgetSize,\n\t\t\t})\n\t\t\tmovingTween:Play()\n\t\t\tresizingCount += 1\n\t\t\tfor i = 1, widgetTweenInfo.Time * 100 do\n\t\t\t\ttask.delay(i/100, function()\n\t\t\t\t\tIcon.iconChanged:Fire(icon)\n\t\t\t\tend)\n\t\t\tend\n\t\t\ttask.delay(widgetTweenInfo.Time-0.2, function()\n\t\t\t\tresizingCount -= 1\n\t\t\t\ttask.defer(function()\n\t\t\t\t\tif resizingCount == 0 then\n\t\t\t\t\t\ticon.resizingComplete:Fire()\n\t\t\t\t\tend\n\t\t\t\tend)\n\t\t\tend)\n\t\t\ticon:updateParent()\n\t\tend)\n\tend\n\tlocal Utility = require(script.Parent.Parent.Utility)\n\tlocal handleLabelAndImageChanges = Utility.createStagger(0.01, handleLabelAndImageChangesUnstaggered)\n\tlocal firstTimeSettingFontFace = true\n\ticon:setBehaviour(\"IconLabel\", \"Text\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"IconLabel\", \"FontFace\", function(value)\n\t\tlocal previousFontFace = iconLabel.FontFace\n\t\tif previousFontFace == value then\n\t\t\treturn\n\t\tend\n\t\ttask.spawn(function()\n\t\t\t--[[\n\t\t\tlocal fontLink = value.Family\n\t\t\tif string.match(fontLink, \"rbxassetid://\") then\n\t\t\t\tlocal ContentProvider = game:GetService(\"ContentProvider\")\n\t\t\t\tlocal assets = {fontLink}\n\t\t\t\tContentProvider:PreloadAsync(assets)\n\t\t\tend--]]\n\n\t\t\t-- Afaik there's no way to determine when a Font Family has\n\t\t\t-- loaded (even with ContentProvider), so we just have to try\n\t\t\t-- a few times and hope it loads within the refresh period\n\t\t\thandleLabelAndImageChanges()\n\t\t\tif firstTimeSettingFontFace then\n\t\t\t\tfirstTimeSettingFontFace = false\n\t\t\t\tfor i = 1, 10 do\n\t\t\t\t\ttask.wait(1)\n\t\t\t\t\thandleLabelAndImageChanges()\n\t\t\t\tend\n\t\t\tend\n\t\tend)\n\tend)\n\tlocal function updateBorderSize()\n\t\ttask.defer(function()\n\t\t\tlocal borderOffset = widget:GetAttribute(\"BorderSize\")\n\t\t\tlocal alignment = icon.alignment\n\t\t\tlocal alignmentOffset = (iconSpot.Visible == false and 0) or (alignment == \"Right\" and -borderOffset) or borderOffset\n\t\t\tmenu.Position = UDim2.new(0, alignmentOffset, 0, 0)\n\t\t\tmenuGap.Size = UDim2.fromOffset(borderOffset, 0)\n\t\t\tmenuUIListLayout.Padding = UDim.new(0, 0)\n\t\t\thandleLabelAndImageChanges()\n\t\tend)\n\tend\n\ticon:setBehaviour(\"Widget\", \"BorderSize\", updateBorderSize)\n\ticon:setBehaviour(\"IconSpot\", \"Visible\", updateBorderSize)\n\ticon.startMenuUpdate:Connect(handleLabelAndImageChanges)\n\ticon.updateSize:Connect(handleLabelAndImageChanges)\n\ticon:setBehaviour(\"ContentsList\", \"HorizontalAlignment\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"Widget\", \"Visible\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"Widget\", \"DesiredWidth\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"Widget\", \"MinimumWidth\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"Widget\", \"MinimumHeight\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"Indicator\", \"Visible\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"IconImageRatio\", \"AspectRatio\", handleLabelAndImageChanges)\n\ticon:setBehaviour(\"IconImage\", \"Image\", function(value)\n\t\tlocal textureId = (tonumber(value) and \"http://www.roblox.com/asset/?id=\"..value) or value or \"\"\n\t\tif iconImage.Image ~= textureId then\n\t\t\thandleLabelAndImageChanges()\n\t\tend\n\t\treturn textureId\n\tend)\n\ticon.alignmentChanged:Connect(function(newAlignment)\n\t\tif newAlignment == \"Center\" then\n\t\t\tnewAlignment = \"Left\"\n\t\tend\n\t\tmenuUIListLayout.HorizontalAlignment = Enum.HorizontalAlignment[newAlignment]\n\t\tupdateBorderSize()\n\tend)\n\n\t-- Localization support (refresh icon size whenever player changes language changes in-game)\n\tlocal Players = game:GetService(\"Players\")\n\tlocal localPlayer = Players.LocalPlayer\n\tlocal lastLocaleId = localPlayer.LocaleId\n\ticon.janitor:add(localPlayer:GetPropertyChangedSignal(\"LocaleId\"):Connect(function()\n\t\ttask.delay(0.2, function()\n\t\t\tlocal newLocaleId = localPlayer.LocaleId\n\t\t\tif newLocaleId ~= lastLocaleId then\n\t\t\t\tlastLocaleId = newLocaleId\n\t\t\t\ticon:refresh()\n\t\t\t\ttask.wait(0.5)\n\t\t\t\ticon:refresh()\n\t\t\tend\n\t\tend)\n\tend))\n\t\n\tlocal iconImageScale = Instance.new(\"NumberValue\")\n\ticonImageScale.Name = \"IconImageScale\"\n\ticonImageScale.Parent = iconImage\n\ticonImageScale:GetPropertyChangedSignal(\"Value\"):Connect(function()\n\t\ticonImage.Size = UDim2.new(iconImageScale.Value, 0, iconImageScale.Value, 0)\n\tend)\n\n\tlocal UIAspectRatioConstraint = Instance.new(\"UIAspectRatioConstraint\")\n\tUIAspectRatioConstraint.Name = \"IconImageRatio\"\n\tUIAspectRatioConstraint.AspectType = Enum.AspectType.FitWithinMaxSize\n\tUIAspectRatioConstraint.DominantAxis = Enum.DominantAxis.Height\n\tUIAspectRatioConstraint.Parent = iconImage\n\n\tlocal iconGradient = Instance.new(\"UIGradient\")\n\ticonGradient.Name = \"IconGradient\"\n\ticonGradient.Enabled = true\n\ticonGradient.Parent = button\n\n\tlocal iconSpotGradient = Instance.new(\"UIGradient\")\n\ticonSpotGradient.Name = \"IconSpotGradient\"\n\ticonSpotGradient.Enabled = true\n\ticonSpotGradient.Parent = iconSpot\n\n\treturn widget\nend"
  },
  {
    "path": "src/Features/Gamepad.lua",
    "content": "-- As the name suggests, this handles everything related to gamepads\n-- (i.e. Xbox or Playstation controllers) and their navigation\n-- I created a separate module for gamepads (and not touchpads or\n-- keyboards) because gamepads are greatly more unqiue and require\n-- additional tailored programming\n\n\n\n-- SERVICES\nlocal GamepadService = game:GetService(\"GamepadService\")\nlocal UserInputService = game:GetService(\"UserInputService\")\nlocal GuiService = game:GetService(\"GuiService\")\n\n\n\n-- LOCAL\nlocal DEFAULT_HIGHLIGHT_KEY = Enum.KeyCode.DPadUp -- The default key to highlight the topbar icon\nlocal GAMEPAD_INPUT = Enum.PreferredInput.Gamepad\nlocal Gamepad = {}\nlocal Icon\n\n\n\n-- FUNCTIONS\n-- This is called upon the Icon initializing\nfunction Gamepad.start(incomingIcon)\n\t\n\t-- Public variables\n\tIcon = incomingIcon\n\tIcon.highlightKey = if Icon.highlightKey ~= nil then Icon.highlightKey else DEFAULT_HIGHLIGHT_KEY -- What controller key to highlight the topbar (or set to false to disable)\n\tIcon.highlightIcon = false -- Change to a specific icon if you'd like to highlight a specific icon instead of the left-most\n\t\n\t-- We defer so the developer can make changes before the\n\t-- gamepad controls are initialized\n\ttask.delay(1, function()\n\t\t-- Some local utility\n\t\tlocal iconsDict = Icon.iconsDictionary\n\t\tlocal function getIconFromSelectedObject()\n\t\t\tlocal clickRegion = GuiService.SelectedObject\n\t\t\tlocal iconUID = clickRegion and clickRegion:GetAttribute(\"CorrespondingIconUID\")\n\t\t\tlocal icon = iconUID and iconsDict[iconUID]\n\t\t\treturn icon\n\t\tend\n\t\t\n\t\t-- This enables users to instantly open up their last selected icon\n\t\tlocal previousHighlightedIcon\n\t\tlocal usedIndicatorOnce = DEFAULT_HIGHLIGHT_KEY ~= Icon.highlightKey\n\t\tlocal usedBOnce = DEFAULT_HIGHLIGHT_KEY ~= Icon.highlightKey\n\t\tlocal Selection = require(script.Parent.Parent.Elements.Selection)\n\t\tlocal function updateSelectedObject()\n\t\t\tlocal icon = getIconFromSelectedObject()\n\t\t\tlocal isUsingGamepad = UserInputService.PreferredInput == GAMEPAD_INPUT\n\t\t\tif icon then\n\t\t\t\tif isUsingGamepad then\n\t\t\t\t\tlocal clickRegion = icon:getInstance(\"ClickRegion\")\n\t\t\t\t\tlocal selection = icon.selection\n\t\t\t\t\tif not selection then\n\t\t\t\t\t\tselection = icon.janitor:add(Selection(Icon))\n\t\t\t\t\t\tselection:SetAttribute(\"IgnoreVisibilityUpdater\", true)\n\t\t\t\t\t\tselection.Parent = icon.widget\n\t\t\t\t\t\ticon.selection = selection\n\t\t\t\t\t\ticon:refreshAppearance(selection) --icon:clipOutside(selection)\n\t\t\t\t\tend\n\t\t\t\t\tclickRegion.SelectionImageObject = selection.Selection\n\t\t\t\tend\n\t\t\t\tif previousHighlightedIcon and previousHighlightedIcon ~= icon then\n\t\t\t\t\tpreviousHighlightedIcon:setIndicator()\n\t\t\t\tend\n\t\t\t\tlocal newIndicator = if isUsingGamepad and not usedBOnce and not icon.parentIconUID then Enum.KeyCode.ButtonB else nil\n\t\t\t\tpreviousHighlightedIcon = icon\n\t\t\t\tIcon.lastHighlightedIcon = icon\n\t\t\t\ticon:setIndicator(newIndicator)\n\t\t\telse\n\t\t\t\tlocal newIndicator = if isUsingGamepad and not usedIndicatorOnce then Icon.highlightKey else nil\n\t\t\t\tif not previousHighlightedIcon then\n\t\t\t\t\tpreviousHighlightedIcon = Gamepad.getIconToHighlight()\n\t\t\t\tend\n\t\t\t\tif newIndicator == Icon.highlightKey then\n\t\t\t\t\t-- We only display the highlightKey once to show\n\t\t\t\t\t-- the user how to highlight the topbar icon\n\t\t\t\t\tusedIndicatorOnce = true\n\t\t\t\telse\n\t\t\t\t\t--usedBOnce = true\n\t\t\t\tend\n\t\t\t\tif previousHighlightedIcon then\n\t\t\t\t\tpreviousHighlightedIcon:setIndicator(newIndicator)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\tGuiService:GetPropertyChangedSignal(\"SelectedObject\"):Connect(updateSelectedObject)\n\n\t\t-- This listens for a gamepad being present/added/removed\n\t\tlocal function preferredInputChanged()\n\t\t\tlocal preferredInput = UserInputService.PreferredInput\n\t\t\tlocal isUsingGamepad = preferredInput == GAMEPAD_INPUT\n\n\t\t\tif not isUsingGamepad then\n\t\t\t\tusedIndicatorOnce = false\n\t\t\t\tusedBOnce = false\n\t\t\tend\n\t\t\tupdateSelectedObject()\n\t\tend\n\t\tUserInputService:GetPropertyChangedSignal(\"PreferredInput\"):Connect(preferredInputChanged)\n\t\tpreferredInputChanged()\n\n\t\t-- This allows for easy highlighting of the topbar when the\n\t\t-- when ``Icon.highlightKey`` (i.e. DPadUp) is pressed.\n\t\t-- If you'd like to disable, do ``Icon.highlightKey = false``\n\t\tUserInputService.InputBegan:Connect(function(input, touchingAnObject)\n\t\t\tif input.UserInputType == Enum.UserInputType.MouseButton1 then\n\t\t\t\t-- Sometimes the Roblox gamepad glitches when combined with a cursor\n\t\t\t\t-- This fixes that by unhighlighting if the cursor is pressed down\n\t\t\t\t-- (i.e. a mouse click)\n\t\t\t\tlocal icon = getIconFromSelectedObject()\n\t\t\t\tif icon then\n\t\t\t\t\tGuiService.SelectedObject = nil\n\t\t\t\tend\n\t\t\t\treturn\n\t\t\tend\n\t\t\tif input.KeyCode ~= Icon.highlightKey then\n\t\t\t\treturn\n\t\t\tend\n\t\t\tlocal iconToHighlight = Gamepad.getIconToHighlight()\n\t\t\tif iconToHighlight then\n\t\t\t\tif GamepadService.GamepadCursorEnabled then\n\t\t\t\t\ttask.wait(0.2)\n\t\t\t\t\tGamepadService:DisableGamepadCursor()\n\t\t\t\tend\n\t\t\t\tlocal clickRegion = iconToHighlight:getInstance(\"ClickRegion\")\n\t\t\t\tGuiService.SelectedObject = clickRegion\n\t\t\tend\n\t\tend)\n\tend)\nend\n\nfunction Gamepad.getIconToHighlight()\n\t-- If an icon has already been selected, returns the last selected icon\n\t-- Else if more than 0 icons, it selects the left-most icon\n\tlocal iconsDict = Icon.iconsDictionary\n\tlocal iconToHighlight = Icon.highlightIcon or Icon.lastHighlightedIcon\n\tif not iconToHighlight then\n\t\tlocal currentX\n\t\tfor _, icon in pairs(iconsDict) do\n\t\t\tif icon.parentIconUID then\n\t\t\t\tcontinue\n\t\t\tend\n\t\t\tlocal thisX = icon.widget.AbsolutePosition.X\n\t\t\tif not currentX or thisX < currentX then\n\t\t\t\ticonToHighlight = icon\n\t\t\t\tcurrentX = iconToHighlight.widget.AbsolutePosition.X\n\t\t\tend\n\t\tend\n\tend\n\treturn iconToHighlight\nend\n\n-- This called when the icon's ClickRegion is created\nfunction Gamepad.registerButton(buttonInstance)\n\t-- This provides a basic level of support for controllers by making\n\t-- the icons easy to highlight via the virtual cursor, then\n\t-- when selected, focuses in on the selected icon and hops\n\t-- between other nearby icons simply by toggling the joystick\n\tlocal inputBegan = false\n\tbuttonInstance.InputBegan:Connect(function(input)\n\t\t-- Two wait frames required to ensure inputBegan is detected within\n\t\t-- UserInputService.InputBegan. We do this because object.InputBegan\n\t\t-- does not return the correct input objects (unlike the service)\n\t\tinputBegan = true\n\t\ttask.wait()\n\t\ttask.wait()\n\t\tinputBegan = false\n\tend)\n\tlocal connection = UserInputService.InputBegan:Connect(function(input)\n\t\ttask.wait()\n\t\tif input.KeyCode == Enum.KeyCode.ButtonA and inputBegan then\n\t\t\t-- We focus on an icon when selected via the virtual cursor\n\t\t\ttask.wait(0.2)\n\t\t\tGamepadService:DisableGamepadCursor()\n\t\t\tGuiService.SelectedObject = buttonInstance\n\t\t\treturn\n\t\tend\n\t\tlocal isSelected = GuiService.SelectedObject == buttonInstance\n\t\tlocal unselectKeyCodes = {\"ButtonB\", \"ButtonSelect\"}\n\t\tlocal keyName = input.KeyCode.Name\n\t\tif table.find(unselectKeyCodes, keyName) and isSelected then\n\t\t\t-- We unfocus when back button is pressed, but ignore\n\t\t\t-- if the virtual cursor is disabled otherwise it will be\n\t\t\t-- impossible to select the topbar\n\t\t\tif not(keyName == \"ButtonSelect\" and not GamepadService.GamepadCursorEnabled) then\n\t\t\t\tGuiService.SelectedObject = nil\n\t\t\tend\n\t\tend\n\tend)\n\tbuttonInstance.Destroying:Once(function()\n\t\tconnection:Disconnect()\n\tend)\nend\n\n\n\nreturn Gamepad\n"
  },
  {
    "path": "src/Features/Overflow.lua",
    "content": "-- When designing your game for many devices and screen sizes, icons may occasionally\n-- particularly for smaller devices like phones, overlap with other icons or the bounds\n-- of the screen. The overflow handler solves this challenge by moving the out-of-bounds\n-- icon into an overflow menu (with a limited scrolling canvas) preventing overlaps occuring\n\n\n\n-- LOCAL\nlocal Overflow = {}\nlocal holders = {}\nlocal orderedAvailableIcons = {}\nlocal iconsDict\nlocal currentCamera = workspace.CurrentCamera\nlocal overflowIcons = {}\nlocal overflowIconUIDs = {}\nlocal Utility = require(script.Parent.Parent.Utility)\nlocal beginCheckingCenterIcons = false\nlocal beganSecondaryCenterCheck = false\nlocal Icon\n\n\n\n-- FUNCTIONS\n-- This is called upon the Icon initializing\nfunction Overflow.start(incomingIcon)\n\tIcon = incomingIcon\n\ticonsDict = Icon.iconsDictionary\n\tlocal primaryScreenGui\n\tfor _, screenGui in pairs(Icon.container) do\n\t\tif primaryScreenGui == nil and screenGui.ScreenInsets == Enum.ScreenInsets.TopbarSafeInsets then\n\t\t\tprimaryScreenGui = screenGui\n\t\tend\n\t\tfor _, holder in pairs(screenGui.Holders:GetChildren()) do\n\t\t\tif holder:GetAttribute(\"IsAHolder\") then\n\t\t\t\tholders[holder.Name] = holder\n\t\t\tend\n\t\tend\n\tend\n\n\t-- We listen for changes in icons (such as them being added, removed,\n\t-- the setting of a different alignment, the widget size changing, etc)\n\tlocal beginOverflow = false\n\tlocal updateBoundaries = Utility.createStagger(0.1, function(ignoreAvailable)\n\t\tif not beginOverflow then\n\t\t\treturn\n\t\tend\n\t\tif not ignoreAvailable then\n\t\t\tOverflow.updateAvailableIcons(\"Center\")\n\t\tend\n\t\tOverflow.updateBoundary(\"Left\")\n\t\tOverflow.updateBoundary(\"Right\")\n\tend)\n\ttask.delay(0.5, function()\n\t\tbeginOverflow = true\n\t\tupdateBoundaries()\n\tend)\n\ttask.delay(2, function()\n\t\t-- This is essential to prevent central icons begin added\n\t\t-- left or right due to incomplete UIListLayout calculations\n\t\t-- within the first few frames\n\t\tbeginCheckingCenterIcons = true\n\t\tupdateBoundaries()\n\tend)\n\tIcon.iconAdded:Connect(updateBoundaries)\n\tIcon.iconRemoved:Connect(updateBoundaries)\n\tIcon.iconChanged:Connect(updateBoundaries)\n\tcurrentCamera:GetPropertyChangedSignal(\"ViewportSize\"):Connect(function()\n\t\tupdateBoundaries(true)\n\tend)\n\tprimaryScreenGui:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(function()\n\t\tupdateBoundaries(true)\n\tend)\nend\n\nfunction Overflow.getWidth(icon, getMaxWidth)\n\tlocal widget = icon.widget\n\treturn widget:GetAttribute(\"TargetWidth\") or widget.AbsoluteSize.X\nend\n\nfunction Overflow.getAvailableIcons(alignment)\n\tlocal ourOrderedIcons = orderedAvailableIcons[alignment]\n\tif not ourOrderedIcons then\n\t\tourOrderedIcons = Overflow.updateAvailableIcons(alignment)\n\tend\n\treturn ourOrderedIcons\nend\n\nfunction Overflow.updateAvailableIcons(alignment)\n\n\t-- We only track items that are directly on the topbar (i.e. not within a parent icon)\n\tlocal ourTotal = 0\n\tlocal ourOrderedIcons = {}\n\tfor _, icon in pairs(iconsDict) do\n\t\tlocal parentUID = icon.parentIconUID\n\t\tlocal isDirectlyOnTopbar = not parentUID or overflowIconUIDs[parentUID]\n\t\tlocal isOverflow = overflowIconUIDs[icon.UID]\n\t\tif isDirectlyOnTopbar and icon.alignment == alignment and not isOverflow and icon.isEnabled then\n\t\t\ttable.insert(ourOrderedIcons, icon)\n\t\t\tourTotal += 1\n\t\tend\n\tend\n\n\t-- Ignore if no icons are available\n\tif ourTotal <= 0 then\n\t\treturn {}\n\tend\n\n\t-- This sorts these icons by smallest order, or if equal, left-most position\n\t-- (even for the right alignment because all icons are sorted left-to-right)\n\ttable.sort(ourOrderedIcons, function(iconA, iconB)\n\t\tlocal orderA = iconA.widget.LayoutOrder\n\t\tlocal orderB = iconB.widget.LayoutOrder\n\t\tlocal hasParentA = iconA.parentIconUID\n\t\tlocal hasParentB = iconB.parentIconUID\n\t\tif hasParentA == hasParentB then\n\t\t\tif orderA < orderB then\n\t\t\t\treturn true\n\t\t\tend\n\t\t\tif orderA > orderB then\n\t\t\t\treturn false\n\t\t\tend\n\t\t\treturn iconA.widget.AbsolutePosition.X < iconB.widget.AbsolutePosition.X\n\t\telseif hasParentB then\n\t\t\treturn false\n\t\telseif hasParentA then\n\t\t\treturn true\n\t\tend\n\t\treturn nil\n\tend)\n\n\t-- Finish up\n\torderedAvailableIcons[alignment] = ourOrderedIcons\n\treturn ourOrderedIcons\n\nend\n\nfunction Overflow.getRealXPositions(alignment, orderedIcons)\n\t-- We calculate the the absolute position of icons instead of reading\n\t-- directly to determine where they would be if not within an overflow\n\tlocal isLeft = alignment == \"Left\"\n\tlocal holder = holders[alignment]\n\tlocal holderXPos = holder.AbsolutePosition.X\n\tlocal holderXSize = holder.AbsoluteSize.X\n\tlocal holderUIList = holder.UIListLayout\n\tlocal topbarInset = holderUIList.Padding.Offset\n\tlocal absoluteX = (isLeft and holderXPos) or holderXPos + holderXSize\n\tlocal realXPositions = {}\n\tif isLeft then\n\t\tUtility.reverseTable(orderedIcons)\n\tend\n\tfor i = #orderedIcons, 1, -1 do\n\t\tlocal icon = orderedIcons[i]\n\t\tlocal sizeX = Overflow.getWidth(icon)\n\t\tif not isLeft then\n\t\t\tabsoluteX -= sizeX\n\t\tend\n\t\trealXPositions[icon.UID] = absoluteX\n\t\tif isLeft then\n\t\t\tabsoluteX += sizeX\n\t\tend\n\t\tabsoluteX += (isLeft and topbarInset) or -topbarInset\n\tend\n\treturn realXPositions\nend\n\nfunction Overflow.updateBoundary(alignment)\n\n\t-- We only track items that are directly on the topbar (i.e. not within a parent icon) or within an overflow\n\tlocal holder = holders[alignment]\n\tlocal holderUIList = holder.UIListLayout\n\tlocal holderXPos = holder.AbsolutePosition.X\n\tlocal holderXSize = holder.AbsoluteSize.X\n\tlocal topbarInset = holderUIList.Padding.Offset\n\tlocal topbarPadding = holderUIList.Padding.Offset\n\tlocal BOUNDARY_GAP = topbarInset\n\tlocal ourOrderedIcons = Overflow.updateAvailableIcons(alignment)\n\tlocal boundWidth = 0\n\tlocal ourTotal = 0\n\tfor _, icon in pairs(ourOrderedIcons) do\n\t\tboundWidth += Overflow.getWidth(icon) + topbarPadding\n\t\tourTotal += 1\n\tend\n\tif ourTotal <= 0 then\n\t\treturn\n\tend\n\t\n\t-- These are the icons with menus which icons will be moved into\n\t-- when overflowing\n\tlocal isCentral = alignment == \"Center\"\n\tlocal isLeft = alignment == \"Left\"\n\tlocal isRight = not isLeft\n\tlocal overflowIcon = overflowIcons[alignment]\n\tif not overflowIcon and not isCentral and #ourOrderedIcons > 0 then\n\t\tlocal order = (isLeft and -9999999) or 9999999\n\t\toverflowIcon = Icon.new()--:setLabel(`{alignment}`)\n\t\toverflowIcon:setImage(6069276526, \"Deselected\")\n\t\toverflowIcon:setName(\"Overflow\"..alignment)\n\t\toverflowIcon:setOrder(order)\n\t\toverflowIcon:setAlignment(alignment)\n\t\toverflowIcon:autoDeselect(false)\n\t\toverflowIcon.isAnOverflow = true\n\t\t--overflowIcon:freezeMenu()\n\t\toverflowIcon:select(\"OverflowStart\", overflowIcon)\n\t\toverflowIcon:setEnabled(false)\n\t\toverflowIcons[alignment] = overflowIcon\n\t\toverflowIconUIDs[overflowIcon.UID] = true\n\t\tif not Icon.closeableOverflowMenus then\n\t\t\tlocal iconSpot = overflowIcon:getInstance(\"IconSpot\")\n\t\t\ticonSpot.Visible = false\n\t\tend\n\tend\n\n\t-- The default boundary is the point where both the left-most-right-icon\n\t-- and left-most-right-icon meet OR the opposite side of the screen\n\tlocal oppositeAlignment = (alignment == \"Left\" and \"Right\") or \"Left\"\n\tlocal oppositeOrderedIcons = Overflow.updateAvailableIcons(oppositeAlignment)\n\tlocal nearestOppositeIcon = (isLeft and oppositeOrderedIcons[1]) or (isRight and oppositeOrderedIcons[#oppositeOrderedIcons])\n\tlocal oppositeOverflowIcon = overflowIcons[oppositeAlignment]\n\tlocal boundary = (isLeft and holderXPos + holderXSize) or holderXPos\n\tif nearestOppositeIcon then\n\t\tlocal oppositeRealXPositions = Overflow.getRealXPositions(oppositeAlignment, oppositeOrderedIcons)\n\t\tlocal oppositeX = oppositeRealXPositions[nearestOppositeIcon.UID]\n\t\tlocal oppositeXSize = Overflow.getWidth(nearestOppositeIcon)\n\t\tboundary = (isLeft and oppositeX - BOUNDARY_GAP) or oppositeX + oppositeXSize + BOUNDARY_GAP\n\tend\n\t\n\t-- We get the left-most icon (if left alignment) or right-most-icon (if\n\t-- right alignment) of the central icons group to see if we need to change\n\t-- the boundary (if the central icon boundary is smaller than the alignment\n\t-- boundary then we use the central)\n\tlocal totalChecks = 0\n\tlocal usingNearestCenter = false\n\tlocal function checkToShiftCentralIcon()\n\t\tlocal centerOrderedIcons = Overflow.getAvailableIcons(\"Center\")\n\t\tlocal centerPos = (isLeft and 1) or #centerOrderedIcons\n\t\tlocal nearestCenterIcon = centerOrderedIcons[centerPos]\n\t\tlocal function secondaryCheck()\n\t\t\tif not beganSecondaryCenterCheck then\n\t\t\t\tbeganSecondaryCenterCheck = true\n\t\t\t\ttask.delay(3, Overflow.updateBoundary, alignment)\n\t\t\tend\n\t\tend\n\t\tif nearestCenterIcon and not nearestCenterIcon.hasRelocatedInOverflow then\n\t\t\tlocal ourNearestIcon = (isLeft and ourOrderedIcons[#ourOrderedIcons]) or (isRight and ourOrderedIcons[1])\n\t\t\tlocal centralNearestXPos = nearestCenterIcon.widget.AbsolutePosition.X\n\t\t\tlocal ourNearestXPos = ourNearestIcon.widget.AbsolutePosition.X\n\t\t\tlocal ourNearestXSize = Overflow.getWidth(ourNearestIcon)\n\t\t\tlocal centerBoundary = (isLeft and centralNearestXPos-BOUNDARY_GAP) or centralNearestXPos + Overflow.getWidth(nearestCenterIcon) + BOUNDARY_GAP\n\t\t\tlocal removeBoundary = (isLeft and ourNearestXPos + ourNearestXSize) or ourNearestXPos\n\t\t\tlocal hasShifted = false\n\t\t\tif isLeft then\n\t\t\t\tif centerBoundary < removeBoundary then\n\t\t\t\t\tif not beginCheckingCenterIcons then\n\t\t\t\t\t\tsecondaryCheck()\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\tnearestCenterIcon:align(\"Left\")\n\t\t\t\t\tnearestCenterIcon.hasRelocatedInOverflow = true\n\t\t\t\t\thasShifted = true\n\t\t\t\tend\n\t\t\telseif isRight then\n\t\t\t\tif centerBoundary > removeBoundary then\n\t\t\t\t\tif not beginCheckingCenterIcons or removeBoundary < 0 then\n\t\t\t\t\t\tsecondaryCheck()\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\tnearestCenterIcon:align(\"Right\")\n\t\t\t\t\tnearestCenterIcon.hasRelocatedInOverflow = true\n\t\t\t\t\thasShifted = true\n\t\t\t\tend\n\t\t\tend\n\t\t\tif hasShifted then\n\t\t\t\ttotalChecks += 1\n\t\t\t\tif totalChecks <= 4 then\n\t\t\t\t\tOverflow.updateAvailableIcons(\"Center\")\n\t\t\t\t\tcheckToShiftCentralIcon()\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\tcheckToShiftCentralIcon()\n\t\n\t--[[\n\tThis updates the maximum size of the overflow menus\n\tThe menu determines its bounds from the smallest of either:\n\t \t1. The closest center-aligned icon (i.e. the boundary)\n\t \t2. The edge of the opposite overflow menu UNLESS...\n\t \t3. ... the edge exceeds more than half the screenGui\n\t--]]\n\tif overflowIcon then\n\t\tlocal menuBoundary = boundary\n\t\tlocal menu = overflowIcon:getInstance(\"Menu\")\n\t\tlocal holderXEndPos = holderXPos + holderXSize\n\t\tlocal menuWidth = holderXSize\n\t\tif menu and oppositeOverflowIcon then\n\t\t\tlocal oppositeWidget = oppositeOverflowIcon.widget\n\t\t\tlocal oppositeXPos = oppositeWidget.AbsolutePosition.X\n\t\t\tlocal oppositeXSize = Overflow.getWidth(oppositeOverflowIcon)\n\t\t\tlocal oppositeBoundary = (isLeft and oppositeXPos - BOUNDARY_GAP) or oppositeXPos + oppositeXSize + BOUNDARY_GAP\n\t\t\tlocal oppositeMenu = oppositeOverflowIcon:getInstance(\"Menu\")\n\t\t\tlocal isDominant = menu.AbsoluteCanvasSize.X >= oppositeMenu.AbsoluteCanvasSize.X\n\t\t\tif not usingNearestCenter then\n\t\t\t\tlocal halfwayXPos = holderXPos + holderXSize/2\n\t\t\t\tlocal halfwayBoundary = (isLeft and halfwayXPos - BOUNDARY_GAP/2) or halfwayXPos + BOUNDARY_GAP/2\n\t\t\t\tmenuBoundary = halfwayBoundary\n\t\t\t\tif isDominant then\n\t\t\t\t\tmenuBoundary = oppositeBoundary\n\t\t\t\tend\n\t\t\tend\n\t\t\tmenuWidth = (isLeft and menuBoundary - holderXPos) or (holderXEndPos - menuBoundary)\n\t\tend\n\t\tlocal currentMaxWidth = menu and menu:GetAttribute(\"MaxWidth\")\n\t\tmenuWidth = Utility.round(menuWidth)\n\t\tif menu and currentMaxWidth ~= menuWidth then\n\t\t\tmenu:SetAttribute(\"MaxWidth\", menuWidth)\n\t\tend\n\tend\n\n\t-- Parent ALL icons of that alignment into the overflow if at least on\n\t-- sibling exceeds the bounds.\n\t-- We calculate the the absolute position of icons instead of reading\n\t-- directly to determine where they would be if not within an overflow\n\tlocal joinOverflow = false\n\tlocal realXPositions = Overflow.getRealXPositions(alignment, ourOrderedIcons)\n\tfor i = #ourOrderedIcons, 1, -1 do\n\t\tlocal icon = ourOrderedIcons[i]\n\t\tlocal widgetX = Overflow.getWidth(icon)\n\t\tlocal xPos = realXPositions[icon.UID]\n\t\tif (isLeft and xPos + widgetX >= boundary) or (isRight and xPos <= boundary) then\n\t\t\tjoinOverflow = true\n\t\tend\n\tend\n\tfor i = #ourOrderedIcons, 1, -1 do\n\t\tlocal icon = ourOrderedIcons[i]\n\t\tlocal isOverflow = overflowIconUIDs[icon.UID]\n\t\tif not isOverflow then\n\t\t\tif joinOverflow and not icon.parentIconUID then\n\t\t\t\ticon:joinMenu(overflowIcon)\n\t\t\telseif not joinOverflow and icon.parentIconUID then\n\t\t\t\ticon:leave()\n\t\t\tend\n\t\tend\n\tend\n\t\n\t-- Hide the overflows when not in use\n\tif overflowIcon.isEnabled ~= joinOverflow then\n\t\toverflowIcon:setEnabled(joinOverflow)\n\tend\n\t\n\t-- Have the menus auto selected\n\tif overflowIcon.isEnabled and not overflowIcon.overflowAlreadyOpened then\n\t\toverflowIcon.overflowAlreadyOpened = true\n\t\toverflowIcon:select()\n\tend\n\nend\n\n\n\nreturn Overflow"
  },
  {
    "path": "src/Features/Themes/Classic.lua",
    "content": "-- This is to provide backwards compatability with the old Roblox\n-- topbar while experiences transition over to the new topbar\n-- You don't need to apply this yourself, topbarplus automatically\n-- applies it if the old roblox topbar is detected\n\n\nreturn {\n\t{\"Selection\", \"Size\", UDim2.new(1, -6, 1, -5)},\n\t{\"Selection\", \"Position\", UDim2.new(0, 3, 0, 3)},\n\t\n\t{\"Widget\", \"MinimumWidth\", 32, \"Deselected\"},\n\t{\"Widget\", \"MinimumHeight\", 32, \"Deselected\"},\n\t{\"Widget\", \"BorderSize\", 0, \"Deselected\"},\n\t{\"IconCorners\", \"CornerRadius\", UDim.new(0, 9), \"Deselected\"},\n\t{\"IconButton\", \"BackgroundTransparency\", 0.5, \"Deselected\"},\n\t{\"IconLabel\", \"TextSize\", 14, \"Deselected\"},\n\t{\"Dropdown\", \"BackgroundTransparency\", 0.5, \"Deselected\"},\n\t{\"Notice\", \"Position\", UDim2.new(1, -12, 0, -3), \"Deselected\"},\n\t{\"Notice\", \"Size\", UDim2.new(0, 15, 0, 15), \"Deselected\"},\n\t{\"NoticeLabel\", \"TextSize\", 11, \"Deselected\"},\n\t\n\t{\"IconSpot\", \"BackgroundColor3\", Color3.fromRGB(0, 0, 0), \"Selected\"},\n\t{\"IconSpot\", \"BackgroundTransparency\", 0.702, \"Selected\"},\n\t{\"IconSpotGradient\", \"Enabled\", false, \"Selected\"},\n\t{\"IconOverlay\", \"BackgroundTransparency\", 0.97, \"Selected\"},\n\t\n}"
  },
  {
    "path": "src/Features/Themes/Default.lua",
    "content": "-- Themes in v3 work simply by applying the value (agument[3])\n-- to the property (agument[2]) of an instance within the icon which\n-- matches the name of argument[1]. Argument[1] can also be used to\n-- specify a collection of instances with a corresponding 'collective'\n-- value. A colletive is simply an attribute applied to some instances\n-- within the icon to group them together (such as \"IconCorners\").\n-- If the property (argument[2]) does not exist within the instance,\n-- it will instead be applied as an attribute on the instance:\n-- (i.e. ``instance:SetAttribute(argument[2], [argument[3])``)\n-- Use argument[4] to specify a state: \"Deselected\", \"Selected\"\n-- or \"Viewing\". If argument[4] is empty the state will default\n-- to \"Deselected\".\n-- I've designed themes this way so you have full control over\n-- the appearance of the widget and its descendants\n\n\nreturn {\n\t\n\t-- When no state is specified the modification is applied to *all* states (Deselected, Selected and Viewing)\n\t{\"IconCorners\", \"CornerRadius\", UDim.new(1, 0)},\n\t{\"Selection\", \"RotationSpeed\", 1},\n\t{\"Selection\", \"Size\", UDim2.new(1, 0, 1, 1)},\n\t{\"Selection\", \"Position\", UDim2.new(0, 0, 0, 0)},\n\t{\"SelectionGradient\", \"Color\", ColorSequence.new({\n\t\tColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 255)),\n\t\tColorSequenceKeypoint.new(1, Color3.fromRGB(86, 86, 86)),\n\t})},\n\t\n\t-- When the icon is deselected\n\t{\"IconImage\", \"Image\", \"\", \"Deselected\"},\n\t{\"IconLabel\", \"Text\", \"\", \"Deselected\"},\n\t{\"IconLabel\", \"Position\", UDim2.fromOffset(0, 0), \"Deselected\"}, -- 0, -1\n\t{\"Widget\", \"DesiredWidth\", 44, \"Deselected\"},\n\t{\"Widget\", \"MinimumWidth\", 44, \"Deselected\"},\n\t{\"Widget\", \"MinimumHeight\", 44, \"Deselected\"},\n\t{\"Widget\", \"BorderSize\", 4, \"Deselected\"},\n  \t{\"IconButton\", \"BackgroundColor3\", Color3.fromRGB(18, 18, 21), \"Deselected\"},\n\t{\"IconButton\", \"BackgroundTransparency\", 0.08, \"Deselected\"},\n\t{\"IconImageScale\", \"Value\", 0.5, \"Deselected\"},\n\t{\"IconImageCorner\", \"CornerRadius\", UDim.new(0, 0), \"Deselected\"},\n\t{\"IconImage\", \"ImageColor3\", Color3.fromRGB(255, 255, 255), \"Deselected\"},\n\t{\"IconImage\", \"ImageTransparency\", 0, \"Deselected\"},\n\t{\"IconImageRatio\", \"AspectRatio\", 1, \"Deselected\"},\n\t{\"IconLabel\", \"FontFace\", Font.new(\"rbxasset://fonts/families/BuilderSans.json\", Enum.FontWeight.Bold, Enum.FontStyle.Normal), \"Deselected\"},\n\t{\"IconLabel\", \"TextSize\", 16, \"Deselected\"},\n\t{\"IconSpot\", \"BackgroundTransparency\", 1, \"Deselected\"},\n\t{\"IconOverlay\", \"BackgroundTransparency\", 0.85, \"Deselected\"},\n\t{\"IconSpotGradient\", \"Enabled\", false, \"Deselected\"},\n\t{\"IconGradient\", \"Enabled\", false, \"Deselected\"},\n\t{\"ClickRegion\", \"Active\", true, \"Deselected\"},  -- This is set to false within scrollers to ensure scroller can be dragged on mobile\n\t{\"Menu\", \"Active\", false, \"Deselected\"},\n\t{\"ContentsList\", \"HorizontalAlignment\", Enum.HorizontalAlignment.Center, \"Deselected\"},\n  \t{\"Dropdown\", \"BackgroundColor3\", Color3.fromRGB(18, 18, 21), \"Deselected\"},\n\t{\"Dropdown\", \"BackgroundTransparency\", 0.08, \"Deselected\"},\n\t{\"Dropdown\", \"MaxIcons\", 4.5, \"Deselected\"},\n\t{\"Menu\", \"MaxIcons\", 4, \"Deselected\"},\n\t{\"Notice\", \"Position\", UDim2.new(1, -12, 0, -1), \"Deselected\"},\n\t{\"Notice\", \"Size\", UDim2.new(0, 20, 0, 20), \"Deselected\"},\n\t{\"NoticeLabel\", \"TextSize\", 13, \"Deselected\"},\n\t{\"PaddingLeft\", \"Size\", UDim2.new(0, 9, 1, 0), \"Deselected\"},\n\t{\"PaddingRight\", \"Size\", UDim2.new(0, 11, 1, 0), \"Deselected\"},\n\t\n\t-- When the icon is selected\n\t-- Selected also inherits everything from Deselected if nothing is set\n\t{\"IconSpot\", \"BackgroundTransparency\", 0.7, \"Selected\"},\n\t{\"IconSpot\", \"BackgroundColor3\", Color3.fromRGB(255, 255, 255), \"Selected\"},\n\t{\"IconSpotGradient\", \"Enabled\", true, \"Selected\"},\n\t{\"IconSpotGradient\", \"Rotation\", 45, \"Selected\"},\n\t{\"IconSpotGradient\", \"Color\", ColorSequence.new(Color3.fromRGB(96, 98, 100), Color3.fromRGB(77, 78, 80)), \"Selected\"},\n\t\n\t\n\t-- When a cursor is hovering above, a controller highlighting, or touchpad (mobile) pressing (but not released)\n\t--{\"IconSpot\", \"BackgroundTransparency\", 0.75, \"Viewing\"},\n\t\n}"
  },
  {
    "path": "src/Features/Themes/init.lua",
    "content": "-- The functions here are dedicated solely to managing theme state\n-- and updating the appearance of instances to match that state.\n-- You don't need to use any of these functions, the useful ones\n-- have been abstracted as icon methods\n\n\n\n-- LOCAL\nlocal Themes = {}\nlocal Utility = require(script.Parent.Parent.Utility)\nlocal baseTheme = require(script.Default)\n\n\n\n-- FUNCTIONS\nfunction Themes.getThemeValue(stateGroup, instanceName, property, iconState)\n\tif stateGroup then\n\t\tfor _, detail in pairs(stateGroup) do\n\t\t\tlocal checkingInstanceName, checkingPropertyName, checkingValue = unpack(detail)\n\t\t\tif instanceName == checkingInstanceName and property == checkingPropertyName then\n\t\t\t\treturn checkingValue\n\t\t\tend\n\t\tend\n\tend\n\treturn nil\nend\n\nfunction Themes.getInstanceValue(instance, property)\n\tlocal success, value = pcall(function()\n\t\treturn instance[property]\n\tend)\n\tif not success then\n\t\tvalue = instance:GetAttribute(property)\n\tend\n\treturn value\nend\n\nfunction Themes.getRealInstance(instance)\n\tif not instance:GetAttribute(\"IsAClippedClone\") then\n\t\treturn\n\tend\n\tlocal originalInstance = instance:FindFirstChild(\"OriginalInstance\")\n\tif not originalInstance then\n\t\treturn\n\tend\n\treturn originalInstance.Value\nend\n\nfunction Themes.getClippedClone(instance)\n\tif not instance:GetAttribute(\"HasAClippedClone\") then\n\t\treturn\n\tend\n\tlocal clippedClone = instance:FindFirstChild(\"ClippedClone\")\n\tif not clippedClone then\n\t\treturn\n\tend\n\treturn clippedClone.Value\nend\n\nfunction Themes.refresh(icon, instance, specificProperty)\n\t-- Some instances such as notices need immediate refreshing upon creation as\n\t-- they're added in after the initial refresh period\n\tif specificProperty then\n\t\tlocal stateGroup = icon:getStateGroup()\n\t\tlocal value = Themes.getThemeValue(stateGroup, instance.Name, specificProperty) or Themes.getInstanceValue(instance, specificProperty)\n\t\tThemes.apply(icon, instance, specificProperty, value, true)\n\t\treturn\n\tend\n\t-- If no property is specified we update all properties that exist within\n\t-- the applied theme appearance\n\tlocal stateGroup = icon:getStateGroup()\n\tif not stateGroup then\n\t\treturn\n\tend\n\tlocal validInstances = {[instance.Name] = instance}\n\tfor _, child in pairs(instance:GetDescendants()) do\n\t\tlocal collective = child:GetAttribute(\"Collective\")\n\t\tif collective then\n\t\t\tvalidInstances[collective] = child\n\t\tend\n\t\tvalidInstances[child.Name] = child\n\tend\n\tfor _, detail in pairs(stateGroup) do\n\t\tlocal checkingInstanceName, checkingPropertyName, checkingValue = unpack(detail)\n\t\tlocal instanceToUpdate = validInstances[checkingInstanceName]\n\t\tif instanceToUpdate then\n\t\t\tThemes.apply(icon, instanceToUpdate.Name, checkingPropertyName, checkingValue, true)\n\t\tend\n\tend\n\treturn\nend\n\nfunction Themes.apply(icon, collectiveOrInstanceNameOrInstance, property, value, forceApply)\n\t-- This is responsible for **applying** appearance changes to instances within the icon\n\t-- however it IS NOT responsible for updating themes. Use :modifyTheme for that.\n\t-- This also calls callbacks given by :setBehaviour before applying these property changes\n\t-- to the given instances\n\tif icon.isDestroyed then\n\t\treturn\n\tend\n\tlocal instances\n\tlocal collectiveOrInstanceName = collectiveOrInstanceNameOrInstance\n\tif typeof(collectiveOrInstanceNameOrInstance) == \"Instance\" then\n\t\tinstances = {collectiveOrInstanceNameOrInstance}\n\t\tcollectiveOrInstanceName = collectiveOrInstanceNameOrInstance.Name\n\telse\n\t\tinstances = icon:getInstanceOrCollective(collectiveOrInstanceNameOrInstance)\n\tend\n\tlocal key = collectiveOrInstanceName..\"-\"..property\n\tlocal customBehaviour = icon.customBehaviours[key]\n\tfor _, instance in pairs(instances) do\n\t\tlocal clippedClone = Themes.getClippedClone(instance)\n\t\tif clippedClone then\n\t\t\t-- This means theme effects are applied to both the original\n\t\t\t-- instance and its clone (instead of just the instance).\n\t\t\t-- This is important for some properties such as position\n\t\t\t-- and size which might be dictated by the clone\n\t\t\ttable.insert(instances, clippedClone)\n\t\tend\n\tend\n\tfor _, instance in pairs(instances) do\n\t\tif property == \"Position\" and Themes.getClippedClone(instance) then\n\t\t\t-- The clone manages the position of the real instance so ignore\n\t\t\tcontinue\n\t\telseif property == \"Size\" and Themes.getRealInstance(instance) then\n\t\t\t-- The real instance manages the size of the clone so ignore\n\t\t\tcontinue\n\t\tend\n\t\tlocal currentValue = Themes.getInstanceValue(instance, property)\n\t\tif not forceApply and value == currentValue then\n\t\t\tcontinue\n\t\tend\n\t\tif customBehaviour then\n\t\t\tlocal newValue = customBehaviour(value, instance, property)\n\t\t\tif newValue ~= nil then\n\t\t\t\tvalue = newValue\n\t\t\tend\n\t\tend\n\t\tlocal success = pcall(function()\n\t\t\tinstance[property] = value\n\t\tend)\n\t\tif not success then\n\t\t\t-- If property is not a real property, we set\n\t\t\t-- the value as an attribute instead. This is useful\n\t\t\t-- for instance in :setWidth where we also want to\n\t\t\t-- specify a desired width for every state which can\n\t\t\t-- then be easily read by the widget element\n\t\t\tinstance:SetAttribute(property, value)\n\t\tend\n\tend\nend\n\nfunction Themes.getModifications(modifications)\n\tif typeof(modifications[1]) ~= \"table\" then\n\t\t-- This enables users to do :modifyTheme({a,b,c,d})\n\t\t-- in addition of :modifyTheme({{a,b,c,d}})\n\t\tmodifications = {modifications}\n\tend\n\treturn modifications\nend\n\nfunction Themes.merge(detail, modification, callback)\n\tlocal instanceName, property, value, stateName = table.unpack(modification)\n\tlocal checkingInstanceName, checkingPropertyName, _, checkingStateName = table.unpack(detail)\n\tif instanceName == checkingInstanceName and property == checkingPropertyName and Themes.statesMatch(stateName, checkingStateName) then\n\t\tdetail[3] = value\n\t\tif callback then\n\t\t\tcallback(detail)\n\t\tend\n\t\treturn true\n\tend\n\treturn false\nend\n\nfunction Themes.modify(icon, modifications, modificationsUID)\n\t-- This is what the 'old set' used to do (although for clarity that behaviour has now been\n\t-- split into two methods, .modifyTheme and .apply).\n\t-- modifyTheme is responsible for UPDATING the internal values within a theme for a particular\n\t-- state, then checking to see if the appearance of the icon needs to be updated.\n\t-- If no iconState is specified, the change is applied to both Deselected and Selected\n\t-- A modification can also be 'undone' using :removeModification and passing in\n\t-- the UID returned from this method\n\ttask.spawn(function()\n\t\tmodificationsUID = modificationsUID or Utility.generateUID()\n\t\tmodifications = Themes.getModifications(modifications)\n\t\tfor _, modification in pairs(modifications) do\n\t\t\tlocal instanceName, property, value, iconState = table.unpack(modification)\n\t\t\tif iconState == nil then\n\t\t\t\t-- If no state specified, apply to all states\n\t\t\t\tThemes.modify(icon, {instanceName, property, value, \"Selected\"}, modificationsUID)\n\t\t\t\tThemes.modify(icon, {instanceName, property, value, \"Viewing\"}, modificationsUID)\n\t\t\tend\n\t\t\tlocal chosenState = Utility.formatStateName(iconState or \"Deselected\")\n\t\t\tlocal stateGroup = icon:getStateGroup(chosenState)\n\t\t\tlocal function nowSetIt()\n\t\t\t\tif chosenState == icon.activeState then\n\t\t\t\t\tThemes.apply(icon, instanceName, property, value)\n\t\t\t\tend\n\t\t\tend\n\t\t\tlocal function updateRecord()\n\t\t\t\tfor stateName, detail in pairs(stateGroup) do\n\t\t\t\t\tlocal didMerge = Themes.merge(detail, modification, function(detail)\n\t\t\t\t\t\tdetail[5] = modificationsUID\n\t\t\t\t\t\tnowSetIt()\n\t\t\t\t\tend)\n\t\t\t\t\tif didMerge then\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tlocal detail = {instanceName, property, value, chosenState, modificationsUID}\n\t\t\t\ttable.insert(stateGroup, detail)\n\t\t\t\tnowSetIt()\n\t\t\tend\n\t\t\tupdateRecord()\n\t\tend\n\tend)\n\treturn modificationsUID\nend\n\nfunction Themes.remove(icon, modificationsUID)\n\tfor iconState, stateGroup in pairs(icon.appearance) do\n\t\tfor i = #stateGroup, 1, -1 do\n\t\t\tlocal detail = stateGroup[i]\n\t\t\tlocal checkingUID = detail[5]\n\t\t\tif checkingUID == modificationsUID then\n\t\t\t\ttable.remove(stateGroup, i)\n\t\t\tend\n\t\tend\n\tend\n\tThemes.rebuild(icon)\nend\n\nfunction Themes.removeWith(icon, instanceName, property, state)\n\tfor iconState, stateGroup in pairs(icon.appearance) do\n\t\tif state == iconState or not state then\n\t\t\tfor i = #stateGroup, 1, -1 do\n\t\t\t\tlocal detail = stateGroup[i]\n\t\t\t\tlocal detailName = detail[1]\n\t\t\t\tlocal detailProperty = detail[2]\n\t\t\t\tif detailName == instanceName and detailProperty == property then\n\t\t\t\t\ttable.remove(stateGroup, i)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\tThemes.rebuild(icon)\nend\n\nfunction Themes.change(icon)\n\t-- This changes the theme to the appearance of whatever\n\t-- state is currently active\n\tlocal stateGroup = icon:getStateGroup()\n\tfor _, detail in pairs(stateGroup) do\n\t\tlocal instanceName, property, value = unpack(detail)\n\t\tThemes.apply(icon, instanceName, property, value)\n\tend\nend\n\nfunction Themes.set(icon, theme)\n\t-- This is responsible for processing the final appearance of a given theme (such as\n\t-- ensuring Deselected merge into missing Selected, saving that internal state,\n\t-- then checking to see if the appearance of the icon needs to be updated\n\tlocal themesJanitor = icon.themesJanitor\n\tthemesJanitor:clean()\n\tthemesJanitor:add(icon.stateChanged:Connect(function()\n\t\tThemes.change(icon)\n\tend))\n\tif typeof(theme) == \"Instance\" and theme:IsA(\"ModuleScript\") then\n\t\ttheme = require(theme)\n\tend\n\ticon.appliedTheme = theme\n\tThemes.rebuild(icon)\nend\n\nfunction Themes.statesMatch(state1, state2)\n\t-- States match if they have the same name OR if nil (because unspecified represents all states)\n\tlocal state1lower = (state1 and string.lower(state1))\n\tlocal state2lower = (state2 and string.lower(state2))\n\treturn state1lower == state2lower or not state1 or not state2\nend\n\nfunction Themes.rebuild(icon)\n\t-- A note for my future self: this code can be optimised further by\n\t-- converting appearance into a instanceName-property dictionary\n\t-- as apposed to an array of every potential change. When converting\n\t-- in the future, .modify and .apply would also have to be updated.\n\tlocal appliedTheme = icon.appliedTheme\n\tlocal statesArray = {\"Deselected\", \"Selected\", \"Viewing\"}\n\tlocal function generateTheme()\n\t\tfor _, stateName in pairs(statesArray) do\n\t\t\t-- This applies themes in layers\n\t\t\t-- The last layers take higher priority as they overwrite\n\t\t\t-- any duplicate earlier applied effects\n\t\t\tlocal stateAppearance = {}\n\t\t\tlocal function updateDetails(theme, incomingStateName)\n\t\t\t\t-- This ensures there's always a base 'default' layer\n\t\t\t\tif not theme then\n\t\t\t\t\treturn\n\t\t\t\tend\n\t\t\t\tfor _, detail in pairs(theme) do\n\t\t\t\t\tlocal modificationsUID = detail[5]\n\t\t\t\t\tlocal detailStateName = detail[4]\n\t\t\t\t\tif Themes.statesMatch(incomingStateName, detailStateName) then\n\t\t\t\t\t\tlocal key = detail[1]..\"-\"..detail[2]\n\t\t\t\t\t\tlocal newDetail = Utility.copyTable(detail)\n\t\t\t\t\t\tnewDetail[5] = modificationsUID\n\t\t\t\t\t\tstateAppearance[key] = newDetail\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\t-- First we apply the base theme (i.e. the Default module)\n\t\t\tif stateName == \"Selected\" then\n\t\t\t\tupdateDetails(baseTheme, \"Deselected\")\n\t\t\tend\n\t\t\tupdateDetails(baseTheme, \"Empty\")\n\t\t\tupdateDetails(baseTheme, stateName)\n\t\t\t-- Next we apply any custom themes by the games developer\n\t\t\tif appliedTheme ~= baseTheme then\n\t\t\t\tif stateName == \"Selected\" then\n\t\t\t\t\tupdateDetails(appliedTheme, \"Deselected\")\n\t\t\t\tend\n\t\t\t\tupdateDetails(baseTheme, \"Empty\")\n\t\t\t\tupdateDetails(appliedTheme, stateName)\n\t\t\tend\n\t\t\t-- Finally we apply any modifications that have already been made\n\t\t\t-- Modifiers are all the changes made using icon:modifyTheme(...)\n\t\t\tlocal alreadyAppliedTheme = {}\n\t\t\tlocal alreadyAppliedGroup = icon.appearance[stateName]\n\t\t\tif alreadyAppliedGroup then\n\t\t\t\tfor _, modifier in pairs(alreadyAppliedGroup) do\n\t\t\t\t\tlocal modificationsUID = modifier[5]\n\t\t\t\t\tif modificationsUID ~= nil then\n\t\t\t\t\t\tlocal modification = {modifier[1], modifier[2], modifier[3], stateName, modificationsUID}\n\t\t\t\t\t\ttable.insert(alreadyAppliedTheme, modification)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\tupdateDetails(alreadyAppliedTheme, stateName)\n\t\t\t-- This now converts it into our final appearance\n\t\t\tlocal finalStateAppearance = {}\n\t\t\tfor _, detail in pairs(stateAppearance) do\n\t\t\t\ttable.insert(finalStateAppearance, detail)\n\t\t\tend\n\t\t\ticon.appearance[stateName] = finalStateAppearance\n\t\tend\n\t\tThemes.change(icon)\n\tend\n\tgenerateTheme()\nend\n\n\n\nreturn Themes"
  },
  {
    "path": "src/Packages/GoodSignal.lua",
    "content": "--------------------------------------------------------------------------------\n--               Batched Yield-Safe Signal Implementation                     --\n-- This is a Signal class which has effectively identical behavior to a       --\n-- normal RBXScriptSignal, with the only difference being a couple extra      --\n-- stack frames at the bottom of the stack trace when an error is thrown.     --\n-- This implementation caches runner coroutines, so the ability to yield in   --\n-- the signal handlers comes at minimal extra cost over a naive signal        --\n-- implementation that either always or never spawns a thread.                --\n--                                                                            --\n-- API:                                                                       --\n--   local Signal = require(THIS MODULE)                                      --\n--   local sig = Signal.new()                                                 --\n--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --\n--   sig:Fire(arg1, arg2, ...)                                                --\n--   connection:Disconnect()                                                  --\n--   sig:DisconnectAll()                                                      --\n--   local arg1, arg2, ... = sig:Wait()                                       --\n--                                                                            --\n-- Licence:                                                                   --\n--   Licenced under the MIT licence.                                          --\n--                                                                            --\n-- Authors:                                                                   --\n--   stravant - July 31st, 2021 - Created the file.                           --\n--------------------------------------------------------------------------------\n\n-- The currently idle thread to run the next handler on\nlocal freeRunnerThread = nil\n\n-- Function which acquires the currently idle handler runner thread, runs the\n-- function fn on it, and then releases the thread, returning it to being the\n-- currently idle one.\n-- If there was a currently idle runner thread already, that's okay, that old\n-- one will just get thrown and eventually GCed.\nlocal function acquireRunnerThreadAndCallEventHandler(fn, ...)\n\tlocal acquiredRunnerThread = freeRunnerThread\n\tfreeRunnerThread = nil\n\tfn(...)\n\t-- The handler finished running, this runner thread is free again.\n\tfreeRunnerThread = acquiredRunnerThread\nend\n\n-- Coroutine runner that we create coroutines of. The coroutine can be \n-- repeatedly resumed with functions to run followed by the argument to run\n-- them with.\nlocal function runEventHandlerInFreeThread()\n\t-- Note: We cannot use the initial set of arguments passed to\n\t-- runEventHandlerInFreeThread for a call to the handler, because those\n\t-- arguments would stay on the stack for the duration of the thread's\n\t-- existence, temporarily leaking references. Without access to raw bytecode\n\t-- there's no way for us to clear the \"...\" references from the stack.\n\twhile true do\n\t\tacquireRunnerThreadAndCallEventHandler(coroutine.yield())\n\tend\nend\n\n-- Connection class\nlocal Connection = {}\nConnection.__index = Connection\n\nfunction Connection.new(signal, fn)\n\treturn setmetatable({\n\t\t_connected = true,\n\t\t_signal = signal,\n\t\t_fn = fn,\n\t\t_next = false,\n\t}, Connection)\nend\n\nfunction Connection:Disconnect()\n\tself._connected = false\n\n\t-- Unhook the node, but DON'T clear it. That way any fire calls that are\n\t-- currently sitting on this node will be able to iterate forwards off of\n\t-- it, but any subsequent fire calls will not hit it, and it will be GCed\n\t-- when no more fire calls are sitting on it.\n\tif self._signal._handlerListHead == self then\n\t\tself._signal._handlerListHead = self._next\n\telse\n\t\tlocal prev = self._signal._handlerListHead\n\t\twhile prev and prev._next ~= self do\n\t\t\tprev = prev._next\n\t\tend\n\t\tif prev then\n\t\t\tprev._next = self._next\n\t\tend\n\tend\nend\nConnection.Destroy = Connection.Disconnect\n\n-- Make Connection strict\nsetmetatable(Connection, {\n\t__index = function(tb, key)\n\t\terror((\"Attempt to get Connection::%s (not a valid member)\"):format(tostring(key)), 2)\n\tend,\n\t__newindex = function(tb, key, value)\n\t\terror((\"Attempt to set Connection::%s (not a valid member)\"):format(tostring(key)), 2)\n\tend\n})\n\n-- Signal class\nlocal Signal = {}\nSignal.__index = Signal\n\nfunction Signal.new()\n\treturn setmetatable({\n\t\t_handlerListHead = false,\n\t}, Signal)\nend\n\nfunction Signal:Connect(fn)\n\tlocal connection = Connection.new(self, fn)\n\tif self._handlerListHead then\n\t\tconnection._next = self._handlerListHead\n\t\tself._handlerListHead = connection\n\telse\n\t\tself._handlerListHead = connection\n\tend\n\treturn connection\nend\n\n-- Disconnect all handlers. Since we use a linked list it suffices to clear the\n-- reference to the head handler.\nfunction Signal:DisconnectAll()\n\tself._handlerListHead = false\nend\nSignal.Destroy = Signal.DisconnectAll\n\n-- Signal:Fire(...) implemented by running the handler functions on the\n-- coRunnerThread, and any time the resulting thread yielded without returning\n-- to us, that means that it yielded to the Roblox scheduler and has been taken\n-- over by Roblox scheduling, meaning we have to make a new coroutine runner.\nfunction Signal:Fire(...)\n\tlocal item = self._handlerListHead\n\twhile item do\n\t\tif item._connected then\n\t\t\tif not freeRunnerThread then\n\t\t\t\tfreeRunnerThread = coroutine.create(runEventHandlerInFreeThread)\n\t\t\t\t-- Get the freeRunnerThread to the first yield\n\t\t\t\tcoroutine.resume(freeRunnerThread)\n\t\t\tend\n\t\t\ttask.spawn(freeRunnerThread, item._fn, ...)\n\t\tend\n\t\titem = item._next\n\tend\nend\n\n-- Implement Signal:Wait() in terms of a temporary connection using\n-- a Signal:Connect() which disconnects itself.\nfunction Signal:Wait()\n\tlocal waitingCoroutine = coroutine.running()\n\tlocal cn;\n\tcn = self:Connect(function(...)\n\t\tcn:Disconnect()\n\t\ttask.spawn(waitingCoroutine, ...)\n\tend)\n\treturn coroutine.yield()\nend\n\n-- Implement Signal:Once() in terms of a connection which disconnects\n-- itself before running the handler.\nfunction Signal:Once(fn)\n\tlocal cn;\n\tcn = self:Connect(function(...)\n\t\tif cn._connected then\n\t\t\tcn:Disconnect()\n\t\tend\n\t\tfn(...)\n\tend)\n\treturn cn\nend\n\n-- Make signal strict\nsetmetatable(Signal, {\n\t__index = function(tb, key)\n\t\terror((\"Attempt to get Signal::%s (not a valid member)\"):format(tostring(key)), 2)\n\tend,\n\t__newindex = function(tb, key, value)\n\t\terror((\"Attempt to set Signal::%s (not a valid member)\"):format(tostring(key)), 2)\n\tend\n})\n\nreturn Signal"
  },
  {
    "path": "src/Packages/Janitor.lua",
    "content": "--[[\n-------------------------------------\nThis package was modified by ForeverHD.\n\nPACKAGE MODIFICATIONS:\n\t1. Added pascalCase aliases for all methods\n\t2. Modified behaviour of :add so that it takes both objects and promises (previously only objects)\n\t3. Slight change to how promises are tracked\n\t4. Added isAnInstanceBeingDestroyed check to line 228\n\t5. Added 'OriginalTraceback' to help determine where an error was added to the janitor\n\t6. Likely some additional changes which weren't record here\n\t7. Removed comments as these were detected by Moonwave\n-------------------------------------\n--]]\n\n\n\n-- Janitor\n-- Original by Validark\n-- Modifications by pobammer\n-- roblox-ts support by OverHash and Validark\n-- LinkToInstance fixed by Elttob.\n\nlocal RunService = game:GetService(\"RunService\")\nlocal Heartbeat = RunService.Heartbeat\nlocal function getPromiseReference()\n\treturn false\nend\n\nlocal IndicesReference = newproxy(true)\ngetmetatable(IndicesReference).__tostring = function()\n\treturn \"IndicesReference\"\nend\n\nlocal LinkToInstanceIndex = newproxy(true)\ngetmetatable(LinkToInstanceIndex).__tostring = function()\n\treturn \"LinkToInstanceIndex\"\nend\n\nlocal METHOD_NOT_FOUND_ERROR = \"Object %s doesn't have method %s, are you sure you want to add it? Traceback: %s\"\nlocal NOT_A_PROMISE = \"Invalid argument #1 to 'Janitor:AddPromise' (Promise expected, got %s (%s))\"\n\nlocal Janitor = {\n\tIGNORE_MEMORY_DEBUG = true,\n\tClassName = \"Janitor\";\n\t__index = {\n\t\tCurrentlyCleaning = true;\n\t\t[IndicesReference] = nil;\n\t};\n}\n\nlocal TypeDefaults = {\n\t[\"function\"] = true;\n\t[\"Promise\"] = \"cancel\";\n\tRBXScriptConnection = \"Disconnect\";\n}\n\nfunction Janitor.new()\n\treturn setmetatable({\n\t\tCurrentlyCleaning = false;\n\t\t[IndicesReference] = nil;\n\t}, Janitor)\nend\n\nfunction Janitor.Is(Object)\n\treturn type(Object) == \"table\" and getmetatable(Object) == Janitor\nend\n\nJanitor.is = Janitor.Is\n\nfunction Janitor.__index:Add(Object, MethodName, Index)\n\tif Index then\n\t\tself:Remove(Index)\n\n\t\tlocal This = self[IndicesReference]\n\t\tif not This then\n\t\t\tThis = {}\n\t\t\tself[IndicesReference] = This\n\t\tend\n\n\t\tThis[Index] = Object\n\tend\n\n\tlocal objectType = typeof(Object)\n\tif objectType == \"table\" and string.match(tostring(Object), \"Promise\") then\n\t\tobjectType = \"Promise\"\n\t\t--local status = Object:getStatus()\n\t\t--print(\"status =\", status, status == \"Rejected\")\n\tend\n\tMethodName = MethodName or TypeDefaults[objectType] or \"Destroy\"\n\tif type(Object) ~= \"function\" and not Object[MethodName] then\n\t\twarn(string.format(METHOD_NOT_FOUND_ERROR, tostring(Object), tostring(MethodName), debug.traceback(nil :: any, 2)))\n\tend\n\n\tlocal OriginalTraceback = debug.traceback(\"\")\n\tself[Object] = {MethodName, OriginalTraceback}\n\treturn Object\nend\nJanitor.__index.Give = Janitor.__index.Add\n\n-- My version of Promise has PascalCase, but I converted it to use lowerCamelCase for this release since obviously that's important to do.\n\nfunction Janitor.__index:AddPromise(PromiseObject)\n\tlocal Promise = getPromiseReference()\n\tif Promise then\n\t\tif not Promise.is(PromiseObject) then\n\t\t\terror(string.format(NOT_A_PROMISE, typeof(PromiseObject), tostring(PromiseObject)))\n\t\tend\n\t\tif PromiseObject:getStatus() == Promise.Status.Started then\n\t\t\tlocal Id = newproxy(false)\n\t\t\tlocal NewPromise = self:Add(Promise.new(function(Resolve, _, OnCancel)\n\t\t\t\tif OnCancel(function()\n\t\t\t\t\t\tPromiseObject:cancel()\n\t\t\t\t\tend) then\n\t\t\t\t\treturn\n\t\t\t\tend\n\n\t\t\t\tResolve(PromiseObject)\n\t\t\tend), \"cancel\", Id)\n\n\t\t\tNewPromise:finallyCall(self.Remove, self, Id)\n\t\t\treturn NewPromise\n\t\telse\n\t\t\treturn PromiseObject\n\t\tend\n\telse\n\t\treturn PromiseObject\n\tend\nend\nJanitor.__index.GivePromise = Janitor.__index.AddPromise\n\n-- This will assume whether or not the object is a Promise or a regular object.\nfunction Janitor.__index:AddObject(Object)\n\tlocal Id = newproxy(false)\n\tlocal Promise = getPromiseReference()\n\tif Promise and Promise.is(Object) then\n\t\tif Object:getStatus() == Promise.Status.Started then\n\t\t\tlocal NewPromise = self:Add(Promise.resolve(Object), \"cancel\", Id)\n\t\t\tNewPromise:finallyCall(self.Remove, self, Id)\n\t\t\treturn NewPromise, Id\n\t\telse\n\t\t\treturn Object\n\t\tend\n\telse\n\t\treturn self:Add(Object, false, Id), Id\n\tend\nend\n\nJanitor.__index.GiveObject = Janitor.__index.AddObject\n\nfunction Janitor.__index:Remove(Index)\n\tlocal This = self[IndicesReference]\n\tif This then\n\t\tlocal Object = This[Index]\n\n\t\tif Object then\n\t\t\tlocal ObjectDetail = self[Object]\n\t\t\tlocal MethodName = ObjectDetail and ObjectDetail[1]\n\n\t\t\tif MethodName then\n\t\t\t\tif MethodName == true then\n\t\t\t\t\tObject()\n\t\t\t\telse\n\t\t\t\t\tlocal ObjectMethod = Object[MethodName]\n\t\t\t\t\tif ObjectMethod then\n\t\t\t\t\t\tObjectMethod(Object)\n\t\t\t\t\tend\n\t\t\t\tend\n\n\t\t\t\tself[Object] = nil\n\t\t\tend\n\n\t\t\tThis[Index] = nil\n\t\tend\n\tend\n\n\treturn self\nend\n\nfunction Janitor.__index:Get(Index)\n\tlocal This = self[IndicesReference]\n\tif This then\n\t\treturn This[Index]\n\tend\n\treturn nil\nend\n\nfunction Janitor.__index:Cleanup()\n\tif not self.CurrentlyCleaning then\n\t\tself.CurrentlyCleaning = nil\n\t\tfor Object, ObjectDetail in next, self do\n\t\t\tif Object == IndicesReference then\n\t\t\t\tcontinue\n\t\t\tend\n\n\t\t\t-- Weird decision to rawset directly to the janitor in Agent. This should protect against it though.\n\t\t\tlocal TypeOf = type(Object)\n\t\t\tif TypeOf == \"string\" or TypeOf == \"number\" then\n\t\t\t\tself[Object] = nil\n\t\t\t\tcontinue\n\t\t\tend\n\n\t\t\tlocal MethodName = ObjectDetail[1]\n\t\t\tlocal OriginalTraceback = ObjectDetail[2]\n\t\t\tlocal function warnUser(warning)\n\t\t\t\tlocal cleanupLine = debug.traceback(\"\", 3)--string.gsub(debug.traceback(\"\", 3), \"%c\", \"\")\n\t\t\t\tlocal addedLine = OriginalTraceback\n\t\t\t\twarn(\"-------- Janitor Error --------\"..\"\\n\"..tostring(warning)..\"\\n\"..cleanupLine..\"\"..addedLine)\n\t\t\tend\n\t\t\tif MethodName == true then\n\t\t\t\tlocal success, warning = pcall(Object)\n\t\t\t\tif not success then\n\t\t\t\t\twarnUser(warning)\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tlocal ObjectMethod = Object[MethodName]\n\t\t\t\tif ObjectMethod then\n\t\t\t\t\tlocal success, warning = pcall(ObjectMethod, Object)\n\t\t\t\t\tlocal isAnInstanceBeingDestroyed = typeof(Object) == \"Instance\" and ObjectMethod == \"Destroy\"\n\t\t\t\t\tif not success and not isAnInstanceBeingDestroyed then\n\t\t\t\t\t\twarnUser(warning)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tself[Object] = nil\n\t\tend\n\n\t\tlocal This = self[IndicesReference]\n\t\tif This then\n\t\t\tfor Index in next, This do\n\t\t\t\tThis[Index] = nil\n\t\t\tend\n\n\t\t\tself[IndicesReference] = {}\n\t\tend\n\n\t\tself.CurrentlyCleaning = false\n\tend\nend\n\nJanitor.__index.Clean = Janitor.__index.Cleanup\n\nfunction Janitor.__index:Destroy()\n\tself:Cleanup()\n\t--table.clear(self)\n\t--setmetatable(self, nil)\nend\n\nJanitor.__call = Janitor.__index.Cleanup\n\nlocal Disconnect = {Connected = true}\nDisconnect.__index = Disconnect\nfunction Disconnect:Disconnect()\n\tif self.Connected then\n\t\tself.Connected = false\n\t\tself.Connection:Disconnect()\n\tend\nend\n\nfunction Disconnect:__tostring()\n\treturn \"Disconnect<\" .. tostring(self.Connected) .. \">\"\nend\n\nfunction Janitor.__index:LinkToInstance(Object, AllowMultiple)\n\tlocal Connection\n\tlocal IndexToUse = AllowMultiple and newproxy(false) or LinkToInstanceIndex\n\tlocal IsNilParented = Object.Parent == nil\n\tlocal ManualDisconnect = setmetatable({}, Disconnect)\n\n\tlocal function ChangedFunction(_DoNotUse, NewParent)\n\t\tif ManualDisconnect.Connected then\n\t\t\t_DoNotUse = nil\n\t\t\tIsNilParented = NewParent == nil\n\n\t\t\tif IsNilParented then\n\t\t\t\tcoroutine.wrap(function()\n\t\t\t\t\tHeartbeat:Wait()\n\t\t\t\t\tif not ManualDisconnect.Connected then\n\t\t\t\t\t\treturn\n\t\t\t\t\telseif not Connection.Connected then\n\t\t\t\t\t\tself:Cleanup()\n\t\t\t\t\telse\n\t\t\t\t\t\twhile IsNilParented and Connection.Connected and ManualDisconnect.Connected do\n\t\t\t\t\t\t\tHeartbeat:Wait()\n\t\t\t\t\t\tend\n\n\t\t\t\t\t\tif ManualDisconnect.Connected and IsNilParented then\n\t\t\t\t\t\t\tself:Cleanup()\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\tend)()\n\t\t\tend\n\t\tend\n\tend\n\n\tConnection = Object.AncestryChanged:Connect(ChangedFunction)\n\tManualDisconnect.Connection = Connection\n\n\tif IsNilParented then\n\t\tChangedFunction(nil, Object.Parent)\n\tend\n\n\tObject = nil\n\treturn self:Add(ManualDisconnect, \"Disconnect\", IndexToUse)\nend\n\nfunction Janitor.__index:LinkToInstances(...)\n\tlocal ManualCleanup = Janitor.new()\n\tfor _, Object in ipairs({...}) do\n\t\tManualCleanup:Add(self:LinkToInstance(Object, true), \"Disconnect\")\n\tend\n\n\treturn ManualCleanup\nend\n\nfor FunctionName, Function in next, Janitor.__index do\n\tlocal NewFunctionName = string.sub(string.lower(FunctionName), 1, 1) .. string.sub(FunctionName, 2)\n\tJanitor.__index[NewFunctionName] = Function\nend\n\nreturn Janitor"
  },
  {
    "path": "src/Reference.lua",
    "content": "-- This module enables you to place Icon wherever you like within the data model while\n-- still enabling third-party applications (such as HDAdmin/Nanoblox) to locate it\n-- This is necessary to prevent two TopbarPlus applications initiating at runtime which would\n-- cause icons to overlap with each other\n\nlocal replicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Reference = {}\nReference.objectName = \"TopbarPlusReference\"\n\nfunction Reference.addToReplicatedStorage()\n\tlocal existingItem = replicatedStorage:FindFirstChild(Reference.objectName)\n    if existingItem then\n        return false\n    end\n    local objectValue = Instance.new(\"ObjectValue\")\n\tobjectValue.Name = Reference.objectName\n    objectValue.Value = script.Parent\n    objectValue.Parent = replicatedStorage\n    return objectValue\nend\n\nfunction Reference.getObject()\n\tlocal objectValue = replicatedStorage:FindFirstChild(Reference.objectName)\n    if objectValue then\n        return objectValue\n    end\n    return false\nend\n\nreturn Reference"
  },
  {
    "path": "src/Types.lua",
    "content": "--!strict\n\n-- GoodSignal Types (...but simpler!)\n\n--- Connection\n\ntype Connection<Variant... = ...any> = {\n\tDisconnect: (self: Connection<Variant...>) -> (),\n}\n\n--- Signal\n\ntype Signal<Variant... = ...any> = {\n\tConnect: (self: Signal<Variant...>, func: (Variant...) -> ()) -> Connection<Variant...>,\n    Once: (self: Signal<Variant...>, func: (Variant...) -> ()) -> Connection<Variant...>,\n\tWait: (self: Signal<Variant...>) -> Variant...,\n}\n\n----------------------\n\nexport type IconState = \"Deselected\" | \"Selected\" | \"Viewing\"\nexport type Events = \"selected\" | \"deselected\" | \"toggled\" | \"viewingStarted\" | \"viewingEnded\" | \"notified\"\nexport type Alignment = \"Left\" | \"Center\" | \"Right\"\nexport type EventSource = \"User\" | \"OneClick\" | \"AutoDeselect\" | \"HideParentFeature\" | \"Overflow\"\nexport type Modification = { any }\n\n\ntype StaticFunctions = {\n\tgetIcons: typeof(\n\t\t--[[\n\t\t\tReturns a dictionary of icons where the key is the icon's UID and value the icon.\n\t\t]]\n\t\tfunction(): { Icon }\n\t\t\treturn (nil :: any) :: { Icon }\n\t\tend\n\t),\n\tgetIcon: typeof(\n\t\t--[[\n\t\t\tReturns an icon of the given name or UID.\n\t\t]]\n\t\tfunction(nameOrUID: string): Icon?\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetTopbarEnabled: typeof(\n\t\t--[[\n\t\t\tWhen set to <code>false</code> all TopbarPlus ScreenGuis are hidden.\n\t\t\tThis does not impact Roblox's Topbar.\n\t\t]]\n\t\tfunction(enabled: boolean)\n\n\t\tend\n\t),\n\tmodifyBaseTheme: typeof(\n\t\t--[[\n\t\t\tUpdates the appearance of all icons.\n\t\t]]\n\t\tfunction(modifications: { Modification })\n\n\t\tend\n\t),\n\tsetDisplayOrder: typeof(\n\t\t--[[\n\t\t\tSets the base DisplayOrder of all TopbarPlus ScreenGuis.\n\t\t]]\n\t\tfunction(order: number)\n\n\t\tend\n\t),\n}\n\ntype Methods = {\n\t\n\t-- CLASS FUNCTIONS\n\tsetName: typeof(\n\t\t--[[\n\t\t\tSets the name of the Widget instance. This can be used in conjunction with <code>Icon.getIcon(name)</code>\n\t\t]]\n\t\tfunction(self: Icon, name: string): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tgetInstance: typeof(\n\t\t--[[\n\t\t\tReturns the first descendant found within the widget of name <code>instanceName</code>.\n\t\t]]\n\t\tfunction(self: Icon, instanceName: string): Instance?\n\t\t\treturn (nil :: any) :: Instance?\n\t\tend\n\t),\n\tmodifyTheme: typeof(\n\t\t--[[\n\t\t\tUpdates the appearance of the icon.\n\t\t]]\n\t\tfunction(self: Icon, modifications: {Modification} | Modification): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tmodifyChildTheme: typeof(\n\t\t--[[\n\t\t\tUpdates the appearance of all icons that are parented to this icon (for example when a menu or dropdown).\n\t\t]]\n\t\tfunction(self: Icon, modifications: { Modification }): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetEnabled: typeof(\n\t\t--[[\n\t\t\tWhen set to <code>false</code> the icon will be disabled and hidden.\n\t\t]]\n\t\tfunction(self: Icon, enabled: boolean): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tselect: typeof(\n\t\t--[[\n\t\t\tSelects the icon (as if it were clicked once).\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tdeselect: typeof(\n\t\t--[[\n\t\t\tDeselects the icon (as if it were clicked, then clicked again).\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tnotify: typeof(\n\t\t--[[\n\t\t\tPrompts a notice bubble which accumulates the further it is prompted.\n\t\t\tIf the icon belongs to a dropdown or menu, then the notice will appear on the parent icon when the parent icon is deselected.\n\t\t]]\n\t\tfunction(self: Icon, clearNoticeEvent: Signal?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tclearNotices: typeof(\n\t\t--[[\n\t\t\t\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tdisableOverlay: typeof(\n\t\t--[[\n\t\t\tWhen set to <code>true</code>, disables the shade effect which appears when the icon is pressed and released.\n\t\t]]\n\t\tfunction(self: Icon, disabled: boolean): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetImage: typeof(\n\t\t--[[\n\t\t\tApplies an image to the icon based on the given <code>imageId</code>. <code>imageId</code> can be an assetId or a complete asset string.\n\t\t]]\n\t\tfunction(self: Icon, imageId: string | number, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetLabel: typeof(\n\t\t--[[\n\t\t\t\n\t\t]]\n\t\tfunction(self: Icon, text: string, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetOrder: typeof(\n\t\t--[[\n\t\t\t\n\t\t]]\n\t\tfunction(self: Icon, order: number, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetCornerRadius: typeof(\n\t\t--[[\n\t\t\t\n\t\t]]\n\t\tfunction(self: Icon, udim: UDim2, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\talign: typeof(\n\t\t--[[\n\t\t\tThis enables you to set the icon to the <code>\"Left\"</code> (default), <code>\"Center\"</code> or <code>\"Right\"</code> side of the screen.\n\t\t]]\n\t\tfunction(self: Icon, alignment: Alignment?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetWidth: typeof(\n\t\t--[[\n\t\t\tThis sets the minimum width the icon can be (it can be larger for instance when setting a long label). The default width is <code>44</code>.\n\t\t]]\n\t\tfunction(self: Icon, minimumSize: number, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetImageScale: typeof(\n\t\t--[[\n\t\t\tHow large the image is relative to the icon. The default value is <code>0.5</code>.\n\t\t]]\n\t\tfunction(self: Icon, scale: number, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetImageRatio: typeof(\n\t\t--[[\n\t\t\tHow stretched the image will appear. The default value is <code>1</code> (a perfect square).\n\t\t]]\n\t\tfunction(self: Icon, ratio: number, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetTextSize: typeof(\n\t\t--[[\n\t\t\tThe size of the icon labels' text. The default value is <code>16</code>.\n\t\t]]\n\t\tfunction(self: Icon, textSize: number, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetTextColor: typeof(\n\t\t--[[\n\t\t\tThe color of the icon labels' text\n\t\t]]\n\t\tfunction(self: Icon, color: Color3, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetTextFont: typeof(\n\t\t--[[\n\t\t\tSets the labels FontFace.\n\t\t\t<code>font</code> can be a font family name (such as <code>\"Creepster\"</code>),\n\t\t\ta font enum (such as <code>Enum.Font.Bangers</code>),\n\t\t\ta font ID (such as <code>12187370928</code>),\n\t\t\tor font family link (such as <code>\"rbxasset://fonts/families/Sarpanch.json\"</code>).\n\t\t]]\n\t\tfunction(self: Icon, font: string | Enum.Font, fontWeight: Enum.FontWeight?, fontStyle: Enum.FontSize?, iconState: IconState?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tbindToggleItem: typeof(\n\t\t--[[\n\t\t\tBinds a GuiObject or LayerCollector to appear and disappeared when the icon is toggled.\n\t\t]]\n\t\tfunction(self: Icon, guiObjectOrLayerCollector: GuiObject | LayerCollector): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tunbindToggleItem: typeof(\n\t\t--[[\n\t\t\tUnbinds the given GuiObject or LayerCollector from the toggle.\n\t\t]]\n\t\tfunction(self: Icon, guiObjectOrLayerCollector: GuiObject | LayerCollector): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tbindEvent: typeof(\n\t\t--[[\n\t\t\tConnects to an icon event with <code>iconEventName</code>.\n\t\t\tIt's important to remember all event names are in <code>camelCase</code>.\n\t\t\t<code>callback</code> is called with arguments <code>(self, ...)</code> when the event is triggered.\n\t\t]]\n\t\tfunction(self: Icon, event: Events, callback: (...any) -> ()): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tunbindEvent: typeof(\n\t\t--[[\n\t\t\tUnbinds the connection of the associated <code>iconEventName</code>.\n\t\t]]\n\t\tfunction(self: Icon, event: Events): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tbindToggleKey: typeof(\n\t\t--[[\n\t\t\tBinds a keycode which toggles the icon when pressed.\n\t\t]]\n\t\tfunction(self: Icon, keycode: Enum.KeyCode): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tunbindToggleKey: typeof(\n\t\t--[[\n\t\t\tUnbinds the given keycode.\n\t\t]]\n\t\tfunction(self: Icon, keycode: Enum.KeyCode): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tcall: typeof(\n\t\t--[[\n\t\t\tCalls the function immediately via <code>task.spawn</code>.\n\t\t\tThe first argument passed is the icon itself.\n\t\t\tThis is useful when needing to extend the behaviour of an icon while remaining in the chain.\n\t\t]]\n\t\tfunction(self: Icon, func: (self: Icon) -> (...any), ...: any): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\taddToJanitor: typeof(\n\t\t--[[\n\t\t\tPasses the given userdata to the icons janitor to be destroyed/disconnected on the icons destruction.\n\t\t\tIf a function is passed, it will be called when the icon is destroyed.\n\t\t]]\n\t\tfunction(self: Icon, userdata: unknown): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tlock: typeof(\n\t\t--[[\n\t\t\tPrevents the icon being toggled by user-input (such as clicking), however, the icon can still be toggled via localscript using methods such as <code>icon:select()</code>.\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tunlock: typeof(\n\t\t--[[\n\t\t\tRe-enables user-input to toggle the icon again.\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tdebounce: typeof(\n\t\t--[[\n\t\t\tLocks the icon, yields for the given time, then unlocks the icon, effectively shorthand for <code>icon:lock() task.wait(seconds) icon:unlock()</code>.\n\t\t\tThis is useful for applying cooldowns (to prevent an icon from being pressed again) after an icon has been selected or deselected.\n\t\t]]\n\t\tfunction(self: Icon, seconds: number): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tautoDeselect: typeof(\n\t\t--[[\n\t\t\tWhen set to <code>true</code> (the default) the icon is deselected when another icon (with autoDeselect enabled) is pressed.\n\t\t\tSet to <code>false</code> to prevent the icon being deselected when another icon is selected (a useful behaviour in dropdowns).\n\t\t]]\n\t\tfunction(self: Icon, enabled: boolean?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\toneClick: typeof(\n\t\t--[[\n\t\t\tWhen set to true the icon will automatically deselect when selected.\n\t\t\tThis creates the effect of a single click button.\n\t\t]]\n\t\tfunction(self: Icon, enabled: boolean?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetCaption: typeof(\n\t\t--[[\n\t\t\tSets a caption. To remove, pass <code>nil</code> as <code>text</code>.\n\t\t]]\n\t\tfunction(self: Icon, text: string?): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetCaptionHint: typeof(\n\t\t--[[\n\t\t\tThis customizes the appearance of the caption's hint without having to use <code>icon:bindToggleKey</code>.\n\t\t]]\n\t\tfunction(self: Icon, keyCode: Enum.KeyCode): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetDropdown: typeof(\n\t\t--[[\n\t\t\tCreates a vertical dropdown based upon the given table array of icons.\n\t\t\tPass an empty table <code>{}</code> to remove the dropdown.\n\t\t]]\n\t\tfunction(self: Icon, icons: { Icon }): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tjoinDropdown: typeof(\n\t\t--[[\n\t\t\tJoins the dropdown of <code>parentIcon</code>.\n\t\t\tThis is what <code>icon:setDropdown</code> calls internally on the icons within its array.\n\t\t]]\n\t\tfunction(self: Icon, parent: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetMenu: typeof(\n\t\t--[[\n\t\t\tCreates a horizontal menu based upon the given array of icons.\n\t\t\tPass an empty table <code>{}</code> to remove the menu.\n\t\t]]\n\t\tfunction(self: Icon, icons: { Icon }): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tsetFixedMenu: typeof(\n\t\t--[[\n\t\t\tCreates a menu that is always selected and has it's close button hidden.\n\t\t\tPass an empty table <code>{}</code> to remove the menu.\n\t\t]]\n\t\tfunction(self: Icon, icons: { Icon }): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tjoinMenu: typeof(\n\t\t--[[\n\t\t\tJoins the menu of <code>parentIcon</code>.\n\t\t\tThis is what <code>icon:setMenu</code> calls internally on the icons within its array.\n\t\t]]\n\t\tfunction(self: Icon, parentIcon: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tleave: typeof(\n\t\t--[[\n\t\t\tUnparents an icon from a parentIcon if it belongs to a dropdown or menu.\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tconvertLabelToNumberSpinner: typeof(\n\t\t--[[\n\t\t\tUnparents an icon from a parentIcon if it belongs to a dropdown or menu.\n\t\t]]\n\t\tfunction(self: Icon, numberSpinner: any, func: (...any) -> (...any), ...: any): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n\tdestroy: typeof(\n\t\t--[[\n\t\t\tClears all connections and destroys all instances associated with the icon.\n\t\t]]\n\t\tfunction(self: Icon): Icon\n\t\t\treturn nil :: any\n\t\tend\n\t),\n} & StaticFunctions\n\ntype Fields = {\n\t-- CLASS PROPERTIES\n\tname: string,\n\tisSelected: boolean,\n\tisEnabled: boolean,\n\ttotalNotices: number,\n\tlocked: boolean,\n\n\t-- CLASS EVENTS\n\tselected: Signal<EventSource>,\n\tdeselected: Signal<EventSource>,\n\ttoggled: Signal<boolean, EventSource>,\n\tviewingStarted: Signal,\n\tviewingEnded: Signal,\n\tnotified: Signal,\n}\n\nexport type Icon = Methods & StaticFunctions --typeof(setmetatable({} :: Fields, MT))\n\nexport type StaticIcon = {\n\tnew: typeof(\n\t\t--[[\n\t\t\tConstructs an empty <code>32x32</code> icon on the topbar.\n\t\t]]\n\t\tfunction(): Icon\n\t\t\treturn (nil :: any) :: Icon\n\t\tend\n\t),\n} & StaticFunctions\n\nreturn {}"
  },
  {
    "path": "src/Utility.lua",
    "content": "-- Just generic utility functions which I use and repeat across all my projects\n\n\n\n-- LOCAL\nlocal Utility = {}\nlocal Players = game:GetService(\"Players\")\nlocal localPlayer = Players.LocalPlayer\n\n\n\n-- FUNCTIONS\nfunction Utility.createStagger(delayTime, callback, delayInitially)\n\t-- This creates and returns a function which when called\n\t-- acts identically to callback, however will only be called\n\t-- for a maximum of once per delayTime. If the returned function\n\t-- is called more than once during the delayTime, then it will\n\t-- wait until the expiryTime then perform another recall.\n\t-- This is useful for visual interfaces and effects which may be\n\t-- triggered multiple times within a frame or short period, but which\n\t-- we don't necessary need to (for performance reasons).\n\tlocal staggerActive = false\n\tlocal multipleCalls = false\n\tif not delayTime or delayTime == 0 then\n\t\t-- We make 0.01 instead of 0 because devices can now run at\n\t\t-- different frame rates\n\t\tdelayTime = 0.01\n\tend\n\tlocal function staggeredCallback(...)\n\t\tif staggerActive then\n\t\t\tmultipleCalls = true\n\t\t\treturn\n\t\tend\n\t\tlocal packedArgs = table.pack(...)\n\t\tstaggerActive = true\n\t\tmultipleCalls = false\n\t\ttask.spawn(function()\n\t\t\tif delayInitially then\n\t\t\t\ttask.wait(delayTime)\n\t\t\tend\n\t\t\tcallback(table.unpack(packedArgs))\n\t\tend)\n\t\ttask.delay(delayTime, function()\n\t\t\tstaggerActive = false\n\t\t\tif multipleCalls then\n\t\t\t\t-- This means it has been called at least once during\n\t\t\t\t-- the stagger period, so call again\n\t\t\t\tstaggeredCallback(table.unpack(packedArgs))\n\t\t\tend\n\t\tend)\n\tend\n\treturn staggeredCallback\nend\n\nfunction Utility.round(n)\n\t-- Credit to Darkmist101 for this\n\treturn math.floor(n + 0.5)\nend\n\nfunction Utility.reverseTable(t)\n\tfor i = 1, math.floor(#t/2) do\n\t\tlocal j = #t - i + 1\n\t\tt[i], t[j] = t[j], t[i]\n\tend\nend\n\nfunction Utility.copyTable(t)\n\t-- Credit to Stephen Leitnick (September 13, 2017) for this function from TableUtil\n\tassert(type(t) == \"table\", \"First argument must be a table\")\n\tlocal tCopy = table.create(#t)\n\tfor k,v in pairs(t) do\n\t\tif (type(v) == \"table\") then\n\t\t\ttCopy[k] = Utility.copyTable(v)\n\t\telse\n\t\t\ttCopy[k] = v\n\t\tend\n\tend\n\treturn tCopy\nend\n\nlocal validCharacters = {\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\",\"j\",\"k\",\"l\",\"m\",\"n\",\"o\",\"p\",\"q\",\"r\",\"s\",\"t\",\"u\",\"v\",\"w\",\"x\",\"y\",\"z\",\"A\",\"B\",\"C\",\"D\",\"E\",\"F\",\"G\",\"H\",\"I\",\"J\",\"K\",\"L\",\"M\",\"N\",\"O\",\"P\",\"Q\",\"R\",\"S\",\"T\",\"U\",\"V\",\"W\",\"X\",\"Y\",\"Z\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"0\",\"<\",\">\",\"?\",\"@\",\"{\",\"}\",\"[\",\"]\",\"!\",\"(\",\")\",\"=\",\"+\",\"~\",\"#\"}\nfunction Utility.generateUID(length)\n\tlength = length or 8\n\tlocal UID = \"\"\n\tlocal list = validCharacters\n\tlocal total = #list\n\tfor i = 1, length do\n\t\tlocal randomCharacter = list[math.random(1, total)]\n\t\tUID = UID..randomCharacter\n\tend\n\treturn UID\nend\n\nlocal instanceTrackers = {}\nfunction Utility.setVisible(instance, bool, sourceUID)\n\t-- This effectively works like a buff object but\n\t-- incredibly simplified. It stacks false values\n\t-- so that if there is more than more than, the \n\t-- instance remains hidden even if set visible true\n\tlocal tracker = instanceTrackers[instance]\n\tif not tracker then\n\t\ttracker = {}\n\t\tinstanceTrackers[instance] = tracker\n\t\tinstance.Destroying:Once(function()\n\t\t\tinstanceTrackers[instance] = nil\n\t\tend)\n\tend\n\tif not bool then\n\t\ttracker[sourceUID] = true\n\telse\n\t\ttracker[sourceUID] = nil\n\tend\n\tlocal isVisible = bool\n\tif bool then\n\t\tfor sourceUID, _ in pairs(tracker) do\n\t\t\tisVisible = false\n\t\t\tbreak\n\t\tend\n\tend\n\tinstance.Visible = isVisible\nend\n\nfunction Utility.formatStateName(incomingStateName)\n\treturn string.upper(string.sub(incomingStateName, 1, 1))..string.lower(string.sub(incomingStateName, 2))\nend\n\nfunction Utility.localPlayerRespawned(callback)\n\t-- The client localscript may be located under a ScreenGui with ResetOnSpawn set to true\n\t-- In these scenarios, traditional methods like CharacterAdded won't be called by the\n\t-- time the localscript has been destroyed, therefore we listen for removing instead\n\t-- If humanoid and health == 0, then reset/died normally, else was\n\t-- forcefully reset via a method such as LoadCharacter\n\t-- We wrap this behaviour in case any additional quirks need to be accounted for\n\tlocalPlayer.CharacterRemoving:Connect(callback)\nend\n\nfunction Utility.getClippedContainer(screenGui)\n\t-- We always want clipped items to display in front hence\n\t-- why we have this\n\tlocal clippedContainer = screenGui:FindFirstChild(\"ClippedContainer\")\n\tif not clippedContainer then\n\t\tclippedContainer = Instance.new(\"Folder\")\n\t\tclippedContainer.Name = \"ClippedContainer\"\n\t\tclippedContainer.Parent = screenGui\n\tend\n\treturn clippedContainer\nend\n\nlocal Janitor = require(script.Parent.Packages.Janitor)\nlocal GuiService = game:GetService(\"GuiService\")\nfunction Utility.clipOutside(icon, instance)\n\tlocal cloneJanitor = icon.janitor:add(Janitor.new())\n\tinstance.Destroying:Once(function()\n\t\tcloneJanitor:Destroy()\n\tend)\n\ticon.janitor:add(instance)\n\n\tlocal originalParent = instance.Parent\n\tlocal clone = cloneJanitor:add(Instance.new(\"Frame\"))\n\tclone:SetAttribute(\"IsAClippedClone\", true)\n\tclone.Name = instance.Name\n\tclone.AnchorPoint = instance.AnchorPoint\n\tclone.Size = instance.Size\n\tclone.Position = instance.Position\n\tclone.BackgroundTransparency = 1\n\tclone.LayoutOrder = instance.LayoutOrder\n\tclone.Parent = originalParent\n\n\tlocal valueInstance = Instance.new(\"ObjectValue\")\n\tvalueInstance.Name = \"OriginalInstance\"\n\tvalueInstance.Value = instance\n\tvalueInstance.Parent = clone\n\n\tlocal valueInstanceCopy = valueInstance:Clone()\n\tinstance:SetAttribute(\"HasAClippedClone\", true)\n\tvalueInstanceCopy.Name = \"ClippedClone\"\n\tvalueInstanceCopy.Value = clone\n\tvalueInstanceCopy.Parent = instance\n\n\tlocal screenGui\n\tlocal Icon = require(icon.iconModule)\n\tlocal container = Icon.container\n\tlocal function updateScreenGui()\n\t\tlocal originalScreenGui = originalParent:FindFirstAncestorWhichIsA(\"ScreenGui\")\n\t\tscreenGui = if string.match(originalScreenGui.Name, \"Clipped\") then originalScreenGui else container[originalScreenGui.Name..\"Clipped\"]\n\t\tinstance.AnchorPoint = Vector2.new(0, 0)\n\t\tinstance.Parent = Utility.getClippedContainer(screenGui)\n\tend\n\tcloneJanitor:add(icon.alignmentChanged:Connect(updateScreenGui))\n\tupdateScreenGui()\n\n\t-- Lets copy over children that modify size\n\tfor _, child in pairs(instance:GetChildren()) do\n\t\tif child:IsA(\"UIAspectRatioConstraint\") then\n\t\t\tchild:Clone().Parent = clone\n\t\tend\n\tend\n\n\t-- If the icon is hidden, its important we are too (as\n\t-- setting a parent to visible = false no longer makes\n\t-- this hidden)\n\tlocal widget = icon.widget\n\tlocal isOutsideParent = false\n\tlocal ignoreVisibilityUpdater = instance:GetAttribute(\"IgnoreVisibilityUpdater\")\n\tlocal function updateVisibility()\n\t\tif ignoreVisibilityUpdater then\n\t\t\treturn\n\t\tend\n\t\tlocal isVisible = widget.Visible\n\t\tif isOutsideParent then\n\t\t\tisVisible = false\n\t\tend\n\t\tUtility.setVisible(instance, isVisible, \"ClipHandler\")\n\tend\n\tcloneJanitor:add(widget:GetPropertyChangedSignal(\"Visible\"):Connect(updateVisibility))\n\n\tlocal previousScroller\n\tlocal function checkIfOutsideParentXBounds()\n\t\t-- Defer so that roblox's properties reflect their true values\n\t\ttask.defer(function()\n\t\t\t-- If the instance is within a parent item (such as a dropdown or menu)\n\t\t\t-- then we hide it if it exceeds the bounds of that parent\n\t\t\tlocal parentInstance\n\t\t\tlocal ourUID = icon.UID\n\t\t\tlocal nextIconUID = ourUID\n\t\t\tlocal shouldClipToParent = instance:GetAttribute(\"ClipToJoinedParent\")\n\t\t\tif shouldClipToParent then\n\t\t\t\tfor i = 1, 10 do -- This is safer than while true do and should never be > 4 parents\n\t\t\t\t\tlocal nextIcon = Icon.getIconByUID(nextIconUID)\n\t\t\t\t\tif not nextIcon then\n\t\t\t\t\t\tbreak\n\t\t\t\t\tend\n\t\t\t\t\tlocal nextParentInstance = nextIcon.joinedFrame\n\t\t\t\t\tnextIconUID = nextIcon.parentIconUID\n\t\t\t\t\tif not nextParentInstance then\n\t\t\t\t\t\tbreak\n\t\t\t\t\tend\n\t\t\t\t\tparentInstance = nextParentInstance\n\t\t\t\t\tif parentInstance and parentInstance.Name == \"DropdownScroller\" then\n\t\t\t\t\t\tbreak\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\tif not parentInstance then\n\t\t\t\tisOutsideParent = false\n\t\t\t\tupdateVisibility()\n\t\t\t\treturn\n\t\t\tend\n\t\t\tlocal pos = instance.AbsolutePosition\n\t\t\tlocal halfSize = instance.AbsoluteSize/2\n\t\t\tlocal parentPos = parentInstance.AbsolutePosition\n\t\t\tlocal parentSize = parentInstance.AbsoluteSize\n\t\t\tlocal posHalf = (pos + halfSize)\n\t\t\tlocal exceededLeft = posHalf.X < parentPos.X\n\t\t\tlocal exceededRight = posHalf.X > (parentPos.X + parentSize.X)\n\t\t\tlocal exceededTop = posHalf.Y < parentPos.Y\n\t\t\tlocal exceededBottom = posHalf.Y > (parentPos.Y + parentSize.Y)\n\t\t\tlocal hasExceeded = exceededLeft or exceededRight or exceededTop or exceededBottom\n\t\t\tif hasExceeded ~= isOutsideParent then\n\t\t\t\tisOutsideParent = hasExceeded\n\t\t\t\tupdateVisibility()\n\t\t\tend\n\t\t\tif parentInstance:IsA(\"ScrollingFrame\") and previousScroller ~= parentInstance then\n\t\t\t\tpreviousScroller = parentInstance\n\t\t\t\tlocal connection = parentInstance:GetPropertyChangedSignal(\"AbsoluteWindowSize\"):Connect(function()\n\t\t\t\t\tcheckIfOutsideParentXBounds()\n\t\t\t\tend)\n\t\t\t\tcloneJanitor:add(connection, \"Disconnect\", \"TrackUtilityScroller-\"..ourUID)\n\t\t\tend\n\t\tend)\n\tend\n\n\tlocal camera = workspace.CurrentCamera\n\tlocal additionalOffsetX = instance:GetAttribute(\"AdditionalOffsetX\") or 0\n\tlocal function trackProperty(property)\n\t\tlocal absoluteProperty = \"Absolute\"..property\n\t\tlocal function updateProperty()\n\t\t\tlocal cloneValue = clone[absoluteProperty]\n\t\t\tlocal absoluteValue = UDim2.fromOffset(cloneValue.X, cloneValue.Y)\n\t\t\tif property == \"Position\" then\n\n\t\t\t\t-- This binds the instances within the bounds of the screen\n\t\t\t\tlocal SIDE_PADDING = 4\n\t\t\t\tlocal limitX = camera.ViewportSize.X - instance.AbsoluteSize.X - SIDE_PADDING\n\t\t\t\tlocal inputX = absoluteValue.X.Offset\n\t\t\t\tif inputX < SIDE_PADDING then\n\t\t\t\t\tinputX = SIDE_PADDING\n\t\t\t\telseif inputX > limitX then\n\t\t\t\t\tinputX = limitX\n\t\t\t\tend\n\t\t\t\tabsoluteValue = UDim2.fromOffset(inputX, absoluteValue.Y.Offset)\n\n\t\t\t\t-- AbsolutePosition does not perfectly match with TopbarInsets enabled\n\t\t\t\t-- This corrects this\n\t\t\t\tlocal topbarInset = GuiService.TopbarInset\n\t\t\t\tlocal viewportWidth = workspace.CurrentCamera.ViewportSize.X\n\t\t\t\tlocal guiWidth = screenGui.AbsoluteSize.X\n\t\t\t\tlocal guiOffset = screenGui.AbsolutePosition.X\n\t\t\t\t--local widthDifference = guiOffset - topbarInset.Min.X\n\t\t\t\tlocal oldTopbarCenterOffset = 0--widthDifference/30\n\t\t\t\tlocal offsetX = if Icon.isOldTopbar then guiOffset else viewportWidth - guiWidth - oldTopbarCenterOffset\n\t\t\t\t\n\t\t\t\t-- Also add additionalOffset\n\t\t\t\toffsetX -= additionalOffsetX\n\t\t\t\tabsoluteValue += UDim2.fromOffset(-offsetX, topbarInset.Height)\n\n\t\t\t\t-- Finally check if within its direct parents bounds\n\t\t\t\tcheckIfOutsideParentXBounds()\n\n\t\t\tend\n\t\t\tinstance[property] = absoluteValue\n\t\tend\n\t\t\n\t\t-- This defer is essential as the listener may be in a different screenGui to the actor\n\t\tlocal updatePropertyStaggered = Utility.createStagger(0.01, updateProperty)\n\t\tcloneJanitor:add(clone:GetPropertyChangedSignal(absoluteProperty):Connect(updatePropertyStaggered))\n\t\tcloneJanitor:add(clone:GetAttributeChangedSignal(\"ForceUpdate\"):Connect(function()\n\t\t\tupdatePropertyStaggered()\n\t\tend))\n\n\t\t-- This is to patch a weirddddd bug with ScreenGuis with SreenInsets set to\n\t\t-- 'TopbarSafeInsets'. For some reason the absolute position of gui instances\n\t\t-- within this type of screenGui DO NOT accurately update to match their new\n\t\t-- real world position; instead they jump around almost randomly for a few frames.\n\t\t-- I have spent way too many hours trying to solve this bug, I think the only way\n\t\t-- for the time being is to not use ScreenGuis with TopbarSafeInsets, but I don't\n\t\t-- have time to redesign the entire system around that at the moment.\n\t\t-- Here's a GIF of this bug: https://i.imgur.com/VitHdC1.gif\n\t\tlocal updatePropertyPatch = Utility.createStagger(0.5, updateProperty, true)\n\t\tcloneJanitor:add(clone:GetPropertyChangedSignal(absoluteProperty):Connect(updatePropertyPatch))\n\t\t\n\t\t-- When the screenGui is resized (such as when chat is hidden/shown), we need\n\t\t-- to update the position of the clone. Ths especially fixes the following:\n\t\t-- https://devforum.roblox.com/t/bug/1017485/1732\n\t\tif property == \"Position\" then\n\t\t\tcloneJanitor:add(screenGui:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(function()\n\t\t\t\tupdatePropertyStaggered()\n\t\t\tend))\n\t\tend\n\n\tend\n\ttask.delay(0.1, checkIfOutsideParentXBounds)\n\tcheckIfOutsideParentXBounds()\n\tupdateVisibility()\n\ttrackProperty(\"Position\")\n\t\n\t-- Track visiblity changes\n\tcloneJanitor:add(instance:GetPropertyChangedSignal(\"Visible\"):Connect(function()\n\t\t--print(\"Visiblity changed:\", instance, clone, instance.Visible)\n\t\t--clone.Visible = instance.Visible\n\tend))\n\n\t-- To ensure accurate positioning, it's important the clone also remains the same size as the instance\n\tlocal shouldTrackCloneSize = instance:GetAttribute(\"TrackCloneSize\")\n\tif shouldTrackCloneSize then\n\t\ttrackProperty(\"Size\")\n\telse\n\t\tcloneJanitor:add(instance:GetPropertyChangedSignal(\"AbsoluteSize\"):Connect(function()\n\t\t\tlocal absolute = instance.AbsoluteSize\n\t\t\tclone.Size = UDim2.fromOffset(absolute.X, absolute.Y)\n\t\tend))\n\tend\n\n\treturn clone\nend\n\nfunction Utility.joinFeature(originalIcon, parentIcon, iconsArray, scrollingFrameOrFrame)\n\n\t-- This is resonsible for moving the icon under a feature like a dropdown\n\tlocal joinJanitor = originalIcon.joinJanitor\n\tjoinJanitor:clean()\n\tif not scrollingFrameOrFrame then\n\t\toriginalIcon:leave()\n\t\treturn\n\tend\n\toriginalIcon.parentIconUID = parentIcon.UID\n\toriginalIcon.joinedFrame = scrollingFrameOrFrame\n\tlocal function updateAlignent()\n\t\tlocal parentAlignment = parentIcon.alignment\n\t\tif parentAlignment == \"Center\" then\n\t\t\tparentAlignment = \"Left\"\n\t\tend\n\t\toriginalIcon:setAlignment(parentAlignment, true)\n\tend\n\tjoinJanitor:add(parentIcon.alignmentChanged:Connect(updateAlignent))\n\tupdateAlignent()\n\toriginalIcon:modifyTheme({\"IconButton\", \"BackgroundTransparency\", 1}, \"JoinModification\")\n\toriginalIcon:modifyTheme({\"ClickRegion\", \"Active\", false}, \"JoinModification\")\n\tif parentIcon.childModifications then\n\t\t-- We defer so that the default values (such as dropdown\n\t\t-- minimum width can be applied before any custom\n\t\t-- child modifications from the user)\n\t\ttask.defer(function()\n\t\t\toriginalIcon:modifyTheme(parentIcon.childModifications, parentIcon.childModificationsUID)\n\t\tend)\n\tend\n\t--\n\tlocal clickRegion = originalIcon:getInstance(\"ClickRegion\")\n\tlocal function makeSelectable()\n\t\tclickRegion.Selectable = parentIcon.isSelected\n\tend\n\tjoinJanitor:add(parentIcon.toggled:Connect(makeSelectable))\n\ttask.defer(makeSelectable)\n\tjoinJanitor:add(function()\n\t\tclickRegion.Selectable = true\n\tend)\n\t--\n\n\t-- We track icons in arrays and dictionaries using their UID instead of the icon\n\t-- itself to prevent heavy cyclical tables when printing the icons\n\tlocal originalIconUID = originalIcon.UID\n\ttable.insert(iconsArray, originalIconUID)\n\tparentIcon:autoDeselect(false)\n\tparentIcon.childIconsDict[originalIconUID] = true\n\tif not parentIcon.isEnabled then\n\t\tparentIcon:setEnabled(true)\n\tend\n\toriginalIcon.joinedParent:Fire(parentIcon)\n\n\t-- This is responsible for removing it from that feature and updating\n\t-- their parent icon so its informed of the icon leaving it\n\tjoinJanitor:add(function()\n\t\tlocal joinedFrame = originalIcon.joinedFrame\n\t\tif not joinedFrame then\n\t\t\treturn\n\t\tend\n\t\tfor i, iconUID in pairs(iconsArray) do\n\t\t\tif iconUID == originalIconUID then\n\t\t\t\ttable.remove(iconsArray, i)\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\t\tlocal Icon = require(originalIcon.iconModule)\n\t\tlocal parentIcon = Icon.getIconByUID(originalIcon.parentIconUID)\n\t\tif not parentIcon then\n\t\t\treturn\n\t\tend\n\t\toriginalIcon:setAlignment(originalIcon.originalAlignment)\n\t\toriginalIcon.parentIconUID = false\n\t\toriginalIcon.joinedFrame = false\n\t\t--originalIcon:setBehaviour(\"IconButton\", \"BackgroundTransparency\", nil, true)\n\t\toriginalIcon:removeModification(\"JoinModification\")\n\t\t\n\t\tlocal parentHasNoChildren = true\n\t\tlocal parentChildIcons = parentIcon.childIconsDict\n\t\tparentChildIcons[originalIconUID] = nil\n\t\tfor childIconUID, _ in pairs(parentChildIcons) do\n\t\t\tparentHasNoChildren = false\n\t\t\tbreak\n\t\tend\n\t\tif parentHasNoChildren and not parentIcon.isAnOverflow then\n\t\t\tparentIcon:setEnabled(false)\n\t\tend\n\t\tupdateAlignent()\n\n\tend)\n\nend\n\n\n\nreturn Utility"
  },
  {
    "path": "src/VERSION.lua",
    "content": "--!strict\n-- LOCAL\nlocal VERSION = {}\n\n\n\n-- SHARED\nVERSION.appVersion = \"v3.4.0\"\nVERSION.latestVersion = nil :: string?\n\n\n\n-- FUNCTIONS\nfunction VERSION.getLatestVersion(): string?\n\tlocal DEVELOPMENT_PLACE_ID = 117501901079852\n\tlocal latestVersion = VERSION.latestVersion\n\tif latestVersion then\n\t\treturn latestVersion\n\tend\n\tlocal placeName = \"\"\n\twhile true do\n\t\tlocal success, hdDevelopmentDetails = pcall(function()\n\t\t\treturn game:GetService(\"MarketplaceService\"):GetProductInfo(DEVELOPMENT_PLACE_ID)\n\t\tend)\n\t\tif success and hdDevelopmentDetails then\n\t\t\tplaceName = hdDevelopmentDetails.Name\n\t\t\tbreak\n\t\tend\n\t\ttask.wait(1)\n\tend\n\tlatestVersion = string.match(placeName, \"^TopbarPlus (.*)$\")\n\tif latestVersion then\n\t\tlatestVersion = latestVersion:gsub(\"%s+\", \"\") -- Remove all whitespace (spaces, tabs, newlines)\n\tend\n\tVERSION.latestVersion = latestVersion\n\treturn latestVersion\nend\n\nfunction VERSION.getAppVersion()\n\treturn VERSION.appVersion\nend\n\nfunction VERSION.isUpToDate()\n\tlocal latestVersion = VERSION.getLatestVersion()\n\tlocal appVersion = VERSION.getAppVersion()\n\treturn latestVersion ~= nil and latestVersion == appVersion\nend\n\n\n\nreturn VERSION"
  },
  {
    "path": "src/init.lua",
    "content": "--!nonstrict\n--[[\n\t\n\tThe majority of this code is an interface designed to make it easy for you to\n\twork with TopbarPlus (most methods for instance reference :modifyTheme()).\n\tThe processing overhead mainly consists of applying themes and calculating \n\tappearance (such as size and width of labels) which is handled in about\n\t200 lines of code here and the Widget UI module. This has been achieved\n\tin v3 by outsourcing a majority of previous calculations to inbuilt Roblox\n\tfeatures like UIListLayouts.\n\n\n\tv3 provides inbuilt support for controllers (simply press DPadUp),\n\ttouch devices (phones, tablets , etc), localization (automatic resizing\n\tof widgets, autolocalize for relevant labels), backwards compatability\n\twith the old topbar, and more.\n\n\n\tMy primary goals for the v3 re-write have been to:\n\t\t\n\t1. Improve code readability and organisation (reduced lines of code within\n\t   Icon+IconController from 3200 to ~950, separated UI elements, etc)\n\t\t\n\t2. Improve ease-of-use (themes now actually make sense and can account\n\t   for any modifications you want, converted to a package for\n\t   quick installation and easy-comparisons of new updates, etc)\n\t\n\t3. Provide support for all key features of the new Roblox topbar\n\t   while improving performance of the module (deferring and collecting\n\t   changes then calling as a singular, utilizing inbuilt Roblox features\n\t   such as UILIstLayouts, etc)\n\n--]]\n\n\n\n-- SERVICES\nlocal UserInputService = game:GetService(\"UserInputService\")\nlocal ContentProvider = game:GetService(\"ContentProvider\")\nlocal StarterGui = game:GetService(\"StarterGui\")\nlocal Players = game:GetService(\"Players\")\nlocal Types = require(script.Types)\n\n\n\n-- TYPES\nexport type Icon = Types.Icon\n\n\n\n-- REFERENCE HANDLER\n-- Multiple Icons packages may exist at runtime (for instance if the developer additionally uses HD Admin)\n-- therefore this ensures that the first required package becomes the dominant and only functioning module\nlocal iconModule = script\nlocal Reference = require(iconModule.Reference)\nlocal referenceObject = Reference.getObject()\nlocal leadPackage = referenceObject and referenceObject.Value\nif leadPackage and leadPackage ~= iconModule then\n\treturn require(leadPackage) :: Types.StaticIcon\nend\nif not referenceObject then\n\tReference.addToReplicatedStorage()\nend\n\n\n\n-- MODULES\nlocal Signal = require(iconModule.Packages.GoodSignal)\nlocal Janitor = require(iconModule.Packages.Janitor)\nlocal Utility = require(iconModule.Utility)\nlocal Themes = require(iconModule.Features.Themes)\nlocal Gamepad = require(iconModule.Features.Gamepad)\nlocal Overflow = require(iconModule.Features.Overflow)\nlocal Icon = {}\nIcon.__index = Icon\n\n\n\n--- LOCAL\nlocal localPlayer = Players.LocalPlayer\nlocal themes = iconModule.Features.Themes\nlocal iconsDict = {}\nlocal anyIconSelected = Signal.new()\nlocal elements = iconModule.Elements\nlocal totalCreatedIcons = 0\nlocal preferredInput = {\n\tmobile = Enum.PreferredInput.Touch,\n\tdesktop = Enum.PreferredInput.KeyboardAndMouse,\n\tconsole = Enum.PreferredInput.Gamepad\n}\n\n\n\n-- PUBLIC VARIABLES\nIcon.baseDisplayOrderChanged = Signal.new()\nIcon.baseDisplayOrder = 10\nIcon.baseTheme = require(themes.Default)\nIcon.isOldTopbar = false -- Logic has been moved to Container\nIcon.iconsDictionary = iconsDict\nIcon.insetHeightChanged = Signal.new()\nIcon.container = require(elements.Container)(Icon)\nIcon.topbarEnabled = true\nIcon.iconAdded = Signal.new()\nIcon.iconRemoved = Signal.new()\nIcon.iconChanged = Signal.new()\n\n\n\n-- PUBLIC FUNCTIONS\nfunction Icon.getIcons()\n\treturn Icon.iconsDictionary\nend\n\nfunction Icon.getIconByUID(UID)\n\tlocal match = Icon.iconsDictionary[UID]\n\tif match then\n\t\treturn match\n\tend\n\treturn nil\nend\n\nfunction Icon.getIcon(nameOrUID)\n\tlocal match = Icon.getIconByUID(nameOrUID)\n\tif match then\n\t\treturn match\n\tend\n\tfor _, icon in pairs(iconsDict) do\n\t\tif icon.name == nameOrUID then\n\t\t\treturn icon\n\t\tend\n\tend\n\treturn nil\nend\n\nfunction Icon.setTopbarEnabled(bool, isInternal)\n\tif typeof(bool) ~= \"boolean\" then\n\t\tbool = Icon.topbarEnabled\n\tend\n\tif not isInternal then\n\t\tIcon.topbarEnabled = bool\n\tend\n\tfor _, screenGui in pairs(Icon.container) do\n\t\tscreenGui.Enabled = bool\n\tend\nend\n\nfunction Icon.modifyBaseTheme(modifications)\n\tmodifications = Themes.getModifications(modifications)\n\tfor _, modification in pairs(modifications) do\n\t\tfor _, detail in pairs(Icon.baseTheme) do\n\t\t\tThemes.merge(detail, modification)\n\t\tend\n\tend\n\tfor _, icon in pairs(iconsDict) do\n\t\ticon:setTheme(Icon.baseTheme)\n\tend\nend\n\nfunction Icon.setDisplayOrder(int)\n\tIcon.baseDisplayOrder = int\n\tIcon.baseDisplayOrderChanged:Fire(int)\nend\n\n\n\n-- SETUP\ntask.defer(Gamepad.start, Icon)\ntask.defer(Overflow.start, Icon)\ntask.defer(function()\n\tlocal playerGui = localPlayer:WaitForChild(\"PlayerGui\")\n\tfor _, screenGui in pairs(Icon.container) do\n\t\tscreenGui.Parent = playerGui\n\tend\n\trequire(iconModule.Attribute)\nend)\n\n\n\n-- CONSTRUCTOR\nfunction Icon.new()\n\tlocal self = {}\n\tsetmetatable(self, Icon)\n\n\t--- Janitors (for cleanup)\n\tlocal janitor = Janitor.new()\n\tself.janitor = janitor\n\tself.themesJanitor = janitor:add(Janitor.new())\n\tself.singleClickJanitor = janitor:add(Janitor.new())\n\tself.captionJanitor = janitor:add(Janitor.new())\n\tself.joinJanitor = janitor:add(Janitor.new())\n\tself.menuJanitor = janitor:add(Janitor.new())\n\tself.dropdownJanitor = janitor:add(Janitor.new())\n\n\t-- Register\n\tlocal iconUID = Utility.generateUID()\n\ticonsDict[iconUID] = self\n\tjanitor:add(function()\n\t\ticonsDict[iconUID] = nil\n\tend)\n\n\t-- Signals (events)\n\tself.selected = janitor:add(Signal.new())\n\tself.deselected = janitor:add(Signal.new())\n\tself.toggled = janitor:add(Signal.new())\n\tself.viewingStarted = janitor:add(Signal.new())\n\tself.viewingEnded = janitor:add(Signal.new())\n\tself.stateChanged = janitor:add(Signal.new())\n\tself.notified = janitor:add(Signal.new())\n\tself.noticeStarted = janitor:add(Signal.new())\n\tself.noticeChanged = janitor:add(Signal.new())\n\tself.endNotices = janitor:add(Signal.new())\n\tself.toggleKeyAdded = janitor:add(Signal.new())\n\tself.fakeToggleKeyChanged = janitor:add(Signal.new())\n\tself.alignmentChanged = janitor:add(Signal.new())\n\tself.updateSize = janitor:add(Signal.new())\n\tself.resizingComplete = janitor:add(Signal.new())\n\tself.joinedParent = janitor:add(Signal.new())\n\tself.menuSet = janitor:add(Signal.new())\n\tself.dropdownSet = janitor:add(Signal.new())\n\tself.updateMenu = janitor:add(Signal.new())\n\tself.startMenuUpdate = janitor:add(Signal.new())\n\tself.childThemeModified = janitor:add(Signal.new())\n\tself.indicatorSet = janitor:add(Signal.new())\n\tself.dropdownChildAdded = janitor:add(Signal.new())\n\tself.menuChildAdded = janitor:add(Signal.new())\n\n\t-- Properties\n\tself.iconModule = iconModule\n\tself.UID = iconUID\n\tself.isEnabled = true\n\tself.enabled = self.isEnabled -- Backwards compatability\n\tself.isSelected = false\n\tself.isViewing = false\n\tself.joinedFrame = false\n\tself.parentIconUID = false\n\tself.deselectWhenOtherIconSelected = true\n\tself.totalNotices = 0\n\tself.activeState = \"Deselected\"\n\tself.alignment = \"\"\n\tself.originalAlignment = \"\"\n\tself.appliedTheme = {}\n\tself.appearance = {}\n\tself.cachedInstances = {}\n\tself.cachedNamesToInstances = {}\n\tself.cachedCollectives = {}\n\tself.bindedToggleKeys = {}\n\tself.customBehaviours = {}\n\tself.toggleItems = {}\n\tself.bindedEvents = {}\n\tself.notices = {}\n\tself.menuIcons = {}\n\tself.dropdownIcons = {}\n\tself.childIconsDict = {}\n\tself.creationTime = os.clock()\n\n\t-- Widget is the new name for an icon\n\tlocal widget = janitor:add(require(elements.Widget)(self, Icon))\n\tself.widget = widget\n\tself:setAlignment()\n\t\n\t-- It's important we set an order otherwise icons will not align\n\t-- correctly within menus\n\ttotalCreatedIcons += 1\n\tlocal ourOrder = 1+(totalCreatedIcons*0.01)\n\tself:setOrder(ourOrder, \"deselected\")\n\tself:setOrder(ourOrder, \"selected\")\n\n\t-- This applies the default them\n\tself:setTheme(Icon.baseTheme)\n\n\t-- Button Clicked (for states \"Selected\" and \"Deselected\")\n\tlocal clickRegion = self:getInstance(\"ClickRegion\")\n\tlocal hasUsedMouseButton1Click = false\n\tlocal lastToggleTime = 0\n\tlocal DEBOUNCE_TIME = 0.1 -- 100ms debounce to prevent rapid toggles\n\n\tlocal function handleToggle()\n\t\tif self.locked then\n\t\t\treturn\n\t\tend\n\n\t\t-- Debounce logic to prevent rapid toggling\n\t\tlocal currentTime = tick()\n\t\tif currentTime - lastToggleTime < DEBOUNCE_TIME then\n\t\t\treturn\n\t\tend\n\t\tlastToggleTime = currentTime\n\n\t\tif self.isSelected then\n\t\t\tself:deselect(\"User\", self)\n\t\telse\n\t\t\tself:select(\"User\", self)\n\t\tend\n\tend\n\n\tclickRegion.MouseButton1Click:Connect(function()\n\t\thasUsedMouseButton1Click = true\n\t\thandleToggle()\n\tend)\n\n\tclickRegion.TouchTap:Connect(function()\n\t\t-- This resolves the bug report by @28Pixels:\n\t\t-- https://devforum.roblox.com/t/topbarplus/1017485/1104\n\t\t-- Only use TouchTap if MouseButton1Click has never fired\n\t\t-- This handles edge cases where ONLY TouchTap works\n\t\t-- Also prevents double-toggle bug with multi-touch on mobile\n\t\t-- Credit to @sayer80 for this fix\n\t\tif not hasUsedMouseButton1Click then\n\t\t\thandleToggle()\n\t\tend\n\tend)\n\n\t-- Keys can be bound to toggle between Selected and Deselected\n\tjanitor:add(UserInputService.InputBegan:Connect(function(input, touchingAnObject)\n\t\tif self.locked then\n\t\t\treturn\n\t\tend\n\t\tif self.bindedToggleKeys[input.KeyCode] and not touchingAnObject then\n\t\t\thandleToggle()\n\t\tend\n\tend))\n\n\t-- Button Hovering (for state \"Viewing\")\n\t-- Hovering is a state only for devices with keyboards\n\t-- and controllers (not touchpads)\n\tlocal function viewingStarted(dontSetState)\n\t\tif self.locked then\n\t\t\treturn\n\t\tend\n\t\tself.isViewing = true\n\t\tself.viewingStarted:Fire(true)\n\t\tif not dontSetState then\n\t\t\tself:setState(\"Viewing\", \"User\", self)\n\t\tend\n\tend\n\tlocal function viewingEnded()\n\t\tif self.locked then\n\t\t\treturn\n\t\tend\n\t\tself.isViewing = false\n\t\tself.viewingEnded:Fire(true)\n\t\tself:setState(nil, \"User\", self)\n\tend\n\tself.joinedParent:Connect(function()\n\t\tif self.isViewing then\n\t\t\tviewingEnded()\n\t\tend\n\tend)\n\tclickRegion.MouseEnter:Connect(function()\n\t\tlocal dontSetState = UserInputService.PreferredInput ~= preferredInput.desktop\n\t\tviewingStarted(dontSetState)\n\tend)\n\tlocal touchCount = 0\n\tjanitor:add(UserInputService.TouchEnded:Connect(viewingEnded))\n\tclickRegion.MouseLeave:Connect(viewingEnded)\n\tclickRegion.SelectionGained:Connect(viewingStarted)\n\tclickRegion.SelectionLost:Connect(viewingEnded)\n\tclickRegion.MouseButton1Down:Connect(function()\n\t\tif not self.locked and UserInputService.PreferredInput == preferredInput.mobile then\n\t\t\ttouchCount += 1\n\t\t\tlocal myTouchCount = touchCount\n\t\t\ttask.delay(0.2, function()\n\t\t\t\tif myTouchCount == touchCount then\n\t\t\t\t\tviewingStarted()\n\t\t\t\tend\n\t\t\tend)\n\t\tend\n\tend)\n\tclickRegion.MouseButton1Up:Connect(function()\n\t\ttouchCount += 1\n\tend)\n\n\t-- Handle overlay on viewing\n\tlocal iconOverlay = self:getInstance(\"IconOverlay\")\n\tself.viewingStarted:Connect(function()\n\t\ticonOverlay.Visible = not self.overlayDisabled\n\tend)\n\tself.viewingEnded:Connect(function()\n\t\ticonOverlay.Visible = false\n\tend)\n\n\t-- Deselect when another icon is selected\n\tjanitor:add(anyIconSelected:Connect(function(incomingIcon)\n\t\tif incomingIcon ~= self and self.deselectWhenOtherIconSelected and incomingIcon.deselectWhenOtherIconSelected then\n\t\t\tself:deselect(\"AutoDeselect\", incomingIcon)\n\t\tend\n\tend))\n\n\t-- This checks if the script calling this module is a descendant of a ScreenGui\n\t-- with 'ResetOnSpawn' set to true. If it is, then we destroy the icon the\n\t-- client respawns. This solves one of the most asked about questions on the post\n\t-- The only caveat this may not work if the player doesn't uniquely name their ScreenGui and the frames\n\t-- the LocalScript rests within\n\tlocal source =  debug.info(2, \"s\")\n\tlocal sourcePath = string.split(source, \".\")\n\tlocal origin = game\n\tlocal originsScreenGui\n\tfor i, sourceName in pairs(sourcePath) do\n\t\torigin = origin:FindFirstChild(sourceName)\n\t\tif not origin then\n\t\t\tbreak\n\t\tend\n\t\tif origin:IsA(\"ScreenGui\") then\n\t\t\toriginsScreenGui = origin\n\t\tend\n\tend\n\tif origin and originsScreenGui and originsScreenGui.ResetOnSpawn == true then\n\t\tself.originsScreenGui = originsScreenGui\n\t\tUtility.localPlayerRespawned(function()\n\t\t\tself:destroy()\n\t\tend)\n\tend\n\n\t-- Additional children behaviour when toggled (mostly notices)\n\tself.toggled:Connect(function(isSelected)\n\t\tself.noticeChanged:Fire(self.totalNotices)\n\t\tfor childIconUID, _ in pairs(self.childIconsDict) do\n\t\t\tlocal childIcon = Icon.getIconByUID(childIconUID)\n\t\t\tchildIcon.noticeChanged:Fire(childIcon.totalNotices)\n\t\t\tif not isSelected and childIcon.isSelected then\n\t\t\t\t-- If an icon within a menu or dropdown is also\n\t\t\t\t-- a dropdown or menu, then close it\n\t\t\t\tfor _, _ in pairs(childIcon.childIconsDict) do\n\t\t\t\t\tchildIcon:deselect(\"HideParentFeature\", self)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend)\n\t\n\t-- This closes/reopens the chat or playerlist if the icon is a dropdown\n\t-- In the future I'd prefer to use the position+size of the chat\n\t-- to determine whether to close dropdown (instead of non-right-set)\n\t-- but for reasons mentioned here it's unreliable at the time of\n\t-- writing this: https://devforum.roblox.com/t/here/2794915\n\t-- I could also make this better by accounting for multiple\n\t-- dropdowns being open (not just this one) but this will work\n\t-- fine for almost every use case for now.\n\tself.selected:Connect(function()\n\t\tlocal isDropdown = #self.dropdownIcons > 0\n\t\tif isDropdown then\n\t\t\tif StarterGui:GetCore(\"ChatActive\") and self.alignment ~= \"Right\" then\n\t\t\t\tself.chatWasPreviouslyActive = true\n\t\t\t\tStarterGui:SetCore(\"ChatActive\", false)\n\t\t\tend\n\t\t\tif StarterGui:GetCoreGuiEnabled(\"PlayerList\") and self.alignment ~= \"Left\" then\n\t\t\t\tself.playerlistWasPreviouslyActive = true\n\t\t\t\tStarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, false)\n\t\t\tend\n\t\tend\n\tend)\n\tself.deselected:Connect(function()\n\t\tif self.chatWasPreviouslyActive then\n\t\t\tself.chatWasPreviouslyActive = nil\n\t\t\tStarterGui:SetCore(\"ChatActive\", true)\n\t\tend\n\t\tif self.playerlistWasPreviouslyActive then\n\t\t\tself.playerlistWasPreviouslyActive = nil\n\t\t\tStarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, true)\n\t\tend\n\tend)\n\t\n\t-- There's a rare occassion where the appearance is not\n\t-- fully set to deselected so this ensures the icons\n\t-- appearance is fully as it should be\n\ttask.delay(0.1, function()\n\t\tif self.activeState == \"Deselected\" then\n\t\t\tself.stateChanged:Fire(\"Deselected\")\n\t\t\tself:refresh()\n\t\tend\n\tend)\n\t\n\t-- Call icon added\n\tIcon.iconAdded:Fire(self)\n\n\treturn self\nend\n\n\n\n-- METHODS\nfunction Icon:setName(name)\n\tself.widget.Name = name\n\tself.name = name\n\treturn self\nend\n\nfunction Icon:setState(incomingStateName, fromSource, sourceIcon)\n\t-- This is responsible for acknowleding a change in stage (such as from \"Deselected\" to \"Viewing\" when\n\t-- a users mouse enters the widget), then informing other systems of this state change to then act upon\n\t-- (such as the theme handler applying the theme which corresponds to that state).\n\tif not incomingStateName then\n\t\tincomingStateName = (self.isSelected and \"Selected\") or \"Deselected\"\n\tend\n\tlocal stateName = Utility.formatStateName(incomingStateName)\n\tlocal previousStateName = self.activeState\n\tif previousStateName == stateName then\n\t\treturn\n\tend\n\tlocal currentIsSelected = self.isSelected\n\tself.activeState = stateName\n\tif stateName == \"Deselected\" then\n\t\tself.isSelected = false\n\t\tif currentIsSelected then\n\t\t\tself.toggled:Fire(false, fromSource, sourceIcon)\n\t\t\tself.deselected:Fire(fromSource, sourceIcon)\n\t\tend\n\t\tself:_setToggleItemsVisible(false, fromSource, sourceIcon)\n\telseif stateName == \"Selected\" then\n\t\tself.isSelected = true\n\t\tif not currentIsSelected then\n\t\t\tself.toggled:Fire(true, fromSource, sourceIcon)\n\t\t\tself.selected:Fire(fromSource, sourceIcon)\n\t\t\tanyIconSelected:Fire(self, fromSource, sourceIcon)\n\t\tend\n\t\tself:_setToggleItemsVisible(true, fromSource, sourceIcon)\n\tend\n\tself.stateChanged:Fire(stateName, fromSource, sourceIcon)\nend\n\nfunction Icon:getInstance(name)\n\t-- This enables us to easily retrieve instances located within the icon simply by passing its name.\n\t-- Every important/significant instance is named uniquely therefore this is no worry of overlap.\n\t-- We cache the result for more performant retrieval in the future.\n\tlocal instance = self.cachedNamesToInstances[name]\n\tif instance then\n\t\treturn instance\n\tend\n\tlocal function cacheInstance(childName, child)\n\t\tlocal currentCache = self.cachedInstances[child]\n\t\tif not currentCache then\n\t\t\tlocal collectiveName = child:GetAttribute(\"Collective\")\n\t\t\tlocal cachedCollective = collectiveName and self.cachedCollectives[collectiveName]\n\t\t\tif cachedCollective then\n\t\t\t\ttable.insert(cachedCollective, child)\n\t\t\tend\n\t\t\tself.cachedNamesToInstances[childName] = child\n\t\t\tself.cachedInstances[child] = true\n\t\t\tchild.Destroying:Once(function()\n\t\t\t\tself.cachedNamesToInstances[childName] = nil\n\t\t\t\tself.cachedInstances[child] = nil\n\t\t\tend)\n\t\tend\n\tend\n\tlocal widget = self.widget\n\tcacheInstance(\"Widget\", widget)\n\tif name == \"Widget\" then\n\t\treturn widget\n\tend\n\n\tlocal returnChild\n\tlocal function scanChildren(parentInstance)\n\t\tfor _, child in pairs(parentInstance:GetChildren()) do\n\t\t\tlocal widgetUID = child:GetAttribute(\"WidgetUID\")\n\t\t\tif widgetUID and widgetUID ~= self.UID then\n\t\t\t\t-- This prevents instances within other icons from being recorded\n\t\t\t\t-- (for instance when other icons are added to this icons menu)\n\t\t\t\tcontinue\n\t\t\tend\n\t\t\t-- If the child is a fake placeholder instance (such as dropdowns, notices, etc)\n\t\t\t-- then its important we scan the real original instance instead of this clone\n\t\t\tlocal realChild = Themes.getRealInstance(child)\n\t\t\tif realChild then\n\t\t\t\tchild = realChild\n\t\t\tend\n\t\t\t-- Finally scan its children\n\t\t\tscanChildren(child)\n\t\t\tif child:IsA(\"GuiBase\") or child:IsA(\"UIBase\") or child:IsA(\"ValueBase\") then\n\t\t\t\tlocal childName = child.Name\n\t\t\t\tcacheInstance(childName, child)\n\t\t\t\tif childName == name then\n\t\t\t\t\treturnChild = child\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\tscanChildren(widget)\n\treturn returnChild\nend\n\nfunction Icon:getCollective(name)\n\t-- A collective is an array of instances within the Widget that have been\n\t-- grouped together based on a given name. This just makes it easy\n\t-- to act on multiple instances at once which share similar behaviours.\n\t-- For instance, if we want to change the icons corner size, all corner instances\n\t-- with the attribute \"Collective\" and value \"WidgetCorner\" could be updated\n\t-- instantly by doing Themes.apply(icon, \"WidgetCorner\", newSize)\n\tlocal collective = self.cachedCollectives[name]\n\tif collective then\n\t\treturn collective\n\tend\n\tcollective = {}\n\tfor instance, _ in pairs(self.cachedInstances) do\n\t\tif instance:GetAttribute(\"Collective\") == name then\n\t\t\ttable.insert(collective, instance)\n\t\tend\n\tend\n\tself.cachedCollectives[name] = collective\n\treturn collective\nend\n\nfunction Icon:getInstanceOrCollective(collectiveOrInstanceName)\n\t-- Similar to :getInstance but also accounts for 'Collectives', such as UICorners and returns\n\t-- an array of instances instead of a single instance\n\tlocal instances = {}\n\tlocal instance = self:getInstance(collectiveOrInstanceName)\n\tif instance then\n\t\ttable.insert(instances, instance)\n\tend\n\tif #instances == 0 then\n\t\tinstances = self:getCollective(collectiveOrInstanceName)\n\tend\n\treturn instances\nend\n\nfunction Icon:getStateGroup(iconState)\n\tlocal chosenState = iconState or self.activeState\n\tlocal stateGroup = self.appearance[chosenState]\n\tif not stateGroup then\n\t\tstateGroup = {}\n\t\tself.appearance[chosenState] = stateGroup\n\tend\n\treturn stateGroup\nend\n\nfunction Icon:refreshAppearance(instance, specificProperty)\n\tThemes.refresh(self, instance, specificProperty)\n\treturn self\nend\n\nfunction Icon:refresh()\n\tself:refreshAppearance(self.widget)\n\tself.updateSize:Fire()\n\treturn self\nend\n\nfunction Icon:updateParent()\n\tlocal parentIcon = Icon.getIconByUID(self.parentIconUID)\n\tif parentIcon then\n\t\tparentIcon.updateSize:Fire()\n\tend\nend\n\nfunction Icon:setBehaviour(collectiveOrInstanceName, property, callback, refreshAppearance)\n\t-- You can specify your own custom callback to handle custom logic just before\n\t-- an instances property is changed by using :setBehaviour()\n\tlocal key = collectiveOrInstanceName..\"-\"..property\n\tself.customBehaviours[key] = callback\n\tif refreshAppearance then\n\t\tlocal instances = self:getInstanceOrCollective(collectiveOrInstanceName)\n\t\tfor _, instance in pairs(instances) do\n\t\t\tself:refreshAppearance(instance, property)\n\t\tend\n\tend\nend\n\nfunction Icon:modifyTheme(modifications, customModificationUID)\n\tlocal modificationUID = Themes.modify(self, modifications, customModificationUID)\n\treturn self, modificationUID\nend\n\nfunction Icon:modifyChildTheme(modifications, modificationUID)\n\t-- Same as modifyTheme except for its children (i.e. icons\n\t-- within its dropdown or menu)\n\tself.childModifications = modifications\n\tself.childModificationsUID = modificationUID\n\tfor childIconUID, _ in pairs(self.childIconsDict) do\n\t\tlocal childIcon = Icon.getIconByUID(childIconUID)\n\t\tchildIcon:modifyTheme(modifications, modificationUID)\n\tend\n\tself.childThemeModified:Fire()\n\treturn self\nend\n\nfunction Icon:removeModification(modificationUID)\n\tThemes.remove(self, modificationUID)\n\treturn self\nend\n\nfunction Icon:removeModificationWith(instanceName, property, state)\n\tThemes.removeWith(self, instanceName, property, state)\n\treturn self\nend\n\nfunction Icon:setTheme(theme)\n\tThemes.set(self, theme)\n\treturn self\nend\n\nfunction Icon:setEnabled(bool)\n\tself.isEnabled = bool\n\tself.enabled = self.isEnabled\n\tself.widget.Visible = bool\n\tself:updateParent()\n\treturn self\nend\n\nfunction Icon:select(fromSource, sourceIcon)\n\tself:setState(\"Selected\", fromSource, sourceIcon)\n\treturn self\nend\n\nfunction Icon:deselect(fromSource, sourceIcon)\n\tself:setState(\"Deselected\", fromSource, sourceIcon)\n\treturn self\nend\n\nfunction Icon:notify(customClearSignal, noticeId)\n\t-- Generates a notification which appears in the top right of the icon. Useful for example for prompting\n\t-- users of changes/updates within your UI such as a Catalog\n\t-- 'customClearSignal' is a signal object (e.g. icon.deselected) or\n\t-- Roblox event (e.g. Instance.new(\"BindableEvent\").Event)\n\tlocal notice = self.notice\n\tif not notice then\n\t\tnotice = require(elements.Notice)(self, Icon)\n\t\tself.notice = notice\n\tend\n\tself.noticeStarted:Fire(customClearSignal, noticeId)\n\treturn self\nend\n\nfunction Icon:clearNotices()\n\tself.endNotices:Fire()\n\treturn self\nend\n\nfunction Icon:disableOverlay(bool)\n\tself.overlayDisabled = bool\n\treturn self\nend\nIcon.disableStateOverlay = Icon.disableOverlay\n\nfunction Icon:setImage(imageId, iconState)\n\tself:modifyTheme({\"IconImage\", \"Image\", imageId, iconState})\n\t\n\t-- This code ensures icon images are preloaded if they haven't been fetched yet\n\ttask.spawn(function()\n\t\tlocal newIdContent = if tonumber(imageId) then `rbxassetid://{imageId}` else imageId\n\t\tlocal initialAssetFetchStatus = ContentProvider:GetAssetFetchStatus(newIdContent)\n\t\n\t\tif initialAssetFetchStatus ~= Enum.AssetFetchStatus.Success then\n\t\t\tpcall(ContentProvider.PreloadAsync, ContentProvider, { newIdContent })\n\t\tend\n\tend)\n\t\t\n\treturn self\nend\n\nfunction Icon:setLabel(text, iconState)\n\tself:modifyTheme({\"IconLabel\", \"Text\", text, iconState})\n\treturn self\nend\n\nfunction Icon:setOrder(int, iconState)\n\t-- We multiply by 100 to allow for custom increments inbetween\n\t-- (.01, .02, etc) as LayoutOrders only support integers\n\tlocal newInt = int*100\n\tself:modifyTheme({\"IconSpot\", \"LayoutOrder\", newInt, iconState})\n\tself:modifyTheme({\"Widget\", \"LayoutOrder\", newInt, iconState})\n\treturn self\nend\n\nfunction Icon:setCornerRadius(udim, iconState)\n\tself:modifyTheme({\"IconCorners\", \"CornerRadius\", udim, iconState})\n\treturn self\nend\n\nfunction Icon:align(leftCenterOrRight, isFromParentIcon)\n\t-- Determines the side of the screen the icon will be ordered\n\tlocal direction = tostring(leftCenterOrRight):lower()\n\tif direction == \"mid\" or direction == \"centre\" then\n\t\tdirection = \"center\"\n\tend\n\tif direction ~= \"left\" and direction ~= \"center\" and direction ~= \"right\" then\n\t\tdirection = \"left\"\n\tend\n\tlocal screenGui = (direction == \"center\" and Icon.container.TopbarCentered) or Icon.container.TopbarStandard\n\tlocal holders = screenGui.Holders\n\tlocal finalDirection = string.upper(string.sub(direction, 1, 1))..string.sub(direction, 2)\n\tif not isFromParentIcon then\n\t\tself.originalAlignment = finalDirection\n\tend\n\tlocal joinedFrame = self.joinedFrame\n\tlocal alignmentHolder = holders[finalDirection]\n\tself.screenGui = screenGui\n\tself.alignmentHolder = alignmentHolder\n\tif not self.isDestroyed then\n\t\tself.widget.Parent = joinedFrame or alignmentHolder\n\tend\n\tself.alignment = finalDirection\n\tself.alignmentChanged:Fire(finalDirection)\n\tIcon.iconChanged:Fire(self)\n\treturn self\nend\nIcon.setAlignment = Icon.align\n\nfunction Icon:setLeft()\n\tself:setAlignment(\"Left\")\n\treturn self\nend\n\nfunction Icon:setMid()\n\tself:setAlignment(\"Center\")\n\treturn self\nend\n\nfunction Icon:setRight()\n\tself:setAlignment(\"Right\")\n\treturn self\nend\n\nfunction Icon:setWidth(offsetMinimum, iconState)\n\t-- This sets a minimum X offset size for the widget, useful\n\t-- for example if you're constantly changing the label\n\t-- but don't want the icon to resize every time\n\tself:modifyTheme({\"Widget\", \"DesiredWidth\", offsetMinimum, iconState})\n\treturn self\nend\n\nfunction Icon:setImageScale(number, iconState)\n\tself:modifyTheme({\"IconImageScale\", \"Value\", number, iconState})\n\treturn self\nend\n\nfunction Icon:setImageRatio(number, iconState)\n\tself:modifyTheme({\"IconImageRatio\", \"AspectRatio\", number, iconState})\n\treturn self\nend\n\nfunction Icon:setTextSize(number, iconState)\n\tself:modifyTheme({\"IconLabel\", \"TextSize\", number, iconState})\n\treturn self\nend\n\nfunction Icon:setTextFont(font, fontWeight, fontStyle, iconState)\n\tfontWeight = fontWeight or Enum.FontWeight.Regular\n\tfontStyle = fontStyle or Enum.FontStyle.Normal\n\tlocal fontFace\n\tlocal fontType = typeof(font)\n\tif fontType == \"number\" then\n\t\tfontFace = Font.fromId(font, fontWeight, fontStyle)\n\telseif fontType == \"EnumItem\" then\n\t\tfontFace = Font.fromEnum(font)\n\telseif fontType == \"string\" then\n\t\tif not font:match(\"rbxasset\") then\n\t\t\tfontFace = Font.fromName(font, fontWeight, fontStyle)\n\t\tend\n\tend\n\tif not fontFace then\n\t\tfontFace = Font.new(font, fontWeight, fontStyle)\n\tend\n\tself:modifyTheme({\"IconLabel\", \"FontFace\", fontFace, iconState})\n\treturn self\nend\n\nfunction Icon:setTextColor(Color, iconState)\n\tif Color == nil or Color == \"\" or (type(Color) ~= \"userdata\" or typeof(Color) ~= \"Color3\") then\n\t\tif Color ~= nil and Color ~= \"\" then\n\t\t\twarn(\"setTextColor item must be a Color3 value! Changed the color to white.\")\n\t\tend\n\t\tColor = Color3.fromRGB(255, 255, 255)\n\tend\n\n\tself:modifyTheme({\"IconLabel\", \"TextColor3\", Color, iconState})\n\treturn self\nend\n\nfunction Icon:bindToggleItem(guiObjectOrLayerCollector)\n\tif not guiObjectOrLayerCollector:IsA(\"GuiObject\") and not guiObjectOrLayerCollector:IsA(\"LayerCollector\") then\n\t\terror(\"Toggle item must be a GuiObject or LayerCollector!\")\n\tend\n\tself.toggleItems[guiObjectOrLayerCollector] = true\n\tself:_updateSelectionInstances()\n\treturn self\nend\n\nfunction Icon:unbindToggleItem(guiObjectOrLayerCollector)\n\tself.toggleItems[guiObjectOrLayerCollector] = nil\n\tself:_updateSelectionInstances()\n\treturn self\nend\n\nfunction Icon:_updateSelectionInstances()\n\t-- This is to assist with controller navigation and selection\n\t-- It converts the value true to an array\n\tfor guiObjectOrLayerCollector, _ in pairs(self.toggleItems) do\n\t\tlocal buttonInstancesArray = {}\n\t\tfor _, instance in pairs(guiObjectOrLayerCollector:GetDescendants()) do\n\t\t\tif (instance:IsA(\"TextButton\") or instance:IsA(\"ImageButton\")) and instance.Active then\n\t\t\t\ttable.insert(buttonInstancesArray, instance)\n\t\t\tend\n\t\tend\n\t\tself.toggleItems[guiObjectOrLayerCollector] = buttonInstancesArray\n\tend\nend\n\nfunction Icon:_setToggleItemsVisible(bool, fromSource, sourceIcon)\n\tfor toggleItem, _ in pairs(self.toggleItems) do\n\t\tif not sourceIcon or sourceIcon == self or sourceIcon.toggleItems[toggleItem] == nil then\n\t\t\tlocal property = \"Visible\"\n\t\t\tif toggleItem:IsA(\"LayerCollector\") then\n\t\t\t\tproperty = \"Enabled\"\n\t\t\tend\n\t\t\ttoggleItem[property] = bool\n\t\tend\n\tend\nend\n\nfunction Icon:bindEvent(iconEventName, eventFunction)\n\tlocal event = self[iconEventName]\n\tassert(event and typeof(event) == \"table\" and event.Connect, \"argument[1] must be a valid topbarplus icon event name!\")\n\tassert(typeof(eventFunction) == \"function\", \"argument[2] must be a function!\")\n\tself.bindedEvents[iconEventName] = event:Connect(function(...)\n\t\teventFunction(self, ...)\n\tend)\n\treturn self\nend\n\nfunction Icon:unbindEvent(iconEventName)\n\tlocal eventConnection = self.bindedEvents[iconEventName]\n\tif eventConnection then\n\t\teventConnection:Disconnect()\n\t\tself.bindedEvents[iconEventName] = nil\n\tend\n\treturn self\nend\n\nfunction Icon:bindToggleKey(keyCodeEnum)\n\tassert(typeof(keyCodeEnum) == \"EnumItem\", \"argument[1] must be a KeyCode EnumItem!\")\n\tself.bindedToggleKeys[keyCodeEnum] = true\n\tself.toggleKeyAdded:Fire(keyCodeEnum)\n\tself:setCaption(\"_hotkey_\")\n\treturn self\nend\n\nfunction Icon:unbindToggleKey(keyCodeEnum)\n\tassert(typeof(keyCodeEnum) == \"EnumItem\", \"argument[1] must be a KeyCode EnumItem!\")\n\tself.bindedToggleKeys[keyCodeEnum] = nil\n\treturn self\nend\n\nfunction Icon:call(callback, ...)\n\tlocal packedArgs = table.pack(...)\n\ttask.spawn(function()\n\t\tcallback(self, table.unpack(packedArgs))\n\tend)\n\treturn self\nend\n\nfunction Icon:addToJanitor(callback, methodName, index)\n\tself.janitor:add(callback, methodName, index)\n\treturn self\nend\n\nfunction Icon:lock()\n\t-- This disables all user inputs related to the icon (such as clicking buttons, pressing keys, etc)\n\tlocal clickRegion = self:getInstance(\"ClickRegion\")\n\tclickRegion.Visible = false\n\tself.locked = true\n\treturn self\nend\n\nfunction Icon:unlock()\n\tlocal clickRegion = self:getInstance(\"ClickRegion\")\n\tclickRegion.Visible = true\n\tself.locked = false\n\treturn self\nend\n\nfunction Icon:debounce(seconds)\n\tself:lock()\n\ttask.wait(seconds)\n\tself:unlock()\n\treturn self\nend\n\nfunction Icon:autoDeselect(bool)\n\t-- When set to true the icon will deselect itself automatically whenever\n\t-- another icon is selected\n\tif bool == nil then\n\t\tbool = true\n\tend\n\tself.deselectWhenOtherIconSelected = bool\n\treturn self\nend\n\nfunction Icon:oneClick(bool)\n\t-- When set to true the icon will automatically deselect when selected, this creates\n\t-- the effect of a single click button\n\tlocal singleClickJanitor = self.singleClickJanitor\n\tsingleClickJanitor:clean()\n\tif bool or bool == nil then\n\t\tsingleClickJanitor:add(self.selected:Connect(function()\n\t\t\tself:deselect(\"OneClick\", self)\n\t\tend))\n\tend\n\tself.oneClickEnabled = true\n\treturn self\nend\n\nfunction Icon:setCaption(text)\n\tif text == \"_hotkey_\" and (self.captionText) then\n\t\treturn self\n\tend\n\tlocal captionJanitor = self.captionJanitor\n\tself.captionJanitor:clean()\n\tif not text or text == \"\" then\n\t\tself.caption = nil\n\t\tself.captionText = nil\n\t\treturn self\n\tend\n\tlocal caption = captionJanitor:add(require(elements.Caption)(self))\n\tcaption:SetAttribute(\"CaptionText\", text)\n\tself.caption = caption\n\tself.captionText = text\n\treturn self\nend\n\nfunction Icon:setCaptionHint(keyCodeEnum)\n\tassert(typeof(keyCodeEnum) == \"EnumItem\", \"argument[1] must be a KeyCode EnumItem!\")\n\tself.fakeToggleKey = keyCodeEnum\n\tself.fakeToggleKeyChanged:Fire(keyCodeEnum)\n\tself:setCaption(\"_hotkey_\")\n\treturn self\nend\n\nfunction Icon:leave()\n\tlocal joinJanitor = self.joinJanitor\n\tjoinJanitor:clean()\n\treturn self\nend\n\nfunction Icon:joinMenu(parentIcon)\n\tUtility.joinFeature(self, parentIcon, parentIcon.menuIcons, parentIcon:getInstance(\"Menu\"))\n\tparentIcon.menuChildAdded:Fire(self)\n\treturn self\nend\n\nfunction Icon:setMenu(arrayOfIcons)\n\tself.menuSet:Fire(arrayOfIcons)\n\treturn self\nend\n\nfunction Icon:setFixedMenu(arrayOfIcons)\n\tself:freezeMenu(arrayOfIcons)\n\tself:setMenu(arrayOfIcons)\nend\nIcon.setFrozenMenu = Icon.setFixedMenu\n\nfunction Icon:freezeMenu()\n\t-- A frozen menu is a menu which is permanently locked in the\n\t-- the selected state (with its toggle hidden)\n\tself:select(\"FrozenMenu\", self)\n\tself:bindEvent(\"deselected\", function(icon)\n\t\ticon:select(\"FrozenMenu\", self)\n\tend)\n\tself:modifyTheme({\"IconSpot\", \"Visible\", false})\nend\n\nfunction Icon:joinDropdown(parentIcon)\n\tparentIcon:getDropdown()\n\tUtility.joinFeature(self, parentIcon, parentIcon.dropdownIcons, parentIcon:getInstance(\"DropdownScroller\"))\n\tparentIcon.dropdownChildAdded:Fire(self)\n\treturn self\nend\n\nfunction Icon:getDropdown()\n\tlocal dropdown = self.dropdown\n\tif not dropdown then\n\t\tdropdown = require(elements.Dropdown)(self)\n\t\tself.dropdown = dropdown\n\t\tself:clipOutside(dropdown)\n\tend\n\treturn dropdown\nend\n\nfunction Icon:setDropdown(arrayOfIcons)\n\tself:getDropdown()\n\tself.dropdownSet:Fire(arrayOfIcons)\n\treturn self\nend\n\nfunction Icon:clipOutside(instance)\n\t-- This is essential for items such as notices and dropdowns which will exceed the bounds of the widget. This is an issue\n\t-- because the widget must have ClipsDescendents enabled to hide items for instance when the menu is closing or opening.\n\t-- This creates an invisible frame which matches the size and position of the instance, then the instance is parented outside of\n\t-- the widget and tracks the clone to match its size and position. In order for themes, etc to work the applying system checks\n\t-- to see if an instance is a clone, then if it is, it applies it to the original instance instead of the clone.\n\tlocal instanceClone = Utility.clipOutside(self, instance)\n\tself:refreshAppearance(instance)\n\treturn self, instanceClone\nend\n\nfunction Icon:setIndicator(keyCode)\n\t-- An indicator is a direction button prompt with an image of the given keycode. This is useful for instance\n\t-- with controllers to show the user what button to press to highlight the topbar. You don't need\n\t-- to set an indicator for controllers as this is handled internally within the Gamepad module\n\tlocal indicator = self.indicator\n\tif not indicator then\n\t\tindicator = self.janitor:add(require(elements.Indicator)(self, Icon))\n\t\tself.indicator = indicator\n\tend\n\tself.indicatorSet:Fire(keyCode)\nend\n\nfunction Icon:convertLabelToNumberSpinner(numberSpinner, callback)\n\ttask.defer(function()\n\t\t\n\t\tlocal label = self:getInstance(\"IconLabel\")\n\t\tlabel.Transparency = 1\n\t\tnumberSpinner.Parent = label.Parent\n\t\tnumberSpinner.Size = UDim2.fromScale(1, 1)\n\t\tnumberSpinner.AnchorPoint = Vector2.new(0.5, 0.5)\n\t\tnumberSpinner.Position = UDim2.new(0.5, 0, 0.5, 0)\n\t\tnumberSpinner.TextXAlignment = Enum.TextXAlignment.Center\n\t\tnumberSpinner.ClipsDescendants = false\n\n\t\tlocal propertiesToChangeLabel = {\n\t\t\t\"FontFace\",\n\t\t\t\"BorderSizePixel\",\n\t\t\t\"BorderColor3\",\n\t\t\t\"Rotation\",\n\t\t\t\"TextStrokeTransparency\",\n\t\t\t\"TextStrokeColor3\",\n\t\t\t\"TextStrokeTransparency\",\n\t\t\t\"TextColor3\",\n\t\t}\n\t\tfor _, property in ipairs(propertiesToChangeLabel) do\n\t\t\tnumberSpinner[property] = label[property]\n\t\t\tself:addToJanitor(label:GetPropertyChangedSignal(property):Connect(function()\n\t\t\t\tnumberSpinner[property] = label[property]\n\t\t\tend))\n\t\tend\n\n\t\tlocal minDigits = 0\n\t\tlocal maxDigits = 8\n\t\tlocal function getSpinnerSizeAndDigitCount()\n\t\t\tlocal TotalSize = 0\n\t\t\tlocal numOfDigits = 0\n\t\t\tfor i, child in numberSpinner.Frame:GetChildren() do\n\t\t\t\tlocal name = string.lower(child.Name)\n\t\t\t\tif name == \"digit\" then\n\t\t\t\t\tTotalSize += child.AbsoluteSize.X\n\t\t\t\t\tnumOfDigits += 1\n\t\t\t\telseif name == \"prefix\" or name == \"suffix\" or name == \"comma\" then\n\t\t\t\t\tif child.Text ~= \"\" then\n\t\t\t\t\t\tTotalSize += child.AbsoluteSize.X\n\t\t\t\t\t\tnumOfDigits += 1\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend\n\t\t\treturn TotalSize, numOfDigits\n\t\tend\n\t\t\n\t\tlocal function getLabelParentContainerXSize()\n\t\t\tlocal firstParent = label.Parent\n\t\t\tlocal nextParent = firstParent and firstParent.Parent\n\t\t\tif nextParent == nil then\n\t\t\t\treturn 0\n\t\t\tend\n\t\t\tif nextParent.IconImage.Visible == true then\n\t\t\t\treturn numberSpinner.Frame.AbsoluteSize.X + label.Parent.Parent.IconImage.AbsoluteSize.X\n\t\t\telse\n\t\t\t\treturn nextParent.AbsoluteSize.X\n\t\t\tend\n\t\tend\n\t\tlocal function getNumberSpinnerXSize()\n\t\t\treturn numberSpinner.Frame.AbsoluteSize.X\n\t\tend\n\n\t\tlocal function adjustSize()\n\t\t\tlocal totalDigitXSize, numOfDigits = getSpinnerSizeAndDigitCount()\n\t\t\tif numOfDigits < 18 then\n\t\t\t\tself:setLabel(numberSpinner.Value)\n\t\t\tend\n\n\t\t\tlocal NumberSpinnerXSize = getNumberSpinnerXSize()\n\n\t\t\twhile totalDigitXSize < NumberSpinnerXSize and self.isDestroyed ~= true do\n\t\t\t\ttask.wait(0.05)\n\t\t\t\tif numOfDigits > minDigits and numOfDigits < maxDigits then\n\t\t\t\t\tnumberSpinner.TextSize = label.TextSize\n\t\t\t\t\tbreak\n\t\t\t\telse\n\t\t\t\t\tnumberSpinner.TextSize += 1\n\t\t\t\tend\n\n\t\t\t\tNumberSpinnerXSize = getNumberSpinnerXSize()\n\t\t\t\ttotalDigitXSize, numOfDigits = getSpinnerSizeAndDigitCount()\n\t\t\tend\n\n\t\t\tlocal labelParentContainerXSize = getLabelParentContainerXSize()\n\t\t\twhile totalDigitXSize > labelParentContainerXSize and self.isDestroyed ~= true do\n\t\t\t\ttask.wait(0.05)\n\t\t\t\tif numOfDigits < maxDigits and numOfDigits > minDigits then\n\t\t\t\t\tnumberSpinner.TextSize = label.TextSize\n\t\t\t\t\tbreak\n\t\t\t\telse\n\t\t\t\t\tnumberSpinner.TextSize -= 1\n\t\t\t\tend\n\n\t\t\t\tlabelParentContainerXSize = getLabelParentContainerXSize()\n\t\t\t\ttotalDigitXSize, numOfDigits = getSpinnerSizeAndDigitCount()\n\t\t\tend\n\t\tend\n\n\t\tself:addToJanitor(numberSpinner.Frame.ChildAdded:Connect(adjustSize))\n\t\tself:addToJanitor(numberSpinner.Frame.ChildRemoved:Connect(adjustSize))\n\t\tself:addToJanitor(self.iconAdded:Connect(function()\n\t\t\ttask.wait(1)\n\t\t\tadjustSize()\n\t\tend))\n\n\t\tself:updateParent()\n\n\t\t-- This corrects text to the size of a normal label\n\t\tnumberSpinner.Name = \"LabelSpinner\"\n\t\tnumberSpinner.Prefix = \"$\"\n\t\tnumberSpinner.Commas = true\n\t\tnumberSpinner.Decimals = 0\n\t\tnumberSpinner.Duration = 0.25\n\t\tnumberSpinner.Value = 10\n\t\ttask.wait(0.2)\n\t\t\n\t\tif typeof(callback) == \"function\" then\n\t\t\tcallback()\n\t\tend\n\t\t\n\tend)\n\treturn self\nend\n\n\n\n-- DESTROY/CLEANUP\nfunction Icon:destroy()\n\tif self.isDestroyed then\n\t\treturn\n\tend\n\tself:clearNotices()\n\tif self.parentIconUID then\n\t\tself:leave()\n\tend\n\tself.isDestroyed = true\n\tself.janitor:clean()\n\tIcon.iconRemoved:Fire(self)\nend\nIcon.Destroy = Icon.destroy\n\nreturn Icon :: Types.StaticIcon\n"
  },
  {
    "path": "wally.toml",
    "content": "[package]\nname = \"1foreverhd/topbarplus\"\ndescription = \"Construct dynamic and intuitive topbar icons. Enhance the appearance and behaviour of these icons with features such as themes, dropdowns and menus.\"\nlicense = \"MPL2\"\nversion = \"3.4.0\"\nregistry = \"https://github.com/UpliftGames/wally-index\"\nrealm = \"shared\"\nexclude = [\n    \"**\",\n]\ninclude = [\n    \"default.project.json\",\n    \"src\",\n    \"src/**\",\n    \"LICENSE\",\n    \"wally.toml\",\n]\n\n[dependencies]"
  },
  {
    "path": "withLink.project.json",
    "content": "{\n    \"name\": \"topbarplus\",\n    \"tree\": {\n      \"$path\": \"src\"\n    }\n  }"
  },
  {
    "path": "withoutLink.project.json",
    "content": "{\n    \"name\": \"topbarplus\",\n    \"globIgnorePaths\": [\"**/PackageLink.model.json\"],\n    \"tree\": {\n      \"$path\": \"src\"\n    }\n  }"
  }
]