Showing preview only (242K chars total). Download the full file or copy to clipboard to get everything.
Repository: 1ForeverHD/TopbarPlus
Branch: main
Commit: f44992bf0299
Files: 45
Total size: 228.5 KB
Directory structure:
gitextract_lekhork2/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── BuildRelease.yml
├── .gitignore
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── LICENSE
├── PackageLink.model.json
├── README.md
├── aftman.toml
├── default.project.json
├── docs/
│ ├── api.md
│ ├── contributing.md
│ ├── features.md
│ ├── index.md
│ ├── installation.md
│ ├── javascripts/
│ │ └── tags.js
│ └── third_parties.md
├── mkdocs.yml
├── rotriever.toml
├── selene.toml
├── serve.project.json
├── src/
│ ├── Attribute.lua
│ ├── Elements/
│ │ ├── Caption.lua
│ │ ├── Container.lua
│ │ ├── Dropdown.lua
│ │ ├── Indicator.lua
│ │ ├── Menu.lua
│ │ ├── Notice.lua
│ │ ├── Selection.lua
│ │ └── Widget.lua
│ ├── Features/
│ │ ├── Gamepad.lua
│ │ ├── Overflow.lua
│ │ └── Themes/
│ │ ├── Classic.lua
│ │ ├── Default.lua
│ │ └── init.lua
│ ├── Packages/
│ │ ├── GoodSignal.lua
│ │ └── Janitor.lua
│ ├── Reference.lua
│ ├── Types.lua
│ ├── Utility.lua
│ ├── VERSION.lua
│ └── init.lua
├── wally.toml
├── withLink.project.json
└── withoutLink.project.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.html linguist-detectable=false
================================================
FILE: .github/workflows/BuildRelease.yml
================================================
name: Build and Release
on:
push:
tags:
- "v*"
jobs:
build:
name: Create release
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ok-nick/setup-aftman@v0.4.2
- name: Build asset
run: rojo build --output Icon.rbxm withLink.project.json
- name: Git Release
uses: anton-yurchenko/git-release@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: |
./Icon.rbxm
================================================
FILE: .gitignore
================================================
# Project place file
/Icon.rbxm
# macOS
.DS_Store
# Rojo
sourcemap.json
# Built documentation
/site
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"johnnymorganz.luau-lsp",
"evaera.vscode-rojo",
"kampfkarren.selene-vscode",
"johnnymorganz.stylua"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"robloxLsp.diagnostics.disable": [
"undefined-global"
]
}
================================================
FILE: LICENSE
================================================
TopbarPlus Credit
==================================
By using TopbarPlus in your experience or application, you agree to either:
1. Keep Attribute unchanged, or
2. To credit TopbarPlus in your experience's description, or in a devforum
post linked from your experience's description.
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
================================================
FILE: PackageLink.model.json
================================================
{
"ClassName": "PackageLink"
}
================================================
FILE: README.md
================================================
https://devforum.roblox.com/t/topbarplus/1017485
================================================
FILE: aftman.toml
================================================
# This file lists tools managed by Aftman, a cross-platform toolchain manager.
# For more information, see https://github.com/LPGhatguy/aftman
# To add a new tool, add an entry to this table.
[tools]
rojo = "rojo-rbx/rojo@7.4.1"
selene = "Kampfkarren/selene@0.26.1"
stylua = "JohnnyMorganz/StyLua@0.20.0"
wally = "UpliftGames/Wally@0.3.2"
================================================
FILE: default.project.json
================================================
{
"name": "topbarplus",
"tree": {
"$path": "src"
}
}
================================================
FILE: docs/api.md
================================================
[themes]: https://1foreverhd.github.io/TopbarPlus/features/#modify-theme
[alignments]: https://1foreverhd.github.io/TopbarPlus/features/#alignments
[font family]: https://create.roblox.com/docs/reference/engine/datatypes/Font/#fromEnum
[toggle keys]: https://1foreverhd.github.io/TopbarPlus/features/#toggle-keys
[captions]: https://1foreverhd.github.io/TopbarPlus/features/#captions
[icon event]: https://1foreverhd.github.io/TopbarPlus/api/#events
[menus]: https://1foreverhd.github.io/TopbarPlus/features/#menus
[dropdowns]: https://1foreverhd.github.io/TopbarPlus/features/#dropdowns
[numberSpinner]: https://devforum.roblox.com/t/numberspinner-module/1105961
## Functions
#### getIcons
```lua
local icons = Icon.getIcons()
```
Returns a dictionary of icons where the key is the icon's UID and value the icon.
----
#### getIcon
```lua
local icon = Icon.getIcon(nameOrUID)
```
Returns an icon of the given name or UID.
----
#### setTopbarEnabled
```lua
Icon.setTopbarEnabled(bool)
```
When set to ``false`` all TopbarPlus ScreenGuis are hidden. This does not impact Roblox's Topbar.
----
#### modifyBaseTheme
```lua
Icon.modifyBaseTheme(modifications)
```
Updates the appearance of *all* icons. See [themes] for more details.
----
#### setDisplayOrder
```lua
Icon.setDisplayOrder(integer)
```
Sets the base DisplayOrder of all TopbarPlus ScreenGuis.
----
## Constructors
#### new
```lua
local icon = Icon.new()
```
Constructs an empty ``32x32`` icon on the topbar.
----
## Methods
#### setName
{chainable}
```lua
icon:setName(name)
```
Sets the name of the Widget instance. This can be used in conjunction with ``Icon.getIcon(name)``.
----
#### getInstance
```lua
local instance = icon:getInstance(instanceName)
```
Returns the first descendant found within the widget of name ``instanceName``.
----
#### modifyTheme
{chainable}
```lua
icon:modifyTheme(modifications)
```
Updates the appearance of the icon. See [themes] for more details.
----
#### modifyChildTheme
{chainable}
```lua
icon:modifyChildTheme(modifications)
```
Updates the appearance of all icons that are parented to this icon (for example when a menu or dropdown). See [themes] for more details.
----
#### setEnabled
{chainable}
```lua
icon:setEnabled(bool)
```
When set to ``false`` the icon will be disabled and hidden.
----
#### select
{chainable}
```lua
icon:select()
```
Selects the icon (as if it were clicked once).
----
#### deselect
{chainable}
```lua
icon:deselect()
```
Deselects the icon (as if it were clicked, then clicked again).
----
#### notify
{chainable}
```lua
icon:notify(clearNoticeEvent)
```
Prompts 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.
----
#### clearNotices
{chainable}
```lua
icon:clearNotices()
```
----
#### disableOverlay
{chainable}
```lua
icon:disableStateOverlay(bool)
```
When set to ``true``, disables the shade effect which appears when the icon is pressed and released.
----
#### setImage
{chainable} {toggleable}
```lua
icon:setImage(imageId, iconState)
```
Applies an image to the icon based on the given ``imageId``. ``imageId`` can be an assetId or a complete asset string.
----
#### setLabel
{chainable} {toggleable}
```lua
icon:setLabel(text, iconState)
```
----
#### setOrder
{chainable} {toggleable}
```lua
icon:setOrder(order, iconState)
```
----
#### setCornerRadius
{chainable} {toggleable}
```lua
icon:setCornerRadius(scale, offset, iconState)
```
----
#### align
{chainable}
```lua
icon:align(alignment)
```
This enables you to set the icon to the ``"Left"`` (default), ``"Center"`` or ``"Right"`` side of the screen. See [alignments] for more details.
----
#### setWidth
{chainable} {toggleable}
```lua
icon:setWidth(minimumSize, iconState)
```
This sets the minimum width the icon can be (it can be larger for instance when setting a long label). The default width is ``44``.
----
#### setImageScale
{chainable} {toggleable}
```lua
icon:setImageScale(number, iconState)
```
How large the image is relative to the icon. The default value is ``0.5``.
----
#### setImageRatio
{chainable} {toggleable}
```lua
icon:setImageRatio(number, iconState)
```
How stretched the image will appear. The default value is ``1`` (a perfect square).
----
#### setTextSize
{chainable} {toggleable}
```lua
icon:setTextSize(number, iconState)
```
The size of the icon labels' text. The default value is ``16``.
----
#### setTextColor
{chainable} {toggleable}
```lua
icon:setTextColor(color, iconState)
```
The color of the icon labels' text.
----
#### setTextFont
{chainable} {toggleable}
```lua
icon:setTextFont(font, fontWeight, fontStyle, iconState)
```
Sets 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"`).
----
#### bindToggleItem
{chainable}
```lua
icon:bindToggleItem(guiObjectOrLayerCollector)
```
Binds a GuiObject or LayerCollector to appear and disappeared when the icon is toggled.
----
#### unbindToggleItem
{chainable}
```lua
icon:unbindToggleItem(guiObjectOrLayerCollector)
```
Unbinds the given GuiObject or LayerCollector from the toggle.
----
#### bindEvent
{chainable}
```lua
icon:bindEvent(iconEventName, callback)
```
Connects 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.
----
#### unbindEvent
{chainable}
```lua
icon:unbindEvent(iconEventName)
```
Unbinds the connection of the associated ``iconEventName``.
----
#### bindToggleKey
{chainable}
```lua
icon:bindToggleKey(keyCodeEnum)
```
Binds a [keycode](https://developer.roblox.com/en-us/api-reference/enum/KeyCode) which toggles the icon when pressed. See [toggle keys] for more details.
----
#### unbindToggleKey
{chainable}
```lua
icon:unbindToggleKey(keyCodeEnum)
```
Unbinds the given keycode.
----
#### call
{chainable}
```lua
icon:call(func)
```
Calls 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.
----
#### addToJanitor
{chainable}
```lua
icon:addToJanitor(userdata)
```
Passes 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.
----
#### lock
{chainable}
```lua
icon:lock()
```
Prevents 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()``.
----
#### unlock
{chainable}
```lua
icon:unlock()
```
Re-enables user-input to toggle the icon again.
----
#### debounce
{chainable} {yields}
```lua
icon:debounce(seconds)
```
Locks 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.
----
#### autoDeselect
{chainable}
```lua
icon:autoDeselect(true)
```
When 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).
----
#### oneClick
{chainable}
```lua
icon:oneClick(bool)
```
When set to true the icon will automatically deselect when selected. This creates the effect of a single click button.
----
#### setCaption
{chainable}
```lua
icon:setCaption(text)
```
Sets a caption. To remove, pass ``nil`` as ``text``. See [captions] for more details.
----
#### setCaptionHint
{chainable}
```lua
icon:setCaptionHint(keyCodeEnum)
```
This customizes the appearance of the caption's hint without having to use ``icon:bindToggleKey``.
----
#### setDropdown
{chainable}
```lua
icon:setDropdown(arrayOfIcons)
```
Creates a vertical dropdown based upon the given ``table array`` of ``icons``. Pass an empty table ``{}`` to remove the dropdown. See [dropdowns] for more details.
----
#### joinDropdown
{chainable}
```lua
icon:joinDropdown(parentIcon)
```
Joins the dropdown of `parentIcon`. This is what ``icon:setDropdown`` calls internally on the icons within its array.
----
#### setMenu
{chainable}
```lua
icon:setMenu(arrayOfIcons)
```
Creates a horizontal menu based upon the given array of icons. Pass an empty table ``{}`` to remove the menu. See [menus] for more details.
----
#### setFixedMenu
{chainable}
```lua
icon:setFixedMenu(arrayOfIcons)
```
Creates 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.
----
#### joinMenu
{chainable}
```lua
icon:joinMenu(parentIcon)
```
Joins the menu of `parentIcon`. This is what ``icon:setMenu`` calls internally on the icons within its array.
----
#### leave
{chainable}
```lua
icon:leave()
```
Unparents an icon from a parentIcon if it belongs to a dropdown or menu.
----
#### convertLabelToNumberSpinner
{chainable}
```lua
icon:convertLabelToNumberSpinner(numberSpinner, readyCallback)
```
Accepts a [numberSpinner] and converts the icon's label into that spinner. For example:
```lua
Icon.new()
:align("Right")
:setLabel("Points")
:setWidth(80)
:call(function(pointsIcon)
local NumberSpinner = require(ReplicatedStorage.NumberSpinner)
local numberSpinner = NumberSpinner.new()
pointsIcon:convertLabelToNumberSpinner(numberSpinner, function()
numberSpinner.Name = "LabelSpinner"
numberSpinner.Prefix = "$"
numberSpinner.Commas = true
numberSpinner.Decimals = 0
numberSpinner.Duration = 0.25
while true do
numberSpinner.Value = math.random(1,1000)
task.wait(1)
end
end)
end)
```
!!! warning
Any changes to the NumberSpinner must be made within ``readyCallback`` otherwise you risk breaking the icon's appearance
----
#### destroy
{chainable}
```lua
icon:destroy()
```
Clears all connections and destroys all instances associated with the icon.
----
## Events
#### selected
```lua
icon.selected:Connect(function(fromSource)
-- fromSource can be useful for checking if the behaviour was triggered by a user (such as clicking)
-- fromSource values include "User", "OneClick", "AutoDeselect", "HideParentFeature", "Overflow"
local sourceName = fromSource or "Unknown"
print("The icon was selected by the "..sourceName)
end)
```
----
#### deselected
```lua
icon.deselected:Connect(function(fromSource)
local sourceName = fromSource or "Unknown"
print("The icon was deselected by the "..sourceName)
end)
```
----
#### toggled
```lua
icon.toggled:Connect(function(isSelected, fromSource)
local stateName = (isSelected and "selected") or "deselected"
print(`The icon was {stateName}!`)
end)
```
----
#### viewingStarted
```lua
icon.viewingStarted:Connect(function()
print("A mouse, long-pressed finger or gamepad selection is hovering over the icon")
end)
```
----
#### viewingEnded
```lua
icon.viewingEnded:Connect(function()
print("The input is no longer viewing (hovering over) the icon")
end)
```
----
#### notified
```lua
icon.notified:Connect(function()
print("New notice")
end)
```
----
## Properties
#### name
{read-only}
```lua
local string = icon.name --[default: "Widget"]
```
----
#### isSelected
{read-only}
```lua
local bool = icon.isSelected
```
----
#### isEnabled
{read-only}
```lua
local bool = icon.isEnabled
```
----
#### totalNotices
{read-only}
```lua
local int = icon.totalNotices
```
----
#### locked
{read-only}
```lua
local bool = icon.locked
```
================================================
FILE: docs/contributing.md
================================================
[discussion thread]: https://devforum.roblox.com/t/topbarplus-v2-construct-dynamic-and-intuitive-topbar-icons/1017485
[Python]: https://www.python.org/
[Material for MKDocs]: https://squidfunk.github.io/mkdocs-material/
[ForeverHD on the devforum]: https://devforum.roblox.com/u/ForeverHD/summary
[TopbarPlus repository]: https://github.com/1ForeverHD/TopbarPlus
[open an issue]: https://github.com/1ForeverHD/TopbarPlus/issues
## Bug Reports
- To submit a bug report, [open an issue] or create a response at the [discussion thread].
- Ensure your report includes a detailed explanation of the problem with any relavent images, videos, etc (such as console errors).
- Aim to include a link to a stipped-down uncopylocked Roblox place which reproduces the bug.
## Questions and Feedback
- Be sure to first check out the documentation before asking a question.
- We recommend asking all questions and posting feedback to the [discussion thread].
## Submitting a resource (video tutorial, port, etc)
- Fancy making a tutorial or resource for TopbarPlus? Feel free to get in touch and we can provide tips, best practices, etc.
- We'll feature approved resources on the [discussion thread].
- To submit a resource, [open an issue], or reach out on the [discussion thread] or to [ForeverHD on the devforum].
## Suggestions and Code
- TopbarPlus is completely free and open source; any suggestions and code contributions are greatly appreciated!
- To make a suggestion, [open an issue] or create a response at the [discussion thread].
- 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!).
- For smaller contributions (a few lines of code, fixing typos, etc) feel free to send a pull request right away.
- Make sure to merge your pull requests into the #development branch.
- Some tools you'll find useful when working on this project:
- [Rojo](https://rojo.space/docs/)
- [Material for MKDocs]
- [Roblox LSP](https://devforum.roblox.com/t/roblox-lsp-full-intellisense-for-roblox-and-luau/717745)
## Documentation
- 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.
- To test documentation:
1. Install [Python] (which comes with pip).
2. Install [Material for MKDocs].
3. Visit the [TopbarPlus repository].
4. Click *Fork* in the top right corner.
5. Clone this fork into your local repository.
6. Change directory to this clone ``cd TopbarPlus``.
7. Swap to the development branch ``git checkout development``.
8. Call ``mkdocs serve`` within your terminal.
9. Open your local website (it will look something like ``http://0.0.0.0:8000``)
10. Any changes to ``mkdocs.yml`` or the files within ``docs`` will now update live to this local site.
!!! important
All pull requests must be made to the ***development*** branch.
================================================
FILE: docs/features.md
================================================
[icon states]: https://1foreverhd.github.io/TopbarPlus/#states
[v3 Playground]: https://www.roblox.com/games/117501901079852/TopbarPlus
### Images
```lua
Icon.new:setImage(shopImageId)
```
<a><img src="https://i.imgur.com/IEJfUye.png" width="50%"/></a>
------------------------------
### Labels
```lua
icon:setLabel("Shop")
```
<a><img src="https://i.imgur.com/d0nVAc6.png" width="50%"/></a>
```lua
icon:setImage(shopImageId)
icon:setLabel("Shop")
```
<a><img src="https://i.imgur.com/vJHvJWI.png" width="50%"/></a>
------------------------------
### Alignments
```lua
-- Aligns the icon to the left bounds of the screen
-- This is the default behaviour so you do not need to do anything
-- This was formerly called :setLeft()
icon:align("Left")
```
```lua
-- Aligns the icon in the middle of the screen
-- This was formerly called :setMid()
icon:align("Center")
```
```lua
-- Aligns the icon to the right bounds of the screen
-- This was formerly called :setRight()
icon:align("Right")
```
------------------------------
### Notices
```lua
icon:notify()
```
<a><img src="https://i.imgur.com/xFBbVoA.png" width="50%"/></a>
------------------------------
### Captions
```lua
icon:setCaption("Open Shop")
```
<a><img src="https://i.imgur.com/QpecT2Y.gif" width="50%"/></a>
------------------------------
### Dropdowns
Dropdowns are vertical navigation frames that contain an array of icons:
```lua
Icon.new()
:setLabel("Example")
:modifyTheme({"Dropdown", "MaxIcons", 3})
:modifyChildTheme({"Widget", "MinimumWidth", 158})
:setDropdown({
Icon.new()
:setLabel("Category 1")
,
Icon.new()
:setLabel("Category 2")
,
Icon.new()
:setLabel("Category 3")
,
Icon.new()
:setLabel("Category 4")
,
})
```
<a><img src="https://i.imgur.com/ZMt6bhr.gif" width="50%"/></a>
!!! warning
Icons containing a dropdown can join other menus but not dropdowns.
------------------------------
### Menus
Menus are horizontal navigation frames that contain an array of icons:
```lua
Icon.new()
:setLabel("Example")
:modifyTheme({"Menu", "MaxIcons", 2})
:setMenu({
Icon.new()
:setLabel("Item 1")
,
Icon.new()
:setLabel("Item 2")
,
Icon.new()
:setLabel("Item 3")
,
Icon.new()
:setLabel("Item 4")
,
})
```
<a><img src="https://i.imgur.com/tXLrD8t.gif" width="50%"/></a>
------------------------------
### Fixed Menus
Fixed Menus are the same as normal menus, except forcefully opened (selected), with their close button hidden:
```lua
Icon.new()
:modifyTheme({"Menu", "MaxIcons", 3})
:setFixedMenu({
Icon.new()
:setLabel("Item 1")
,
Icon.new()
:setLabel("Item 2")
,
Icon.new()
:setLabel("Item 3")
,
Icon.new()
:setLabel("Item 4")
,
Icon.new()
:setLabel("Item 5")
,
})
```
<a><img src="https://i.imgur.com/LgJCj4X.gif" width="50%"/></a>
------------------------------
### Modify Theme
You can modify the appearance of an icon doing:
```lua
icon:modifyTheme(modifications)
```
You can modify the appearance of *all* icons doing:
```lua
Icon.modifyBaseTheme(modifications)
```
``modifications`` can be either a single array describing a change, or a *colllection* of these arrays. For example, both the following are valid:
```lua
-- Single array
icon:modifyTheme({"IconLabel", "TextSize", 16})
-- Collection of arrays
icon:modifyTheme({
{"Widget", "MinimumWidth", 290},
{"IconCorners", "CornerRadius", UDim.new(0, 0)}
})
```
A modification array has 4 components:
```lua
{name, property, value, iconState}
```
> **1. `name`** {required}
This can be:
- "Widget" (which is the icon container frame)
- The name of an instance within the widget such as ``IconGradient``, ``IconSpot``, ``Menu``, etc
- 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'.
> **2. `property`** {required}
This can be either:
- A property from the instance (Name, BackgroundColor3, Text, etc)
- Or if the property doesn't exist, an attribute of that property name will be set
> **3. `value`** {required}
The value you want the property to become (``"Hello"``, ``Color3.fromRGB(255, 100, 50)``, etc)
> **4. `iconState`** {optional}
This determines *when* the modification is applied. See [icon states] for more details.
You can find example arrays under the 'Default' module:
<a><img src="https://i.imgur.com/idH1SRi.png" width="100%"/></a>
------------------------------
### One Click Icons
You can convert icons into single click icons (icons which instantly
deselect when selected) by doing:
```lua
icon:oneClick()
```
For example:
```lua
Icon.new()
:setImage(shopImageId)
:setLabel("Shop")
:bindEvent("deselected", function()
shop.Visible = not shop.Visible
end)
:oneClick()
```
<a><img src="https://i.imgur.com/Ma2mpjB.gif" width="50%"/></a>
------------------------------
### Toggle Items
Binds a GuiObject (such as a frame) to appear or disappear when the icon is toggled
```lua
icon:bindToggleItem(shopFrame)
```
It is equivalent to doing:
```lua
icon.deselected:Connect(function()
shopFrame.Visible = false
end)
icon.selected:Connect(function()
shopFrame.Visible = true
end)
```
------------------------------
### Toggle Keys
Binds 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.
```lua
Icon.new()
:setLabel("Shop")
:bindToggleKey(Enum.KeyCode.V)
:setCaption("Open Shop")
```
<a><img src="https://i.imgur.com/GsdNfXr.gif" width="50%"/></a>
------------------------------
### Gamepad & Console Support
TopbarPlus comes with inbuilt support for gamepads (such as Xbox and PlayStation
controllers) and console screens:
<a><img src="https://i.imgur.com/N0n2Zau.gif" width="100%"/></a>
To 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.
To change the default trigger keycode (from DPadUp) do:
```lua
Icon.highlightKey = Enum.KeyCode.NEW_KEYCODE
```
------------------------------
### Overflows
When 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:
<a><img src="https://i.imgur.com/9jrHBaJ.gif" width="100%"/></a>
Overflows will appear when left-set or right-set icons exceed the boundary of the closest opposite-aligned icon or viewport.
If a center-aligned icon exceeds the bounds of another icon, its alignment will be set to the alignment of the icon it exceeded:
<a><img src="https://i.imgur.com/fAds4Ph.gif" width="100%"/></a>
------------------------------
These examples and more can be tested, viewed and edited at the [v3 Playground].
================================================
FILE: docs/index.md
================================================
[icon:setOrder]: https://1foreverhd.github.io/TopbarPlus/api/#setorder
[Feature Guide]: https://1foreverhd.github.io/TopbarPlus/features
[Icon API]: https://1foreverhd.github.io/TopbarPlus/api
[TopbarPlus DevForum Thread]: https://devforum.roblox.com/t/topbarplus/1017485
### About
TopbarPlus 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.
TopbarPlus fully supports PC, Mobile, Tablet and Gamepads (Consoles), and comes with internal features such as 'overflows' to ensure icons remain within suitable bounds.
----------
### Construction
Creating an icon is as simple as:
``` lua
-- Within a LocalScript in StarterPlayerScripts and assuming the Icon package is placed in ReplicatedStorage
local Icon = require(game:GetService("ReplicatedStorage").Icon)
local icon = Icon.new()
```
This constructs an empty ``32x32`` icon on the topbar.
!!! info
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.
To add an image and label, do:
```lua
icon:setImage(imageId)
icon:setLabel("Label")
```
----------
### Chaining
These methods are 'chainable' therefore can alternatively be called doing:
```lua
Icon.new()
:setImage(imageId)
:setLabel("Label")
```
You may want to act upon nested icons. You can achieve this using ``:call``
which returns the icon as the first argument within the function you pass:
```lua
Icon.new()
:setName("TestIcon")
:call(function(icon)
print(icon.name)
-- This will print 'TestIcon'!
end)
```
!!! info
Chainable methods have a ``chainable`` tag next to their name within the API Icon docs.
----------
### States
Sometimes 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:
```lua
"Deselected" -- Applies the value when the icon is deselected (i.e. not pressed)
"Selected" -- Applies the value when the icon is selected (i.e. pressed)
"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
```
!!! info
If no ``iconState`` is specified (i.e. is nil) the value will be applied to all states.
```lua
-- It doesn't matter if you do "deselected", "Deselected" or "dEsElEcTeD"; iconStates are not case sensitive
Icon.new()
:setImage(4882429582)
:setLabel("Closed", "Deselected")
:setLabel("Open", "Selected")
:setLabel("Viewing", "Viewing")
```
<a><img src="https://i.imgur.com/0QrDmi6.gif" width="50%"/></a>
----------
### Additional
By default icons will deselect when another icon is selected. You can disable this behaviour doing:
```lua
icon:autoDeselect(false)
```
You 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].
Have a question or issue? Feel free to reach out at the [TopbarPlus DevForum Thread].
================================================
FILE: docs/installation.md
================================================
#### Take the model
{recommended}
1. Take the [TopbarPlus model](https://create.roblox.com/store/asset/92368439343389/TopbarPlus).
2. Open the toolbox and navigate to Inventory -> My Models.
3. Click TopbarPlus to insert into your game and place anywhere within ``ReplicatedStorage`` or ``Workspace``.
4. 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':
<a><img src="https://i.imgur.com/kIZdT83.png" width="50%"/></a>
5. You can receive automatic updates by enabling 'AutoUpdate' within the PackageLink:
<a><img src="https://i.imgur.com/2hGbjTS.png" width="50%"/></a>
!!! info
All v3 updates will be backwards compatible so you don't need to worry about updates interfering with your code.
!!! warning
Try not to modify any code within the Icon package otherwise it will break the package link.
!!! important
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.
-------------------------------------
#### Download from Releases
1. Visit the [latest release](https://github.com/1ForeverHD/TopbarPlus/releases/latest).
2. Under *Assets*, download ``TopbarPlus.rbxm``.
3. Within studio, navigate to MODEL -> Model and insert the file anywhere within ``ReplicatedStorage``.
-------------------------------------
#### With Rojo
1. Setup with [Rojo](https://rojo.space/).
2. Visit the [TopbarPlus repository](https://github.com/1ForeverHD/TopbarPlus).
3. Click *Fork* in the top right corner.
4. Clone this fork into your local repository.
5. Modify the ``serve.project.json`` file to your desired location (by default TopbarPlus is built directly into ``Workspace``).
6. Call ``rojo serve`` (terminal or VSC plugin) and connect to the rojo studio plugin.
-------------------------------------
#### With Wally
TopbarPlus is now on Wally! You can find it [here](https://wally.run/package/1foreverhd/topbarplus).
================================================
FILE: docs/javascripts/tags.js
================================================
const style = `.tag {
color: #ffffff;
line-height: .8rem;
padding: 5px;
margin-left: 7px !important;
margin: 0 !important;
background-clip: padding-box;
border-radius: 3px;
display: inline-block;
font-size: .7rem;
font-family: "Roboto";
font-weight: normal;
}
.static {
background-color: rgb(38, 70, 83);
}
.read-only {
background-color: rgb(42, 157, 143);
}
.client-only {
background-color: rgb(89, 140, 206);
}
.server-only {
background-color: rgb(89, 140, 206);
}
.toggleable {
background-color: rgb(178, 92, 162);
}
.chainable {
background-color: rgb(122, 103, 231);
}
.recommended {
background-color: rgb(126, 194, 136);
}
.required {
background-color: rgb(231, 101, 104);
}
.optional {
background-color: rgb(188, 176, 116);
}
.unstable {
background-color: rgb(204, 134, 80);
}
.deprecated {
background-color: rgb(227, 87, 75);
}
.yields {
background-color: rgb(163, 149, 79);
}
.critical {
background-color: rgb(255, 0, 0);
}
h4 {
display: inline;
}`
var replaceStuff = [
["{read-only}", '<p class="tag read-only">read-only</p>'],
["{static}", '<p class="tag static">static</p>'],
["{server-only}", '<p class="tag server-only">server-only</p>'],
["{client-only}", '<p class="tag client-only">client-only</p>'],
["{deprecated}", '<p class="tag deprecated">deprecated</p>'],
["{yields}", '<p class="tag yields">yields</p>'],
["{critical}", '<p class="tag critical">critical</p>'],
["{chainable}", '<p class="tag chainable">chainable</p>'],
["{required}", '<p class="tag required">required</p>'],
["{optional}", '<p class="tag optional">optional</p>'],
["{recommended}", '<p class="tag recommended">recommended</p>'],
["{unstable}", '<p class="tag unstable">unstable</p>'],
["{toggleable}", '<p class="tag toggleable">toggleable</p>'],
];
function replace(element) {
for (var i = 0; i < replaceStuff.length; i++) {
var from = replaceStuff[i][0]
var to = replaceStuff[i][1]
if ((element.innerHTML && element.innerHTML.includes(from))) {
element.innerHTML = element.innerHTML.replace(from, to)
element.style.display = "inline"
}
}
}
const styleElement = document.createElement("style")
styleElement.innerHTML = style
document.head.appendChild(styleElement)
window.onload = function WindowLoad(event) {
var elems = document.body.getElementsByTagName("p")
for (var i = 0; i < elems.length; i++) {
replace(elems.item(i))
}
}
================================================
FILE: docs/third_parties.md
================================================
TopbarPlus supports the use of multiple Icon packages within a single experience assuming all required packages are ``v3.0.0`` or above.
When 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.
This 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.
You don't have to do anything to support multiple packages. Simply use TopbarPlus as normal.
================================================
FILE: mkdocs.yml
================================================
site_name: TopbarPlus v3
site_description: Documentation for TopbarPlus v3
site_author: Ben Horton
site_url: https://1ForeverHD.github.io/TopbarPlus/
repo_name: 1ForeverHD/TopbarPlus
repo_url: https://github.com/1ForeverHD/TopbarPlus
edit_uri: ""
theme:
logo: https://user-images.githubusercontent.com/51117782/104590568-71724f80-5663-11eb-9bc1-344fc2a4193c.png
favicon: https://user-images.githubusercontent.com/51117782/113474423-cefa8900-9467-11eb-8678-d69cbb0b3966.png
name: material
features:
- navigation.tabs
#- navigation.instant
#- navigation.sections
palette:
# Light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: blue
accent: blue
toggle:
icon: material/weather-sunny
name: Switch to dark mode
# Dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: blue
accent: blue
toggle:
icon: material/weather-night
name: Switch to light mode
highlightjs: true
hljs_languages:
- lua
extra_javascript:
- javascripts/tags.js
extra:
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/1ForeverHD/
- icon: fontawesome/brands/twitter
link: https://twitter.com/ForeverHD_
- icon: fontawesome/brands/youtube
link: https://www.youtube.com/channel/UCj9QhyYCvhAwiBHA5B88pYg
markdown_extensions:
- admonition
- codehilite:
guess_lang: false
- toc:
permalink: true
- pymdownx.superfences
nav:
- Home:
- Introduction: index.md
- Features: features.md
- Installation: installation.md
- Third Parties: third_parties.md
- API: api.md
- Contributing: contributing.md
================================================
FILE: rotriever.toml
================================================
[package]
name = "TopbarPlus"
version = "3.0.0"
license = "MPL2"
authors = ["1ForeverHD"]
content_root = "src"
[dependencies]
================================================
FILE: selene.toml
================================================
std = "roblox"
================================================
FILE: serve.project.json
================================================
{
"name": "topbarplus",
"tree": {
"$className": "DataModel",
"Workspace": {
"$className": "Workspace",
"TopbarPlus": {
"$className": "Folder",
"Icon": {
"$path": "src",
"PackageLink": {
"$path": "PackageLink.model.json"
}
}
}
}
}
}
================================================
FILE: src/Attribute.lua
================================================
--[[
TopbarPlus was developed by ForeverHD and is possible thanks to HD Admin.
By using TopbarPlus in your experience or application, you agree to either:
1. Keep Attribute unchanged, or
2. If an experience, to credit TopbarPlus in your description, or in a
devforum post linked from your experience's description.
v3 has involved over 350 hours of work to develop, so please consider supporting
its development by reporting any issues or feedback you have at its repository:
https://github.com/1ForeverHD/TopbarPlus
You can get in touch with me on Discord via the social link here:
https://create.roblox.com/store/asset/92368439343389/TopbarPlus
Many thanks! ~Ben, June 10th 2025
]]
task.defer(function()
local RunService = game:GetService("RunService")
local VERSION = require(script.Parent.VERSION)
local appVersion = VERSION.getAppVersion()
local latestVersion = VERSION.getLatestVersion()
local isOutdated = not VERSION.isUpToDate()
if not RunService:IsStudio() then
print(`🍍 Running TopbarPlus {appVersion} by @ForeverHD & HD Admin`)
end
if isOutdated then
warn(`A new version of TopbarPlus ({latestVersion}) is available: https://devforum.roblox.com/t/topbarplus/1017485`)
end
end)
return {}
================================================
FILE: src/Elements/Caption.lua
================================================
local CAPTION_COLOR = Color3.fromRGB(39, 41, 48)
local TEXT_SIZE = 15
return function(icon)
-- Credit to lolmansReturn and Canary Software for
-- retrieving these values
local clickRegion = icon:getInstance("ClickRegion")
local caption = Instance.new("CanvasGroup")
caption.Name = "Caption"
caption.AnchorPoint = Vector2.new(0.5, 0)
caption.BackgroundTransparency = 1
caption.BorderSizePixel = 0
caption.GroupTransparency = 1
caption.Position = UDim2.fromOffset(0, 0)
caption.Visible = true
caption.ZIndex = 30
caption.Parent = clickRegion
local box = Instance.new("Frame")
box.Name = "Box"
box.AutomaticSize = Enum.AutomaticSize.XY
box.BackgroundColor3 = CAPTION_COLOR
box.Position = UDim2.fromOffset(4, 7)
box.ZIndex = 12
box.Parent = caption
local header = Instance.new("TextLabel")
header.Name = "Header"
header.FontFace = Font.new(
"rbxasset://fonts/families/BuilderSans.json",
Enum.FontWeight.Medium,
Enum.FontStyle.Normal
)
header.Text = "Caption"
header.TextColor3 = Color3.fromRGB(255, 255, 255)
header.TextSize = TEXT_SIZE
header.TextTruncate = Enum.TextTruncate.None
header.TextWrapped = false
header.TextXAlignment = Enum.TextXAlignment.Left
header.AutomaticSize = Enum.AutomaticSize.X
header.BackgroundTransparency = 1
header.LayoutOrder = 1
header.Size = UDim2.fromOffset(0, 16)
header.ZIndex = 18
header.Parent = box
local layout = Instance.new("UIListLayout")
layout.Name = "Layout"
layout.Padding = UDim.new(0, 8)
layout.SortOrder = Enum.SortOrder.LayoutOrder
layout.Parent = box
local UICorner = Instance.new("UICorner")
UICorner.Name = "CaptionCorner"
UICorner.Parent = box
local padding = Instance.new("UIPadding")
padding.Name = "Padding"
padding.PaddingBottom = UDim.new(0, 12)
padding.PaddingLeft = UDim.new(0, 12)
padding.PaddingRight = UDim.new(0, 12)
padding.PaddingTop = UDim.new(0, 12)
padding.Parent = box
local hotkeys = Instance.new("Frame")
hotkeys.Name = "Hotkeys"
hotkeys.AutomaticSize = Enum.AutomaticSize.Y
hotkeys.BackgroundTransparency = 1
hotkeys.LayoutOrder = 3
hotkeys.Size = UDim2.fromScale(1, 0)
hotkeys.Visible = false
hotkeys.Parent = box
local layout1 = Instance.new("UIListLayout")
layout1.Name = "Layout1"
layout1.Padding = UDim.new(0, 6)
layout1.FillDirection = Enum.FillDirection.Vertical
layout1.HorizontalAlignment = Enum.HorizontalAlignment.Center
layout1.HorizontalFlex = Enum.UIFlexAlignment.None
layout1.ItemLineAlignment = Enum.ItemLineAlignment.Automatic
layout1.VerticalFlex = Enum.UIFlexAlignment.None
layout1.SortOrder = Enum.SortOrder.LayoutOrder
layout1.Parent = hotkeys
local keyTag1 = Instance.new("ImageLabel")
keyTag1.Name = "Key1"
keyTag1.Image = "rbxasset://textures/ui/Controls/key_single.png"
keyTag1.ImageTransparency = 0.7
keyTag1.ScaleType = Enum.ScaleType.Slice
keyTag1.SliceCenter = Rect.new(5, 5, 23, 24)
keyTag1.AutomaticSize = Enum.AutomaticSize.X
keyTag1.BackgroundTransparency = 1
keyTag1.LayoutOrder = 1
keyTag1.Size = UDim2.fromOffset(0, 30)
keyTag1.ZIndex = 15
keyTag1.Parent = hotkeys
local inset = Instance.new("UIPadding")
inset.Name = "Inset"
inset.PaddingLeft = UDim.new(0, 8)
inset.PaddingRight = UDim.new(0, 8)
inset.Parent = keyTag1
local labelContent = Instance.new("TextLabel")
labelContent.AutoLocalize = false
labelContent.Name = "LabelContent"
labelContent.FontFace = Font.new(
"rbxasset://fonts/families/GothamSSm.json",
Enum.FontWeight.Medium,
Enum.FontStyle.Normal
)
labelContent.Text = ""
labelContent.TextColor3 = Color3.fromRGB(189, 190, 190)
labelContent.TextSize = TEXT_SIZE
labelContent.AutomaticSize = Enum.AutomaticSize.X
labelContent.BackgroundTransparency = 1
labelContent.Position = UDim2.fromOffset(0, -1)
labelContent.Size = UDim2.fromScale(1, 1)
labelContent.ZIndex = 16
labelContent.Parent = keyTag1
local caret = Instance.new("ImageLabel")
caret.Name = "Caret"
caret.Image = "rbxassetid://101906294438076"
caret.ImageColor3 = CAPTION_COLOR
caret.AnchorPoint = Vector2.new(0, 0.5)
caret.BackgroundTransparency = 1
caret.Position = UDim2.new(0, 0, 0, 4)
caret.Size = UDim2.fromOffset(16, 8)
caret.ZIndex = 12
caret.Parent = caption
local dropShadow = Instance.new("ImageLabel")
dropShadow.Visible = true
dropShadow.Name = "DropShadow"
dropShadow.Image = "rbxassetid://124920646932671"
dropShadow.ImageColor3 = Color3.fromRGB(0, 0, 0)
dropShadow.ImageTransparency = 0.45
dropShadow.ScaleType = Enum.ScaleType.Slice
dropShadow.SliceCenter = Rect.new(12, 12, 13, 13)
dropShadow.BackgroundTransparency = 1
dropShadow.Position = UDim2.fromOffset(0, 5)
dropShadow.Size = UDim2.new(1, 0, 0, 48)
dropShadow.Parent = caption
box:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
dropShadow.Size = UDim2.new(1, 0, 0, box.AbsoluteSize.Y + 8)
end)
-- It's important we match the sizes as this is not
-- handles within clipOutside (as it assumes the sizes
-- are already the same)
local captionJanitor = icon.captionJanitor
local _, captionClone = icon:clipOutside(caption)
captionClone.AutomaticSize = Enum.AutomaticSize.None
local function matchSize()
local absolute = caption.AbsoluteSize
captionClone.Size = UDim2.fromOffset(absolute.X, absolute.Y)
end
captionJanitor:add(caption:GetPropertyChangedSignal("AbsoluteSize"):Connect(matchSize))
matchSize()
-- This handles the appearing/disappearing/positioning of the caption
local isCompletelyEnabled = false
local captionHeader = caption.Box.Header
local UserInputService = game:GetService("UserInputService")
local function updateHotkey(keyCodeEnum)
local hasKeyboard = UserInputService.KeyboardEnabled
local text = caption:GetAttribute("CaptionText") or ""
local hideHeader = text == "_hotkey_"
if not hasKeyboard and hideHeader then
icon:setCaption()
return
end
captionHeader.Text = text
captionHeader.Visible = not hideHeader
if keyCodeEnum then
labelContent.Text = keyCodeEnum.Name
hotkeys.Visible = true
end
if not hasKeyboard then
hotkeys.Visible = false
end
end
caption:GetAttributeChangedSignal("CaptionText"):Connect(updateHotkey)
local EASING_STYLE = Enum.EasingStyle.Quad
local TWEEN_SPEED = 0.2
local TWEEN_INFO_IN = TweenInfo.new(TWEEN_SPEED, EASING_STYLE, Enum.EasingDirection.In)
local TWEEN_INFO_OUT = TweenInfo.new(TWEEN_SPEED, EASING_STYLE, Enum.EasingDirection.Out)
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local function getCaptionPosition(customEnabled)
local enabled = if customEnabled ~= nil then customEnabled else isCompletelyEnabled
local yOut = 2
local yIn = yOut + 8
local yOffset = if enabled then yIn else yOut
return UDim2.new(0.5, 0, 1, yOffset)
end
local function updatePosition(forcedEnabled)
-- Ignore changes if not enabled to reduce redundant calls
if not isCompletelyEnabled then
return
end
-- Currently the one thing which isn't accounted for are the bounds of the screen
-- This would be an issue if someone sets a long caption text for the left or
-- right most icon
local enabled = if forcedEnabled ~= nil then forcedEnabled else isCompletelyEnabled
local startPosition = getCaptionPosition(not enabled)
local endPosition = getCaptionPosition(enabled)
-- It's essential we reset the carets position to prevent the x sizing bounds
-- of the caption from infinitely scaling up
if enabled then
local caretY = caret.Position.Y.Offset
caret.Position = UDim2.fromOffset(0, caretY)
caption.AutomaticSize = Enum.AutomaticSize.XY
caption.Size = UDim2.fromOffset(32, 53)
else
local absolute = caption.AbsoluteSize
caption.AutomaticSize = Enum.AutomaticSize.Y
caption.Size = UDim2.fromOffset(absolute.X, absolute.Y)
end
-- We initially default to the opposite state
local previousCaretX
local function updateCaret()
local caretX = clickRegion.AbsolutePosition.X - caption.AbsolutePosition.X + clickRegion.AbsoluteSize.X/2 - caret.AbsoluteSize.X/2
local caretY = caret.Position.Y.Offset
local newCaretPosition = UDim2.fromOffset(caretX, caretY)
if previousCaretX ~= caretX then
-- Again, it's essential we reset the caret if
-- a difference in X position is detected otherwise
-- a slight quirk with AutomaticCanvas can cause
-- the caption to infinitely scale
previousCaretX = caretX
caret.Position = UDim2.fromOffset(0, caretY)
task.wait()
end
caret.Position = newCaretPosition
end
captionClone.Position = startPosition
updateCaret()
-- Now we tween into the new state
local tweenInfo = (enabled and TWEEN_INFO_IN) or TWEEN_INFO_OUT
local tween = TweenService:Create(captionClone, tweenInfo, {Position = endPosition})
local updateCaretConnection = RunService.Heartbeat:Connect(updateCaret)
tween:Play()
tween.Completed:Once(function()
updateCaretConnection:Disconnect()
end)
end
captionJanitor:add(clickRegion:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
updatePosition()
end))
updatePosition(false)
captionJanitor:add(icon.toggleKeyAdded:Connect(updateHotkey))
for keyCodeEnum, _ in pairs(icon.bindedToggleKeys) do
updateHotkey(keyCodeEnum)
break
end
captionJanitor:add(icon.fakeToggleKeyChanged:Connect(updateHotkey))
local fakeToggleKey = icon.fakeToggleKey
if fakeToggleKey then
updateHotkey(fakeToggleKey)
end
local function setCaptionEnabled(enabled)
if isCompletelyEnabled == enabled then
return
end
local joinedFrame = icon.joinedFrame
if joinedFrame and string.match(joinedFrame.Name, "Dropdown") then
enabled = false
end
isCompletelyEnabled = enabled
local newTransparency = (enabled and 0) or 1
local tweenInfo = (enabled and TWEEN_INFO_IN) or TWEEN_INFO_OUT
local tweenTransparency = TweenService:Create(caption, tweenInfo, {
GroupTransparency = newTransparency
})
tweenTransparency:Play()
if enabled then
captionClone:SetAttribute("ForceUpdate", true)
end
updatePosition()
updateHotkey()
end
local WAIT_DURATION = 0.5
local RECOVER_PERIOD = 0.3
local Icon = require(icon.iconModule)
captionJanitor:add(icon.stateChanged:Connect(function(stateName)
if stateName == "Viewing" then
local lastClock = Icon.captionLastClosedClock
local clockDifference = (lastClock and os.clock() - lastClock) or 999
local waitDuration = (clockDifference < RECOVER_PERIOD and 0) or WAIT_DURATION
task.delay(waitDuration, function()
if icon.activeState == "Viewing" then
setCaptionEnabled(true)
end
end)
else
Icon.captionLastClosedClock = os.clock()
setCaptionEnabled(false)
end
end))
return caption
end
================================================
FILE: src/Elements/Container.lua
================================================
local hasBecomeOldTheme = false
local previousInsetHeight = 0
return function(Icon)
-- Has to be included for the time being due to this bug mentioned here:
-- https://devforum.roblox.com/t/bug/2973508/7
local GuiService = game:GetService("GuiService")
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local container = {}
local Signal = require(script.Parent.Parent.Packages.GoodSignal)
local insetChanged = Signal.new()
local guiInset = GuiService:GetGuiInset()
local startInset = 0
local yDownOffset = 0
local ySizeOffset = 0
local checkCount = 0
local isConsoleScreen = false
local isUsingVR = false
local function checkInset(status)
local currentHeight = GuiService.TopbarInset.Height
local isOldTopbar = currentHeight <= 36
-- These additional checks are needed to ensure *it is actually* the old topbar
-- and not a client which takes a really long time to load
-- There's unfortunately no APIs to do this a prettier way
isConsoleScreen = GuiService:IsTenFootInterface()
isUsingVR = UserInputService.VREnabled
Icon.isOldTopbar = isOldTopbar
checkCount += 1
if currentHeight == 0 and status == nil then
task.defer(function()
task.wait(8)
checkInset("ForceConvertToOld")
end)
elseif checkCount == 1 then
task.delay(5, function()
local localPlayer = Players.LocalPlayer
localPlayer:WaitForChild("PlayerGui")
if checkCount == 1 then
checkInset()
end
end)
end
-- Conver to old theme if verified
if Icon.isOldTopbar and not isConsoleScreen and not isUsingVR and hasBecomeOldTheme == false and (currentHeight ~= 0 or status == "ForceConvertToOld") then
hasBecomeOldTheme = true
task.defer(function()
-- If oldtopbar, apply the Classic theme
local themes = script.Parent.Parent.Features.Themes
local Classic = require(themes.Classic)
Icon.modifyBaseTheme(Classic)
-- Also configure the oldtopbar correctly
local function decideToHideTopbar()
if GuiService.MenuIsOpen then
Icon.setTopbarEnabled(false, true)
else
Icon.setTopbarEnabled()
end
end
GuiService:GetPropertyChangedSignal("MenuIsOpen"):Connect(decideToHideTopbar)
decideToHideTopbar()
end)
end
-- Modify the offsets slightly depending on device type
guiInset = GuiService:GetGuiInset()
startInset = if isOldTopbar then 12 else guiInset.Y - 50
yDownOffset = if isOldTopbar then 2 else 0 --if isOldTopbar then 2 else 0
ySizeOffset = -2
if isConsoleScreen then
startInset = 10
yDownOffset = 0 ---9
end
if GuiService.TopbarInset.Height == 0 and not hasBecomeOldTheme then
yDownOffset += 13
ySizeOffset = 50
end
-- Now inform other areas of the change
insetChanged:Fire(guiInset)
local insetHeight = guiInset.Y
if insetHeight ~= previousInsetHeight then
previousInsetHeight = insetHeight
task.defer(function()
Icon.insetHeightChanged:Fire(insetHeight)
end)
end
end
GuiService:GetPropertyChangedSignal("TopbarInset"):Connect(checkInset)
checkInset("FirstTime")
local screenGui = Instance.new("ScreenGui")
insetChanged:Connect(function()
screenGui:SetAttribute("StartInset", startInset)
end)
screenGui.Name = "TopbarStandard"
screenGui.Enabled = true
screenGui.DisplayOrder = Icon.baseDisplayOrder
screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
screenGui.IgnoreGuiInset = true
screenGui.ResetOnSpawn = false
screenGui.ScreenInsets = Enum.ScreenInsets.TopbarSafeInsets
container[screenGui.Name] = screenGui
Icon.baseDisplayOrderChanged:Connect(function()
screenGui.DisplayOrder = Icon.baseDisplayOrder
end)
local holders = Instance.new("Frame")
holders.Name = "Holders"
holders.BackgroundTransparency = 1
insetChanged:Connect(function()
local holderY = if isUsingVR then 36 else 56
local holderSize = if isConsoleScreen then UDim2.new(1, 0, 0, holderY) else UDim2.new(1, 0, 1, ySizeOffset)
holders.Position = UDim2.new(0, 0, 0, yDownOffset)
holders.Size = holderSize
end)
holders.Visible = true
holders.ZIndex = 1
holders.Parent = screenGui
local screenGuiCenter = screenGui:Clone()
local holdersCenter = screenGuiCenter.Holders
local function updateCenteredHoldersHeight()
holdersCenter.Size = UDim2.new(1, 0, 0, GuiService.TopbarInset.Height+ySizeOffset)
end
screenGuiCenter.Name = "TopbarCentered"
screenGuiCenter.DisplayOrder = Icon.baseDisplayOrder
screenGuiCenter.ScreenInsets = Enum.ScreenInsets.None
Icon.baseDisplayOrderChanged:Connect(function()
screenGuiCenter.DisplayOrder = Icon.baseDisplayOrder
end)
container[screenGuiCenter.Name] = screenGuiCenter
insetChanged:Connect(updateCenteredHoldersHeight)
updateCenteredHoldersHeight()
local screenGuiClipped = screenGui:Clone()
screenGuiClipped.Name = screenGuiClipped.Name.."Clipped"
screenGuiClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)
Icon.baseDisplayOrderChanged:Connect(function()
screenGuiClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)
end)
container[screenGuiClipped.Name] = screenGuiClipped
local screenGuiCenterClipped = screenGuiCenter:Clone()
screenGuiCenterClipped.Name = screenGuiCenterClipped.Name.."Clipped"
screenGuiCenterClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)
Icon.baseDisplayOrderChanged:Connect(function()
screenGuiCenterClipped.DisplayOrder = (Icon.baseDisplayOrder + 1)
end)
container[screenGuiCenterClipped.Name] = screenGuiCenterClipped
local holderReduction = -24
local left = Instance.new("ScrollingFrame")
left:SetAttribute("IsAHolder", true)
left.Name = "Left"
insetChanged:Connect(function()
left.Position = UDim2.fromOffset(startInset, 0)
end)
left.Size = UDim2.new(1, holderReduction, 1, 0)
left.BackgroundTransparency = 1
left.Visible = true
left.ZIndex = 1
left.Active = false
left.ClipsDescendants = true
left.HorizontalScrollBarInset = Enum.ScrollBarInset.None
left.CanvasSize = UDim2.new(0, 0, 1, -1) -- This -1 prevents a dropdown scrolling appearance bug
left.AutomaticCanvasSize = Enum.AutomaticSize.X
left.ScrollingDirection = Enum.ScrollingDirection.X
left.ScrollBarThickness = 0
left.BorderSizePixel = 0
left.Selectable = false
left.ScrollingEnabled = false--true
left.ElasticBehavior = Enum.ElasticBehavior.Never
left.Parent = holders
local UIListLayout = Instance.new("UIListLayout")
insetChanged:Connect(function()
UIListLayout.Padding = UDim.new(0, startInset)
end)
UIListLayout.FillDirection = Enum.FillDirection.Horizontal
UIListLayout.SortOrder = Enum.SortOrder.LayoutOrder
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left
UIListLayout.Parent = left
local center = left:Clone()
insetChanged:Connect(function()
center.UIListLayout.Padding = UDim.new(0, startInset)
end)
center.ScrollingEnabled = false
center.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
center.Name = "Center"
center.Parent = holdersCenter
local right = left:Clone()
insetChanged:Connect(function()
right.UIListLayout.Padding = UDim.new(0, startInset)
end)
right.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right
right.Name = "Right"
right.AnchorPoint = Vector2.new(1, 0)
right.Position = UDim2.new(1, -12, 0, 0)
right.Parent = holders
-- This is important so that all elements update instantly
insetChanged:Fire(guiInset)
return container
end
================================================
FILE: src/Elements/Dropdown.lua
================================================
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Themes = require(script.Parent.Parent.Features.Themes)
local PADDING = 0 -- used to be 8
return function(icon)
local dropdown = Instance.new("Frame") -- Instance.new("CanvasGroup")
dropdown.Name = "Dropdown"
dropdown.AutomaticSize = Enum.AutomaticSize.X
dropdown.BackgroundTransparency = 1
dropdown.BorderSizePixel = 0
dropdown.AnchorPoint = Vector2.new(0.5, 0)
dropdown.Position = UDim2.new(0.5, 0, 1, 10)
dropdown.ZIndex = -2
dropdown.ClipsDescendants = true
dropdown.Parent = icon.widget
-- Account for PreferredTransparency which can be set by every player
local GuiService = game:GetService("GuiService")
icon:setBehaviour("Dropdown", "BackgroundTransparency", function(value)
local preference = GuiService.PreferredTransparency
local newValue = value * preference
if value == 1 then
return value
end
return newValue
end)
icon.janitor:add(GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
icon:refreshAppearance(dropdown, "BackgroundTransparency")
end))
local UICorner = Instance.new("UICorner")
UICorner.Name = "DropdownCorner"
UICorner.CornerRadius = UDim.new(0, 10)
UICorner.Parent = dropdown
local dropdownScroller = Instance.new("ScrollingFrame")
dropdownScroller.Name = "DropdownScroller"
dropdownScroller.AutomaticSize = Enum.AutomaticSize.X
dropdownScroller.BackgroundTransparency = 1
dropdownScroller.BorderSizePixel = 0
dropdownScroller.AnchorPoint = Vector2.new(0, 0)
dropdownScroller.Position = UDim2.new(0, 0, 0, 0)
dropdownScroller.ZIndex = -1
dropdownScroller.ClipsDescendants = true
dropdownScroller.Visible = true
dropdownScroller.VerticalScrollBarInset = Enum.ScrollBarInset.None --ScrollBar
dropdownScroller.VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right
dropdownScroller.Active = false
dropdownScroller.ScrollingEnabled = true
dropdownScroller.AutomaticCanvasSize = Enum.AutomaticSize.Y
dropdownScroller.ScrollBarThickness = 5
dropdownScroller.ScrollBarImageColor3 = Color3.fromRGB(255, 255, 255)
dropdownScroller.ScrollBarImageTransparency = 0.8
dropdownScroller.CanvasSize = UDim2.new(0, 0, 0, 0)
dropdownScroller.Selectable = false
dropdownScroller.Active = true
dropdownScroller.Parent = dropdown
local TweenDuration = Instance.new("NumberValue") -- this helps to change the speed to open / close in modifyTheme()
TweenDuration.Name = "DropdownSpeed"
TweenDuration.Value = 0.07
TweenDuration.Parent = dropdown
local dropdownPadding = Instance.new("UIPadding")
dropdownPadding.Name = "DropdownPadding"
dropdownPadding.PaddingTop = UDim.new(0, PADDING)
dropdownPadding.PaddingBottom = UDim.new(0, PADDING)
dropdownPadding.Parent = dropdownScroller
local dropdownList = Instance.new("UIListLayout")
dropdownList.Name = "DropdownList"
dropdownList.FillDirection = Enum.FillDirection.Vertical
dropdownList.SortOrder = Enum.SortOrder.LayoutOrder
dropdownList.HorizontalAlignment = Enum.HorizontalAlignment.Center
dropdownList.HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly
dropdownList.Parent = dropdownScroller
local dropdownJanitor = icon.dropdownJanitor
local Icon = require(icon.iconModule)
icon.dropdownChildAdded:Connect(function(childIcon)
local _, modificationUID = childIcon:modifyTheme({
{"Widget", "BorderSize", 0},
{"IconCorners", "CornerRadius", UDim.new(0, 10)},
{"Widget", "MinimumWidth", 190},
{"Widget", "MinimumHeight", 58},
{"IconLabel", "TextSize", 20},
{"IconOverlay", "Size", UDim2.new(1, 0, 1, 0)},
{"PaddingLeft", "Size", UDim2.fromOffset(25, 0)},
{"Notice", "Position", UDim2.new(1, -24, 0, 5)},
{"ContentsList", "HorizontalAlignment", Enum.HorizontalAlignment.Left},
{"Selection", "Size", UDim2.new(1, -PADDING, 1, -PADDING)},
{"Selection", "Position", UDim2.new(0, PADDING/2, 0, PADDING/2)},
})
task.defer(function()
childIcon.joinJanitor:add(function()
childIcon:removeModification(modificationUID)
end)
end)
end)
icon.dropdownSet:Connect(function(arrayOfIcons)
for i, otherIconUID in pairs(icon.dropdownIcons) do
local otherIcon = Icon.getIconByUID(otherIconUID)
otherIcon:destroy()
end
if type(arrayOfIcons) == "table" then
for i, otherIcon in pairs(arrayOfIcons) do
otherIcon:joinDropdown(icon)
end
end
end)
local function updateMaxIcons()
--icon:modifyTheme({"Dropdown", "Visible", icon.isSelected})
local maxIcons = dropdown:GetAttribute("MaxIcons")
if not maxIcons then return 0 end
local children = {}
for _, child in pairs(dropdownScroller:GetChildren()) do
if child:IsA("GuiObject") and child.Visible then
table.insert(children, child)
end
end
table.sort(children, function(a, b) return a.AbsolutePosition.Y < b.AbsolutePosition.Y end)
local totalHeight = 0
local maxIconsRoundedUp = math.ceil(maxIcons)
for i = 1, maxIconsRoundedUp do
local child = children[i]
if not child then break end
local height = child.AbsoluteSize.Y
local isReduced = i == maxIconsRoundedUp and maxIconsRoundedUp ~= maxIcons
if isReduced then
height *= (maxIcons - maxIconsRoundedUp + 1)
end
totalHeight += height
end
totalHeight += dropdownPadding.PaddingTop.Offset + dropdownPadding.PaddingBottom.Offset
return totalHeight
end
local openTween = nil
local closeTween = nil
local currentSpeedMultiplier = nil
local currentTweenInfo = nil
local function getTweenInfo()
local speedMultiplier = Themes.getInstanceValue(dropdown, "MaxIcons") or 1
if currentSpeedMultiplier and currentSpeedMultiplier == speedMultiplier and currentTweenInfo then
return currentTweenInfo
end
local newTweenInfo = TweenInfo.new(
TweenDuration.Value * speedMultiplier,
Enum.EasingStyle.Exponential,
Enum.EasingDirection.Out
)
currentTweenInfo = newTweenInfo
currentSpeedMultiplier = speedMultiplier
return newTweenInfo
end
local function updateVisibility()
-- Update visibiliy of dropdown using tween transition
local tweenInfo = getTweenInfo()
if openTween then
openTween:Cancel()
openTween = nil
end
if closeTween then
closeTween:Cancel()
closeTween = nil
end
if icon.isSelected then
local height = updateMaxIcons()
dropdown.Visible = true
dropdown.BackgroundTransparency = 0 -- no transparency so it looks solid
dropdown.Size = UDim2.new(0, dropdown.Size.X.Offset, 0, 0) -- reset height to 0 before tween
openTween = TweenService:Create(dropdown, tweenInfo, {Size = UDim2.new(0, dropdown.Size.X.Offset, 0, height)})
openTween:Play()
openTween.Completed:Connect(function()
openTween = nil
end)
else
local closeTweenInfo = TweenInfo.new(0)
closeTween = TweenService:Create(dropdown, closeTweenInfo, {Size = UDim2.new(0, dropdown.Size.X.Offset, 0, 0)})
closeTween:Play()
closeTween.Completed:Connect(function()
closeTween = nil
end)
end
end
dropdownJanitor:add(icon.toggled:Connect(updateVisibility))
updateVisibility()
--task.delay(0.2, updateVisibility)
local function updateChildSize()
local tweenInfo = getTweenInfo()
if not icon.isSelected then return end
if openTween then
openTween:Cancel()
openTween = nil
end
if closeTween then
closeTween:Cancel()
closeTween = nil
end
RunService.Heartbeat:Wait()
local height = updateMaxIcons()
openTween = TweenService:Create(dropdown, tweenInfo, {Size = UDim2.new(0, dropdown.Size.X.Offset, 0, height)})
openTween:Play()
openTween.Completed:Connect(function()
openTween = nil
end)
end
dropdownJanitor:add(icon.toggled:Connect(updateVisibility))
-- Ensures canvas and size stay synced (original updateMaxIcons logic)
local updateCount = 0
local isUpdating = false
-- This updates the scrolling frame to only display a scroll
-- length equal to the distance produced by its MaxIcons
local function updateMaxIconsListener()
updateCount += 1
if isUpdating then return end
local myUpdateCount = updateCount
isUpdating = true
task.defer(function()
isUpdating = false
if updateCount ~= myUpdateCount then
updateMaxIconsListener()
end
end)
local maxIcons = dropdown:GetAttribute("MaxIcons")
if not maxIcons then return end
local orderedInstances = {}
for _, child in pairs(dropdownScroller:GetChildren()) do
if child:IsA("GuiObject") and child.Visible then
table.insert(orderedInstances, {child, child.AbsolutePosition.Y})
end
end
table.sort(orderedInstances, function(a, b) return a[2] < b[2] end)
local totalHeight = 0
local hasSetNextSelection = false
local maxIconsRoundedUp = math.ceil(maxIcons)
for i = 1, maxIconsRoundedUp do
local group = orderedInstances[i]
if not group then break end
local child = group[1]
local height = child.AbsoluteSize.Y
local isReduced = i == maxIconsRoundedUp and maxIconsRoundedUp ~= maxIcons
if isReduced then
height = height * (maxIcons - maxIconsRoundedUp + 1)
end
totalHeight += height
if isReduced then
continue
end
local iconUID = child:GetAttribute("WidgetUID")
local childIcon = iconUID and Icon.getIconByUID(iconUID)
if childIcon then
local nextSelection = nil
if not hasSetNextSelection then
hasSetNextSelection = true
nextSelection = icon:getInstance("ClickRegion")
end
childIcon:getInstance("ClickRegion").NextSelectionUp = nextSelection
end
end
totalHeight += dropdownPadding.PaddingTop.Offset + dropdownPadding.PaddingBottom.Offset
dropdownScroller.Size = UDim2.fromOffset(0, totalHeight)
end
dropdownJanitor:add(dropdownScroller:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(updateMaxIconsListener))
dropdownJanitor:add(dropdownScroller.ChildAdded:Connect(updateMaxIconsListener))
dropdownJanitor:add(dropdownScroller.ChildRemoved:Connect(updateChildSize)) -- rezise the dropdown when icon delects or adds
dropdownJanitor:add(dropdownScroller.ChildRemoved:Connect(updateMaxIconsListener))
dropdownJanitor:add(dropdown:GetAttributeChangedSignal("MaxIcons"):Connect(updateMaxIconsListener))
dropdownJanitor:add(dropdown:GetAttributeChangedSignal("MaxIcons"):Connect(updateChildSize))
dropdownJanitor:add(icon.childThemeModified:Connect(updateMaxIconsListener))
updateMaxIconsListener()
-- Ensures each child listens to visibility changes
local function connectVisibilityListeners(child)
if child:IsA("GuiObject") then
child:GetPropertyChangedSignal("Visible"):Connect(updateChildSize)
child:GetPropertyChangedSignal("Size"):Connect(updateChildSize) -- -- update max icons when child size changes
end
end
-- For existing children
for _, child in pairs(dropdownScroller:GetChildren()) do
connectVisibilityListeners(child)
end
-- For new children
dropdownScroller.ChildAdded:Connect(function(child)
RunService.Heartbeat:Wait()
connectVisibilityListeners(child)
updateChildSize()
end)
-- On start, hide dropdown (prevent it showing as opened)
dropdown.Visible = false
return dropdown
end
================================================
FILE: src/Elements/Indicator.lua
================================================
return function(icon, Icon)
local widget = icon.widget
local contents = icon:getInstance("Contents")
local indicator = Instance.new("Frame")
indicator.Name = "Indicator"
indicator.LayoutOrder = 9999999
indicator.ZIndex = 6
indicator.Size = UDim2.new(0, 42, 0, 42)
indicator.BorderColor3 = Color3.fromRGB(0, 0, 0)
indicator.BackgroundTransparency = 1
indicator.Position = UDim2.new(1, 0, 0.5, 0)
indicator.BorderSizePixel = 0
indicator.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
indicator.Parent = contents
local indicatorButton = Instance.new("Frame")
indicatorButton.Name = "IndicatorButton"
indicatorButton.BorderColor3 = Color3.fromRGB(0, 0, 0)
indicatorButton.AnchorPoint = Vector2.new(0.5, 0.5)
indicatorButton.BorderSizePixel = 0
indicatorButton.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
indicatorButton.Parent = indicator
local GuiService = game:GetService("GuiService")
local GamepadService = game:GetService("GamepadService")
local ourClickRegion = icon:getInstance("ClickRegion")
local function selectionChanged()
local selectedClickRegion = GuiService.SelectedObject
if selectedClickRegion == ourClickRegion then
indicatorButton.BackgroundTransparency = 1
indicatorButton.Position = UDim2.new(0.5, -2, 0.5, 0)
indicatorButton.Size = UDim2.fromScale(1.2, 1.2)
else
indicatorButton.BackgroundTransparency = 0.75
indicatorButton.Position = UDim2.new(0.5, 2, 0.5, 0)
indicatorButton.Size = UDim2.fromScale(1, 1)
end
end
icon.janitor:add(GuiService:GetPropertyChangedSignal("SelectedObject"):Connect(selectionChanged))
selectionChanged()
local imageLabel = Instance.new("ImageLabel")
imageLabel.LayoutOrder = 2
imageLabel.ZIndex = 15
imageLabel.AnchorPoint = Vector2.new(0.5, 0.5)
imageLabel.Size = UDim2.new(0.5, 0, 0.5, 0)
imageLabel.BackgroundTransparency = 1
imageLabel.Position = UDim2.new(0.5, 0, 0.5, 0)
imageLabel.Image = "rbxasset://textures/ui/Controls/XboxController/DPadUp@2x.png"
imageLabel.Parent = indicatorButton
local UICorner = Instance.new("UICorner")
UICorner.CornerRadius = UDim.new(1, 0)
UICorner.Parent = indicatorButton
local UserInputService = game:GetService("UserInputService")
local function setIndicatorVisible(visibility)
if visibility == nil then
visibility = indicator.Visible
end
if GamepadService.GamepadCursorEnabled then
visibility = false
end
if visibility then
icon:modifyTheme({"PaddingRight", "Size", UDim2.new(0, 0, 1, 0)}, "IndicatorPadding")
elseif indicator.Visible then
icon:removeModification("IndicatorPadding")
end
icon:modifyTheme({"Indicator", "Visible", visibility})
icon.updateSize:Fire()
end
icon.janitor:add(GamepadService:GetPropertyChangedSignal("GamepadCursorEnabled"):Connect(setIndicatorVisible))
icon.indicatorSet:Connect(function(keyCode)
local visibility = false
if keyCode then
imageLabel.Image = UserInputService:GetImageForKeyCode(keyCode)
visibility = true
end
setIndicatorVisible(visibility)
end)
local function updateSize()
local ySize = widget.AbsoluteSize.Y*0.96
indicator.Size = UDim2.new(0, ySize, 0, ySize)
end
widget:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateSize)
updateSize()
return indicator
end
================================================
FILE: src/Elements/Menu.lua
================================================
return function(icon)
local menu = Instance.new("ScrollingFrame")
menu.Name = "Menu"
menu.BackgroundTransparency = 1
menu.Visible = true
menu.ZIndex = 1
menu.Size = UDim2.fromScale(1, 1)
menu.ClipsDescendants = true
menu.TopImage = ""
menu.BottomImage = ""
menu.HorizontalScrollBarInset = Enum.ScrollBarInset.Always
menu.CanvasSize = UDim2.new(0, 0, 1, -1) -- This -1 prevents a dropdown scrolling appearance bug
menu.ScrollingEnabled = true
menu.ScrollingDirection = Enum.ScrollingDirection.X
menu.ZIndex = 20
menu.ScrollBarThickness = 3
menu.ScrollBarImageColor3 = Color3.fromRGB(255, 255, 255)
menu.ScrollBarImageTransparency = 0.8
menu.BorderSizePixel = 0
menu.Selectable = false
local Icon = require(icon.iconModule)
local menuUIListLayout = Icon.container.TopbarStandard:FindFirstChild("UIListLayout", true):Clone()
menuUIListLayout.Name = "MenuUIListLayout"
menuUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
menuUIListLayout.Parent = menu
local menuGap = Instance.new("Frame")
menuGap.Name = "MenuGap"
menuGap.BackgroundTransparency = 1
menuGap.Visible = false
menuGap.AnchorPoint = Vector2.new(0, 0.5)
menuGap.ZIndex = 5
menuGap.Parent = menu
local hasStartedMenu = false
local Themes = require(script.Parent.Parent.Features.Themes)
local function totalChildrenChanged()
local menuJanitor = icon.menuJanitor
local totalIcons = #icon.menuIcons
if hasStartedMenu then
if totalIcons <= 0 then
menuJanitor:clean()
hasStartedMenu = false
end
return
end
hasStartedMenu = true
-- Listen for changes
menuJanitor:add(icon.toggled:Connect(function()
if #icon.menuIcons > 0 then
icon.updateSize:Fire()
end
end))
-- Modify appearance of menu icon when joined
local _, modificationUID = icon:modifyTheme({
{"Menu", "Active", true},
})
task.defer(function()
menuJanitor:add(function()
icon:removeModification(modificationUID)
end)
end)
-- For right-aligned icons, this ensures their menus
-- close button appear instantly when selected (instead
-- of partially hidden from view)
local previousCanvasX = menu.AbsoluteCanvasSize.X
local function rightAlignCanvas()
if icon.alignment == "Right" then
local newCanvasX = menu.AbsoluteCanvasSize.X
local difference = previousCanvasX - newCanvasX
previousCanvasX = newCanvasX
menu.CanvasPosition = Vector2.new(menu.CanvasPosition.X - difference, 0)
end
end
menuJanitor:add(icon.selected:Connect(rightAlignCanvas))
menuJanitor:add(menu:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(rightAlignCanvas))
-- Apply a close selected image if the user hasn't applied thier own
local stateGroup = icon:getStateGroup()
local imageDeselected = Themes.getThemeValue(stateGroup, "IconImage", "Image", "Deselected")
local imageSelected = Themes.getThemeValue(stateGroup, "IconImage", "Image", "Selected")
if imageDeselected == imageSelected then
local fontLink = "rbxasset://fonts/families/FredokaOne.json"
local fontFace = Font.new(fontLink, Enum.FontWeight.Light, Enum.FontStyle.Normal)
icon:removeModificationWith("IconLabel", "Text", "Viewing")
icon:removeModificationWith("IconLabel", "Image", "Viewing")
icon:modifyTheme({
{"IconLabel", "FontFace", fontFace, "Selected"},
{"IconLabel", "Text", "X", "Selected"},
{"IconLabel", "TextSize", 20, "Selected"},
{"IconLabel", "TextStrokeTransparency", 0.8, "Selected"},
{"IconImage", "Image", "", "Selected"},
})
end
-- Change order of spot when alignment changes
local menuGap = icon:getInstance("MenuGap")
local function updateAlignent()
local alignment = icon.alignment
local spotIndex = -99999
local gapIndex = -99998
if alignment == "Right" then
spotIndex = 99999
gapIndex = 99998
end
icon:modifyTheme({"IconSpot", "LayoutOrder", spotIndex})
menuGap.LayoutOrder = gapIndex
end
menuJanitor:add(icon.alignmentChanged:Connect(updateAlignent))
updateAlignent()
-- This updates the scrolling frame to only display a scroll
-- length equal to the distance produced by its MaxIcons
menu:GetAttributeChangedSignal("MenuCanvasWidth"):Connect(function()
local canvasWidth = menu:GetAttribute("MenuCanvasWidth")
local canvasY = menu.CanvasSize.Y
menu.CanvasSize = UDim2.new(0, canvasWidth, canvasY.Scale, canvasY.Offset)
end)
menuJanitor:add(icon.updateMenu:Connect(function()
local maxIcons = menu:GetAttribute("MaxIcons")
if not maxIcons then
return
end
local orderedInstances = {}
for _, child in pairs(menu:GetChildren()) do
local widgetUID = child:GetAttribute("WidgetUID")
if widgetUID and child.Visible then
table.insert(orderedInstances, {child, child.AbsolutePosition.X})
end
end
table.sort(orderedInstances, function(groupA, groupB)
return groupA[2] < groupB[2]
end)
local totalWidth = 0
for i = 1, maxIcons do
local group = orderedInstances[i]
if not group then
break
end
local child = group[1]
local width = child.AbsoluteSize.X + menuUIListLayout.Padding.Offset
totalWidth += width
end
menu:SetAttribute("MenuWidth", totalWidth)
end))
local function startMenuUpdate()
task.delay(0.1, function()
icon.startMenuUpdate:Fire()
end)
end
menuJanitor:add(menu.ChildAdded:Connect(startMenuUpdate))
menuJanitor:add(menu.ChildRemoved:Connect(startMenuUpdate))
menuJanitor:add(menu:GetAttributeChangedSignal("MaxIcons"):Connect(startMenuUpdate))
menuJanitor:add(menu:GetAttributeChangedSignal("MaxWidth"):Connect(startMenuUpdate))
startMenuUpdate()
end
icon.menuChildAdded:Connect(totalChildrenChanged)
icon.menuSet:Connect(function(arrayOfIcons)
-- Reset any previous icons
for i, otherIconUID in pairs(icon.menuIcons) do
local otherIcon = Icon.getIconByUID(otherIconUID)
otherIcon:destroy()
end
-- Apply new icons
if type(arrayOfIcons) == "table" then
for i, otherIcon in pairs(arrayOfIcons) do
otherIcon:joinMenu(icon)
end
end
end)
return menu
end
================================================
FILE: src/Elements/Notice.lua
================================================
return function(icon, Icon)
local notice = Instance.new("Frame")
notice.Name = "Notice"
notice.ZIndex = 25
notice.AutomaticSize = Enum.AutomaticSize.X
notice.BorderColor3 = Color3.fromRGB(0, 0, 0)
notice.BorderSizePixel = 0
notice.BackgroundTransparency = 0.1
notice.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
notice.Visible = false
notice.Parent = icon.widget
local UICorner = Instance.new("UICorner")
UICorner.CornerRadius = UDim.new(1, 0)
UICorner.Parent = notice
local UIStroke = Instance.new("UIStroke")
UIStroke.Parent = notice
local noticeLabel = Instance.new("TextLabel")
noticeLabel.Name = "NoticeLabel"
noticeLabel.ZIndex = 26
noticeLabel.AnchorPoint = Vector2.new(0.5, 0.5)
noticeLabel.AutomaticSize = Enum.AutomaticSize.X
noticeLabel.Size = UDim2.new(1, 0, 1, 0)
noticeLabel.BackgroundTransparency = 1
noticeLabel.Position = UDim2.new(0.5, 0, 0.515, 0)
noticeLabel.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
noticeLabel.FontSize = Enum.FontSize.Size14
noticeLabel.TextColor3 = Color3.fromRGB(0, 0, 0)
noticeLabel.Text = "1"
noticeLabel.TextWrapped = true
noticeLabel.TextWrap = true
noticeLabel.Font = Enum.Font.Arial
noticeLabel.Parent = notice
local iconModule = script.Parent.Parent
local packages = iconModule.Packages
local Janitor = require(packages.Janitor)
local Signal = require(packages.GoodSignal)
local Utility = require(iconModule.Utility)
icon.noticeChanged:Connect(function(totalNotices)
-- Notice amount
if not totalNotices then
return
end
local exceeded99 = totalNotices > 99
local noticeDisplay = (exceeded99 and "99+") or totalNotices
noticeLabel.Text = noticeDisplay
if exceeded99 then
noticeLabel.TextSize = 11
end
-- Should enable
local enabled = true
if totalNotices < 1 then
enabled = false
end
local parentIcon = Icon.getIconByUID(icon.parentIconUID)
local dropdownOrMenuActive = #icon.dropdownIcons > 0 or #icon.menuIcons > 0
if icon.isSelected and dropdownOrMenuActive then
enabled = false
elseif parentIcon and not parentIcon.isSelected then
enabled = false
end
Utility.setVisible(notice, enabled, "NoticeHandler")
end)
icon.noticeStarted:Connect(function(customClearSignal, noticeId)
if not customClearSignal then
customClearSignal = icon.deselected
end
local parentIcon = Icon.getIconByUID(icon.parentIconUID)
if parentIcon then
parentIcon:notify(customClearSignal)
end
local noticeJanitor = icon.janitor:add(Janitor.new())
local noticeComplete = noticeJanitor:add(Signal.new())
noticeJanitor:add(icon.endNotices:Connect(function()
noticeComplete:Fire()
end))
noticeJanitor:add(customClearSignal:Connect(function()
noticeComplete:Fire()
end))
noticeId = noticeId or Utility.generateUID()
icon.notices[noticeId] = {
completeSignal = noticeComplete,
clearNoticeEvent = customClearSignal,
}
local function updateNotice()
icon.noticeChanged:Fire(icon.totalNotices)
end
icon.notified:Fire(noticeId)
icon.totalNotices += 1
updateNotice()
noticeComplete:Once(function()
noticeJanitor:destroy()
icon.totalNotices -= 1
icon.notices[noticeId] = nil
updateNotice()
end)
end)
-- Establish the notice
notice:SetAttribute("ClipToJoinedParent", true)
icon:clipOutside(notice)
return notice
end
================================================
FILE: src/Elements/Selection.lua
================================================
return function(Icon)
-- Credit to lolmansReturn and Canary Software for
-- retrieving these values
local selectionContainer = Instance.new("Frame")
selectionContainer.Name = "SelectionContainer"
selectionContainer.Visible = false
local selection = Instance.new("Frame")
selection.Name = "Selection"
selection.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
selection.BackgroundTransparency = 1
selection.BorderColor3 = Color3.fromRGB(0, 0, 0)
selection.BorderSizePixel = 0
selection.Parent = selectionContainer
local UIStroke = Instance.new("UIStroke")
UIStroke.Name = "UIStroke"
UIStroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
UIStroke.Color = Color3.fromRGB(255, 255, 255)
UIStroke.Thickness = 3
UIStroke.Parent = selection
local selectionGradient = Instance.new("UIGradient")
selectionGradient.Name = "SelectionGradient"
selectionGradient.Parent = UIStroke
local UICorner = Instance.new("UICorner")
UICorner:SetAttribute("Collective", "IconCorners")
UICorner.Name = "UICorner"
UICorner.CornerRadius = UDim.new(1, 0)
UICorner.Parent = selection
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local rotationSpeed = 1
selection:GetAttributeChangedSignal("RotationSpeed"):Connect(function()
rotationSpeed = selection:GetAttribute("RotationSpeed")
end)
RunService.Heartbeat:Connect(function()
if not GuiService.SelectedObject then
return
end
selectionGradient.Rotation = (os.clock() * rotationSpeed * 100) % 360
end)
return selectionContainer
end
================================================
FILE: src/Elements/Widget.lua
================================================
-- I named this 'Widget' instead of 'Icon' to make a clear difference between the icon *object* and
-- the icon (aka Widget) instance.
-- This contains the core components of the icon such as the button, image, label and notice. It's
-- also responsible for handling the automatic resizing of the widget (based upon image visibility and text length)
return function(icon, Icon)
local widget = Instance.new("Frame")
widget:SetAttribute("WidgetUID", icon.UID)
widget.Name = "Widget"
widget.BackgroundTransparency = 1
widget.Visible = true
widget.ZIndex = 20
widget.Active = false
widget.ClipsDescendants = true
local button = Instance.new("Frame")
button.Name = "IconButton"
button.Visible = true
button.ZIndex = 2
button.BorderSizePixel = 0
button.Parent = widget
button.ClipsDescendants = true
button.Active = false -- This is essential for mobile scrollers to work when dragging
icon.deselected:Connect(function()
button.ClipsDescendants = true
task.delay(0.2, function()
if icon.isSelected then
button.ClipsDescendants = false
end
end)
end)
-- Account for PreferredTransparency which can be set by every player
local GuiService = game:GetService("GuiService")
icon:setBehaviour("IconButton", "BackgroundTransparency", function(value)
local preference = GuiService.PreferredTransparency
local newValue = value * preference
if value == 1 then
return value
end
return newValue
end)
icon.janitor:add(GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(function()
icon:refreshAppearance(button, "BackgroundTransparency")
end))
local iconCorner = Instance.new("UICorner")
iconCorner:SetAttribute("Collective", "IconCorners")
iconCorner.Name = "UICorner"
iconCorner.Parent = button
local menu = require(script.Parent.Menu)(icon)
local menuUIListLayout = menu.MenuUIListLayout
local menuGap = menu.MenuGap
menu.Parent = button
local iconSpot = Instance.new("Frame")
iconSpot.Name = "IconSpot"
iconSpot.BackgroundColor3 = Color3.fromRGB(225, 225, 225)
iconSpot.BackgroundTransparency = 0.9
iconSpot.Visible = true
iconSpot.AnchorPoint = Vector2.new(0, 0.5)
iconSpot.ZIndex = 5
iconSpot.Parent = menu
local iconSpotCorner = iconCorner:Clone()
iconSpotCorner.Parent = iconSpot
local overlay = iconSpot:Clone()
overlay.UICorner.Name = "OverlayUICorner"
overlay.Name = "IconOverlay"
overlay.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
overlay.ZIndex = iconSpot.ZIndex + 1
overlay.Size = UDim2.new(1, 0, 1, 0)
overlay.Position = UDim2.new(0, 0, 0, 0)
overlay.AnchorPoint = Vector2.new(0, 0)
overlay.Visible = false
overlay.Parent = iconSpot
local clickRegion = Instance.new("TextButton")
clickRegion:SetAttribute("CorrespondingIconUID", icon.UID)
clickRegion.Name = "ClickRegion"
clickRegion.BackgroundTransparency = 1
clickRegion.Visible = true
clickRegion.Text = ""
clickRegion.ZIndex = 20
clickRegion.Selectable = true
clickRegion.SelectionGroup = true
clickRegion.Parent = iconSpot
local Gamepad = require(script.Parent.Parent.Features.Gamepad)
Gamepad.registerButton(clickRegion)
local clickRegionCorner = iconCorner:Clone()
clickRegionCorner.Parent = clickRegion
local contents = Instance.new("Frame")
contents.Name = "Contents"
contents.BackgroundTransparency = 1
contents.Size = UDim2.fromScale(1, 1)
contents.Parent = iconSpot
local contentsList = Instance.new("UIListLayout")
contentsList.Name = "ContentsList"
contentsList.FillDirection = Enum.FillDirection.Horizontal
contentsList.VerticalAlignment = Enum.VerticalAlignment.Center
contentsList.SortOrder = Enum.SortOrder.LayoutOrder
contentsList.VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly
contentsList.Padding = UDim.new(0, 3)
contentsList.Parent = contents
local paddingLeft = Instance.new("Frame")
paddingLeft.Name = "PaddingLeft"
paddingLeft.LayoutOrder = 1
paddingLeft.ZIndex = 5
paddingLeft.BorderColor3 = Color3.fromRGB(0, 0, 0)
paddingLeft.BackgroundTransparency = 1
paddingLeft.BorderSizePixel = 0
paddingLeft.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
paddingLeft.Parent = contents
local paddingCenter = Instance.new("Frame")
paddingCenter.Name = "PaddingCenter"
paddingCenter.LayoutOrder = 3
paddingCenter.ZIndex = 5
paddingCenter.Size = UDim2.new(0, 0, 1, 0)
paddingCenter.BorderColor3 = Color3.fromRGB(0, 0, 0)
paddingCenter.BackgroundTransparency = 1
paddingCenter.BorderSizePixel = 0
paddingCenter.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
paddingCenter.Parent = contents
local paddingRight = Instance.new("Frame")
paddingRight.Name = "PaddingRight"
paddingRight.LayoutOrder = 5
paddingRight.ZIndex = 5
paddingRight.BorderColor3 = Color3.fromRGB(0, 0, 0)
paddingRight.BackgroundTransparency = 1
paddingRight.BorderSizePixel = 0
paddingRight.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
paddingRight.Parent = contents
local iconLabelContainer = Instance.new("Frame")
iconLabelContainer.Name = "IconLabelContainer"
iconLabelContainer.LayoutOrder = 4
iconLabelContainer.ZIndex = 3
iconLabelContainer.AnchorPoint = Vector2.new(0, 0.5)
iconLabelContainer.Size = UDim2.new(0, 0, 0.5, 0)
iconLabelContainer.BackgroundTransparency = 1
iconLabelContainer.Position = UDim2.new(0.5, 0, 0.5, 0)
iconLabelContainer.Parent = contents
local iconLabel = Instance.new("TextLabel")
local viewportX = workspace.CurrentCamera.ViewportSize.X+200
iconLabel.Name = "IconLabel"
iconLabel.LayoutOrder = 4
iconLabel.ZIndex = 15
iconLabel.AnchorPoint = Vector2.new(0, 0)
iconLabel.Size = UDim2.new(0, viewportX, 1, 0)
iconLabel.ClipsDescendants = false
iconLabel.BackgroundTransparency = 1
iconLabel.Position = UDim2.fromScale(0, 0)
iconLabel.RichText = true
iconLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
iconLabel.TextXAlignment = Enum.TextXAlignment.Left
iconLabel.Text = ""
iconLabel.TextWrapped = true
iconLabel.TextWrap = true
iconLabel.TextScaled = false
iconLabel.Active = false
iconLabel.AutoLocalize = true
iconLabel.Parent = iconLabelContainer
local iconImage = Instance.new("ImageLabel")
iconImage.Name = "IconImage"
iconImage.LayoutOrder = 2
iconImage.ZIndex = 15
iconImage.AnchorPoint = Vector2.new(0, 0.5)
iconImage.Size = UDim2.new(0, 0, 0.5, 0)
iconImage.BackgroundTransparency = 1
iconImage.Position = UDim2.new(0, 11, 0.5, 0)
iconImage.ScaleType = Enum.ScaleType.Stretch
iconImage.Active = false
iconImage.Parent = contents
local iconImageCorner = iconCorner:Clone()
iconImageCorner:SetAttribute("Collective", nil)
iconImageCorner.CornerRadius = UDim.new(0, 0)
iconImageCorner.Name = "IconImageCorner"
iconImageCorner.Parent = iconImage
local TweenService = game:GetService("TweenService")
local resizingCount = 0
local function handleLabelAndImageChangesUnstaggered(forceUpdateString)
-- We defer changes by a frame to eliminate all but 1 requests which
-- could otherwise stack up to 20+ requests in a single frame
-- We then repeat again once to account for any final changes
-- Deferring is also essential because properties are set immediately
-- afterwards (therefore calculations will use the correct values)
task.defer(function()
local indicator = icon.indicator
local usingIndicator = indicator and indicator.Visible
local usingText = usingIndicator or iconLabel.Text ~= ""
local usingImage = iconImage.Image ~= "" and iconImage.Image ~= nil
local _alignment = Enum.HorizontalAlignment.Center
local NORMAL_BUTTON_SIZE = UDim2.fromScale(1, 1)
local buttonSize = NORMAL_BUTTON_SIZE
if usingImage and not usingText then
iconLabelContainer.Visible = false
iconImage.Visible = true
paddingLeft.Visible = false
paddingCenter.Visible = false
paddingRight.Visible = false
elseif not usingImage and usingText then
iconLabelContainer.Visible = true
iconImage.Visible = false
paddingLeft.Visible = true
paddingCenter.Visible = false
paddingRight.Visible = true
elseif usingImage and usingText then
iconLabelContainer.Visible = true
iconImage.Visible = true
paddingLeft.Visible = true
paddingCenter.Visible = not usingIndicator
paddingRight.Visible = not usingIndicator
_alignment = Enum.HorizontalAlignment.Left
end
button.Size = buttonSize
local function getItemWidth(item)
local targetWidth = item:GetAttribute("TargetWidth") or item.AbsoluteSize.X
return targetWidth
end
local contentsPadding = contentsList.Padding.Offset
local initialWidgetWidth = contentsPadding --0
local textWidth = iconLabel.TextBounds.X
iconLabelContainer.Size = UDim2.new(0, textWidth, iconLabel.Size.Y.Scale, 0)
for _, child in pairs(contents:GetChildren()) do
if child:IsA("GuiObject") and child.Visible == true then
local itemWidth = getItemWidth(child)
initialWidgetWidth += itemWidth + contentsPadding
end
end
local widgetMinimumWidth = widget:GetAttribute("MinimumWidth")
local widgetMinimumHeight = widget:GetAttribute("MinimumHeight")
local widgetBorderSize = widget:GetAttribute("BorderSize")
local widgetWidth = math.clamp(initialWidgetWidth, widgetMinimumWidth, viewportX)
local menuIcons = icon.menuIcons
local additionalWidth = 0
local hasMenu = #menuIcons > 0
local showMenu = hasMenu and icon.isSelected
if showMenu then
for _, frame in pairs(menu:GetChildren()) do
if frame ~= iconSpot and frame:IsA("GuiObject") and frame.Visible then
additionalWidth += getItemWidth(frame) + menuUIListLayout.Padding.Offset
end
end
if not iconSpot.Visible then
widgetWidth -= (getItemWidth(iconSpot) + menuUIListLayout.Padding.Offset*2 + widgetBorderSize)
end
additionalWidth -= (widgetBorderSize*0.5)
widgetWidth += additionalWidth - (widgetBorderSize*0.75)
end
menuGap.Visible = showMenu and iconSpot.Visible
local desiredWidth = widget:GetAttribute("DesiredWidth")
if desiredWidth and widgetWidth < desiredWidth then
widgetWidth = desiredWidth
end
icon.updateMenu:Fire()
local preWidth = math.max(widgetWidth-additionalWidth, widgetMinimumWidth)
local spotWidth = preWidth-(widgetBorderSize*2)
local menuWidth = menu:GetAttribute("MenuWidth")
local totalMenuWidth = menuWidth and menuWidth + spotWidth + menuUIListLayout.Padding.Offset + 10
if totalMenuWidth then
local maxWidth = menu:GetAttribute("MaxWidth")
if maxWidth then
totalMenuWidth = math.max(maxWidth, widgetMinimumWidth)
end
menu:SetAttribute("MenuCanvasWidth", widgetWidth)
if totalMenuWidth < widgetWidth then
widgetWidth = totalMenuWidth
end
end
local style = Enum.EasingStyle.Quint
local direction = Enum.EasingDirection.Out
local spotWidthMax = math.max(spotWidth, getItemWidth(iconSpot), iconSpot.AbsoluteSize.X)
local widgetWidthMax = math.max(widgetWidth, getItemWidth(widget), widget.AbsoluteSize.X)
local SPEED = 750
local spotTweenInfo = TweenInfo.new(spotWidthMax/SPEED, style, direction)
local widgetTweenInfo = TweenInfo.new(widgetWidthMax/SPEED, style, direction)
TweenService:Create(iconSpot, spotTweenInfo, {
Position = UDim2.new(0, widgetBorderSize, 0.5, 0),
Size = UDim2.new(0, spotWidth, 1, -widgetBorderSize*2),
}):Play()
TweenService:Create(clickRegion, spotTweenInfo, {
Size = UDim2.new(0, spotWidth, 1, 0),
}):Play()
local newWidgetSize = UDim2.fromOffset(widgetWidth, widgetMinimumHeight)
local updateInstantly = widget.Size.Y.Offset ~= widgetMinimumHeight
if updateInstantly then
widget.Size = newWidgetSize
end
widget:SetAttribute("TargetWidth", newWidgetSize.X.Offset)
local movingTween = TweenService:Create(widget, widgetTweenInfo, {
Size = newWidgetSize,
})
movingTween:Play()
resizingCount += 1
for i = 1, widgetTweenInfo.Time * 100 do
task.delay(i/100, function()
Icon.iconChanged:Fire(icon)
end)
end
task.delay(widgetTweenInfo.Time-0.2, function()
resizingCount -= 1
task.defer(function()
if resizingCount == 0 then
icon.resizingComplete:Fire()
end
end)
end)
icon:updateParent()
end)
end
local Utility = require(script.Parent.Parent.Utility)
local handleLabelAndImageChanges = Utility.createStagger(0.01, handleLabelAndImageChangesUnstaggered)
local firstTimeSettingFontFace = true
icon:setBehaviour("IconLabel", "Text", handleLabelAndImageChanges)
icon:setBehaviour("IconLabel", "FontFace", function(value)
local previousFontFace = iconLabel.FontFace
if previousFontFace == value then
return
end
task.spawn(function()
--[[
local fontLink = value.Family
if string.match(fontLink, "rbxassetid://") then
local ContentProvider = game:GetService("ContentProvider")
local assets = {fontLink}
ContentProvider:PreloadAsync(assets)
end--]]
-- Afaik there's no way to determine when a Font Family has
-- loaded (even with ContentProvider), so we just have to try
-- a few times and hope it loads within the refresh period
handleLabelAndImageChanges()
if firstTimeSettingFontFace then
firstTimeSettingFontFace = false
for i = 1, 10 do
task.wait(1)
handleLabelAndImageChanges()
end
end
end)
end)
local function updateBorderSize()
task.defer(function()
local borderOffset = widget:GetAttribute("BorderSize")
local alignment = icon.alignment
local alignmentOffset = (iconSpot.Visible == false and 0) or (alignment == "Right" and -borderOffset) or borderOffset
menu.Position = UDim2.new(0, alignmentOffset, 0, 0)
menuGap.Size = UDim2.fromOffset(borderOffset, 0)
menuUIListLayout.Padding = UDim.new(0, 0)
handleLabelAndImageChanges()
end)
end
icon:setBehaviour("Widget", "BorderSize", updateBorderSize)
icon:setBehaviour("IconSpot", "Visible", updateBorderSize)
icon.startMenuUpdate:Connect(handleLabelAndImageChanges)
icon.updateSize:Connect(handleLabelAndImageChanges)
icon:setBehaviour("ContentsList", "HorizontalAlignment", handleLabelAndImageChanges)
icon:setBehaviour("Widget", "Visible", handleLabelAndImageChanges)
icon:setBehaviour("Widget", "DesiredWidth", handleLabelAndImageChanges)
icon:setBehaviour("Widget", "MinimumWidth", handleLabelAndImageChanges)
icon:setBehaviour("Widget", "MinimumHeight", handleLabelAndImageChanges)
icon:setBehaviour("Indicator", "Visible", handleLabelAndImageChanges)
icon:setBehaviour("IconImageRatio", "AspectRatio", handleLabelAndImageChanges)
icon:setBehaviour("IconImage", "Image", function(value)
local textureId = (tonumber(value) and "http://www.roblox.com/asset/?id="..value) or value or ""
if iconImage.Image ~= textureId then
handleLabelAndImageChanges()
end
return textureId
end)
icon.alignmentChanged:Connect(function(newAlignment)
if newAlignment == "Center" then
newAlignment = "Left"
end
menuUIListLayout.HorizontalAlignment = Enum.HorizontalAlignment[newAlignment]
updateBorderSize()
end)
-- Localization support (refresh icon size whenever player changes language changes in-game)
local Players = game:GetService("Players")
local localPlayer = Players.LocalPlayer
local lastLocaleId = localPlayer.LocaleId
icon.janitor:add(localPlayer:GetPropertyChangedSignal("LocaleId"):Connect(function()
task.delay(0.2, function()
local newLocaleId = localPlayer.LocaleId
if newLocaleId ~= lastLocaleId then
lastLocaleId = newLocaleId
icon:refresh()
task.wait(0.5)
icon:refresh()
end
end)
end))
local iconImageScale = Instance.new("NumberValue")
iconImageScale.Name = "IconImageScale"
iconImageScale.Parent = iconImage
iconImageScale:GetPropertyChangedSignal("Value"):Connect(function()
iconImage.Size = UDim2.new(iconImageScale.Value, 0, iconImageScale.Value, 0)
end)
local UIAspectRatioConstraint = Instance.new("UIAspectRatioConstraint")
UIAspectRatioConstraint.Name = "IconImageRatio"
UIAspectRatioConstraint.AspectType = Enum.AspectType.FitWithinMaxSize
UIAspectRatioConstraint.DominantAxis = Enum.DominantAxis.Height
UIAspectRatioConstraint.Parent = iconImage
local iconGradient = Instance.new("UIGradient")
iconGradient.Name = "IconGradient"
iconGradient.Enabled = true
iconGradient.Parent = button
local iconSpotGradient = Instance.new("UIGradient")
iconSpotGradient.Name = "IconSpotGradient"
iconSpotGradient.Enabled = true
iconSpotGradient.Parent = iconSpot
return widget
end
================================================
FILE: src/Features/Gamepad.lua
================================================
-- As the name suggests, this handles everything related to gamepads
-- (i.e. Xbox or Playstation controllers) and their navigation
-- I created a separate module for gamepads (and not touchpads or
-- keyboards) because gamepads are greatly more unqiue and require
-- additional tailored programming
-- SERVICES
local GamepadService = game:GetService("GamepadService")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
-- LOCAL
local DEFAULT_HIGHLIGHT_KEY = Enum.KeyCode.DPadUp -- The default key to highlight the topbar icon
local GAMEPAD_INPUT = Enum.PreferredInput.Gamepad
local Gamepad = {}
local Icon
-- FUNCTIONS
-- This is called upon the Icon initializing
function Gamepad.start(incomingIcon)
-- Public variables
Icon = incomingIcon
Icon.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)
Icon.highlightIcon = false -- Change to a specific icon if you'd like to highlight a specific icon instead of the left-most
-- We defer so the developer can make changes before the
-- gamepad controls are initialized
task.delay(1, function()
-- Some local utility
local iconsDict = Icon.iconsDictionary
local function getIconFromSelectedObject()
local clickRegion = GuiService.SelectedObject
local iconUID = clickRegion and clickRegion:GetAttribute("CorrespondingIconUID")
local icon = iconUID and iconsDict[iconUID]
return icon
end
-- This enables users to instantly open up their last selected icon
local previousHighlightedIcon
local usedIndicatorOnce = DEFAULT_HIGHLIGHT_KEY ~= Icon.highlightKey
local usedBOnce = DEFAULT_HIGHLIGHT_KEY ~= Icon.highlightKey
local Selection = require(script.Parent.Parent.Elements.Selection)
local function updateSelectedObject()
local icon = getIconFromSelectedObject()
local isUsingGamepad = UserInputService.PreferredInput == GAMEPAD_INPUT
if icon then
if isUsingGamepad then
local clickRegion = icon:getInstance("ClickRegion")
local selection = icon.selection
if not selection then
selection = icon.janitor:add(Selection(Icon))
selection:SetAttribute("IgnoreVisibilityUpdater", true)
selection.Parent = icon.widget
icon.selection = selection
icon:refreshAppearance(selection) --icon:clipOutside(selection)
end
clickRegion.SelectionImageObject = selection.Selection
end
if previousHighlightedIcon and previousHighlightedIcon ~= icon then
previousHighlightedIcon:setIndicator()
end
local newIndicator = if isUsingGamepad and not usedBOnce and not icon.parentIconUID then Enum.KeyCode.ButtonB else nil
previousHighlightedIcon = icon
Icon.lastHighlightedIcon = icon
icon:setIndicator(newIndicator)
else
local newIndicator = if isUsingGamepad and not usedIndicatorOnce then Icon.highlightKey else nil
if not previousHighlightedIcon then
previousHighlightedIcon = Gamepad.getIconToHighlight()
end
if newIndicator == Icon.highlightKey then
-- We only display the highlightKey once to show
-- the user how to highlight the topbar icon
usedIndicatorOnce = true
else
--usedBOnce = true
end
if previousHighlightedIcon then
previousHighlightedIcon:setIndicator(newIndicator)
end
end
end
GuiService:GetPropertyChangedSignal("SelectedObject"):Connect(updateSelectedObject)
-- This listens for a gamepad being present/added/removed
local function preferredInputChanged()
local preferredInput = UserInputService.PreferredInput
local isUsingGamepad = preferredInput == GAMEPAD_INPUT
if not isUsingGamepad then
usedIndicatorOnce = false
usedBOnce = false
end
updateSelectedObject()
end
UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(preferredInputChanged)
preferredInputChanged()
-- This allows for easy highlighting of the topbar when the
-- when ``Icon.highlightKey`` (i.e. DPadUp) is pressed.
-- If you'd like to disable, do ``Icon.highlightKey = false``
UserInputService.InputBegan:Connect(function(input, touchingAnObject)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
-- Sometimes the Roblox gamepad glitches when combined with a cursor
-- This fixes that by unhighlighting if the cursor is pressed down
-- (i.e. a mouse click)
local icon = getIconFromSelectedObject()
if icon then
GuiService.SelectedObject = nil
end
return
end
if input.KeyCode ~= Icon.highlightKey then
return
end
local iconToHighlight = Gamepad.getIconToHighlight()
if iconToHighlight then
if GamepadService.GamepadCursorEnabled then
task.wait(0.2)
GamepadService:DisableGamepadCursor()
end
local clickRegion = iconToHighlight:getInstance("ClickRegion")
GuiService.SelectedObject = clickRegion
end
end)
end)
end
function Gamepad.getIconToHighlight()
-- If an icon has already been selected, returns the last selected icon
-- Else if more than 0 icons, it selects the left-most icon
local iconsDict = Icon.iconsDictionary
local iconToHighlight = Icon.highlightIcon or Icon.lastHighlightedIcon
if not iconToHighlight then
local currentX
for _, icon in pairs(iconsDict) do
if icon.parentIconUID then
continue
end
local thisX = icon.widget.AbsolutePosition.X
if not currentX or thisX < currentX then
iconToHighlight = icon
currentX = iconToHighlight.widget.AbsolutePosition.X
end
end
end
return iconToHighlight
end
-- This called when the icon's ClickRegion is created
function Gamepad.registerButton(buttonInstance)
-- This provides a basic level of support for controllers by making
-- the icons easy to highlight via the virtual cursor, then
-- when selected, focuses in on the selected icon and hops
-- between other nearby icons simply by toggling the joystick
local inputBegan = false
buttonInstance.InputBegan:Connect(function(input)
-- Two wait frames required to ensure inputBegan is detected within
-- UserInputService.InputBegan. We do this because object.InputBegan
-- does not return the correct input objects (unlike the service)
inputBegan = true
task.wait()
task.wait()
inputBegan = false
end)
local connection = UserInputService.InputBegan:Connect(function(input)
task.wait()
if input.KeyCode == Enum.KeyCode.ButtonA and inputBegan then
-- We focus on an icon when selected via the virtual cursor
task.wait(0.2)
GamepadService:DisableGamepadCursor()
GuiService.SelectedObject = buttonInstance
return
end
local isSelected = GuiService.SelectedObject == buttonInstance
local unselectKeyCodes = {"ButtonB", "ButtonSelect"}
local keyName = input.KeyCode.Name
if table.find(unselectKeyCodes, keyName) and isSelected then
-- We unfocus when back button is pressed, but ignore
-- if the virtual cursor is disabled otherwise it will be
-- impossible to select the topbar
if not(keyName == "ButtonSelect" and not GamepadService.GamepadCursorEnabled) then
GuiService.SelectedObject = nil
end
end
end)
buttonInstance.Destroying:Once(function()
connection:Disconnect()
end)
end
return Gamepad
================================================
FILE: src/Features/Overflow.lua
================================================
-- When designing your game for many devices and screen sizes, icons may occasionally
-- particularly for smaller devices like phones, overlap with other icons or the bounds
-- of the screen. The overflow handler solves this challenge by moving the out-of-bounds
-- icon into an overflow menu (with a limited scrolling canvas) preventing overlaps occuring
-- LOCAL
local Overflow = {}
local holders = {}
local orderedAvailableIcons = {}
local iconsDict
local currentCamera = workspace.CurrentCamera
local overflowIcons = {}
local overflowIconUIDs = {}
local Utility = require(script.Parent.Parent.Utility)
local beginCheckingCenterIcons = false
local beganSecondaryCenterCheck = false
local Icon
-- FUNCTIONS
-- This is called upon the Icon initializing
function Overflow.start(incomingIcon)
Icon = incomingIcon
iconsDict = Icon.iconsDictionary
local primaryScreenGui
for _, screenGui in pairs(Icon.container) do
if primaryScreenGui == nil and screenGui.ScreenInsets == Enum.ScreenInsets.TopbarSafeInsets then
primaryScreenGui = screenGui
end
for _, holder in pairs(screenGui.Holders:GetChildren()) do
if holder:GetAttribute("IsAHolder") then
holders[holder.Name] = holder
end
end
end
-- We listen for changes in icons (such as them being added, removed,
-- the setting of a different alignment, the widget size changing, etc)
local beginOverflow = false
local updateBoundaries = Utility.createStagger(0.1, function(ignoreAvailable)
if not beginOverflow then
return
end
if not ignoreAvailable then
Overflow.updateAvailableIcons("Center")
end
Overflow.updateBoundary("Left")
Overflow.updateBoundary("Right")
end)
task.delay(0.5, function()
beginOverflow = true
updateBoundaries()
end)
task.delay(2, function()
-- This is essential to prevent central icons begin added
-- left or right due to incomplete UIListLayout calculations
-- within the first few frames
beginCheckingCenterIcons = true
updateBoundaries()
end)
Icon.iconAdded:Connect(updateBoundaries)
Icon.iconRemoved:Connect(updateBoundaries)
Icon.iconChanged:Connect(updateBoundaries)
currentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
updateBoundaries(true)
end)
primaryScreenGui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
updateBoundaries(true)
end)
end
function Overflow.getWidth(icon, getMaxWidth)
local widget = icon.widget
return widget:GetAttribute("TargetWidth") or widget.AbsoluteSize.X
end
function Overflow.getAvailableIcons(alignment)
local ourOrderedIcons = orderedAvailableIcons[alignment]
if not ourOrderedIcons then
ourOrderedIcons = Overflow.updateAvailableIcons(alignment)
end
return ourOrderedIcons
end
function Overflow.updateAvailableIcons(alignment)
-- We only track items that are directly on the topbar (i.e. not within a parent icon)
local ourTotal = 0
local ourOrderedIcons = {}
for _, icon in pairs(iconsDict) do
local parentUID = icon.parentIconUID
local isDirectlyOnTopbar = not parentUID or overflowIconUIDs[parentUID]
local isOverflow = overflowIconUIDs[icon.UID]
if isDirectlyOnTopbar and icon.alignment == alignment and not isOverflow and icon.isEnabled then
table.insert(ourOrderedIcons, icon)
ourTotal += 1
end
end
-- Ignore if no icons are available
if ourTotal <= 0 then
return {}
end
-- This sorts these icons by smallest order, or if equal, left-most position
-- (even for the right alignment because all icons are sorted left-to-right)
table.sort(ourOrderedIcons, function(iconA, iconB)
local orderA = iconA.widget.LayoutOrder
local orderB = iconB.widget.LayoutOrder
local hasParentA = iconA.parentIconUID
local hasParentB = iconB.parentIconUID
if hasParentA == hasParentB then
if orderA < orderB then
return true
end
if orderA > orderB then
return false
end
return iconA.widget.AbsolutePosition.X < iconB.widget.AbsolutePosition.X
elseif hasParentB then
return false
elseif hasParentA then
return true
end
return nil
end)
-- Finish up
orderedAvailableIcons[alignment] = ourOrderedIcons
return ourOrderedIcons
end
function Overflow.getRealXPositions(alignment, orderedIcons)
-- We calculate the the absolute position of icons instead of reading
-- directly to determine where they would be if not within an overflow
local isLeft = alignment == "Left"
local holder = holders[alignment]
local holderXPos = holder.AbsolutePosition.X
local holderXSize = holder.AbsoluteSize.X
local holderUIList = holder.UIListLayout
local topbarInset = holderUIList.Padding.Offset
local absoluteX = (isLeft and holderXPos) or holderXPos + holderXSize
local realXPositions = {}
if isLeft then
Utility.reverseTable(orderedIcons)
end
for i = #orderedIcons, 1, -1 do
local icon = orderedIcons[i]
local sizeX = Overflow.getWidth(icon)
if not isLeft then
absoluteX -= sizeX
end
realXPositions[icon.UID] = absoluteX
if isLeft then
absoluteX += sizeX
end
absoluteX += (isLeft and topbarInset) or -topbarInset
end
return realXPositions
end
function Overflow.updateBoundary(alignment)
-- We only track items that are directly on the topbar (i.e. not within a parent icon) or within an overflow
local holder = holders[alignment]
local holderUIList = holder.UIListLayout
local holderXPos = holder.AbsolutePosition.X
local holderXSize = holder.AbsoluteSize.X
local topbarInset = holderUIList.Padding.Offset
local topbarPadding = holderUIList.Padding.Offset
local BOUNDARY_GAP = topbarInset
local ourOrderedIcons = Overflow.updateAvailableIcons(alignment)
local boundWidth = 0
local ourTotal = 0
for _, icon in pairs(ourOrderedIcons) do
boundWidth += Overflow.getWidth(icon) + topbarPadding
ourTotal += 1
end
if ourTotal <= 0 then
return
end
-- These are the icons with menus which icons will be moved into
-- when overflowing
local isCentral = alignment == "Center"
local isLeft = alignment == "Left"
local isRight = not isLeft
local overflowIcon = overflowIcons[alignment]
if not overflowIcon and not isCentral and #ourOrderedIcons > 0 then
local order = (isLeft and -9999999) or 9999999
overflowIcon = Icon.new()--:setLabel(`{alignment}`)
overflowIcon:setImage(6069276526, "Deselected")
overflowIcon:setName("Overflow"..alignment)
overflowIcon:setOrder(order)
overflowIcon:setAlignment(alignment)
overflowIcon:autoDeselect(false)
overflowIcon.isAnOverflow = true
--overflowIcon:freezeMenu()
overflowIcon:select("OverflowStart", overflowIcon)
overflowIcon:setEnabled(false)
overflowIcons[alignment] = overflowIcon
overflowIconUIDs[overflowIcon.UID] = true
if not Icon.closeableOverflowMenus then
local iconSpot = overflowIcon:getInstance("IconSpot")
iconSpot.Visible = false
end
end
-- The default boundary is the point where both the left-most-right-icon
-- and left-most-right-icon meet OR the opposite side of the screen
local oppositeAlignment = (alignment == "Left" and "Right") or "Left"
local oppositeOrderedIcons = Overflow.updateAvailableIcons(oppositeAlignment)
local nearestOppositeIcon = (isLeft and oppositeOrderedIcons[1]) or (isRight and oppositeOrderedIcons[#oppositeOrderedIcons])
local oppositeOverflowIcon = overflowIcons[oppositeAlignment]
local boundary = (isLeft and holderXPos + holderXSize) or holderXPos
if nearestOppositeIcon then
local oppositeRealXPositions = Overflow.getRealXPositions(oppositeAlignment, oppositeOrderedIcons)
local oppositeX = oppositeRealXPositions[nearestOppositeIcon.UID]
local oppositeXSize = Overflow.getWidth(nearestOppositeIcon)
boundary = (isLeft and oppositeX - BOUNDARY_GAP) or oppositeX + oppositeXSize + BOUNDARY_GAP
end
-- We get the left-most icon (if left alignment) or right-most-icon (if
-- right alignment) of the central icons group to see if we need to change
-- the boundary (if the central icon boundary is smaller than the alignment
-- boundary then we use the central)
local totalChecks = 0
local usingNearestCenter = false
local function checkToShiftCentralIcon()
local centerOrderedIcons = Overflow.getAvailableIcons("Center")
local centerPos = (isLeft and 1) or #centerOrderedIcons
local nearestCenterIcon = centerOrderedIcons[centerPos]
local function secondaryCheck()
if not beganSecondaryCenterCheck then
beganSecondaryCenterCheck = true
task.delay(3, Overflow.updateBoundary, alignment)
end
end
if nearestCenterIcon and not nearestCenterIcon.hasRelocatedInOverflow then
local ourNearestIcon = (isLeft and ourOrderedIcons[#ourOrderedIcons]) or (isRight and ourOrderedIcons[1])
local centralNearestXPos = nearestCenterIcon.widget.AbsolutePosition.X
local ourNearestXPos = ourNearestIcon.widget.AbsolutePosition.X
local ourNearestXSize = Overflow.getWidth(ourNearestIcon)
local centerBoundary = (isLeft and centralNearestXPos-BOUNDARY_GAP) or centralNearestXPos + Overflow.getWidth(nearestCenterIcon) + BOUNDARY_GAP
local removeBoundary = (isLeft and ourNearestXPos + ourNearestXSize) or ourNearestXPos
local hasShifted = false
if isLeft then
if centerBoundary < removeBoundary then
if not beginCheckingCenterIcons then
secondaryCheck()
return
end
nearestCenterIcon:align("Left")
nearestCenterIcon.hasRelocatedInOverflow = true
hasShifted = true
end
elseif isRight then
if centerBoundary > removeBoundary then
if not beginCheckingCenterIcons or removeBoundary < 0 then
secondaryCheck()
return
end
nearestCenterIcon:align("Right")
nearestCenterIcon.hasRelocatedInOverflow = true
hasShifted = true
end
end
if hasShifted then
totalChecks += 1
if totalChecks <= 4 then
Overflow.updateAvailableIcons("Center")
checkToShiftCentralIcon()
end
end
end
end
checkToShiftCentralIcon()
--[[
This updates the maximum size of the overflow menus
The menu determines its bounds from the smallest of either:
1. The closest center-aligned icon (i.e. the boundary)
2. The edge of the opposite overflow menu UNLESS...
3. ... the edge exceeds more than half the screenGui
--]]
if overflowIcon then
local menuBoundary = boundary
local menu = overflowIcon:getInstance("Menu")
local holderXEndPos = holderXPos + holderXSize
local menuWidth = holderXSize
if menu and oppositeOverflowIcon then
local oppositeWidget = oppositeOverflowIcon.widget
local oppositeXPos = oppositeWidget.AbsolutePosition.X
local oppositeXSize = Overflow.getWidth(oppositeOverflowIcon)
local oppositeBoundary = (isLeft and oppositeXPos - BOUNDARY_GAP) or oppositeXPos + oppositeXSize + BOUNDARY_GAP
local oppositeMenu = oppositeOverflowIcon:getInstance("Menu")
local isDominant = menu.AbsoluteCanvasSize.X >= oppositeMenu.AbsoluteCanvasSize.X
if not usingNearestCenter then
local halfwayXPos = holderXPos + holderXSize/2
local halfwayBoundary = (isLeft and halfwayXPos - BOUNDARY_GAP/2) or halfwayXPos + BOUNDARY_GAP/2
menuBoundary = halfwayBoundary
if isDominant then
menuBoundary = oppositeBoundary
end
end
menuWidth = (isLeft and menuBoundary - holderXPos) or (holderXEndPos - menuBoundary)
end
local currentMaxWidth = menu and menu:GetAttribute("MaxWidth")
menuWidth = Utility.round(menuWidth)
if menu and currentMaxWidth ~= menuWidth then
menu:SetAttribute("MaxWidth", menuWidth)
end
end
-- Parent ALL icons of that alignment into the overflow if at least on
-- sibling exceeds the bounds.
-- We calculate the the absolute position of icons instead of reading
-- directly to determine where they would be if not within an overflow
local joinOverflow = false
local realXPositions = Overflow.getRealXPositions(alignment, ourOrderedIcons)
for i = #ourOrderedIcons, 1, -1 do
local icon = ourOrderedIcons[i]
local widgetX = Overflow.getWidth(icon)
local xPos = realXPositions[icon.UID]
if (isLeft and xPos + widgetX >= boundary) or (isRight and xPos <= boundary) then
joinOverflow = true
end
end
for i = #ourOrderedIcons, 1, -1 do
local icon = ourOrderedIcons[i]
local isOverflow = overflowIconUIDs[icon.UID]
if not isOverflow then
if joinOverflow and not icon.parentIconUID then
icon:joinMenu(overflowIcon)
elseif not joinOverflow and icon.parentIconUID then
icon:leave()
end
end
end
-- Hide the overflows when not in use
if overflowIcon.isEnabled ~= joinOverflow then
overflowIcon:setEnabled(joinOverflow)
end
-- Have the menus auto selected
if overflowIcon.isEnabled and not overflowIcon.overflowAlreadyOpened then
overflowIcon.overflowAlreadyOpened = true
overflowIcon:select()
end
end
return Overflow
================================================
FILE: src/Features/Themes/Classic.lua
================================================
-- This is to provide backwards compatability with the old Roblox
-- topbar while experiences transition over to the new topbar
-- You don't need to apply this yourself, topbarplus automatically
-- applies it if the old roblox topbar is detected
return {
{"Selection", "Size", UDim2.new(1, -6, 1, -5)},
{"Selection", "Position", UDim2.new(0, 3, 0, 3)},
{"Widget", "MinimumWidth", 32, "Deselected"},
{"Widget", "MinimumHeight", 32, "Deselected"},
{"Widget", "BorderSize", 0, "Deselected"},
{"IconCorners", "CornerRadius", UDim.new(0, 9), "Deselected"},
{"IconButton", "BackgroundTransparency", 0.5, "Deselected"},
{"IconLabel", "TextSize", 14, "Deselected"},
{"Dropdown", "BackgroundTransparency", 0.5, "Deselected"},
{"Notice", "Position", UDim2.new(1, -12, 0, -3), "Deselected"},
{"Notice", "Size", UDim2.new(0, 15, 0, 15), "Deselected"},
{"NoticeLabel", "TextSize", 11, "Deselected"},
{"IconSpot", "BackgroundColor3", Color3.fromRGB(0, 0, 0), "Selected"},
{"IconSpot", "BackgroundTransparency", 0.702, "Selected"},
{"IconSpotGradient", "Enabled", false, "Selected"},
{"IconOverlay", "BackgroundTransparency", 0.97, "Selected"},
}
================================================
FILE: src/Features/Themes/Default.lua
================================================
-- Themes in v3 work simply by applying the value (agument[3])
-- to the property (agument[2]) of an instance within the icon which
-- matches the name of argument[1]. Argument[1] can also be used to
-- specify a collection of instances with a corresponding 'collective'
-- value. A colletive is simply an attribute applied to some instances
-- within the icon to group them together (such as "IconCorners").
-- If the property (argument[2]) does not exist within the instance,
-- it will instead be applied as an attribute on the instance:
-- (i.e. ``instance:SetAttribute(argument[2], [argument[3])``)
-- Use argument[4] to specify a state: "Deselected", "Selected"
-- or "Viewing". If argument[4] is empty the state will default
-- to "Deselected".
-- I've designed themes this way so you have full control over
-- the appearance of the widget and its descendants
return {
-- When no state is specified the modification is applied to *all* states (Deselected, Selected and Viewing)
{"IconCorners", "CornerRadius", UDim.new(1, 0)},
{"Selection", "RotationSpeed", 1},
{"Selection", "Size", UDim2.new(1, 0, 1, 1)},
{"Selection", "Position", UDim2.new(0, 0, 0, 0)},
{"SelectionGradient", "Color", ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(86, 86, 86)),
})},
-- When the icon is deselected
{"IconImage", "Image", "", "Deselected"},
{"IconLabel", "Text", "", "Deselected"},
{"IconLabel", "Position", UDim2.fromOffset(0, 0), "Deselected"}, -- 0, -1
{"Widget", "DesiredWidth", 44, "Deselected"},
{"Widget", "MinimumWidth", 44, "Deselected"},
{"Widget", "MinimumHeight", 44, "Deselected"},
{"Widget", "BorderSize", 4, "Deselected"},
{"IconButton", "BackgroundColor3", Color3.fromRGB(18, 18, 21), "Deselected"},
{"IconButton", "BackgroundTransparency", 0.08, "Deselected"},
{"IconImageScale", "Value", 0.5, "Deselected"},
{"IconImageCorner", "CornerRadius", UDim.new(0, 0), "Deselected"},
{"IconImage", "ImageColor3", Color3.fromRGB(255, 255, 255), "Deselected"},
{"IconImage", "ImageTransparency", 0, "Deselected"},
{"IconImageRatio", "AspectRatio", 1, "Deselected"},
{"IconLabel", "FontFace", Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal), "Deselected"},
{"IconLabel", "TextSize", 16, "Deselected"},
{"IconSpot", "BackgroundTransparency", 1, "Deselected"},
{"IconOverlay", "BackgroundTransparency", 0.85, "Deselected"},
{"IconSpotGradient", "Enabled", false, "Deselected"},
{"IconGradient", "Enabled", false, "Deselected"},
{"ClickRegion", "Active", true, "Deselected"}, -- This is set to false within scrollers to ensure scroller can be dragged on mobile
{"Menu", "Active", false, "Deselected"},
{"ContentsList", "HorizontalAlignment", Enum.HorizontalAlignment.Center, "Deselected"},
{"Dropdown", "BackgroundColor3", Color3.fromRGB(18, 18, 21), "Deselected"},
{"Dropdown", "BackgroundTransparency", 0.08, "Deselected"},
{"Dropdown", "MaxIcons", 4.5, "Deselected"},
{"Menu", "MaxIcons", 4, "Deselected"},
{"Notice", "Position", UDim2.new(1, -12, 0, -1), "Deselected"},
{"Notice", "Size", UDim2.new(0, 20, 0, 20), "Deselected"},
{"NoticeLabel", "TextSize", 13, "Deselected"},
{"PaddingLeft", "Size", UDim2.new(0, 9, 1, 0), "Deselected"},
{"PaddingRight", "Size", UDim2.new(0, 11, 1, 0), "Deselected"},
-- When the icon is selected
-- Selected also inherits everything from Deselected if nothing is set
{"IconSpot", "BackgroundTransparency", 0.7, "Selected"},
{"IconSpot", "BackgroundColor3", Color3.fromRGB(255, 255, 255), "Selected"},
{"IconSpotGradient", "Enabled", true, "Selected"},
{"IconSpotGradient", "Rotation", 45, "Selected"},
{"IconSpotGradient", "Color", ColorSequence.new(Color3.fromRGB(96, 98, 100), Color3.fromRGB(77, 78, 80)), "Selected"},
-- When a cursor is hovering above, a controller highlighting, or touchpad (mobile) pressing (but not released)
--{"IconSpot", "BackgroundTransparency", 0.75, "Viewing"},
}
================================================
FILE: src/Features/Themes/init.lua
================================================
-- The functions here are dedicated solely to managing theme state
-- and updating the appearance of instances to match that state.
-- You don't need to use any of these functions, the useful ones
-- have been abstracted as icon methods
-- LOCAL
local Themes = {}
local Utility = require(script.Parent.Parent.Utility)
local baseTheme = require(script.Default)
-- FUNCTIONS
function Themes.getThemeValue(stateGroup, instanceName, property, iconState)
if stateGroup then
for _, detail in pairs(stateGroup) do
local checkingInstanceName, checkingPropertyName, checkingValue = unpack(detail)
if instanceName == checkingInstanceName and property == checkingPropertyName then
return checkingValue
end
end
end
return nil
end
function Themes.getInstanceValue(instance, property)
local success, value = pcall(function()
return instance[property]
end)
if not success then
value = instance:GetAttribute(property)
end
return value
end
function Themes.getRealInstance(instance)
if not instance:GetAttribute("IsAClippedClone") then
return
end
local originalInstance = instance:FindFirstChild("OriginalInstance")
if not originalInstance then
return
end
return originalInstance.Value
end
function Themes.getClippedClone(instance)
if not instance:GetAttribute("HasAClippedClone") then
return
end
local clippedClone = instance:FindFirstChild("ClippedClone")
if not clippedClone then
return
end
return clippedClone.Value
end
function Themes.refresh(icon, instance, specificProperty)
-- Some instances such as notices need immediate refreshing upon creation as
-- they're added in after the initial refresh period
if specificProperty then
local stateGroup = icon:getStateGroup()
local value = Themes.getThemeValue(stateGroup, instance.Name, specificProperty) or Themes.getInstanceValue(instance, specificProperty)
Themes.apply(icon, instance, specificProperty, value, true)
return
end
-- If no property is specified we update all properties that exist within
-- the applied theme appearance
local stateGroup = icon:getStateGroup()
if not stateGroup then
return
end
local validInstances = {[instance.Name] = instance}
for _, child in pairs(instance:GetDescendants()) do
local collective = child:GetAttribute("Collective")
if collective then
validInstances[collective] = child
end
validInstances[child.Name] = child
end
for _, detail in pairs(stateGroup) do
local checkingInstanceName, checkingPropertyName, checkingValue = unpack(detail)
local instanceToUpdate = validInstances[checkingInstanceName]
if instanceToUpdate then
Themes.apply(icon, instanceToUpdate.Name, checkingPropertyName, checkingValue, true)
end
end
return
end
function Themes.apply(icon, collectiveOrInstanceNameOrInstance, property, value, forceApply)
-- This is responsible for **applying** appearance changes to instances within the icon
-- however it IS NOT responsible for updating themes. Use :modifyTheme for that.
-- This also calls callbacks given by :setBehaviour before applying these property changes
-- to the given instances
if icon.isDestroyed then
return
end
local instances
local collectiveOrInstanceName = collectiveOrInstanceNameOrInstance
if typeof(collectiveOrInstanceNameOrInstance) == "Instance" then
instances = {collectiveOrInstanceNameOrInstance}
collectiveOrInstanceName = collectiveOrInstanceNameOrInstance.Name
else
instances = icon:getInstanceOrCollective(collectiveOrInstanceNameOrInstance)
end
local key = collectiveOrInstanceName.."-"..property
local customBehaviour = icon.customBehaviours[key]
for _, instance in pairs(instances) do
local clippedClone = Themes.getClippedClone(instance)
if clippedClone then
-- This means theme effects are applied to both the original
-- instance and its clone (instead of just the instance).
-- This is important for some properties such as position
-- and size which might be dictated by the clone
table.insert(instances, clippedClone)
end
end
for _, instance in pairs(instances) do
if property == "Position" and Themes.getClippedClone(instance) then
-- The clone manages the position of the real instance so ignore
continue
elseif property == "Size" and Themes.getRealInstance(instance) then
-- The real instance manages the size of the clone so ignore
continue
end
local currentValue = Themes.getInstanceValue(instance, property)
if not forceApply and value == currentValue then
continue
end
if customBehaviour then
local newValue = customBehaviour(value, instance, property)
if newValue ~= nil then
value = newValue
end
end
local success = pcall(function()
instance[property] = value
end)
if not success then
-- If property is not a real property, we set
-- the value as an attribute instead. This is useful
-- for instance in :setWidth where we also want to
-- specify a desired width for every state which can
-- then be easily read by the widget element
instance:SetAttribute(property, value)
end
end
end
function Themes.getModifications(modifications)
if typeof(modifications[1]) ~= "table" then
-- This enables users to do :modifyTheme({a,b,c,d})
-- in addition of :modifyTheme({{a,b,c,d}})
modifications = {modifications}
end
return modifications
end
function Themes.merge(detail, modification, callback)
local instanceName, property, value, stateName = table.unpack(modification)
local checkingInstanceName, checkingPropertyName, _, checkingStateName = table.unpack(detail)
if instanceName == checkingInstanceName and property == checkingPropertyName and Themes.statesMatch(stateName, checkingStateName) then
detail[3] = value
if callback then
callback(detail)
end
return true
end
return false
end
function Themes.modify(icon, modifications, modificationsUID)
-- This is what the 'old set' used to do (although for clarity that behaviour has now been
-- split into two methods, .modifyTheme and .apply).
-- modifyTheme is responsible for UPDATING the internal values within a theme for a particular
-- state, then checking to see if the appearance of the icon needs to be updated.
-- If no iconState is specified, the change is applied to both Deselected and Selected
-- A modification can also be 'undone' using :removeModification and passing in
-- the UID returned from this method
task.spawn(function()
modificationsUID = modificationsUID or Utility.generateUID()
modifications = Themes.getModifications(modifications)
for _, modification in pairs(modifications) do
local instanceName, property, value, iconState = table.unpack(modification)
if iconState == nil then
-- If no state specified, apply to all states
Themes.modify(icon, {instanceName, property, value, "Selected"}, modificationsUID)
Themes.modify(icon, {instanceName, property, value, "Viewing"}, modificationsUID)
end
local chosenState = Utility.formatStateName(iconState or "Deselected")
local stateGroup = icon:getStateGroup(chosenState)
local function nowSetIt()
if chosenState == icon.activeState then
Themes.apply(icon, instanceName, property, value)
end
end
local function updateRecord()
for stateName, detail in pairs(stateGroup) do
local didMerge = Themes.merge(detail, modification, function(detail)
detail[5] = modificationsUID
nowSetIt()
end)
if didMerge then
return
end
end
local detail = {instanceName, property, value, chosenState, modificationsUID}
table.insert(stateGroup, detail)
nowSetIt()
end
updateRecord()
end
end)
return modificationsUID
end
function Themes.remove(icon, modificationsUID)
for iconState, stateGroup in pairs(icon.appearance) do
for i = #stateGroup, 1, -1 do
local detail = stateGroup[i]
local checkingUID = detail[5]
if checkingUID == modificationsUID then
table.remove(stateGroup, i)
end
end
end
Themes.rebuild(icon)
end
function Themes.removeWith(icon, instanceName, property, state)
for iconState, stateGroup in pairs(icon.appearance) do
if state == iconState or not state then
for i = #stateGroup, 1, -1 do
local detail = stateGroup[i]
local detailName = detail[1]
local detailProperty = detail[2]
if detailName == instanceName and detailProperty == property then
table.remove(stateGroup, i)
end
end
end
end
Themes.rebuild(icon)
end
function Themes.change(icon)
-- This changes the theme to the appearance of whatever
-- state is currently active
local stateGroup = icon:getStateGroup()
for _, detail in pairs(stateGroup) do
local instanceName, property, value = unpack(detail)
Themes.apply(icon, instanceName, property, value)
end
end
function Themes.set(icon, theme)
-- This is responsible for processing the final appearance of a given theme (such as
-- ensuring Deselected merge into missing Selected, saving that internal state,
-- then checking to see if the appearance of the icon needs to be updated
local themesJanitor = icon.themesJanitor
themesJanitor:clean()
themesJanitor:add(icon.stateChanged:Connect(function()
Themes.change(icon)
end))
if typeof(theme) == "Instance" and theme:IsA("ModuleScript") then
theme = require(theme)
end
icon.appliedTheme = theme
Themes.rebuild(icon)
end
function Themes.statesMatch(state1, state2)
-- States match if they have the same name OR if nil (because unspecified represents all states)
local state1lower = (state1 and string.lower(state1))
local state2lower = (state2 and string.lower(state2))
return state1lower == state2lower or not state1 or not state2
end
function Themes.rebuild(icon)
-- A note for my future self: this code can be optimised further by
-- converting appearance into a instanceName-property dictionary
-- as apposed to an array of every potential change. When converting
-- in the future, .modify and .apply would also have to be updated.
local appliedTheme = icon.appliedTheme
local statesArray = {"Deselected", "Selected", "Viewing"}
local function generateTheme()
for _, stateName in pairs(statesArray) do
-- This applies themes in layers
-- The last layers take higher priority as they overwrite
-- any duplicate earlier applied effects
local stateAppearance = {}
local function updateDetails(theme, incomingStateName)
-- This ensures there's always a base 'default' layer
if not theme then
return
end
for _, detail in pairs(theme) do
local modificationsUID = detail[5]
local detailStateName = detail[4]
if Themes.statesMatch(incomingStateName, detailStateName) then
local key = detail[1].."-"..detail[2]
local newDetail = Utility.copyTable(detail)
newDetail[5] = modificationsUID
stateAppearance[key] = newDetail
end
end
end
-- First we apply the base theme (i.e. the Default module)
if stateName == "Selected" then
updateDetails(baseTheme, "Deselected")
end
updateDetails(baseTheme, "Empty")
updateDetails(baseTheme, stateName)
-- Next we apply any custom themes by the games developer
if appliedTheme ~= baseTheme then
if stateName == "Selected" then
updateDetails(appliedTheme, "Deselected")
end
updateDetails(baseTheme, "Empty")
updateDetails(appliedTheme, stateName)
end
-- Finally we apply any modifications that have already been made
-- Modifiers are all the changes made using icon:modifyTheme(...)
local alreadyAppliedTheme = {}
local alreadyAppliedGroup = icon.appearance[stateName]
if alreadyAppliedGroup then
for _, modifier in pairs(alreadyAppliedGroup) do
local modificationsUID = modifier[5]
if modificationsUID ~= nil then
local modification = {modifier[1], modifier[2], modifier[3], stateName, modificationsUID}
table.insert(alreadyAppliedTheme, modification)
end
end
end
updateDetails(alreadyAppliedTheme, stateName)
-- This now converts it into our final appearance
local finalStateAppearance = {}
for _, detail in pairs(stateAppearance) do
table.insert(finalStateAppearance, detail)
end
icon.appearance[stateName] = finalStateAppearance
end
Themes.change(icon)
end
generateTheme()
end
return Themes
================================================
FILE: src/Packages/GoodSignal.lua
================================================
--------------------------------------------------------------------------------
-- Batched Yield-Safe Signal Implementation --
-- This is a Signal class which has effectively identical behavior to a --
-- normal RBXScriptSignal, with the only difference being a couple extra --
-- stack frames at the bottom of the stack trace when an error is thrown. --
-- This implementation caches runner coroutines, so the ability to yield in --
-- the signal handlers comes at minimal extra cost over a naive signal --
-- implementation that either always or never spawns a thread. --
-- --
-- API: --
-- local Signal = require(THIS MODULE) --
-- local sig = Signal.new() --
-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
-- sig:Fire(arg1, arg2, ...) --
-- connection:Disconnect() --
-- sig:DisconnectAll() --
-- local arg1, arg2, ... = sig:Wait() --
-- --
-- Licence: --
-- Licenced under the MIT licence. --
-- --
-- Authors: --
-- stravant - July 31st, 2021 - Created the file. --
--------------------------------------------------------------------------------
-- The currently idle thread to run the next handler on
local freeRunnerThread = nil
-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
local acquiredRunnerThread = freeRunnerThread
freeRunnerThread = nil
fn(...)
-- The handler finished running, this runner thread is free again.
freeRunnerThread = acquiredRunnerThread
end
-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread()
-- Note: We cannot use the initial set of arguments passed to
-- runEventHandlerInFreeThread for a call to the handler, because those
-- arguments would stay on the stack for the duration of the thread's
-- existence, temporarily leaking references. Without access to raw bytecode
-- there's no way for us to clear the "..." references from the stack.
while true do
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
end
end
-- Connection class
local Connection = {}
Connection.__index = Connection
function Connection.new(signal, fn)
return setmetatable({
_connected = true,
_signal = signal,
_fn = fn,
_next = false,
}, Connection)
end
function Connection:Disconnect()
self._connected = false
-- Unhook the node, but DON'T clear it. That way any fire calls that are
-- currently sitting on this node will be able to iterate forwards off of
-- it, but any subsequent fire calls will not hit it, and it will be GCed
-- when no more fire calls are sitting on it.
if self._signal._handlerListHead == self then
self._signal._handlerListHead = self._next
else
local prev = self._signal._handlerListHead
while prev and prev._next ~= self do
prev = prev._next
end
if prev then
prev._next = self._next
end
end
end
Connection.Destroy = Connection.Disconnect
-- Make Connection strict
setmetatable(Connection, {
__index = function(tb, key)
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
end,
__newindex = function(tb, key, value)
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
})
-- Signal class
local Signal = {}
Signal.__index = Signal
function Signal.new()
return setmetatable({
_handlerListHead = false,
}, Signal)
end
function Signal:Connect(fn)
local connection = Connection.new(self, fn)
if self._handlerListHead then
connection._next = self._handlerListHead
self._handlerListHead = connection
else
self._handlerListHead = connection
end
return connection
end
-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
function Signal:DisconnectAll()
self._handlerListHead = false
end
Signal.Destroy = Signal.DisconnectAll
-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
function Signal:Fire(...)
local item = self._handlerListHead
while item do
if item._connected then
if not freeRunnerThread then
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
-- Get the freeRunnerThread to the first yield
coroutine.resume(freeRunnerThread)
end
task.spawn(freeRunnerThread, item._fn, ...)
end
item = item._next
end
end
-- Implement Signal:Wait() in terms of a temporary connection using
-- a Signal:Connect() which disconnects itself.
function Signal:Wait()
local waitingCoroutine = coroutine.running()
local cn;
cn = self:Connect(function(...)
cn:Disconnect()
task.spawn(waitingCoroutine, ...)
end)
return coroutine.yield()
end
-- Implement Signal:Once() in terms of a connection which disconnects
-- itself before running the handler.
function Signal:Once(fn)
local cn;
cn = self:Connect(function(...)
if cn._connected then
cn:Disconnect()
end
fn(...)
end)
return cn
end
-- Make signal strict
setmetatable(Signal, {
__index = function(tb, key)
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
end,
__newindex = function(tb, key, value)
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
})
return Signal
================================================
FILE: src/Packages/Janitor.lua
================================================
--[[
-------------------------------------
This package was modified by ForeverHD.
PACKAGE MODIFICATIONS:
1. Added pascalCase aliases for all methods
2. Modified behaviour of :add so that it takes both objects and promises (previously only objects)
3. Slight change to how promises are tracked
4. Added isAnInstanceBeingDestroyed check to line 228
5. Added 'OriginalTraceback' to help determine where an error was added to the janitor
6. Likely some additional changes which weren't record here
7. Removed comments as these were detected by Moonwave
-------------------------------------
--]]
-- Janitor
-- Original by Validark
-- Modifications by pobammer
-- roblox-ts support by OverHash and Validark
-- LinkToInstance fixed by Elttob.
local RunService = game:GetService("RunService")
local Heartbeat = RunService.Heartbeat
local function getPromiseReference()
return false
end
local IndicesReference = newproxy(true)
getmetatable(IndicesReference).__tostring = function()
return "IndicesReference"
end
local LinkToInstanceIndex = newproxy(true)
getmetatable(LinkToInstanceIndex).__tostring = function()
return "LinkToInstanceIndex"
end
local METHOD_NOT_FOUND_ERROR = "Object %s doesn't have method %s, are you sure you want to add it? Traceback: %s"
local NOT_A_PROMISE = "Invalid argument #1 to 'Janitor:AddPromise' (Promise expected, got %s (%s))"
local Janitor = {
IGNORE_MEMORY_DEBUG = true,
ClassName = "Janitor";
__index = {
CurrentlyCleaning = true;
[IndicesReference] = nil;
};
}
local TypeDefaults = {
["function"] = true;
["Promise"] = "cancel";
RBXScriptConnection = "Disconnect";
}
function Janitor.new()
return setmetatable({
CurrentlyCleaning = false;
[IndicesReference] = nil;
}, Janitor)
end
function Janitor.Is(Object)
return type(Object) == "table" and getmetatable(Object) == Janitor
end
Janitor.is = Janitor.Is
function Janitor.__index:Add(Object, MethodName, Index)
if Index then
self:Remove(Index)
local This = self[IndicesReference]
if not This then
This = {}
self[IndicesReference] = This
end
This[Index] = Object
end
local objectType = typeof(Object)
if objectType == "table" and string.match(tostring(Object), "Promise") then
objectType = "Promise"
--local status = Object:getStatus()
--print("status =", status, status == "Rejected")
end
MethodName = MethodName or TypeDefaults[objectType] or "Destroy"
if type(Object) ~= "function" and not Object[MethodName] then
warn(string.format(METHOD_NOT_FOUND_ERROR, tostring(Object), tostring(MethodName), debug.traceback(nil :: any, 2)))
end
local OriginalTraceback = debug.traceback("")
self[Object] = {MethodName, OriginalTraceback}
return Object
end
Janitor.__index.Give = Janitor.__index.Add
-- My version of Promise has PascalCase, but I converted it to use lowerCamelCase for this release since obviously that's important to do.
function Janitor.__index:AddPromise(PromiseObject)
local Promise = getPromiseReference()
if Promise then
if not Promise.is(PromiseObject) then
error(string.format(NOT_A_PROMISE, typeof(PromiseObject), tostring(PromiseObject)))
end
if PromiseObject:getStatus() == Promise.Status.Started then
local Id = newproxy(false)
local NewPromise = self:Add(Promise.new(function(Resolve, _, OnCancel)
if OnCancel(function()
PromiseObject:cancel()
end) then
return
end
Resolve(PromiseObject)
end), "cancel", Id)
NewPromise:finallyCall(self.Remove, self, Id)
return NewPromise
else
return PromiseObject
end
else
return PromiseObject
end
end
Janitor.__index.GivePromise = Janitor.__index.AddPromise
-- This will assume whether or not the object is a Promise or a regular object.
function Janitor.__index:AddObject(Object)
local Id = newproxy(false)
local Promise = getPromiseReference()
if Promise and Promise.is(Object) then
if Object:getStatus() == Promise.Status.Started then
local NewPromise = self:Add(Promise.resolve(Object), "cancel", Id)
NewPromise:finallyCall(self.Remove, self, Id)
return NewPromise, Id
else
return Object
end
else
return self:Add(Object, false, Id), Id
end
end
Janitor.__index.GiveObject = Janitor.__index.AddObject
function Janitor.__index:Remove(Index)
local This = self[IndicesReference]
if This then
local Object = This[Index]
if Object then
local ObjectDetail = self[Object]
local MethodName = ObjectDetail and ObjectDetail[1]
if MethodName then
if MethodName == true then
Object()
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
ObjectMethod(Object)
end
end
self[Object] = nil
end
This[Index] = nil
end
end
return self
end
function Janitor.__index:Get(Index)
local This = self[IndicesReference]
if This then
return This[Index]
end
return nil
end
function Janitor.__index:Cleanup()
if not self.CurrentlyCleaning then
self.CurrentlyCleaning = nil
for Object, ObjectDetail in next, self do
if Object == IndicesReference then
continue
end
-- Weird decision to rawset directly to the janitor in Agent. This should protect against it though.
local TypeOf = type(Object)
if TypeOf == "string" or TypeOf == "number" then
self[Object] = nil
continue
end
local MethodName = ObjectDetail[1]
local OriginalTraceback = ObjectDetail[2]
local function warnUser(warning)
local cleanupLine = debug.traceback("", 3)--string.gsub(debug.traceback("", 3), "%c", "")
local addedLine = OriginalTraceback
warn("-------- Janitor Error --------".."\n"..tostring(warning).."\n"..cleanupLine..""..addedLine)
end
if MethodName == true then
local success, warning = pcall(Object)
if not success then
warnUser(warning)
end
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
local success, warning = pcall(ObjectMethod, Object)
local isAnInstanceBeingDestroyed = typeof(Object) == "Instance" and ObjectMethod == "Destroy"
if not success and not isAnInstanceBeingDestroyed then
warnUser(warning)
end
end
end
self[Object] = nil
end
local This = self[IndicesReference]
if This then
for Index in next, This do
This[Index] = nil
end
self[IndicesReference] = {}
end
self.CurrentlyCleaning = false
end
end
Janitor.__index.Clean = Janitor.__index.Cleanup
function Janitor.__index:Destroy()
self:Cleanup()
--table.clear(self)
--setmetatable(self, nil)
end
Janitor.__call = Janitor.__index.Cleanup
local Disconnect = {Connected = true}
Disconnect.__index = Disconnect
function Disconnect:Disconnect()
if self.Connected then
self.Connected = false
self.Connection:Disconnect()
end
end
function Disconnect:__tostring()
return "Disconnect<" .. tostring(self.Connected) .. ">"
end
function Janitor.__index:LinkToInstance(Object, AllowMultiple)
local Connection
local IndexToUse = AllowMultiple and newproxy(false) or LinkToInstanceIndex
local IsNilParented = Object.Parent == nil
local ManualDisconnect = setmetatable({}, Disconnect)
local function ChangedFunction(_DoNotUse, NewParent)
if ManualDisconnect.Connected then
_DoNotUse = nil
IsNilParented = NewParent == nil
if IsNilParented then
coroutine.wrap(function()
Heartbeat:Wait()
if not ManualDisconnect.Connected then
return
elseif not Connection.Connected then
self:Cleanup()
else
while IsNilParented and Connection.Connected and ManualDisconnect.Connected do
Heartbeat:Wait()
end
if ManualDisconnect.Connected and IsNilParented then
self:Cleanup()
end
end
end)()
end
end
end
Connection = Object.AncestryChanged:Connect(ChangedFunction)
ManualDisconnect.Connection = Connection
if IsNilParented then
ChangedFunction(nil, Object.Parent)
end
Object = nil
return self:Add(ManualDisconnect, "Disconnect", IndexToUse)
end
function Janitor.__index:LinkToInstances(...)
local ManualCleanup = Janitor.new()
for _, Object in ipairs({...}) do
ManualCleanup:Add(self:LinkToInstance(Object, true), "Disconnect")
end
return ManualCleanup
end
for FunctionName, Function in next, Janitor.__index do
local NewFunctionName = string.sub(string.lower(FunctionName), 1, 1) .. string.sub(FunctionName, 2)
Janitor.__index[NewFunctionName] = Function
end
return Janitor
================================================
FILE: src/Reference.lua
================================================
-- This module enables you to place Icon wherever you like within the data model while
-- still enabling third-party applications (such as HDAdmin/Nanoblox) to locate it
-- This is necessary to prevent two TopbarPlus applications initiating at runtime which would
-- cause icons to overlap with each other
local replicatedStorage = game:GetService("ReplicatedStorage")
local Reference = {}
Reference.objectName = "TopbarPlusReference"
function Reference.addToReplicatedStorage()
local existingItem = replicatedStorage:FindFirstChild(Reference.objectName)
if existingItem then
return false
end
local objectValue = Instance.new("ObjectValue")
objectValue.Name = Reference.objectName
objectValue.Value = script.Parent
objectValue.Parent = replicatedStorage
return objectValue
end
function Reference.getObject()
local objectValue = replicatedStorage:FindFirstChild(Reference.objectName)
if objectValue then
return objectValue
end
return false
end
return Reference
================================================
FILE: src/Types.lua
================================================
--!strict
-- GoodSignal Types (...but simpler!)
--- Connection
type Connection<Variant... = ...any> = {
Disconnect: (self: Connection<Variant...>) -> (),
}
--- Signal
type Signal<Variant... = ...any> = {
Connect: (self: Signal<Variant...>, func: (Variant...) -> ()) -> Connection<Variant...>,
Once: (self: Signal<Variant...>, func: (Variant...) -> ()) -> Connection<Variant...>,
Wait: (self: Signal<Variant...>) -> Variant...,
}
----------------------
export type IconState = "Deselected" | "Selected" | "Viewing"
export type Events = "selected" | "deselected" | "toggled" | "viewingStarted" | "viewingEnded" | "notified"
export type Alignment = "Left" | "Center" | "Right"
export type EventSource = "User" | "OneClick" | "AutoDeselect" | "HideParentFeature" | "Overflow"
export type Modification = { any }
type StaticFunctions = {
getIcons: typeof(
--[[
Returns a dictionary of icons where the key is the icon's UID and value the icon.
]]
function(): { Icon }
return (nil :: any) :: { Icon }
end
),
getIcon: typeof(
--[[
Returns an icon of the given name or UID.
]]
function(nameOrUID: string): Icon?
return nil :: any
end
),
setTopbarEnabled: typeof(
--[[
When set to <code>false</code> all TopbarPlus ScreenGuis are hidden.
This does not impact Roblox's Topbar.
]]
function(enabled: boolean)
end
),
modifyBaseTheme: typeof(
--[[
Updates the appearance of all icons.
]]
function(modifications: { Modification })
end
),
setDisplayOrder: typeof(
--[[
Sets the base DisplayOrder of all TopbarPlus ScreenGuis.
]]
function(order: number)
end
),
}
type Methods = {
-- CLASS FUNCTIONS
setName: typeof(
--[[
Sets the name of the Widget instance. This can be used in conjunction with <code>Icon.getIcon(name)</code>
]]
function(self: Icon, name: string): Icon
return nil :: any
end
),
getInstance: typeof(
--[[
Returns the first descendant found within the widget of name <code>instanceName</code>.
]]
function(self: Icon, instanceName: string): Instance?
return (nil :: any) :: Instance?
end
),
modifyTheme: typeof(
--[[
Updates the appearance of the icon.
]]
function(self: Icon, modifications: {Modification} | Modification): Icon
return nil :: any
end
),
modifyChildTheme: typeof(
--[[
Updates the appearance of all icons that are parented to this icon (for example when a menu or dropdown).
]]
function(self: Icon, modifications: { Modification }): Icon
return nil :: any
end
),
setEnabled: typeof(
--[[
When set to <code>false</code> the icon will be disabled and hidden.
]]
function(self: Icon, enabled: boolean): Icon
return nil :: any
end
),
select: typeof(
--[[
Selects the icon (as if it were clicked once).
]]
function(self: Icon): Icon
return nil :: any
end
),
deselect: typeof(
--[[
Deselects the icon (as if it were clicked, then clicked again).
]]
function(self: Icon): Icon
return nil :: any
end
),
notify: typeof(
--[[
Prompts 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.
]]
function(self: Icon, clearNoticeEvent: Signal?): Icon
return nil :: any
end
),
clearNotices: typeof(
--[[
]]
function(self: Icon): Icon
return nil :: any
end
),
disableOverlay: typeof(
--[[
When set to <code>true</code>, disables the shade effect which appears when the icon is pressed and released.
]]
function(self: Icon, disabled: boolean): Icon
return nil :: any
end
),
setImage: typeof(
--[[
Applies an image to the icon based on the given <code>imageId</code>. <code>imageId</code> can be an assetId or a complete asset string.
]]
function(self: Icon, imageId: string | number, iconState: IconState?): Icon
return nil :: any
end
),
setLabel: typeof(
--[[
]]
function(self: Icon, text: string, iconState: IconState?): Icon
return nil :: any
end
),
setOrder: typeof(
--[[
]]
function(self: Icon, order: number, iconState: IconState?): Icon
return nil :: any
end
),
setCornerRadius: typeof(
--[[
]]
function(self: Icon, udim: UDim2, iconState: IconState?): Icon
return nil :: any
end
),
align: typeof(
--[[
This enables you to set the icon to the <code>"Left"</code> (default), <code>"Center"</code> or <code>"Right"</code> side of the screen.
]]
function(self: Icon, alignment: Alignment?): Icon
return nil :: any
end
),
setWidth: typeof(
--[[
This 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>.
]]
function(self: Icon, minimumSize: number, iconState: IconState?): Icon
return nil :: any
end
),
setImageScale: typeof(
--[[
How large the image is relative to the icon. The default value is <code>0.5</code>.
]]
function(self: Icon, scale: number, iconState: IconState?): Icon
return nil :: any
end
),
setImageRatio: typeof(
--[[
How stretched the image will appear. The default value is <code>1</code> (a perfect square).
]]
function(self: Icon, ratio: number, iconState: IconState?): Icon
return nil :: any
end
),
setTextSize: typeof(
--[[
The size of the icon labels' text. The default value is <code>16</code>.
]]
function(self: Icon, textSize: number, iconState: IconState?): Icon
return nil :: any
end
),
setTextColor: typeof(
--[[
The color of the icon labels' text
]]
function(self: Icon, color: Color3, iconState: IconState?): Icon
return nil :: any
end
),
setTextFont: typeof(
--[[
Sets the labels FontFace.
<code>font</code> can be a font family name (such as <code>"Creepster"</code>),
a font enum (such as <code>Enum.Font.Bangers</code>),
a font ID (such as <code>12187370928</code>),
or font family link (such as <code>"rbxasset://fonts/families/Sarpanch.json"</code>).
]]
function(self: Icon, font: string | Enum.Font, fontWeight: Enum.FontWeight?, fontStyle: Enum.FontSize?, iconState: IconState?): Icon
return nil :: any
end
),
bindToggleItem: typeof(
--[[
Binds a GuiObject or LayerCollector to appear and disappeared when the icon is toggled.
]]
function(self: Icon, guiObjectOrLayerCollector: GuiObject | LayerCollector): Icon
return nil :: any
end
),
unbindToggleItem: typeof(
--[[
Unbinds the given GuiObject or LayerCollector from the toggle.
]]
function(self: Icon, guiObjectOrLayerCollector: GuiObject | LayerCollector): Icon
return nil :: any
end
),
bindEvent: typeof(
--[[
Connects to an icon event with <code>iconEventName</code>.
It's important to remember all event names are in <code>camelCase</code>.
<code>callback</code> is called with arguments <code>(self, ...)</code> when the event is triggered.
]]
function(self: Icon, event: Events, callback: (...any) -> ()): Icon
return nil :: any
end
),
unbindEvent: typeof(
--[[
Unbinds the connection of the associated <code>iconEventName</code>.
]]
function(self: Icon, event: Events): Icon
return nil :: any
end
),
bindToggleKey: typeof(
--[[
Binds a keycode which toggles the icon when pressed.
]]
function(self: Icon, keycode: Enum.KeyCode): Icon
return nil :: any
end
),
unbindToggleKey: typeof(
--[[
Unbinds the given keycode.
]]
function(self: Icon, keycode: Enum.KeyCode): Icon
return nil :: any
end
),
call: typeof(
--[[
Calls the function immediately via <code>task.spawn</code>.
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.
]]
function(self: Icon, func: (self: Icon) -> (...any), ...: any): Icon
return nil :: any
end
),
addToJanitor: typeof(
--[[
Passes 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.
]]
function(self: Icon, userdata: unknown): Icon
return nil :: any
end
),
lock: typeof(
--[[
Prevents 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>.
]]
function(self: Icon): Icon
return nil :: any
end
),
unlock: typeof(
--[[
Re-enables user-input to toggle the icon again.
]]
function(self: Icon): Icon
return nil :: any
end
),
debounce: typeof(
--[[
Locks the icon, yields for the given time, then unlocks the icon, effectively shorthand for <code>icon:lock() task.wait(seconds) icon:unlock()</code>.
This is useful for applying cooldowns (to prevent an icon from being pressed again) after an icon has been selected or deselected.
]]
function(self: Icon, seconds: number): Icon
return nil :: any
end
),
autoDeselect: typeof(
--[[
When set to <code>true</code> (the default) the icon is deselected when another icon (with autoDeselect enabled) is pressed.
Set to <code>false</code> to prevent the icon being deselected when another icon is selected (a useful behaviour in dropdowns).
]]
function(self: Icon, enabled: boolean?): Icon
return nil :: any
end
),
oneClick: typeof(
--[[
When set to true the icon will automatically deselect when selected.
This creates the effect of a single click button.
]]
function(self: Icon, enabled: boolean?): Icon
return nil :: any
end
),
setCaption: typeof(
--[[
Sets a caption. To remove, pass <code>nil</code> as <code>text</code>.
]]
function(self: Icon, text: string?): Icon
return nil :: any
end
),
setCaptionHint: typeof(
--[[
This customizes the appearance of the caption's hint without having to use <code>icon:bindToggleKey</code>.
]]
function(self: Icon, keyCode: Enum.KeyCode): Icon
return nil :: any
end
),
setDropdown: typeof(
--[[
Creates a vertical dropdown based upon the given table array of icons.
Pass an empty table <code>{}</code> to remove the dropdown.
]]
function(self: Icon, icons: { Icon }): Icon
return nil :: any
end
),
joinDropdown: typeof(
--[[
Joins the dropdown of <code>parentIcon</code>.
This is what <code>icon:setDropdown</code> calls internally on the icons within its array.
]]
function(self: Icon, parent: Icon): Icon
return nil :: any
end
),
setMenu: typeof(
--[[
Creates a horizontal menu based upon the given array of icons.
Pass an empty table <code>{}</code> to remove the menu.
]]
function(self: Icon, icons: { Icon }): Icon
return nil :: any
end
),
setFixedMenu: typeof(
--[[
Creates a menu that is always selected and has it's close button hidden.
Pass an empty table <code>{}</code> to remove the menu.
]]
function(self: Icon, icons: { Icon }): Icon
return nil :: any
end
),
joinMenu: typeof(
--[[
Joins the menu of <code>parentIcon</code>.
This is what <code>icon:setMenu</code> calls internally on the icons within its array.
]]
function(self: Icon, parentIcon: Icon): Icon
return nil :: any
end
),
leave: typeof(
--[[
Unparents an icon from a parentIcon if it belongs to a dropdown or menu.
]]
function(self: Icon): Icon
return nil :: any
end
),
convertLabelToNumberSpinner: typeof(
--[[
Unparents an icon from a parentIcon if it belongs to a dropdown or menu.
]]
function(self: Icon, numberSpinner: any, func: (...any) -> (...any), ...: any): Icon
return nil :: any
end
),
destroy: typeof(
--[[
Clears all connections and destroys all instances associated with the icon.
]]
function(self: Icon): Icon
return nil :: any
end
),
} & StaticFunctions
type Fields = {
-- CLASS PROPERTIES
name: string,
isSelected: boolean,
isEnabled: boolean,
totalNotices: number,
locked: boolean,
-- CLASS EVENTS
selected: Signal<EventSource>,
deselected: Signal<EventSource>,
toggled: Signal<boolean, EventSource>,
viewingStarted: Signal,
viewingEnded: Signal,
notified: Signal,
}
export type Icon = Methods & StaticFunctions --typeof(setmetatable({} :: Fields, MT))
export type StaticIcon = {
new: typeof(
--[[
Constructs an empty <code>32x32</code> icon on the topbar.
]]
function(): Icon
return (nil :: any) :: Icon
end
),
} & StaticFunctions
return {}
================================================
FILE: src/Utility.lua
================================================
-- Just generic utility functions which I use and repeat across all my projects
-- LOCAL
local Utility = {}
local Players = game:GetService("Players")
local localPlayer = Players.LocalPlayer
-- FUNCTIONS
function Utility.createStagger(delayTime, callback, delayInitially)
-- This creates and returns a function which when called
-- acts identically to callback, however will only be called
-- for a maximum of once per delayTime. If the returned function
-- is called more than once during the delayTime, then it will
-- wait until the expiryTime then perform another recall.
-- This is useful for visual interfaces and effects which may be
-- triggered multiple times within a frame or short period, but which
-- we don't necessary need to (for performance reasons).
local staggerActive = false
local multipleCalls = false
if not delayTime or delayTime == 0 then
-- We make 0.01 instead of 0 because devices can now run at
-- different frame rates
delayTime = 0.01
end
local function staggeredCallback(...)
if staggerActive then
multipleCalls = true
return
end
local packedArgs = table.pack(...)
staggerActive = true
multipleCalls = false
task.spawn(function()
if delayInitially then
task.wait(delayTime)
end
callback(table.unpack(packedArgs))
end)
task.delay(delayTime, function()
staggerActive = false
if multipleCalls then
-- This means it has been called at least once during
-- the stagger period, so call again
staggeredCallback(table.unpack(packedArgs))
end
end)
end
return staggeredCallback
end
function Utility.round(n)
-- Credit to Darkmist101 for this
return math.floor(n + 0.5)
end
function Utility.reverseTable(t)
for i = 1, math.floor(#t/2) do
local j = #t - i + 1
t[i], t[j] = t[j], t[i]
end
end
function Utility.copyTable(t)
-- Credit to Stephen Leitnick (September 13, 2017) for this function from TableUtil
assert(type(t) == "table", "First argument must be a table")
local tCopy = table.create(#t)
for k,v in pairs(t) do
if (type(v) == "table") then
tCopy[k] = Utility.copyTable(v)
else
tCopy[k] = v
end
end
return tCopy
end
local 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","<",">","?","@","{","}","[","]","!","(",")","=","+","~","#"}
function Utility.generateUID(length)
length = length or 8
local UID = ""
local list = validCharacters
local total = #list
for i = 1, length do
local randomCharacter = list[math.random(1, total)]
UID = UID..randomCharacter
end
return UID
end
local instanceTrackers = {}
function Utility.setVisible(instance, bool, sourceUID)
-- This effectively works like a buff object but
-- incredibly simplified. It stacks false values
-- so that if there is more than more than, the
-- instance remains hidden even if set visible true
local tracker = instanceTrackers[instance]
if not tracker then
tracker = {}
instanceTrackers[instance] = tracker
instance.Destroying:Once(function()
instanceTrackers[instance] = nil
end)
end
if not bool then
tracker[sourceUID] = true
else
tracker[sourceUID] = nil
end
local isVisible = bool
if bool then
for sourceUID, _ in pairs(tracker) do
isVisible = false
break
end
end
instance.Visible = isVisible
end
function Utility.formatStateName(incomingStateName)
return string.upper(string.sub(incomingStateName, 1, 1))..string.lower(string.sub(incomingStateName, 2))
end
function Utility.localPlayerRespawned(callback)
-- The client localscript may be located under a ScreenGui with ResetOnSpawn set to true
-- In these scenarios, traditional methods like CharacterAdded won't be called by the
-- time the localscript has been destroyed, therefore we listen for removing instead
-- If humanoid and health == 0, then reset/died normally, else was
-- forcefully reset via a method such as LoadCharacter
-- We wrap this behaviour in case any additional quirks need to be accounted for
localPlayer.CharacterRemoving:Connect(callback)
end
function Utility.getClippedContainer(screenGui)
-- We always want clipped items to display in front hence
-- why we have this
local clippedContainer = screenGui:FindFirstChild("ClippedContainer")
if not clippedContainer then
clippedContainer = Instance.new("Folder")
clippedContainer.Name = "ClippedContainer"
clippedContainer.Parent = screenGui
end
return clippedContainer
end
local Janitor = require(script.Parent.Packages.Janitor)
local GuiService = game:GetService("GuiService")
function Utility.clipOutside(icon, instance)
local cloneJanitor = icon.janitor:add(Janitor.new())
instance.Destroying:Once(function()
cloneJanitor:Destroy()
end)
icon.janitor:add(instance)
local originalParent = instance.Parent
local clone = cloneJanitor:add(Instance.new("Frame"))
clone:SetAttribute("IsAClippedClone", true)
clone.Name = instance.Name
clone.AnchorPoint = instance.AnchorPoint
clone.Size = instance.Size
clone.Position = instance.Position
clone.BackgroundTransparency = 1
clone.LayoutOrder = instance.LayoutOrder
clone.Parent = originalParent
local valueInstance = Instance.new("ObjectValue")
valueInstance.Name = "OriginalInstance"
valueInstance.Value = instance
valueInstance.Parent = clone
local valueInstanceCopy = valueInstance:Clone()
instance:SetAttribute("HasAClippedClone", true)
valueInstanceCopy.Name = "ClippedClone"
valueInstanceCopy.Value = clone
valueInstanceCopy.Parent = instance
local screenGui
local Icon = require(icon.iconModule)
local container = Icon.container
local function updateScreenGui()
local originalScreenGui = originalParent:FindFirstAncestorWhichIsA("ScreenGui")
screenGui = if string.match(originalScreenGui.Name, "Clipped") then originalScreenGui else container[originalScreenGui.Name.."Clipped"]
instance.AnchorPoint = Vector2.new(0, 0)
instance.Parent = Utility.getClippedContainer(screenGui)
end
cloneJanitor:add(icon.alignmentChanged:Connect(updateScreenGui))
updateScreenGui()
-- Lets copy over children that modify size
for _, child in pairs(instance:GetChildren()) do
if child:IsA("UIAspectRatioConstraint") then
child:Clone().Parent = clone
end
end
-- If the icon is hidden, its important we are too (as
-- setting a parent to visible = false no longer makes
-- this hidden)
local widget = icon.widget
local isOutsideParent = false
local ignoreVisibilityUpdater = instance:GetAttribute("IgnoreVisibilityUpdater")
local function updateVisibility()
if ignoreVisibilityUpdater then
return
end
local isVisible = widget.Visible
if isOutsideParent then
isVisible = false
end
Utility.setVisible(instance, isVisible, "ClipHandler")
end
cloneJanitor:add(widget:GetPropertyChangedSignal("Visible"):Connect(updateVisibility))
local previousScroller
local function checkIfOutsideParentXBounds()
-- Defer so that roblox's properties reflect their true values
task.defer(function()
-- If the instance is within a parent item (such as a dropdown or menu)
-- then we hide it if it exceeds the bounds of that parent
local parentInstance
local ourUID = icon.UID
local nextIconUID = ourUID
local shouldClipToParent = instance:GetAttribute("ClipToJoinedParent")
if shouldClipToParent then
for i = 1, 10 do -- This is safer than while true do and should never be > 4 parents
local nextIcon = Icon.getIconByUID(nextIconUID)
if not nextIcon then
break
end
local nextParentInstance = nextIcon.joinedFrame
nextIconUID = nextIcon.parentIconUID
if not nextParentInstance then
break
end
parentInstance = nextParentInstance
if parentInstance and parentInstance.Name == "DropdownScroller" then
break
end
end
end
if not parentInstance then
isOutsideParent = false
updateVisibility()
return
end
local pos = instance.AbsolutePosition
local halfSize = instance.AbsoluteSize/2
local parentPos = parentInstance.AbsolutePosition
local parentSize = parentInstance.AbsoluteSize
local posHalf = (pos + halfSize)
local exceededLeft = posHalf.X < parentPos.X
local exceededRight = posHalf.X > (parentPos.X + parentSize.X)
local exceededTop = posHalf.Y < parentPos.Y
local exceededBottom = posHalf.Y > (parentPos.Y + parentSize.Y)
local hasExceeded = exceededLeft or exceededRight or exceededTop or exceededBottom
if hasExceeded ~= isOutsideParent then
isOutsideParent = hasExceeded
updateVisibility()
end
if parentInstance:IsA("ScrollingFrame") and previousScroller ~= parentInstance then
previousScroller = parentInstance
local connection = parentInstance:GetPropertyChangedSignal("AbsoluteWindowSize"):Connect(function()
checkIfOutsideParentXBounds()
end)
cloneJanitor:add(connection, "Disconnect", "TrackUtilityScroller-"..ourUID)
end
end)
end
local camera = workspace.CurrentCamera
local additionalOffsetX = instance:GetAttribute("AdditionalOffsetX") or 0
local function trackProperty(property)
local absoluteProperty = "Absolute"..property
local function updateProperty()
local cloneValue = clone[absoluteProperty]
local absoluteValue = UDim2.fromOffset(cloneValue.X, cloneValue.Y)
if property == "Position" then
-- This binds the instances within the bounds of the screen
local SIDE_PADDING = 4
local limitX = camera.ViewportSize.X - instance.AbsoluteSize.X - SIDE_PADDING
local inputX = absoluteValue.X.Offset
if inputX < SIDE_PADDING then
inputX = SIDE_PADDING
elseif inputX > limitX then
inputX = limitX
end
absoluteValue = UDim2.fromOffset(inputX, absoluteValue.Y.Offset)
-- AbsolutePosition does not perfectly match with TopbarInsets enabled
-- This corrects this
local topbarInset = GuiService.TopbarInset
local viewportWidth = workspace.CurrentCamera.ViewportSize.X
local guiWidth = screenGui.AbsoluteSize.X
local guiOffset = screenGui.AbsolutePosition.X
--local widthDifference = guiOffset - topbarInset.Min.X
local oldTopbarCenterOffset = 0--widthDifference/30
local offsetX = if Icon.isOldTopbar then guiOffset else viewportWidth - guiWidth - oldTopbarCenterOffset
-- Also add additionalOffset
offsetX -= additionalOffsetX
absoluteValue += UDim2.fromOffset(-offsetX, topbarInset.Height)
-- Finally check if within its direct parents bounds
checkIfOutsideParentXBounds()
end
instance[property] = absoluteValue
end
-- This defer is essential as the listener may be in a different screenGui to the actor
local updatePropertyStaggered = Utility.createStagger(0.01, updateProperty)
cloneJanitor:add(clone:GetPropertyChangedSignal(absoluteProperty):Connect(updatePropertyStaggered))
cloneJanitor:add(clone:GetAttributeChangedSignal("ForceUpdate"):Connect(function()
updatePropertyStaggered()
end))
-- This is to patch a weirddddd bug with ScreenGuis with SreenInsets set to
-- 'TopbarSafeInsets'. For some reason the absolute position of gui instances
-- within this type of screenGui DO NOT accurately update to match their new
-- real world position; instead they jump around almost randomly for a few frames.
-- I have spent way too many hours trying to solve this bug, I think the only way
-- for the time being is to not use ScreenGuis with TopbarSafeInsets, but I don't
-- have time to redesign the entire system around that at the moment.
-- Here's a GIF of this bug: https://i.imgur.com/VitHdC1.gif
local updatePropertyPatch = Utility.createStagger(0.5, updateProperty, true)
cloneJanitor:add(clone:GetPropertyChangedSignal(absoluteProperty):Connect(updatePropertyPatch))
-- When the screenGui is resized (such as when chat is hidden/shown), we need
-- to update the position of the clone. Ths especially fixes the following:
-- https://devforum.roblox.com/t/bug/1017485/1732
if property == "Position" then
cloneJanitor:add(screenGui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
updatePropertyStaggered()
end))
end
end
task.delay(0.1, checkIfOutsideParentXBounds)
checkIfOutsideParentXBounds()
updateVisibility()
trackProperty("Position")
-- Track visiblity changes
cloneJanitor:add(instance:GetPropertyChangedSignal("Visible"):Connect(function()
--print("Visiblity changed:", instance, clone, instance.Visible)
--clone.Visible = instance.Visible
end))
-- To ensure accurate positioning, it's important the clone also remains the same size as the instance
local shouldTrackCloneSize = instance:GetAttribute("TrackCloneSize")
if shouldTrackCloneSize then
trackProperty("Size")
else
cloneJanitor:add(instance:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
local absolute = instance.AbsoluteSize
clone.Size = UDim2.fromOffset(absolute.X, absolute.Y)
end))
end
return clone
end
function Utility.joinFeature(originalIcon, parentIcon, iconsArray, scrollingFrameOrFrame)
-- This is resonsible for moving the icon under a feature like a dropdown
local joinJanitor = originalIcon.joinJanitor
joinJanitor:clean()
if not scrollingFrameOrFrame then
originalIcon:leave()
return
end
originalIcon.parentIconUID = parentIcon.UID
originalIcon.joinedFrame = scrollingFrameOrFrame
local function updateAlignent()
local parentAlignment = parentIcon.alignment
if parentAlignment == "Center" then
parentAlignment = "Left"
end
originalIcon:setAlignment(parentAlignment, true)
end
joinJanitor:add(parentIcon.alignmentChanged:Connect(updateAlignent))
updateAlignent()
originalIcon:modifyTheme({"IconButton", "BackgroundTransparency", 1}, "JoinModification")
originalIcon:modifyTheme({"ClickRegion", "Active", false}, "JoinModification")
if parentIcon.childModifications then
-- We defer so that the default values (such as dropdown
-- minimum width can be applied before any custom
-- child modifications from the user)
task.defer(function()
originalIcon:modifyTheme(parentIcon.childModifications, parentIcon.childModificationsUID)
end)
end
--
local clickRegion = originalIcon:getInstance("ClickRegion")
local function makeSelectable()
clickRegion.Selectable = parentIcon.isSelected
end
joinJanitor:add(parentIcon.toggled:Connect(makeSelectable))
task.defer(makeSelectable)
joinJanitor:add(function()
clickRegion.Selectable = true
end)
--
-- We track icons in arrays and dictionaries using their UID instead of the icon
-- itself to prevent heavy cyclical tables when printing the icons
local originalIconUID = originalIcon.UID
table.insert(iconsArray, originalIconUID)
parentIcon:autoDeselect(false)
parentIcon.childIconsDict[originalIconUID] = true
if not parentIcon.isEnabled then
parentIcon:setEnabled(true)
end
originalIcon.joinedParent:Fire(parentIcon)
-- This is responsible for removing it from that feature and updating
-- their parent icon so its informed of the icon leaving it
joinJanitor:add(function()
local joinedFrame = originalIcon.joinedFrame
if not joinedFrame then
return
end
for i, iconUID in pairs(iconsArray) do
if iconUID == originalIconUID then
table.remove(iconsArray, i)
break
end
end
local Icon = require(originalIcon.iconModule)
local parentIcon = Icon.getIconByUID(originalIcon.parentIconUID)
if not parentIcon then
return
end
originalIcon:setAlignment(originalIcon.originalAlignment)
originalIcon.parentIconUID = false
originalIcon.joinedFrame = false
--originalIcon:setBehaviour("IconButton", "BackgroundTransparency", nil, true)
originalIcon:removeModification("JoinModification")
local parentHasNoChildren = true
local parentChildIcons = parentIcon.childIconsDict
parentChildIcons[originalIconUID] = nil
for childIconUID, _ in pairs(parentChildIcons) do
parentHasNoChildren = false
break
end
if parentHasNoChildren and not parentIcon.isAnOverflow then
parentIcon:setEnabled(false)
end
updateAlignent()
end)
end
return Utility
================================================
FILE: src/VERSION.lua
================================================
--!strict
-- LOCAL
local VERSION = {}
-- SHARED
VERSION.appVersion = "v3.4.0"
VERSION.latestVersion = nil :: string?
-- FUNCTIONS
function VERSION.getLatestVersion(): string?
local DEVELOPMENT_PLACE_ID = 117501901079852
local latestVersion = VERSION.latestVersion
if latestVersion then
return latestVersion
end
local placeName = ""
while true do
local success, hdDevelopmentDetails = pcall(function()
return game:GetSer
gitextract_lekhork2/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── BuildRelease.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── LICENSE ├── PackageLink.model.json ├── README.md ├── aftman.toml ├── default.project.json ├── docs/ │ ├── api.md │ ├── contributing.md │ ├── features.md │ ├── index.md │ ├── installation.md │ ├── javascripts/ │ │ └── tags.js │ └── third_parties.md ├── mkdocs.yml ├── rotriever.toml ├── selene.toml ├── serve.project.json ├── src/ │ ├── Attribute.lua │ ├── Elements/ │ │ ├── Caption.lua │ │ ├── Container.lua │ │ ├── Dropdown.lua │ │ ├── Indicator.lua │ │ ├── Menu.lua │ │ ├── Notice.lua │ │ ├── Selection.lua │ │ └── Widget.lua │ ├── Features/ │ │ ├── Gamepad.lua │ │ ├── Overflow.lua │ │ └── Themes/ │ │ ├── Classic.lua │ │ ├── Default.lua │ │ └── init.lua │ ├── Packages/ │ │ ├── GoodSignal.lua │ │ └── Janitor.lua │ ├── Reference.lua │ ├── Types.lua │ ├── Utility.lua │ ├── VERSION.lua │ └── init.lua ├── wally.toml ├── withLink.project.json └── withoutLink.project.json
SYMBOL INDEX (1 symbols across 1 files)
FILE: docs/javascripts/tags.js
function replace (line 73) | function replace(element) {
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (255K chars).
[
{
"path": ".gitattributes",
"chars": 33,
"preview": "*.html linguist-detectable=false\n"
},
{
"path": ".github/workflows/BuildRelease.yml",
"chars": 534,
"preview": "name: Build and Release\n\non:\n push:\n tags:\n - \"v*\"\n\njobs:\n build:\n name: Create release\n runs-on: ubuntu"
},
{
"path": ".gitignore",
"chars": 157,
"preview": "# 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 "
},
{
"path": ".vscode/extensions.json",
"chars": 166,
"preview": "{\n \"recommendations\": [\n \"johnnymorganz.luau-lsp\",\n \"evaera.vscode-rojo\",\n \"kampfkarren.selene-v"
},
{
"path": ".vscode/settings.json",
"chars": 75,
"preview": "{\n \"robloxLsp.diagnostics.disable\": [\n \"undefined-global\"\n ]\n}"
},
{
"path": "LICENSE",
"chars": 17017,
"preview": "TopbarPlus Credit\n==================================\nBy using TopbarPlus in your experience or application, you agree to"
},
{
"path": "PackageLink.model.json",
"chars": 31,
"preview": "{\n \"ClassName\": \"PackageLink\"\n}"
},
{
"path": "README.md",
"chars": 48,
"preview": "https://devforum.roblox.com/t/topbarplus/1017485"
},
{
"path": "aftman.toml",
"chars": 339,
"preview": "# This file lists tools managed by Aftman, a cross-platform toolchain manager.\n# For more information, see https://githu"
},
{
"path": "default.project.json",
"chars": 72,
"preview": "{\n \"name\": \"topbarplus\",\n \"tree\": {\n \"$path\": \"src\"\n }\n }"
},
{
"path": "docs/api.md",
"chars": 11864,
"preview": "[themes]: https://1foreverhd.github.io/TopbarPlus/features/#modify-theme\n[alignments]: https://1foreverhd.github.io/Topb"
},
{
"path": "docs/contributing.md",
"chars": 3033,
"preview": "[discussion thread]: https://devforum.roblox.com/t/topbarplus-v2-construct-dynamic-and-intuitive-topbar-icons/1017485\n[P"
},
{
"path": "docs/features.md",
"chars": 6853,
"preview": "[icon states]: https://1foreverhd.github.io/TopbarPlus/#states\n[v3 Playground]: https://www.roblox.com/games/11750190107"
},
{
"path": "docs/index.md",
"chars": 3489,
"preview": "[icon:setOrder]: https://1foreverhd.github.io/TopbarPlus/api/#setorder\n[Feature Guide]: https://1foreverhd.github.io/Top"
},
{
"path": "docs/installation.md",
"chars": 2172,
"preview": "#### Take the model\n{recommended}\n\n1. Take the [TopbarPlus model](https://create.roblox.com/store/asset/92368439343389/T"
},
{
"path": "docs/javascripts/tags.js",
"chars": 2565,
"preview": "const style = `.tag {\n color: #ffffff;\n line-height: .8rem;\n padding: 5px;\n margin-left: 7px !important;\n "
},
{
"path": "docs/third_parties.md",
"chars": 647,
"preview": "TopbarPlus supports the use of multiple Icon packages within a single experience assuming all required packages are ``v3"
},
{
"path": "mkdocs.yml",
"chars": 1723,
"preview": "site_name: TopbarPlus v3\nsite_description: Documentation for TopbarPlus v3\nsite_author: Ben Horton\nsite_url: https://1Fo"
},
{
"path": "rotriever.toml",
"chars": 126,
"preview": "[package]\nname = \"TopbarPlus\"\nversion = \"3.0.0\"\nlicense = \"MPL2\"\nauthors = [\"1ForeverHD\"]\ncontent_root = \"src\"\n\n[depende"
},
{
"path": "selene.toml",
"chars": 14,
"preview": "std = \"roblox\""
},
{
"path": "serve.project.json",
"chars": 424,
"preview": "{\n \"name\": \"topbarplus\",\n \"tree\": {\n \"$className\": \"DataModel\",\n\n \"Workspace\": {\n \"$class"
},
{
"path": "src/Attribute.lua",
"chars": 1237,
"preview": "--[[\n\n\tTopbarPlus was developed by ForeverHD and is possible thanks to HD Admin.\n\n\tBy using TopbarPlus in your experienc"
},
{
"path": "src/Elements/Caption.lua",
"chars": 10665,
"preview": "local CAPTION_COLOR = Color3.fromRGB(39, 41, 48)\nlocal TEXT_SIZE = 15\nreturn function(icon)\n\n\t-- Credit to lolmansReturn"
},
{
"path": "src/Elements/Container.lua",
"chars": 7439,
"preview": "local hasBecomeOldTheme = false\nlocal previousInsetHeight = 0\nreturn function(Icon)\n\t\n\t-- Has to be included for the tim"
},
{
"path": "src/Elements/Dropdown.lua",
"chars": 11046,
"preview": "local TweenService = game:GetService(\"TweenService\")\nlocal RunService = game:GetService(\"RunService\")\nlocal Themes = req"
},
{
"path": "src/Elements/Indicator.lua",
"chars": 3225,
"preview": "return function(icon, Icon)\n\n\tlocal widget = icon.widget\n\tlocal contents = icon:getInstance(\"Contents\")\n\tlocal indicator"
},
{
"path": "src/Elements/Menu.lua",
"chars": 6070,
"preview": "return function(icon)\n\n\tlocal menu = Instance.new(\"ScrollingFrame\")\n\tmenu.Name = \"Menu\"\n\tmenu.BackgroundTransparency = 1"
},
{
"path": "src/Elements/Notice.lua",
"chars": 3314,
"preview": "return function(icon, Icon)\n\n\tlocal notice = Instance.new(\"Frame\")\n\tnotice.Name = \"Notice\"\n\tnotice.ZIndex = 25\n\tnotice.A"
},
{
"path": "src/Elements/Selection.lua",
"chars": 1560,
"preview": "return function(Icon)\n\n\t-- Credit to lolmansReturn and Canary Software for\n\t-- retrieving these values\n\tlocal selectionC"
},
{
"path": "src/Elements/Widget.lua",
"chars": 16371,
"preview": "-- I named this 'Widget' instead of 'Icon' to make a clear difference between the icon *object* and\n-- the icon (aka Wid"
},
{
"path": "src/Features/Gamepad.lua",
"chars": 7286,
"preview": "-- As the name suggests, this handles everything related to gamepads\n-- (i.e. Xbox or Playstation controllers) and their"
},
{
"path": "src/Features/Overflow.lua",
"chars": 12690,
"preview": "-- When designing your game for many devices and screen sizes, icons may occasionally\n-- particularly for smaller device"
},
{
"path": "src/Features/Themes/Classic.lua",
"chars": 1155,
"preview": "-- This is to provide backwards compatability with the old Roblox\n-- topbar while experiences transition over to the new"
},
{
"path": "src/Features/Themes/Default.lua",
"chars": 4031,
"preview": "-- Themes in v3 work simply by applying the value (agument[3])\n-- to the property (agument[2]) of an instance within the"
},
{
"path": "src/Features/Themes/init.lua",
"chars": 12244,
"preview": "-- The functions here are dedicated solely to managing theme state\n-- and updating the appearance of instances to match "
},
{
"path": "src/Packages/GoodSignal.lua",
"chars": 6629,
"preview": "--------------------------------------------------------------------------------\n-- Batched Yield-Safe Sig"
},
{
"path": "src/Packages/Janitor.lua",
"chars": 8379,
"preview": "--[[\n-------------------------------------\nThis package was modified by ForeverHD.\n\nPACKAGE MODIFICATIONS:\n\t1. Added pas"
},
{
"path": "src/Reference.lua",
"chars": 1018,
"preview": "-- This module enables you to place Icon wherever you like within the data model while\n-- still enabling third-party app"
},
{
"path": "src/Types.lua",
"chars": 12456,
"preview": "--!strict\n\n-- GoodSignal Types (...but simpler!)\n\n--- Connection\n\ntype Connection<Variant... = ...any> = {\n\tDisconnect: "
},
{
"path": "src/Utility.lua",
"chars": 16170,
"preview": "-- Just generic utility functions which I use and repeat across all my projects\n\n\n\n-- LOCAL\nlocal Utility = {}\nlocal Pla"
},
{
"path": "src/VERSION.lua",
"chars": 1153,
"preview": "--!strict\n-- LOCAL\nlocal VERSION = {}\n\n\n\n-- SHARED\nVERSION.appVersion = \"v3.4.0\"\nVERSION.latestVersion = nil :: string?\n"
},
{
"path": "src/init.lua",
"chars": 37855,
"preview": "--!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"
},
{
"path": "wally.toml",
"chars": 453,
"preview": "[package]\nname = \"1foreverhd/topbarplus\"\ndescription = \"Construct dynamic and intuitive topbar icons. Enhance the appear"
},
{
"path": "withLink.project.json",
"chars": 72,
"preview": "{\n \"name\": \"topbarplus\",\n \"tree\": {\n \"$path\": \"src\"\n }\n }"
},
{
"path": "withoutLink.project.json",
"chars": 126,
"preview": "{\n \"name\": \"topbarplus\",\n \"globIgnorePaths\": [\"**/PackageLink.model.json\"],\n \"tree\": {\n \"$path\": \"src\"\n "
}
]
About this extraction
This page contains the full source code of the 1ForeverHD/TopbarPlus GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (228.5 KB), approximately 59.7k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.