├── Logo.png
├── documentation
├── .gitbook
│ └── assets
│ │ ├── Cover.png
│ │ └── Full sorting example.png
├── custom-fonts
│ ├── upload-font-image.md
│ ├── using-the-fonts.md
│ ├── xml-to-lua.md
│ ├── introduction.md
│ ├── data-module.md
│ ├── import-font-data.md
│ └── font-to-xml.md
├── fundamentals
│ ├── options
│ │ ├── dynamic.md
│ │ ├── pixelated.md
│ │ ├── scale-size
│ │ │ ├── minimum-and-maximum-size.md
│ │ │ └── README.md
│ │ ├── truncation.md
│ │ ├── README.md
│ │ ├── custom-defaults.md
│ │ ├── fonts.md
│ │ └── options-list.md
│ ├── signals
│ │ ├── update.md
│ │ └── README.md
│ ├── text-bounds.md
│ ├── line-breaks.md
│ ├── modification.md
│ └── readme.md
├── README.md
├── fine-control
│ ├── transform-and-style.md
│ ├── introduction.md
│ ├── full-iteration.md
│ └── specific-access.md
└── SUMMARY.md
├── source
├── Options.luau
├── Defaults.luau
├── Fonts.luau
├── CorrectOptions.luau
└── Main.luau
├── LICENSE
└── README.md
/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexanderLindholt/TextPlus/HEAD/Logo.png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexanderLindholt/TextPlus/HEAD/documentation/.gitbook/assets/Cover.png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Full sorting example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexanderLindholt/TextPlus/HEAD/documentation/.gitbook/assets/Full sorting example.png
--------------------------------------------------------------------------------
/documentation/custom-fonts/upload-font-image.md:
--------------------------------------------------------------------------------
1 | # Upload font image
2 |
3 | Upload the image at [the creator hub](https://create.roblox.com/dashboard/creations/upload?assetType=Decal).
4 |
5 | Then go to [your images](https://create.roblox.com/dashboard/creations?activeTab=Image) — _**not decals**_.\
6 | Find your image, click the three dots on it, and then click `Copy Asset ID`.
7 |
--------------------------------------------------------------------------------
/documentation/custom-fonts/using-the-fonts.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: gear
3 | ---
4 |
5 | # Using the fonts
6 |
7 | Using custom fonts is extremely simple.
8 |
9 | Instead of providing a `Font` object, just directly reference the font data table, like this:
10 |
11 | ```lua
12 | local fonts = require(path.to.Fonts)
13 |
14 | Text.Create(
15 | frame,
16 | "Text",
17 | {
18 | Font = fonts.MyFont
19 | }
20 | )
21 | ```
22 |
--------------------------------------------------------------------------------
/documentation/custom-fonts/xml-to-lua.md:
--------------------------------------------------------------------------------
1 | # XML to Luau
2 |
3 | {% stepper %}
4 | {% step %}
5 | ### Visit conversion website
6 |
7 | Head over to my website [LuauXML](https://alexanderlindholt.github.io/LuauXML/).
8 | {% endstep %}
9 |
10 | {% step %}
11 | ### Import XML
12 |
13 | Select or drag-and-drop your XML font file.
14 | {% endstep %}
15 |
16 | {% step %}
17 | ### Copy or save output
18 |
19 | Make sure to save the output for later.
20 | {% endstep %}
21 | {% endstepper %}
22 |
--------------------------------------------------------------------------------
/documentation/custom-fonts/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Text+ offers custom fonts inside of Roblox.\
4 | Any `.ttf` and `.otf` font file is supported.
5 |
6 | It uses 1 image per font.
7 |
8 | {% hint style="info" %}
9 | Weights and styles count as individual fonts.
10 | {% endhint %}
11 |
12 | {% hint style="success" %}
13 | The process is mostly automated, as you will be using some external tools to speed up the process of importing awesome fonts to Roblox!
14 | {% endhint %}
15 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/dynamic.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Dynamic
19 |
20 | `Dynamic` is one of the many options.
21 |
22 | Enabling dynamic will enable automatic re-rendering for when the text frame’s size is updated.
23 |
--------------------------------------------------------------------------------
/documentation/fundamentals/signals/update.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Update
19 |
20 | Every frame has this signal, which fires every time text is rendered in the frame.\
21 | The signal can be retrieved like this:
22 |
23 | ```lua
24 | Text.GetUpdateSignal(frame)
25 | ```
26 |
--------------------------------------------------------------------------------
/documentation/fundamentals/text-bounds.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Text bounds
19 |
20 | Text bounds are automatically calculated upon text creation.\
21 | You can always get the text bounds of a frame like this:
22 |
23 | ```lua
24 | Text.GetBounds(frame)
25 | ```
26 |
27 | The text bounds are represented with a Vector2.
28 |
--------------------------------------------------------------------------------
/documentation/custom-fonts/data-module.md:
--------------------------------------------------------------------------------
1 | # Data module
2 |
3 | {% stepper %}
4 | {% step %}
5 | ### Create module
6 |
7 | Create a new module and name it whatever you want — `Fonts` is recommended.
8 | {% endstep %}
9 |
10 | {% step %}
11 | ### Tag module
12 |
13 | Give the module the [tag](https://create.roblox.com/docs/studio/properties#instance-tags) `Fonts`, so that Text+ can identify it.
14 | {% endstep %}
15 |
16 | {% step %}
17 | ### Module content
18 |
19 | The module has to return a table, like this:
20 |
21 | ```lua
22 | return {
23 |
24 | }
25 | ```
26 | {% endstep %}
27 | {% endstepper %}
28 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/pixelated.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Pixelated
19 |
20 | `Pixelated` is one of the many options.
21 |
22 | If a custom font is used, and this option is enabled, the ImageLabels that make up each character will have their [`ResampleMode`](https://devforum.roblox.com/t/resamplemode-new-property-for-image-gui-objects/1418681) set to `Pixelated`.
23 |
--------------------------------------------------------------------------------
/source/Options.luau:
--------------------------------------------------------------------------------
1 | return {
2 | Font = true,
3 |
4 | Size = true,
5 |
6 | ScaleSize = true,
7 | MinimumSize = true,
8 | MaximumSize = true,
9 |
10 | Color = true,
11 | Transparency = true,
12 |
13 | Pixelated = true,
14 |
15 | Offset = true,
16 | Rotation = true,
17 |
18 | StrokeSize = true,
19 | StrokeColor = true,
20 | StrokeTransparency = true,
21 |
22 | ShadowOffset = true,
23 | ShadowColor = true,
24 | ShadowTransparency = true,
25 |
26 | LineHeight = true,
27 | CharacterSpacing = true,
28 |
29 | Truncate = true,
30 |
31 | XAlignment = true,
32 | YAlignment = true,
33 |
34 | WordSorting = true,
35 | LineSorting = true,
36 |
37 | Dynamic = true
38 | }
--------------------------------------------------------------------------------
/documentation/fundamentals/signals/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: tower-broadcast
3 | layout:
4 | width: default
5 | title:
6 | visible: true
7 | description:
8 | visible: false
9 | tableOfContents:
10 | visible: true
11 | outline:
12 | visible: true
13 | pagination:
14 | visible: true
15 | metadata:
16 | visible: true
17 | ---
18 |
19 | # Signals
20 |
21 | You can enable signals by installing a signal library.
22 |
23 | {% hint style="info" %}
24 | [Signal+](https://devforum.roblox.com/t/3552231) is highly recommended for performance.
25 | {% endhint %}
26 |
27 | Makes sure to [tag](https://create.roblox.com/docs/studio/properties#instance-tags) the module `Signal`.
28 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/scale-size/minimum-and-maximum-size.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Minimum- and maximum-size
19 |
20 | `MinimumSize` and `MaximumSize` are two size-limit options.
21 |
22 | Both options are numbers, that are pixel amounts even though scale-size is enabled.\
23 | You don’t have to provide both, or even any.
24 |
25 | {% hint style="warning" %}
26 | These are only for when scale-size is enabled.
27 | {% endhint %}
28 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/truncation.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Truncation
19 |
20 | `Truncate` is one of the many options.
21 |
22 | ### When enabled:
23 |
24 | When text exceeds the boundaries and can no longer fit within the frame, it will intelligently find/make enough space for `...` at the end.
25 |
26 | For example: `Hello there!` might become `Hello the...` in certain scenarios.
27 |
28 | The dots (called an ellipsis) indicates that there is more text out of sight.
29 |
--------------------------------------------------------------------------------
/documentation/fundamentals/line-breaks.md:
--------------------------------------------------------------------------------
1 | # Line breaks
2 |
3 | You can manually break lines very easily.\
4 | There are two simple ways to do this:
5 |
6 | {% tabs %}
7 | {% tab title="Line-break symbol" %}
8 | You can use `\n` to break to the next line at any place.\
9 | `\n` will not be shown in the text.
10 |
11 | {% hint style="warning" %}
12 | Not a `/` (slash) but a `\` (backslash).
13 | {% endhint %}
14 |
15 | Example:
16 |
17 | ```lua
18 | "First line\nSecond line"
19 | ```
20 | {% endtab %}
21 |
22 | {% tab title="Multiline strings" %}
23 | Using `[[]]` instead of `""` will allow you to create a string that spans multiple lines.\
24 | You simply make an actual line break and it will work.
25 |
26 | Example:
27 |
28 | ```lua
29 | [[First line
30 | Second line]]
31 | ```
32 | {% endtab %}
33 | {% endtabs %}
34 |
--------------------------------------------------------------------------------
/documentation/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: box-archive
3 | ---
4 |
5 | # Installation
6 |
7 | {% stepper %}
8 | {% step %}
9 | ### Get the module
10 |
11 | {% tabs %}
12 | {% tab title="Creator Store" %}
13 | Get Roblox Asset
14 |
15 | * Click `Get Model`.
16 | * Open the ToolBox in Roblox Studio.
17 | * Go to the `Inventory` tab.
18 | * Click on `Text+` to insert.
19 | {% endtab %}
20 |
21 | {% tab title="GitHub" %}
22 | See Latest Release
23 |
24 | * Download the `.rbxm` file.
25 | * Find the file in your file explorer.
26 | * Drag the file into Roblox Studio.
27 | {% endtab %}
28 | {% endtabs %}
29 | {% endstep %}
30 |
31 | {% step %}
32 | ### Place it
33 |
34 | Find a great place for the module, where other scripts can reference it.
35 | {% endstep %}
36 | {% endstepper %}
37 |
--------------------------------------------------------------------------------
/documentation/fine-control/transform-and-style.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: gear
3 | layout:
4 | width: default
5 | title:
6 | visible: true
7 | description:
8 | visible: false
9 | tableOfContents:
10 | visible: true
11 | outline:
12 | visible: true
13 | pagination:
14 | visible: true
15 | metadata:
16 | visible: true
17 | ---
18 |
19 | # Transform and style
20 |
21 | All characters are instances, that can be directly modified.
22 |
23 | * **Roblox fonts:** TextLabels
24 | * **Custom fonts:** ImageLabels
25 |
26 | This means that you can modify and animate them however you want.\
27 | That’s where the creativity plays in — be creative!
28 |
29 | {% hint style="info" %}
30 | For animating, [Tween+](https://devforum.roblox.com/t/3599638) is highly recommended, as it allows for more advanced and customizable animations than TweenService and other alternatives — additionally it includes efficient networking and overall optimization, making it ideal for any use-case.
31 | {% endhint %}
32 |
33 |
--------------------------------------------------------------------------------
/documentation/custom-fonts/import-font-data.md:
--------------------------------------------------------------------------------
1 | # Import font data
2 |
3 | Open the fonts data module you created earlier.
4 |
5 | Add your font like this:
6 |
7 | ```lua
8 | return {
9 | MyFont = -- Paste converted XML here.
10 | }
11 | ```
12 |
13 | Add your image id for the font in the same table as the `Size` and `Characters`, like this:
14 |
15 | ```lua
16 | MyFont = {
17 | Image = 0, -- Image id.
18 | -- Converted XML:
19 | Size = 32,
20 | Characters = {
21 |
22 | }
23 | }
24 | ```
25 |
26 | For fonts that have multiple weights and/or styles, it’s recommended to use the following format:
27 |
28 | ```lua
29 | return {
30 | -- Fonts.
31 | MyFont = {
32 | -- Weights.
33 | Bold = {
34 | -- Styles.
35 | Italic = {
36 | Image = 0, -- Image id.
37 | -- Converted XML:
38 | Size = 32,
39 | Characters = {
40 |
41 | }
42 | }
43 | }
44 | }
45 | }
46 | ```
47 |
48 | {% hint style="success" %}
49 | You will be notified at runtime about any mistakes you made in the font data module.
50 | {% endhint %}
51 |
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Alexander Lindholt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/documentation/fundamentals/modification.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Modification
19 |
20 | You can modify text after the initial creation, like this:
21 |
22 | ```lua
23 | Text.Create(
24 | frame, -- Frame that already has text created within it.
25 | "This text has been modified!" -- New text.
26 | )
27 | ```
28 |
29 | You can even modify the options, like this:
30 |
31 | ```lua
32 | Text.Create(
33 | frame,
34 | "This text has been modified!",
35 | { -- New options (optional).
36 | Size = 12 -- Overwrite size.
37 | -- Everything else will stay like before!
38 | }
39 | )
40 | ```
41 |
42 | {% hint style="info" %}
43 | It will retain all previous options, only overwriting with those you provide as new ones.
44 | {% endhint %}
45 |
46 | {% hint style="success" %}
47 | You can reset any of the options to the default by simply setting it to `false`.
48 | {% endhint %}
49 |
50 |
--------------------------------------------------------------------------------
/documentation/fundamentals/readme.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Introduction
19 |
20 | You’ll be creating text using GUI objects as frames, like this:
21 |
22 | ```lua
23 | local Text = require(script.TextPlus)
24 |
25 | local frame = script.frame
26 |
27 | Text.Create(
28 | frame, -- Parent and boundary.
29 | "This text is awesome!" -- Text.
30 | )
31 | ```
32 |
33 | The text will be wrapped inside of the frame.
34 |
35 | {% hint style="success" %}
36 | The frame can be any [GUI object](https://create.roblox.com/docs/reference/engine/classes/GuiObject).
37 | {% endhint %}
38 |
39 | {% hint style="warning" %}
40 | **Content recognized as a part of the rendered text, including folders, will be cleared upon render.**
41 |
42 | Adding any folders or labels might screw up the rendering process, since instances are cached and re-used.
43 | {% endhint %}
44 |
45 | You can get the current text of a frame at any time like this:
46 |
47 | ```lua
48 | Text.GetText(frame)
49 | ```
50 |
--------------------------------------------------------------------------------
/documentation/fine-control/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Introduction
19 |
20 | The sorting options in the customization are crucial to fine-control. Using word and line sorting, you can not only modify individual characters, but whole words and lines together, easily.
21 |
22 | You can enable the sorting like this:
23 |
24 |
Text.Create(
25 | "This text is awesome!",
26 | frame,
27 | {
28 | LineSorting = true,
29 | WordSorting = true
30 | }
31 | )
32 |
33 |
34 | By default, line and word sorting will both be off, meaning your frame will contain pure characters.
35 |
36 | {% hint style="info" %}
37 | All instances are named numerically, relative to its parent.
38 | {% endhint %}
39 |
40 | Both lines and words will be sorted using folders:\
41 |
\
42 | NAN;_(Full sorting on)_
43 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: ballot
3 | layout:
4 | width: default
5 | title:
6 | visible: true
7 | description:
8 | visible: false
9 | tableOfContents:
10 | visible: true
11 | outline:
12 | visible: true
13 | pagination:
14 | visible: true
15 | metadata:
16 | visible: true
17 | ---
18 |
19 | # Options
20 |
21 | The options are provided in a table. You may provide any amount of options you like.\
22 | Those not provided will fallback to their respective defaults.
23 |
24 | Example:
25 |
26 | Text.Create(
27 | frame,
28 | "This text is awesome!",
29 | { -- Options (optional).
30 | Size = 24,
31 | Color = Color3.fromRGB(255, 255, 255),
32 | XAlignment = "Center",
33 | YAlignment = "Center"
34 | }
35 | )
36 |
37 |
38 |
39 |
40 |
41 |
42 | You can get the current options for a frame at any time like this:
43 |
44 | ```lua
45 | Text.GetOptions(frame)
46 | ```
47 |
48 | {% hint style="info" %}
49 | The options received from the `GetOptions` function will always be valid, and will contain _all_ options except booleans set to `false` and those that aren’t mandatory.
50 | {% endhint %}
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 | An efficient, robust, open-source text-rendering library for
6 | Roblox, featuring custom fonts and advanced text control.
7 |
8 | [

](https://create.roblox.com/store/asset/138658986432597) [

](https://devforum.roblox.com/t/3521684)
9 |
10 |
11 |
12 |
13 |
14 | # ✨ Powerful creativity.
15 | Experience text in Roblox like it should’ve been.
16 | - **Custom fonts:** Use any font file, whether one of your own or one you found online.
17 | - **Advanced control**:
18 | - Easily transform and style any individual character, word or line with freedom.
19 | - Utilize awesome text styling like justified alignments and spacing controls.
20 | - Effortlessly setup text scaling, so that it’s the perfect size on all devices.
21 | - *Differs from `TextScaled`. Learn more at the documentation.*
22 |
23 |
24 |
25 | # ⚡ Fast, stable, intuitive.
26 | - Blazingly fast, lag-free performance.
27 | - Catches errors with clear feedback.
28 | - Proper typing and documentation.
29 |
30 |
31 |
32 | ## Check out the [guides and documentation](https://alexxander.gitbook.io/textplus) to learn all about it!
33 |
--------------------------------------------------------------------------------
/documentation/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [Installation](README.md)
4 |
5 | ## Fundamentals
6 |
7 | * [Introduction](fundamentals/readme.md)
8 | * [Line breaks](fundamentals/line-breaks.md)
9 | * [Options](fundamentals/options/README.md)
10 | * [Options list](fundamentals/options/options-list.md)
11 | * [Fonts](fundamentals/options/fonts.md)
12 | * [Pixelated](fundamentals/options/pixelated.md)
13 | * [Scale-size](fundamentals/options/scale-size/README.md)
14 | * [Minimum- and maximum-size](fundamentals/options/scale-size/minimum-and-maximum-size.md)
15 | * [Truncation](fundamentals/options/truncation.md)
16 | * [Dynamic](fundamentals/options/dynamic.md)
17 | * [Custom defaults](fundamentals/options/custom-defaults.md)
18 | * [Modification](fundamentals/modification.md)
19 | * [Text bounds](fundamentals/text-bounds.md)
20 | * [Signals](fundamentals/signals/README.md)
21 | * [Update](fundamentals/signals/update.md)
22 |
23 | ## Fine-control
24 |
25 | * [Introduction](fine-control/introduction.md)
26 | * [Full iteration](fine-control/full-iteration.md)
27 | * [Specific access](fine-control/specific-access.md)
28 | * [Transform and style](fine-control/transform-and-style.md)
29 |
30 | ## Custom fonts
31 |
32 | * [Introduction](custom-fonts/introduction.md)
33 | * [Data module](custom-fonts/data-module.md)
34 | * [Font to XML](custom-fonts/font-to-xml.md)
35 | * [XML to Luau](custom-fonts/xml-to-lua.md)
36 | * [Upload font image](custom-fonts/upload-font-image.md)
37 | * [Import font data](custom-fonts/import-font-data.md)
38 | * [Using the fonts](custom-fonts/using-the-fonts.md)
39 |
--------------------------------------------------------------------------------
/documentation/fine-control/full-iteration.md:
--------------------------------------------------------------------------------
1 | # Full iteration
2 |
3 | It’s pretty simple to iterate through the text.\
4 | Just make sure you respect sorting.
5 |
6 | ## Simple way
7 |
8 | It’s clean and easy to use the `GetCharacters` function, which will iterate through _all_ characters.\
9 | This function is optimized and respects all sorting.
10 |
11 | You can simply iterate through the characters like this:
12 |
13 | ```lua
14 | for characterNumber, character in Text.GetCharacters(frame) do
15 | -- For Roblox fonts, 'character' will be a TextLabel.
16 | -- For custom fonts, 'character' will be an ImageLabel.
17 | end
18 | ```
19 |
20 | ## Advanced way
21 |
22 | You can also manually loop through.\
23 | This might be useful in certain cases.
24 |
25 | Here’s an example with full sorting on:
26 |
27 | ```lua
28 | for lineNumber, line in frame:GetChildren() do
29 | -- 'line' will be a folder.
30 | for wordNumber, word in line:GetChildren() do
31 | -- 'word' will be a folder.
32 | for characterNumber, character in word:GetChildren() do
33 | -- For Roblox fonts, 'character' will be a TextLabel.
34 | -- For custom fonts, 'character' will be an ImageLabel.
35 | end
36 | end
37 | end
38 | ```
39 |
40 | If you have only one of the sorting types enabled, there will only be one layer of folders, and you’ll have to do something like this:
41 |
42 | ```lua
43 | for wordNumber, word in frame:GetChildren() do
44 | -- 'word' will be a folder.
45 | for characterNumber, character in word:GetChildren() do
46 | -- For Roblox fonts, 'character' will be a TextLabel.
47 | -- For custom fonts, 'character' will be an ImageLabel.
48 | end
49 | end
50 | ```
51 |
52 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/custom-defaults.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: gear
3 | layout:
4 | width: default
5 | title:
6 | visible: true
7 | description:
8 | visible: false
9 | tableOfContents:
10 | visible: true
11 | outline:
12 | visible: true
13 | pagination:
14 | visible: true
15 | metadata:
16 | visible: true
17 | ---
18 |
19 | # Custom defaults
20 |
21 | You can actually replace the default defaults with your own defaults of choice.
22 |
23 | Simply create a new module and name it whatever you want — `TextDefaults` is recommended.\
24 | It’s crucial that you give it the [tag](https://create.roblox.com/docs/studio/properties#instance-tags) `TextDefaults`, so that Text+ can identify it.
25 |
26 | The module has to return a table, like this:
27 |
28 | ```lua
29 | return {
30 |
31 | }
32 | ```
33 |
34 | {% hint style="success" %}
35 | You don’t need to list all options. Those not provided will stay at the default default.
36 | {% endhint %}
37 |
38 | Here’s the default defaults:
39 |
40 | ```lua
41 | return {
42 | Font = Font.new("rbxasset://fonts/families/SourceSansPro.json"),
43 |
44 | Size = 14,
45 |
46 | ScaleSize = nil,
47 | MinimumSize = nil,
48 | MaximumSize = nil,
49 |
50 | Color = Color3.fromRGB(0, 0, 0),
51 | Transparency = 0,
52 |
53 | Offset = Vector2.zero,
54 | Rotation = 0,
55 |
56 | StrokeSize = 5,
57 | StrokeColor = Color3.fromRGB(0, 0, 0),
58 |
59 | ShadowOffset = Vector2.new(0, 20),
60 | ShadowColor = Color3.fromRGB(50, 50, 50),
61 |
62 | LineHeight = 1,
63 | CharacterSpacing = 1,
64 |
65 | Truncate = false,
66 |
67 | XAlignment = "Left",
68 | YAlignment = "Top",
69 |
70 | WordSorting = false,
71 | LineSorting = false,
72 |
73 | Dynamic = false
74 | }
75 | ```
76 |
--------------------------------------------------------------------------------
/source/Defaults.luau:
--------------------------------------------------------------------------------
1 | --!optimize 2
2 |
3 | -- Services.
4 | local CollectionService = game:GetService("CollectionService")
5 |
6 | -- Attempt to find the plugin object.
7 | local plugin = script:FindFirstAncestorOfClass("Plugin")
8 |
9 | -- Options list for validation.
10 | local optionsList = require(script.Parent.Options)
11 |
12 | -- Default defaults.
13 | local defaults = {
14 | Font = Font.new("rbxasset://fonts/families/SourceSansPro.json"),
15 |
16 | Size = 14,
17 |
18 | ScaleSize = nil,
19 | MinimumSize = nil,
20 | MaximumSize = nil,
21 |
22 | Color = Color3.fromRGB(0, 0, 0),
23 | Transparency = 0,
24 |
25 | Pixelated = false,
26 |
27 | Offset = Vector2.zero,
28 | Rotation = 0,
29 |
30 | StrokeSize = 5,
31 | StrokeColor = Color3.fromRGB(0, 0, 0),
32 |
33 | ShadowOffset = Vector2.new(0, 20),
34 | ShadowColor = Color3.fromRGB(50, 50, 50),
35 |
36 | LineHeight = 1,
37 | CharacterSpacing = 1,
38 |
39 | Truncate = false,
40 |
41 | XAlignment = "Left",
42 | YAlignment = "Top",
43 |
44 | WordSorting = false,
45 | LineSorting = false,
46 |
47 | Dynamic = false
48 | }
49 |
50 | -- Merge user defaults.
51 | local userDefaults
52 | if plugin then
53 | for _, instance in plugin:GetDescendants() do
54 | if instance:HasTag("TextDefaults") then
55 | userDefaults = require(instance)
56 | break
57 | end
58 | end
59 | else
60 | userDefaults = CollectionService:GetTagged("TextDefaults")[1]
61 | if userDefaults then userDefaults = require(userDefaults) end
62 | end
63 | if userDefaults and type(userDefaults) == "table" then
64 | for key in userDefaults do
65 | if optionsList[key] then defaults[key] = userDefaults[key] end
66 | end
67 | end
68 |
69 | -- Remove false booleans.
70 | for key, value in defaults do
71 | if value == false then defaults[key] = nil end
72 | end
73 |
74 | -- Return final defaults.
75 | return defaults
--------------------------------------------------------------------------------
/documentation/fine-control/specific-access.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Specific access
19 |
20 | You can always access the exact line, word or character you want by indexing like this:
21 |
22 | ```lua
23 | frame["1"]
24 | ```
25 |
26 | With some sorting enabled, you’ll be able to easily access and loop through specific lines and words.
27 |
28 |
29 |
30 | ### Character (requires having no sorting)
31 |
32 | ```lua
33 | local character = frame["1"] -- (Character-1)
34 | -- For Roblox fonts, 'character' will be a TextLabel.
35 | -- For custom fonts, 'character' will be an ImageLabel.
36 | ```
37 |
38 | ### Word (requires word sorting)
39 |
40 | ```lua
41 | local word = frame["1"] -- (Word-1) — Word sorting.
42 | local word = frame["1"]["1"] -- (Line-1 -> Word-1) — Line+word sorting.
43 | for characterNumber, character in word:GetChildren() do
44 | -- For Roblox fonts, 'character' will be a TextLabel.
45 | -- For custom fonts, 'character' will be an ImageLabel.
46 | end
47 | ```
48 |
49 | ### Line (requires line sorting)
50 |
51 | ```lua
52 | local line = frame["1"] -- (Line-1)
53 |
54 | -- Line and word sorting:
55 | for wordNumber, word in line:GetChildren() do
56 | -- 'word' will be a folder.
57 | for characterNumber, character in word:GetChildren() do
58 | -- For Roblox fonts, 'character' will be a TextLabel.
59 | -- For custom fonts, 'character' will be an ImageLabel.
60 | end
61 | end
62 |
63 | -- Line sorting:
64 | for characterNumber, character in line:GetChildren() do
65 | -- For Roblox fonts, 'character' will be a TextLabel.
66 | -- For custom fonts, 'character' will be an ImageLabel.
67 | end
68 | ```
69 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/fonts.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Fonts
19 |
20 | `Font` is one of the many options.
21 |
22 | It accepts two types of data:
23 |
24 | * [A `Font` object](#user-content-fn-1)[^1].
25 | * A custom font data table.
26 |
27 | You can use Roblox’s officially supported fonts like this:
28 |
29 | ```lua
30 | Text.Create(
31 | frame,
32 | "This text is awesome!",
33 | {
34 | Font = Font.new(
35 | "rbxasset://fonts/families/Arial.json", -- Family.
36 | Enum.FontWeight.Regular, -- Weight.
37 | Enum.FontStyle.Normal -- Style.
38 | )
39 | }
40 | )
41 | ```
42 |
43 | ### Built-in fonts
44 |
45 | You can find a lot of fonts on [the documentation page](https://create.roblox.com/docs/reference/engine/datatypes/Font).
46 |
47 | Simply copy the asset id from the font list and paste it into the `Font` object’s `Family`.
48 |
49 | ### Creator store fonts
50 |
51 | Alternatively, browse many more fonts at [the creator store](https://create.roblox.com/store/fonts).
52 |
53 | Click `Get Font`.\
54 | Create a `TextLabel` in Roblox Studio and apply the font to it.\
55 | Make sure you have the `TextLabel` selected, then run this in the command bar:
56 |
57 | ```lua
58 | print(game.Selection:Get()[1].FontFace.Family)
59 | ```
60 |
61 | It will output the asset id you need.\
62 | Simply copy-and-paste it into the `Font` object’s `Family`.
63 |
64 | ### Custom fonts
65 |
66 | If it’s still not enough, custom fonts offer endless possibilities.\
67 | Learn all about it in the dedicated section:
68 |
69 | {% content-ref url="broken-reference" %}
70 | [Broken link](broken-reference)
71 | {% endcontent-ref %}
72 |
73 | [^1]: A Roblox font object created with `Font.new()`.
74 |
--------------------------------------------------------------------------------
/documentation/custom-fonts/font-to-xml.md:
--------------------------------------------------------------------------------
1 | # Font to XML
2 |
3 | {% stepper %}
4 | {% step %}
5 | #### Visit font generation website
6 |
7 | Head over to [snowb.org](https://snowb.org/), where we’re going to generate the necessary font files.\
8 | It’s a free, simple bitmap font generation website.
9 | {% endstep %}
10 |
11 | {% step %}
12 | #### Select characters
13 |
14 | You’ll have to specify which characters you want to include in your font.\
15 | This is done in the `Glyphs` section in the top-left corner.
16 |
17 | There’s often already all the characters you need by default.\
18 | But feel free to add any extra characters you desire!
19 | {% endstep %}
20 |
21 | {% step %}
22 | #### Font settings
23 |
24 | Head over to the `Font` section, located right below `Glyphs`.
25 |
26 | Get a `.ttf` or `.otf` font file, whether it’s one of your own or one you found online.\
27 | Press `ADD FONT FILE`, and select your file.
28 |
29 | Set `Font Size` to your largest intended use case.
30 |
31 | {% hint style="warning" %}
32 | For pixel-art fonts, you should set the size significantly smaller of course.
33 | {% endhint %}
34 | {% endstep %}
35 |
36 | {% step %}
37 | #### Layout settings
38 |
39 | Head over to the `Layout` section, located right below `Font`.
40 |
41 | Set `Padding` and `Spacing` to `0`.
42 | {% endstep %}
43 |
44 | {% step %}
45 | #### Fill settings
46 |
47 | Head over to the `Fill` section, located in the top-right corner.
48 |
49 | Set `Color` to pure white (255, 255, 255, 100).
50 |
51 | {% hint style="success" %}
52 | A pure white color means leaving your original font color untouched.
53 | {% endhint %}
54 | {% endstep %}
55 |
56 | {% step %}
57 | #### Generate and export files
58 |
59 | Click `Export` in the top bar.\
60 | Input a `File Name` for your font.\
61 | For the `Export Type` select the `.fnt (BMFont XML)` option.
62 |
63 | Press `Save` in the bottom-right corner of the pop-up.\
64 | It should save a `.fnt` and a `.png` file.
65 | {% endstep %}
66 | {% endstepper %}
67 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/scale-size/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout:
3 | width: default
4 | title:
5 | visible: true
6 | description:
7 | visible: false
8 | tableOfContents:
9 | visible: true
10 | outline:
11 | visible: true
12 | pagination:
13 | visible: true
14 | metadata:
15 | visible: true
16 | ---
17 |
18 | # Scale-size
19 |
20 | There are six scale-size options:
21 |
22 | * "RootX"
23 | * "RootY"
24 | * "RootXY"\
25 |
26 | * "FrameX"
27 | * "FrameY"
28 | * "FrameXY"
29 |
30 | Enabling scale-size will make `Size` a percentage of the root/frame size instead of a fixed pixel amount. Additionally, it will make all other offsets and sizes relative to the `Size`.
31 |
32 | It will use the specified axis — for `XY` it will get the in-between of the two axis sizes.
33 |
34 | {% hint style="info" %}
35 | When using a\
36 | \- ScreenGui: `Root` is the user’s screen.\
37 | \- SurfaceGui: `Root` is the SurfaceGui.
38 |
39 | `Frame` will always be the text the frame is contained within.
40 | {% endhint %}
41 |
42 | ## Visualizing & calculating size (for root only)
43 |
44 | You can easily find the `Size` that maintains the aspect ratio you are seeing in the studio viewport using the code snippets I’ve made for you.
45 |
46 | Run the code for your GUI root in the command bar:
47 |
48 | ### ScreenGui
49 |
50 | ```lua
51 | local scaleSize = "X" local textLabelSize = 14 local viewportSize = workspace.CurrentCamera.ViewportSize if scaleSize == "XY" then viewportSize = (viewportSize.X + viewportSize.Y)/2 else viewportSize = viewportSize[scaleSize] end warn(math.round(textLabelSize/viewportSize*100*1000)/1000)
52 | ```
53 |
54 | ### SurfaceGui
55 |
56 | _Make sure to select your SurfaceGui before running._
57 |
58 | {% code fullWidth="false" %}
59 | ```lua
60 | local scaleSize = "X" local textLabelSize = 14 local surfaceGui = game.Selection:Get()[1] local adornee = surfaceGui.Adornee if not adornee then adornee = surfaceGui.Parent end local viewportSize = nil local face = surfaceGui.Face local pixelsPerStud = surfaceGui.PixelsPerStud local partSize = adornee.Size if face == Enum.NormalId.Front or face == Enum.NormalId.Back then viewportSize = Vector2.new(partSize.X*pixelsPerStud, partSize.Y*pixelsPerStud) elseif face == Enum.NormalId.Left or face == Enum.NormalId.Right then viewportSize = Vector2.new(partSize.Z*pixelsPerStud, partSize.Y*pixelsPerStud) else viewportSize = Vector2.new(partSize.X*pixelsPerStud, partSize.Z*pixelsPerStud) end if scaleSize == "XY" then viewportSize = (viewportSize.X + viewportSize.Y)/2 else viewportSize = viewportSize[scaleSize] end warn(math.round(textLabelSize/viewportSize*100*1000)/1000)
61 | ```
62 | {% endcode %}
63 |
64 | \
65 | NAN;_Make sure to input your desired `ScaleSize` type/axis and the TextLabel’s size._
66 |
67 | It should print out a number in the `Output` window. This is what you want to input for the `Size` to maintain the aspect ratio you are seeing in studio.
68 |
--------------------------------------------------------------------------------
/documentation/fundamentals/options/options-list.md:
--------------------------------------------------------------------------------
1 | ---
2 | icon: list
3 | layout:
4 | width: default
5 | title:
6 | visible: true
7 | description:
8 | visible: false
9 | tableOfContents:
10 | visible: true
11 | outline:
12 | visible: true
13 | pagination:
14 | visible: true
15 | metadata:
16 | visible: true
17 | ---
18 |
19 | # Options list
20 |
21 | {% hint style="success" %}
22 | Most of the following defaults can be overwritten by [custom defaults](custom-defaults.md).
23 | {% endhint %}
24 |
25 | {% hint style="warning" %}
26 | Default values marked with _**bold italic**_ can’t be overwritten by custom defaults.
27 | {% endhint %}
28 |
29 | {% hint style="info" %}
30 | _**C****o****l****o****re****d**** groups:**_\
31 | NAN;_**All**** ****`nil`**** ****by default, but if 1 or more of them are provided, they will default to the defaults.**_
32 | {% endhint %}
33 |
34 | * **Font:** Font | CustomFont\
35 | NAN;_Default:_ `Font.new("rbxasset://fonts/families/SourceSansPro.json")`
36 |
37 |
38 | * **Size:** number\
39 | NAN;_Default:_ `14`\
40 |
41 | * **ScaleSize:** "RootX" | "RootY" | "RootXY" |\
42 | "FrameX" | "FrameY" | "FrameXY"\
43 | NAN;_Default:_ `nil`
44 | * **MinimumSize:** number\
45 | NAN;_Default:_ `nil`
46 | * **MaximumSize:** number\
47 | NAN;_Default:_ `nil`\
48 |
49 | * **Color:** Color3\
50 | NAN;_Default:_ `Color3.fromRGB(0, 0, 0)`
51 | * **Transparency:** number\
52 | NAN;_Default:_ `0`\
53 |
54 | * **Pixelated:** boolean\
55 | NAN;_Default:_ `false`\
56 |
57 | * **Offset:** Vector2\
58 | NAN;_Default:_ `Vector2.new(0, 0)`
59 | * **Rotation:** number\
60 | NAN;_Default:_ `0`\
61 |
62 | * **StrokeSize:** number\
63 | NAN;_Default:_ `5`
64 | * **StrokeColor:** Color3\
65 | NAN;_Default:_ `Color3.fromRGB(0, 0, 0)`
66 | * **StrokeTransparency:** number\
67 | NAN;_Default:_ _**`Transparency`**_\
68 |
69 | * **ShadowOffset:** Vector2\
70 | NAN;_Default:_ `Vector2.new(0, 20)`
71 | * **ShadowColor:** Color3\
72 | NAN;_Default:_ `Color3.fromRGB(50, 50, 50)`
73 | * **ShadowTransparency:** number\
74 | NAN;_Default:_ _**`Transparency`**_\
75 |
76 | * **LineHeight:** number\
77 | NAN;_Default:_ `1`
78 | * **CharacterSpacing:** number\
79 | NAN;_Default:_ `1`\
80 |
81 | * **Truncate:** boolean\
82 | NAN;_Default:_ `false`\
83 |
84 | * **XAlignment:** "Left" | "Center" | "Right" | "Justified"\
85 | NAN;_Default:_ `"Left"`
86 | * **YAlignment:** "Top" | "Center" | "Bottom" | "Justified"\
87 | NAN;_Default:_ `"Top"`\
88 |
89 | * **WordSorting:** boolean\
90 | NAN;_Default:_ `false`
91 | * **LineSorting:** boolean\
92 | NAN;_Default:_ `false`\
93 |
94 | * **Dynamic:** boolean\
95 | NAN;_Default:_ `false`
96 |
--------------------------------------------------------------------------------
/source/Fonts.luau:
--------------------------------------------------------------------------------
1 | --!optimize 2
2 |
3 | -- Services.
4 | local CollectionService = game:GetService("CollectionService")
5 | local Players = game:GetService("Players")
6 |
7 | -- Load and store all custom fonts.
8 | local userFonts = {}
9 |
10 | for _, module in CollectionService:GetTagged("Fonts") do
11 | if module:IsA("ModuleScript") then
12 | local fullModuleName = module:GetFullName()
13 |
14 | local fonts = require(module)
15 |
16 | if type(fonts) ~= "table" then
17 | warn("'"..fullModuleName.."' font data is not a table.")
18 | else
19 | if not next(fonts) then
20 | warn("'"..fullModuleName.."' font data table is empty.")
21 | else
22 | local player = Players.LocalPlayer
23 | local load
24 | if player then -- If running on client.
25 | local screenGui = Instance.new("ScreenGui")
26 | screenGui.Parent = player.PlayerGui
27 |
28 | local loading = 0
29 | load = function(image) -- For preloading the font image assets.
30 | -- Increment counter for currently loading images.
31 | loading += 1
32 |
33 | -- Setup image label for loading the current font.
34 | local label = Instance.new("ImageLabel")
35 | label.Size = UDim2.fromOffset(1, 1) -- As small as possible.
36 | label.BackgroundTransparency = 1
37 | label.ImageTransparency = 0.999 -- Trick to make the image invisible and still have it be loaded.
38 | label.ResampleMode = Enum.ResamplerMode.Pixelated
39 | label.Image = "rbxassetid://"..tostring(image)
40 | label.Parent = screenGui -- It's crucial that we put it in a visible ScreenGui, otherwise it won't be loaded.
41 |
42 | -- Detect load.
43 | coroutine.resume(coroutine.create(function()
44 | while true do
45 | task.wait()
46 | if label.IsLoaded then
47 | if loading == 1 then
48 | screenGui:Destroy()
49 | else
50 | loading -= 1
51 | end
52 | return
53 | end
54 | end
55 | end))
56 | end
57 | end
58 |
59 | local function handleCharacters(characters, size)
60 | local invertedFontSize = 1/size -- To avoid expensive division.
61 |
62 | for key, value in characters do
63 | -- Verify format.
64 | if type(key) ~= "string" then return end
65 | if type(value) ~= "table" then return end
66 | if type(value[1]) ~= "number" then return end
67 | if type(value[2]) ~= "number" then return end
68 | if typeof(value[3]) ~= "Vector2" then return end
69 | if type(value[4]) ~= "number" then return end
70 | if type(value[5]) ~= "number" then return end
71 | if type(value[6]) ~= "number" then return end
72 |
73 | -- Precalculate normalized offset and x-advance.
74 | value[4] *= invertedFontSize
75 | value[5] *= invertedFontSize
76 | value[6] *= invertedFontSize
77 | end
78 |
79 | return true
80 | end
81 |
82 | local remove = {} -- Because immediate removal will throw off the loop.
83 | local freeze = {} -- Because freezing before removal will result in errors.
84 |
85 | local processFonts
86 |
87 | local function handleTable(key, value, currentPath)
88 | if value.Image or value.Size or value.Characters then
89 | -- Verify format.
90 | if type(value.Image) ~= "number" then
91 | warn("Missing an image ID in '"..currentPath.."'")
92 | table.insert(remove, key)
93 | return
94 | end
95 | if type(value.Size) ~= "number" then
96 | warn("Missing a size in '"..currentPath.."'")
97 | table.insert(remove, key)
98 | return
99 | end
100 | if type(value.Characters) ~= "table" then
101 | warn("Missing characters in '"..currentPath.."'")
102 | table.insert(remove, key)
103 | return
104 | end
105 | if not handleCharacters(value.Characters, value.Size) then -- If not valid characters then.
106 | warn("Invalid characters in '"..currentPath.."'")
107 | table.insert(remove, key)
108 | return
109 | end
110 |
111 | -- Insert for later freeze.
112 | table.insert(freeze, key)
113 |
114 | -- Insert the font into raw fonts table.
115 | userFonts[value] = true
116 |
117 | -- Preload images.
118 | if player then -- If running on client.
119 | load(value.Image)
120 | end
121 | else
122 | processFonts(value, currentPath)
123 | table.freeze(value)
124 | end
125 | end
126 | processFonts = function(parent, parentPath)
127 | for key, value in parent do
128 | if type(value) ~= "table" then
129 | table.insert(remove, key)
130 | else
131 | handleTable(key, value, parentPath.."."..key)
132 | end
133 | end
134 |
135 | for index, key in remove do
136 | parent[key] = nil
137 | remove[index] = nil
138 | end
139 | for index, key in freeze do
140 | table.freeze(parent[key])
141 | freeze[index] = nil
142 | end
143 | end
144 |
145 | handleTable("", fonts, fullModuleName)
146 | if #freeze > 0 then table.freeze(fonts) end
147 | end
148 | end
149 | end
150 | end
151 |
152 | -- Return the global user fonts table.
153 | return userFonts
--------------------------------------------------------------------------------
/source/CorrectOptions.luau:
--------------------------------------------------------------------------------
1 | --!optimize 2
2 | --!native
3 |
4 | -- Services.
5 | local TextService = game:GetService("TextService")
6 |
7 | -- Option defaults.
8 | local defaults = require(script.Parent.Defaults)
9 |
10 | -- User fonts.
11 | local userFonts = require(script.Parent.Fonts)
12 |
13 | -- Option lists for validity checks.
14 | local scaleSizeTypes = {
15 | RootX = true,
16 | RootY = true,
17 | RootXY = true,
18 |
19 | FrameX = true,
20 | FrameY = true,
21 | FrameXY = true
22 | }
23 |
24 | local xAlignments = {
25 | Left = true,
26 | Center = true,
27 | Right = true,
28 | Justified = true
29 | }
30 | local yAlignments = {
31 | Top = true,
32 | Center = true,
33 | Bottom = true,
34 | Justified = true
35 | }
36 |
37 | -- Text params for verifying Roblox fonts.
38 | local textBoundsParams = Instance.new("GetTextBoundsParams")
39 | textBoundsParams.Text = ""
40 |
41 | -- Options corrector.
42 | return function(options)
43 | if not scaleSizeTypes[options.ScaleSize] then
44 | options.ScaleSize = defaults.ScaleSize
45 | end
46 | if not scaleSizeTypes[options.ScaleSize] then
47 | -- Scale-size disabled.
48 | options.ScaleSize = nil
49 | options.MinimumSize = nil
50 | options.MaximumSize = nil
51 |
52 | if type(options.Size) ~= "number" then
53 | options.Size = defaults.Size
54 | elseif options.Size < 1 then
55 | options.Size = 1
56 | end
57 | else
58 | -- Scale-size enabled.
59 | if type(options.MinimumSize) ~= "number" then
60 | options.MinimumSize = defaults.MinimumSize
61 | end
62 | if type(options.MinimumSize) ~= "number" then
63 | options.MinimumSize = nil
64 | elseif options.MinimumSize < 1 then
65 | options.MinimumSize = 1
66 | end
67 |
68 | if type(options.MaximumSize) ~= "number" then
69 | options.MaximumSize = defaults.MaximumSize
70 | end
71 | if type(options.MaximumSize) ~= "number" then
72 | options.MaximumSize = nil
73 | elseif options.MaximumSize < 1 then
74 | options.MaximumSize = 1
75 | end
76 |
77 | if type(options.Size) ~= "number" then
78 | options.Size = defaults.Size
79 | end
80 | end
81 |
82 | local font = options.Font
83 | if font == nil then
84 | options.Font = defaults.Font
85 |
86 | -- Roblox font size limit.
87 | if options.Size > 100 then
88 | options.Size = 100
89 | end
90 | elseif typeof(font) == "Font" then -- Roblox font.
91 | -- Verify font.
92 | textBoundsParams.Font = options.Font
93 | textBoundsParams.Text = ""
94 | local _, result = pcall(TextService.GetTextBoundsAsync, TextService, textBoundsParams)
95 | if type(result) == "string" then
96 | warn("Invalid font. Fallback to default.")
97 | options.Font = defaults.Font
98 | end
99 |
100 | -- Roblox font size limit.
101 | if options.Size > 100 then
102 | options.Size = 100
103 | end
104 | else
105 | if not userFonts[font] then
106 | -- Warn about invalid font.
107 | warn("Invalid font. Fallback to default.")
108 |
109 | -- Apply default font.
110 | options.Font = defaults.Font
111 |
112 | -- Roblox font size limit.
113 | if options.Size > 100 then
114 | options.Size = 100
115 | end
116 | end
117 | end
118 |
119 | local lineHeight = options.LineHeight
120 | if type(lineHeight) ~= "number" then
121 | options.LineHeight = defaults.LineHeight
122 | elseif lineHeight < 0 then
123 | options.LineHeight = 0
124 | end
125 | local characterSpacing = options.CharacterSpacing
126 | if type(characterSpacing) ~= "number" then
127 | options.CharacterSpacing = defaults.CharacterSpacing
128 | elseif characterSpacing < 0 then
129 | options.CharacterSpacing = 0
130 | end
131 |
132 | if typeof(options.Color) ~= "Color3" then
133 | options.Color = defaults.Color
134 | end
135 | if type(options.Transparency) ~= "number" then
136 | options.Transparency = defaults.Transparency
137 | end
138 |
139 | local pixelated = options.Pixelated
140 | if pixelated == false then
141 | options.Pixelated = nil
142 | elseif pixelated ~= true then
143 | options.Pixelated = defaults.Pixelated
144 | end
145 |
146 | if typeof(options.Offset) ~= "Vector2" then
147 | options.Offset = defaults.Offset
148 | end
149 | if type(options.Rotation) ~= "number" then
150 | options.Rotation = defaults.Rotation
151 | end
152 |
153 | local strokeSize = options.StrokeSize
154 | local strokeColor = options.StrokeColor
155 | local strokeTransparency = options.StrokeTransparency
156 | if type(strokeSize) ~= "number" then
157 | if typeof(strokeColor) == "Color3" then
158 | options.StrokeSize = defaults.StrokeSize
159 | if type(strokeTransparency) ~= "number" then
160 | options.StrokeTransparency = options.Transparency
161 | end
162 | elseif type(strokeTransparency) == "number" then
163 | options.StrokeSize = defaults.StrokeSize
164 | if type(strokeColor) ~= "number" then
165 | options.StrokeColor = defaults.StrokeColor
166 | end
167 | else
168 | options.StrokeSize = nil
169 | options.StrokeColor = nil
170 | options.StrokeTransparency = nil
171 | end
172 | else
173 | if strokeSize < 1 then
174 | options.StrokeSize = 1
175 | end
176 | if typeof(strokeColor) ~= "Color3" then
177 | options.StrokeColor = defaults.StrokeColor
178 | end
179 | if type(strokeTransparency) ~= "number" then
180 | options.StrokeTransparency = options.Transparency
181 | end
182 | end
183 |
184 | local shadowOffset = options.ShadowOffset
185 | local shadowColor = options.ShadowColor
186 | local shadowTransparency = options.ShadowTransparency
187 | if typeof(shadowOffset) ~= "Vector2" then
188 | if typeof(shadowColor) == "Color3" then
189 | options.ShadowOffset = defaults.ShadowOffset
190 | if type(shadowTransparency) ~= "number" then
191 | options.ShadowTransparency = options.Transparency
192 | end
193 | elseif type(shadowTransparency) == "number" then
194 | options.ShadowOffset = defaults.ShadowOffset
195 | if type(shadowColor) ~= "number" then
196 | options.ShadowColor = defaults.ShadowColor
197 | end
198 | else
199 | options.ShadowOffset = nil
200 | options.ShadowColor = nil
201 | options.ShadowTransparency = nil
202 | end
203 | else
204 | if typeof(shadowColor) ~= "Color3" then
205 | options.ShadowColor = defaults.ShadowColor
206 | end
207 | if type(shadowTransparency) ~= "number" then
208 | options.ShadowTransparency = options.Transparency
209 | end
210 | end
211 |
212 | local truncate = options.Truncate
213 | if truncate == false then
214 | options.Truncate = nil
215 | elseif truncate ~= true then
216 | options.Truncate = defaults.Truncate
217 | end
218 |
219 | if not xAlignments[options.XAlignment] then
220 | options.XAlignment = defaults.XAlignment
221 | end
222 | if not yAlignments[options.YAlignment] then
223 | options.YAlignment = defaults.YAlignment
224 | end
225 |
226 | local wordSorting = options.WordSorting
227 | if wordSorting == false then
228 | options.WordSorting = nil
229 | elseif wordSorting ~= true then
230 | options.WordSorting = defaults.WordSorting
231 | end
232 |
233 | local lineSorting = options.LineSorting
234 | if lineSorting == false then
235 | options.LineSorting = nil
236 | elseif lineSorting ~= true then
237 | options.LineSorting = defaults.LineSorting
238 | end
239 | end
--------------------------------------------------------------------------------
/source/Main.luau:
--------------------------------------------------------------------------------
1 | --!optimize 2
2 | --!native
3 |
4 | --[[
5 |
6 | TTTTTTTTTTTT
7 | TTTTTTTTTTTTTTTTTTTTTT
8 | TTTTTTTTTTTTTTTTTTTTTT
9 | TT TTTTT ttttt
10 | TTTTTT xxx tttttt
11 | TTTTTT eeeeeeeeee xxxxxxx xxxxxx tttttt +++++
12 | TTTTTT eeeeeeeeeeeeee xxxxxx xxxxx ttttttttttttt +++++
13 | TTTTTT eeeeeee eeeeee xxxxxx xxxxx ttttttttttttt +++++
14 | TTTTTT eeeeee eeeee xxxxxxxxxxx tttttt +++++++++++
15 | TTTTTT eeeeeeeeeeeeeeeeee xxxxxxxxxx tttttt +++++++++++++++++++
16 | TTTTTT eeeeeeeeeeeeeeeeeee xxxxxxxxx tttttt +++++++++++++++++++
17 | TTTTTT eeeee ee xxxxxxxxxxx tttttt +++ +++++
18 | TTTTTT eeeeee xxxxx xxxxxx tttttt +++++
19 | TTTTTT eeeeee eeeeeee xxxxx xxxxxxx tttttt +++++
20 | TTTTTT eeeeeeeeeeeeeee xxxxxx xxxxxx ttttttttt +++++
21 | eeeeeeeeee xxxxxx ttttttttt
22 | ttttttt
23 |
24 | v1.29.1
25 |
26 | An efficient, robust, open-source text-rendering library for
27 | Roblox, featuring custom fonts and advanced text control.
28 |
29 |
30 | GitHub (repository):
31 | https://github.com/AlexanderLindholt/TextPlus
32 |
33 | GitBook (documentation):
34 | https://alexxander.gitbook.io/TextPlus
35 |
36 | DevForum (topic):
37 | https://devforum.roblox.com/t/3521684
38 |
39 |
40 | --------------------------------------------------------------------------------
41 | MIT License
42 |
43 | Copyright (c) 2025 Alexander Lindholt
44 |
45 | Permission is hereby granted, free of charge, to any person obtaining a copy
46 | of this software and associated documentation files (the "Software"), to deal
47 | in the Software without restriction, including without limitation the rights
48 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
49 | copies of the Software, and to permit persons to whom the Software is
50 | furnished to do so, subject to the following conditions:
51 |
52 | The above copyright notice and this permission notice shall be included in all
53 | copies or substantial portions of the Software.
54 |
55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
58 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
59 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
60 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
61 | SOFTWARE.
62 | --------------------------------------------------------------------------------
63 |
64 | ]]--
65 |
66 | -- Services.
67 | local CollectionService = game:GetService("CollectionService")
68 | local TextService = game:GetService("TextService")
69 |
70 | -- Attempt to find the plugin object.
71 | local plugin = script:FindFirstAncestorOfClass("Plugin")
72 |
73 | -- Signal library.
74 | local Signal
75 | if plugin then
76 | for _, instance in plugin:GetDescendants() do
77 | if instance:HasTag("Signal") then
78 | Signal = require(instance)
79 | if type(Signal) == "table" and Signal.new then Signal = Signal.new end
80 | break
81 | end
82 | end
83 | else
84 | Signal = CollectionService:GetTagged("Signal")[1]
85 | if Signal then
86 | Signal = require(Signal)
87 | if type(Signal) == "table" and Signal.new then Signal = Signal.new end
88 | end
89 | end
90 |
91 | -- Player viewport.
92 | local camera = workspace.CurrentCamera
93 |
94 | -- Character for when a character is missing in a custom font.
95 | local missingCharacter = "rbxassetid://75989824347198"
96 |
97 | -- Options list for validation.
98 | local optionsList = require(script.Options)
99 | -- Option defaults.
100 | local defaults = require(script.Defaults)
101 | -- Options corrector.
102 | local correctOptions = require(script.CorrectOptions)
103 |
104 | -- Instance recycling.
105 | local textLabelsAmount, textLabels = 0, {}
106 | local imageLabelsAmount, imageLabels = 0, {}
107 | local uiStrokesAmount, uiStrokes = 0, {}
108 | local foldersAmount, folders = 0, {}
109 | local function getTextLabel()
110 | local instance = textLabels[textLabelsAmount]
111 | if not instance then
112 | textLabelsAmount += 1
113 | return Instance.new("TextLabel")
114 | end
115 | textLabels[textLabelsAmount] = nil
116 | textLabelsAmount -= 1
117 | return instance
118 | end
119 | local function getImageLabel()
120 | local instance = imageLabels[imageLabelsAmount]
121 | if not instance then
122 | imageLabelsAmount += 1
123 | return Instance.new("ImageLabel")
124 | end
125 | imageLabels[imageLabelsAmount] = nil
126 | imageLabelsAmount -= 1
127 | return instance
128 | end
129 | local function getUIStroke()
130 | local instance = uiStrokes[uiStrokesAmount]
131 | if not instance then
132 | uiStrokesAmount += 1
133 | return Instance.new("UIStroke")
134 | end
135 | uiStrokes[uiStrokesAmount] = nil
136 | uiStrokesAmount -= 1
137 | return instance
138 | end
139 | local function getFolder()
140 | local instance = folders[foldersAmount]
141 | if not instance then
142 | foldersAmount += 1
143 | return Instance.new("Folder")
144 | end
145 | folders[foldersAmount] = nil
146 | foldersAmount -= 1
147 | return instance
148 | end
149 |
150 | -- Types.
151 | export type CustomFont = {
152 | Image: number,
153 | Size: number,
154 | Characters: {
155 | [string]: {}
156 | }
157 | }
158 | export type Options = {
159 | Font: Font | CustomFont?,
160 |
161 | Size: number?,
162 |
163 | ScaleSize:
164 | "RootX" | "RootY" | "RootXY" |
165 | "FrameX" | "FrameY" | "FrameXY"?,
166 | MinimumSize: number?,
167 | MaximumSize: number?,
168 |
169 | Color: Color3?,
170 | Transparency: number?,
171 |
172 | Pixelated: boolean?,
173 |
174 | Offset: Vector2?,
175 | Rotation: number?,
176 |
177 | StrokeSize: number?,
178 | StrokeColor: Color3?,
179 | StrokeTransparency: number?,
180 |
181 | ShadowOffset: Vector2?,
182 | ShadowColor: number?,
183 | ShadowTransparency: number?,
184 |
185 | LineHeight: number?,
186 | CharacterSpacing: number?,
187 |
188 | Truncate: boolean?,
189 |
190 | XAlignment: "Left" | "Center" | "Right" | "Justified"?,
191 | YAlignment: "Top" | "Center" | "Bottom" | "Justified"?,
192 |
193 | WordSorting: boolean?,
194 | LineSorting: boolean?,
195 |
196 | Dynamic: boolean?
197 | }
198 |
199 | type Connection = {
200 | Connected: boolean,
201 | Disconnect: typeof(
202 | -- Erases the connection.
203 | function(connection: Connection) end
204 | )
205 | }
206 | type Signal = {
207 | Connect: typeof(
208 | -- Connects a function.
209 | function(signal: Signal, callback: (Parameters...) -> ()): Connection end
210 | ),
211 | Once: typeof(
212 | -- Connects a function, then auto-disconnects after the first call.
213 | function(signal: Signal, callback: (Parameters...) -> ()): Connection end
214 | ),
215 | Wait: typeof(
216 | -- Yields the calling thread until the next fire.
217 | function(signal: Signal): Parameters... end
218 | ),
219 |
220 | Fire: typeof(
221 | -- Runs all connected functions, and resumes all waiting threads.
222 | function(signal: Signal, ...: Parameters...) end
223 | ),
224 |
225 | DisconnectAll: typeof(
226 | -- Erases all connections.
227 | -- Much faster than calling Disconnect on each.
228 | function(signal: Signal) end
229 | ),
230 | Destroy: typeof(
231 | -- Erases all connections and methods, making the signal unusable.
232 | -- Remove references to the signal to delete it completely.
233 | function(signal: Signal) end
234 | )
235 | }
236 |
237 | -- Frame data tables.
238 | local frameText: {string} = {}
239 | local frameOptions: {Options} = {}
240 | local frameTextBounds: {Vector2} = {}
241 | local frameSizeConnections: {RBXScriptSignal} = {}
242 | local frameUpdateSignals: {Signal} = if Signal then {} else nil
243 |
244 | -- Roblox built-in text rendering stuff.
245 | local textBoundsParams = Instance.new("GetTextBoundsParams")
246 | textBoundsParams.Size = 100 -- Size limit for Roblox's built-in text-rendering.
247 |
248 | local characterWidthCache = {}
249 |
250 | -- Custom fonts.
251 | local userFonts = require(script.Fonts)
252 |
253 | -- Module.
254 | local module = {}
255 |
256 | --[[
257 | Returns the last rendered text string for a frame.
258 | ]]--
259 | module.GetText = function(frame: GuiObject): Options
260 | -- Get, verify and return text.
261 | local text = frameText[frame]
262 | if not text then error("Invalid frame.", 2) end
263 | return text
264 | end
265 | --[[
266 | Returns the current options for a frame.
267 | ]]--
268 | module.GetOptions = function(frame: GuiObject): Options
269 | -- Get, verify and return options.
270 | local options = frameOptions[frame]
271 | if not options then error("Invalid frame.", 2) end
272 | return frameOptions[frame]
273 | end
274 | --[[
275 | Returns the last rendered text's bounds for a frame.
276 | ]]--
277 | module.GetBounds = function(frame: GuiObject): Vector2
278 | -- Get, verify and return text bounds.
279 | local textBounds = frameTextBounds[frame]
280 | if not textBounds then error("Invalid frame.", 2) end
281 | return textBounds
282 | end
283 |
284 | --[[
285 | Returns the update signal for a frame.
286 | ]]--
287 | module.GetUpdateSignal = function(frame: GuiObject): Signal
288 | -- Get, verify and return signal.
289 | local signal = frameUpdateSignals[frame]
290 | if not signal then error("Invalid frame.", 2) end
291 | return signal
292 | end
293 |
294 | --[[
295 | Returns a function for iterating through all characters in a frame.
296 |
297 | Ignores sorting folders.
298 | Works with any sorting.
299 | ]]--
300 | module.GetCharacters = function(frame: GuiObject): {TextLabel | ImageLabel}
301 | -- Get and verify options.
302 | local options = frameOptions[frame]
303 | if not options then error("Invalid frame.", 2) end
304 |
305 | -- Create and return iterator.
306 | return coroutine.wrap(function()
307 | -- Identify sorting.
308 | local lineSorting, wordSorting = options.LineSorting, options.WordSorting
309 |
310 | if lineSorting and wordSorting then -- Full sorting.
311 | -- Global character counter.
312 | local index = 0
313 |
314 | -- Loop through lines.
315 | for _, line in frame:GetChildren() do
316 | -- Verify instance.
317 | if line:IsA("Folder") then
318 | -- Loop through words.
319 | for _, word in line:GetChildren() do
320 | -- Loop through characters.
321 | for _, character in word:GetChildren() do
322 | -- Increment global character counter.
323 | index += 1
324 | -- Pass parameters to loop.
325 | coroutine.yield(index, character)
326 | end
327 | end
328 | end
329 | end
330 | elseif lineSorting or wordSorting then -- One sorting.
331 | -- Global character counter.
332 | local index = 0
333 |
334 | -- Loop through words/lines.
335 | for _, folder in frame:GetChildren() do
336 | -- Verify instance.
337 | if folder:IsA("Folder") then
338 | -- Loop through characters.
339 | for _, character in folder:GetChildren() do
340 | -- Increment global character counter.
341 | index += 1
342 | -- Pass parameters to loop.
343 | coroutine.yield(index, character)
344 | end
345 | end
346 | end
347 | else -- No sorting.
348 | -- Identify character instance class for verification.
349 | local characterClass = if type(options.Font) == "table" then "TextLabel" else "ImageLabel"
350 |
351 | -- Loop through characters.
352 | for index, character in frame:GetChildren() do
353 | -- Verify instance.
354 | if character:IsA(characterClass) then
355 | -- Pass parameters to loop.
356 | coroutine.yield(index, character)
357 | end
358 | end
359 | end
360 | end)
361 | end
362 |
363 | local function clear(frame)
364 | -- Get options.
365 | local options = frameOptions[frame]
366 |
367 | -- Identify character instance class and storage table.
368 | local characterTable, characterClass = nil
369 | if type(options.Font) == "table" then
370 | characterTable = imageLabels
371 | characterClass = "ImageLabel"
372 | else
373 | characterTable = textLabels
374 | characterClass = "TextLabel"
375 | end
376 |
377 | -- Setup character stashing.
378 | local function stashCharacter(character)
379 | -- Remove and store character instance.
380 | character.Parent = nil
381 | table.insert(characterTable, character)
382 |
383 | -- Remove and store character's stroke if existent.
384 | local stroke = character:FindFirstChildOfClass("UIStroke")
385 | if stroke then
386 | stroke.Parent = nil
387 | uiStrokes[uiStrokesAmount + 1] = stroke
388 | end
389 |
390 | -- Remove and store the main character if this is a shadow.
391 | local main = character:FindFirstChildOfClass(characterClass)
392 | if main then
393 | -- Remove and store the main character instance.
394 | main.Parent = nil
395 | table.insert(characterTable, main)
396 |
397 | -- Remove and store the main character's stroke if existent.
398 | local mainStroke = main:FindFirstChildOfClass("UIStroke")
399 | if mainStroke then
400 | mainStroke.Parent = nil
401 | uiStrokes[uiStrokesAmount + 1] = mainStroke
402 | end
403 | end
404 | end
405 |
406 | -- Identify sorting.
407 | local lineSorting, wordSorting = options.LineSorting, options.WordSorting
408 |
409 | if lineSorting and wordSorting then -- Full sorting.
410 | -- Loop through lines.
411 | for _, line in frame:GetChildren() do
412 | -- Verify instance.
413 | if not line:IsA("Folder") then continue end
414 |
415 | -- Remove and store line folder.
416 | line.Parent = nil
417 | folders[foldersAmount + 1] = line
418 |
419 | -- Loop through words.
420 | for _, word in line:GetChildren() do
421 | -- Remove and store word folder.
422 | word.Parent = nil
423 | folders[foldersAmount + 1] = word
424 |
425 | -- Loop through characters.
426 | for _, character in word:GetChildren() do
427 | stashCharacter(character)
428 | end
429 | end
430 | end
431 | elseif lineSorting or wordSorting then -- One sorting.
432 | -- Loop through words/lines.
433 | for _, folder in frame:GetChildren() do
434 | -- Verify instance.
435 | if not folder:IsA("Folder") then continue end
436 |
437 | -- Remove and store word/line folder.
438 | folder.Parent = nil
439 | folders[foldersAmount + 1] = folder
440 |
441 | -- Loop through characters.
442 | for _, character in folder:GetChildren() do
443 | stashCharacter(character)
444 | end
445 | end
446 | else -- No sorting.
447 | -- Loop through characters.
448 | for _, character in frame:GetChildren() do
449 | -- Verify instance.
450 | if not character:IsA(characterClass) then continue end
451 |
452 | stashCharacter(character)
453 | end
454 | end
455 | end
456 | local function render(frame, text, options)
457 | -- Cache frame size.
458 | local frameSize = frame.AbsoluteSize
459 |
460 | local frameWidth = frameSize.X
461 | local frameHeight = frameSize.Y
462 |
463 | -- Handle options.
464 | local font = options.Font
465 |
466 | local size = options.Size
467 |
468 | local color = options.Color
469 | local transparency = options.Transparency
470 |
471 | local offset = options.Offset; local offsetX, offsetY
472 | local rotation = options.Rotation
473 |
474 | local strokeSize = options.StrokeSize
475 |
476 | local shadowOffset = options.ShadowOffset; local shadowOffsetX, shadowOffsetY
477 |
478 | local lineHeight = options.LineHeight
479 | local characterSpacing = options.CharacterSpacing
480 |
481 | local truncationEnabled = options.Truncate
482 |
483 | local xAlignment = options.XAlignment
484 | local yAlignment = options.YAlignment
485 |
486 | local wordSorting = options.WordSorting
487 | local lineSorting = options.LineSorting
488 |
489 | local scaleSize = options.ScaleSize
490 | if scaleSize then
491 | -- Scale size.
492 | if scaleSize:sub(1, 1) == "R" then -- Relative to root.
493 | -- Find root size.
494 | local root = frame:FindFirstAncestorOfClass("GuiBase")
495 | local rootSize = if root then
496 | if root:IsA("ScreenGui") then
497 | camera.ViewportSize
498 | else
499 | root.AbsoluteSize
500 | else
501 | Vector2.zero
502 |
503 | -- Scale size.
504 | if scaleSize == "RootX" then
505 | size = size*0.01*rootSize.X
506 | elseif scaleSize == "RootY" then
507 | size = size*0.01*rootSize.Y
508 | else
509 | size = size*0.01*(rootSize.X + rootSize.Y)/2
510 | end
511 | else -- Relative to frame.
512 | if scaleSize == "FrameX" then
513 | size = size*0.01*frameWidth
514 | elseif scaleSize == "FrameY" then
515 | size = size*0.01*frameHeight
516 | else
517 | size = size*0.01*(frameWidth + frameHeight)/2
518 | end
519 | end
520 |
521 | -- Limit scaled size.
522 | if size < 1 then
523 | size = 1
524 | else
525 | -- Custom limits.
526 | local minimumSize = options.MinimumSize
527 | if minimumSize and options.Size < minimumSize then
528 | options.Size = minimumSize
529 | end
530 | local maximumSize = options.MaximumSize
531 | if maximumSize and options.Size > maximumSize then
532 | options.Size = maximumSize
533 | end
534 |
535 | -- Roblox font limit.
536 | if type(font) ~= "table" and size > 100 then
537 | size = 100
538 | end
539 | end
540 |
541 | -- Ensure integer size.
542 | size = math.round(size)
543 |
544 | -- Scale the related options.
545 | offsetX, offsetY = math.round(offset.X*0.01*size), math.round(offset.Y*0.01*size)
546 | if strokeSize then strokeSize = math.round(strokeSize*0.01*size) end
547 | if shadowOffset then shadowOffsetX, shadowOffsetY = math.round(shadowOffset.X*0.01*size), math.round(shadowOffset.Y*0.01*size) end
548 | else
549 | -- Ensure integer size.
550 | size = math.round(size)
551 |
552 | -- Save offsets in optimized format.
553 | offsetX, offsetY = offset.X, offset.Y
554 | if shadowOffset then shadowOffsetX, shadowOffsetY = shadowOffset.X, shadowOffset.Y end
555 | end
556 |
557 | lineHeight *= size
558 |
559 | -- Setup character functions.
560 | local getCharacterWidth, createCharacter
561 | if type(font) == "table" then
562 | -- Custom font.
563 | local image = "rbxassetid://"..tostring(font.Image)
564 | local scaleFactor = size/font.Size
565 | local characters = font.Characters
566 | local resampleMode = if options.Pixelated then Enum.ResamplerMode.Pixelated else Enum.ResamplerMode.Default
567 |
568 | --[[
569 | Character data (table):
570 | [1] = number - Size x
571 | [2] = number - Size y
572 | [3] = Vector2 - Image offset
573 | [4] = number - Offset x
574 | [5] = number - Offset y
575 | [6] = number - X advance
576 | ]]--
577 |
578 | getCharacterWidth = function(character)
579 | local data = characters[character]
580 | return if data then
581 | data[6]*size*characterSpacing
582 | else -- Missing character.
583 | size*characterSpacing -- The 'missing' character is square, so height and width is the same.
584 | end
585 | if shadowOffset then
586 | -- Shadow.
587 | local shadowColor = options.ShadowColor
588 | local shadowTransparency = options.ShadowTransparency
589 |
590 | createCharacter = function(character, x, y)
591 | -- Calculate information.
592 | local data = characters[character]
593 | if data then
594 | -- Cache character data.
595 | local width = data[1]
596 | local height = data[2]
597 | local imageSize = Vector2.new(width, height)
598 | local imageOffset = data[3]
599 |
600 | -- Calculate position and size.
601 | local realX = x + data[4]*size
602 | local realY = y + data[5]*size
603 | local characterSize = UDim2.fromOffset(
604 | math.round(realX + width*scaleFactor) - math.round(realX),
605 | math.round(realY + height*scaleFactor) - math.round(realY)
606 | )
607 |
608 | -- Character shadow.
609 | local shadow = getImageLabel()
610 | do
611 | -- Stylize.
612 | shadow.BackgroundTransparency = 1
613 | shadow.Image = image
614 | shadow.ImageColor3 = shadowColor
615 | shadow.ImageTransparency = shadowTransparency
616 | shadow.ResampleMode = resampleMode
617 | -- Image cutout.
618 | shadow.ImageRectSize = imageSize
619 | shadow.ImageRectOffset = imageOffset
620 | -- Transformation.
621 | shadow.Size = characterSize
622 | shadow.Position = UDim2.fromOffset(
623 | math.round(realX) + offsetX + shadowOffsetX,
624 | math.round(realY) + offsetY + shadowOffsetY
625 | )
626 | shadow.Rotation = rotation
627 | end
628 | -- Main character.
629 | do
630 | -- Create and stylize.
631 | local main = getImageLabel()
632 | main.BackgroundTransparency = 1
633 | main.Image = image
634 | main.ImageColor3 = color
635 | main.ImageTransparency = transparency
636 | main.ResampleMode = resampleMode
637 | -- Image cutout.
638 | main.ImageRectSize = imageSize
639 | main.ImageRectOffset = imageOffset
640 | -- Transformation.
641 | main.Size = characterSize
642 | main.Position = UDim2.fromOffset(-shadowOffsetX, -shadowOffsetY) -- Counteract the shadow offset.
643 | -- Name and parent.
644 | main.Name = "Main"
645 | main.Parent = shadow
646 | end
647 |
648 | -- Return character instance.
649 | return shadow
650 | else -- Missing character.
651 | -- Create and stylize.
652 | local imageLabel = getImageLabel()
653 | imageLabel.BackgroundTransparency = 1
654 | imageLabel.Image = missingCharacter
655 | imageLabel.ImageColor3 = color
656 | imageLabel.ImageTransparency = transparency
657 | imageLabel.ResampleMode = resampleMode
658 | -- Transformation.
659 | imageLabel.Size = UDim2.fromOffset(size, size)
660 | imageLabel.Position = UDim2.fromOffset(
661 | math.round(x + size) + offsetX,
662 | math.round(y + size) + offsetY
663 | )
664 | imageLabel.Rotation = rotation
665 |
666 | -- Return character instance.
667 | return imageLabel
668 | end
669 | end
670 | else
671 | -- No shadow.
672 | createCharacter = function(character, x, y)
673 | local data = characters[character]
674 | if data then
675 | -- Create and stylize.
676 | local imageLabel = getImageLabel()
677 | imageLabel.BackgroundTransparency = 1
678 | imageLabel.Image = image
679 | imageLabel.ImageColor3 = color
680 | imageLabel.ImageTransparency = transparency
681 | imageLabel.ResampleMode = resampleMode
682 | -- Image cutout.
683 | local width = data[1]
684 | local height = data[2]
685 | imageLabel.ImageRectSize = Vector2.new(width, height)
686 | imageLabel.ImageRectOffset = data[3]
687 | -- Transformation.
688 | local realX = x + data[4]*size
689 | local realY = y + data[5]*size
690 | imageLabel.Size = UDim2.fromOffset(
691 | math.round(realX + width*scaleFactor) - math.round(realX),
692 | math.round(realY + height*scaleFactor) - math.round(realY)
693 | )
694 | imageLabel.Position = UDim2.fromOffset(
695 | math.round(realX) + offsetX,
696 | math.round(realY) + offsetY
697 | )
698 | imageLabel.Rotation = rotation
699 |
700 | -- Return character instance.
701 | return imageLabel
702 | else -- Missing character.
703 | -- Create and stylize.
704 | local imageLabel = getImageLabel()
705 | imageLabel.BackgroundTransparency = 1
706 | imageLabel.Image = missingCharacter
707 | imageLabel.ImageColor3 = color
708 | imageLabel.ImageTransparency = transparency
709 | -- Transformation.
710 | imageLabel.Size = UDim2.fromOffset(size, size)
711 | imageLabel.Position = UDim2.fromOffset(
712 | math.round(x + size) + offsetX,
713 | math.round(y + size) + offsetY
714 | )
715 | imageLabel.Rotation = rotation
716 |
717 | -- Return character instance.
718 | return imageLabel
719 | end
720 | end
721 | end
722 | else
723 | -- Roblox font.
724 | local strokeColor, strokeTransparency
725 | if strokeSize then
726 | if strokeSize < 1 then strokeSize = 1 end -- Limit again, in case it was scaled.
727 | strokeColor = options.StrokeColor
728 | strokeTransparency = options.StrokeTransparency
729 | end
730 |
731 | local invertedCharacterSpacing = 1/characterSpacing -- To avoid expensive division.
732 | local fontKey = font.Family..tostring(font.Weight.Value)..tostring(font.Style.Value)
733 |
734 | getCharacterWidth = function(character)
735 | local characterKey = character..fontKey
736 | local width = characterWidthCache[characterKey]
737 | if not width then
738 | textBoundsParams.Text = character
739 | width = TextService:GetTextBoundsAsync(textBoundsParams).X*0.01
740 | characterWidthCache[characterKey] = width
741 | end
742 | return width*size*characterSpacing
743 | end
744 | if shadowOffset then
745 | -- Shadow.
746 | local shadowColor = options.ShadowColor
747 | local shadowTransparency = options.ShadowTransparency
748 |
749 | createCharacter = function(character, x, y, width)
750 | -- Calculate size.
751 | local characterSize = UDim2.fromOffset(math.round(width*invertedCharacterSpacing), size)
752 |
753 | -- Character shadow.
754 | local shadow = getTextLabel()
755 | do
756 | -- Stylize.
757 | shadow.BackgroundTransparency = 1
758 | shadow.Text = character
759 | shadow.TextSize = size
760 | shadow.TextColor3 = shadowColor
761 | shadow.TextTransparency = shadowTransparency
762 | shadow.FontFace = font
763 | shadow.TextXAlignment = Enum.TextXAlignment.Left
764 | shadow.TextYAlignment = Enum.TextYAlignment.Top
765 | -- Transformation.
766 | shadow.Size = characterSize
767 | shadow.Rotation = rotation
768 | shadow.Position = UDim2.fromOffset(
769 | x + offsetX + shadowOffsetX,
770 | y + offsetY + shadowOffsetY
771 | )
772 | end
773 | -- Main character.
774 | local main = getTextLabel()
775 | do
776 | -- Stylize.
777 | main.BackgroundTransparency = 1
778 | main.Text = character
779 | main.TextSize = size
780 | main.TextColor3 = color
781 | main.TextTransparency = transparency
782 | main.FontFace = font
783 | main.TextXAlignment = Enum.TextXAlignment.Left
784 | main.TextYAlignment = Enum.TextYAlignment.Top
785 | -- Transform.
786 | main.Size = characterSize
787 | main.Position = UDim2.fromOffset(-shadowOffsetX, -shadowOffsetY) -- Counteract the shadow offset.
788 | -- Name and parent.
789 | main.Name = "Main"
790 | main.Parent = shadow
791 | end
792 | -- Apply stroke if options are given.
793 | if strokeSize then
794 | do
795 | local uiStroke = getUIStroke()
796 | uiStroke.Thickness = strokeSize
797 | uiStroke.Color = strokeColor
798 | uiStroke.Transparency = strokeTransparency
799 | uiStroke.Parent = main
800 | end
801 | do
802 | local uiStroke = getUIStroke()
803 | uiStroke.Thickness = strokeSize
804 | uiStroke.Color = strokeColor
805 | uiStroke.Transparency = strokeTransparency
806 | uiStroke.Parent = shadow
807 | end
808 | end
809 |
810 | -- Return character instance.
811 | return shadow
812 | end
813 | else
814 | -- No shadow.
815 | createCharacter = function(character, x, y, width)
816 | -- Create and stylize.
817 | local textLabel = getTextLabel()
818 | textLabel.BackgroundTransparency = 1
819 | textLabel.Text = character
820 | textLabel.TextSize = size
821 | textLabel.TextColor3 = color
822 | textLabel.TextTransparency = transparency
823 | textLabel.FontFace = font
824 | textLabel.TextXAlignment = Enum.TextXAlignment.Left
825 | textLabel.TextYAlignment = Enum.TextYAlignment.Top
826 | -- Transformation.
827 | textLabel.Size = UDim2.fromOffset(math.round(width*invertedCharacterSpacing), size)
828 | textLabel.Rotation = rotation
829 | textLabel.Position = UDim2.fromOffset(
830 | x + offsetX,
831 | y + offsetY
832 | )
833 | -- Apply stroke if options are given.
834 | if strokeSize then
835 | local uiStroke = getUIStroke()
836 | uiStroke.Thickness = strokeSize
837 | uiStroke.Color = strokeColor
838 | uiStroke.Transparency = strokeTransparency
839 | uiStroke.Parent = textLabel
840 | end
841 | -- Return character instance.
842 | return textLabel
843 | end
844 | end
845 | end
846 |
847 | -- Calculate base information.
848 | local textWidth = if xAlignment == "Justified" then frameWidth else 0
849 |
850 | local spaceWidth = getCharacterWidth(" ")
851 |
852 | local dotWidth = getCharacterWidth(".")
853 | local ellipsisWidth = dotWidth*3
854 |
855 | local lines = {}
856 |
857 | local truncated, truncate
858 | if truncationEnabled then
859 | truncate = function()
860 | -- Line count.
861 | local linesAmount = #lines
862 |
863 | -- Access last line.
864 | local line = lines[linesAmount]
865 | local lineWords = line[1]
866 |
867 | -- If the line is empty, we can simply put ellipsis here.
868 | if #lineWords == 0 then
869 | line[2] = ellipsisWidth
870 |
871 | local dot = {".", dotWidth}
872 | lineWords[1] = {dot, dot, dot}
873 | return
874 | end
875 |
876 | -- Calculate potential line width.
877 | local potentialLineWidth = ellipsisWidth
878 | for _, wordCharacters in lineWords do
879 | if wordCharacters then
880 | for _, characterData in wordCharacters do
881 | potentialLineWidth += characterData[2]
882 | end
883 | end
884 | potentialLineWidth += spaceWidth
885 | end
886 |
887 | -- Remove words one by one and check for space every time.
888 | for index = #lineWords, 1, -1 do
889 | local wordCharacters = lineWords[index]
890 |
891 | -- There may be empty words, caused by consecutive spaces. We skip those.
892 | if not wordCharacters then
893 | lineWords[index] = nil
894 | potentialLineWidth -= spaceWidth
895 | continue
896 | end
897 |
898 | -- Check for space at the end of the word.
899 | if potentialLineWidth < frameWidth then
900 | -- Update line width cache.
901 | line[2] = potentialLineWidth
902 |
903 | -- Add ellipsis and exit.
904 | local dot = {".", dotWidth}
905 | local charactersAmount = #wordCharacters
906 | wordCharacters[charactersAmount + 1] = dot
907 | wordCharacters[charactersAmount + 2] = dot
908 | wordCharacters[charactersAmount + 3] = dot
909 | return
910 | end
911 |
912 | -- Remove characters one by one and check for space every time.
913 | for index = #wordCharacters, 2, -1 do
914 | potentialLineWidth -= wordCharacters[index][2]
915 | wordCharacters[index] = nil
916 |
917 | if potentialLineWidth < frameWidth then
918 | -- Update line width cache.
919 | line[2] = potentialLineWidth
920 |
921 | -- Add ellipsis and exit.
922 | local dot = {".", dotWidth}
923 | local charactersAmount = #wordCharacters
924 | wordCharacters[charactersAmount + 1] = dot
925 | wordCharacters[charactersAmount + 2] = dot
926 | wordCharacters[charactersAmount + 3] = dot
927 | return
928 | end
929 | end
930 |
931 | -- Subtract remaining word width from potential, and remove word.
932 | potentialLineWidth -= spaceWidth + wordCharacters[1][2]
933 | lineWords[index] = nil
934 | end
935 |
936 | -- Stop or continue.
937 | if linesAmount == 1 then
938 | -- Last line, so we have no option but to put the ellipsis here.
939 | line[2] = ellipsisWidth
940 |
941 | local dot = {".", dotWidth}
942 | table.insert(lineWords, {dot, dot, dot})
943 | else
944 | -- Erase current line and repeat truncation on next line.
945 | lines[linesAmount] = nil
946 | truncate()
947 | end
948 | end
949 | end
950 |
951 | local lineWords = {}
952 | local lineWidth = -spaceWidth
953 |
954 | local lineIndex = 1
955 |
956 | for _, line in text:split("\n") do
957 | -- Process line.
958 | if line == "" then -- Means consecutive line-breaks.
959 | if #lineWords > 0 then
960 | -- Update text width.
961 | if lineWidth > textWidth then
962 | textWidth = lineWidth
963 | end
964 | -- Add current line.
965 | lines[lineIndex] = {lineWords, lineWidth}
966 | lineIndex += 1
967 | end
968 | -- Add empty line.
969 | lines[lineIndex] = {{}, 0}
970 | lineIndex += 1
971 | -- Reset line data.
972 | lineWidth = -spaceWidth
973 | lineWords = {}
974 | else
975 | -- Process words.
976 | local wordIndex = 1
977 | for _, word in line:split(" ") do
978 | if word == "" then -- Means consecutive spaces.
979 | lineWords[wordIndex] = false
980 | wordIndex += 1
981 | lineWidth += spaceWidth
982 | else
983 | local wordWidth = spaceWidth
984 | local wordCharacters = {}
985 |
986 | local characterIndex = 1
987 | for character in word:gmatch(utf8.charpattern) do
988 | local characterWidth = getCharacterWidth(character)
989 | wordWidth += characterWidth
990 | wordCharacters[characterIndex] = {character, characterWidth}
991 | characterIndex += 1
992 | end
993 |
994 | if lineWidth + wordWidth > frameWidth and wordIndex > 1 then
995 | -- Update text width.
996 | if lineWidth < frameWidth and lineWidth > textWidth then
997 | textWidth = lineWidth
998 | end
999 |
1000 | -- Truncate if necessary.
1001 | if truncationEnabled and lineIndex*lineHeight + size > frameHeight then
1002 | -- Add word to line.
1003 | lineWords[wordIndex] = wordCharacters
1004 | wordIndex += 1
1005 | -- Add current line.
1006 | lines[lineIndex] = {lineWords, lineWidth}
1007 | lineIndex += 1
1008 |
1009 | -- Truncate and exit.
1010 | truncate()
1011 | truncated = true
1012 | break
1013 | else
1014 | -- Add current line.
1015 | lines[lineIndex] = {lineWords, lineWidth}
1016 | lineIndex += 1
1017 |
1018 | -- Initalize next line with the word that exceeded the boundary.
1019 | lineWords = {wordCharacters}
1020 | wordIndex = 2
1021 | lineWidth = wordWidth
1022 | end
1023 | else
1024 | -- Add word to line.
1025 | lineWords[wordIndex] = wordCharacters
1026 | wordIndex += 1
1027 | lineWidth += wordWidth
1028 | end
1029 | end
1030 | end
1031 |
1032 | -- Update text width.
1033 | if lineWidth > textWidth then
1034 | textWidth = lineWidth
1035 | end
1036 |
1037 | -- Exit if truncated.
1038 | if truncated then break end
1039 |
1040 | -- Add current line.
1041 | lines[lineIndex] = {lineWords, lineWidth}
1042 | lineIndex += 1
1043 | -- Reset line data.
1044 | lineWords = {}
1045 | lineWidth = -spaceWidth
1046 | end
1047 | end
1048 |
1049 | -- Calculate final information and render.
1050 | local textHeight, lineGap, y
1051 | if yAlignment == "Top" then
1052 | textHeight = (lineIndex - 2)*lineHeight + size
1053 | lineGap = 0
1054 | y = 0
1055 | elseif yAlignment == "Center" then
1056 | textHeight = (lineIndex - 2)*lineHeight + size
1057 | lineGap = 0
1058 | y = math.round((frameHeight - textHeight)/2)
1059 | elseif yAlignment == "Bottom" then
1060 | textHeight = (lineIndex - 2)*lineHeight + size
1061 | lineGap = 0
1062 | y = frameHeight - textHeight
1063 | else
1064 | -- Justified alignment.
1065 | if #lines == 1 then
1066 | textHeight = size
1067 | lineGap = 0
1068 | y = 0
1069 | else
1070 | textHeight = frameHeight
1071 | local linesAmount = lineIndex - 2
1072 | lineGap = (frameHeight - (linesAmount*lineHeight + size))/linesAmount
1073 | y = 0
1074 | end
1075 | end
1076 |
1077 | local globalWordCount = 0 -- In case specifically only word sorting is enabled.
1078 | local globalCharacterCount = 0 -- In case no sorting is enabled.
1079 |
1080 | for lineIndex, lineData in lines do
1081 | -- Get the current line's words.
1082 | local words = lineData[1]
1083 |
1084 | -- Horizontal alignment.
1085 | local wordGap, x
1086 | if xAlignment == "Left" then
1087 | wordGap = 0
1088 | x = 0
1089 | elseif xAlignment == "Center" then
1090 | wordGap = 0
1091 | x = math.round((frameWidth - lineData[2])/2)
1092 | elseif xAlignment == "Right" then
1093 | wordGap = 0
1094 | x = frameWidth - lineData[2]
1095 | else
1096 | -- Justified alignment.
1097 | local wordsAmount = #words
1098 | wordGap = if wordsAmount > 1 then
1099 | (frameWidth - lineData[2])/(wordsAmount - 1)
1100 | else
1101 | 0
1102 |
1103 | x = 0
1104 | end
1105 |
1106 | -- Line sorting.
1107 | local lineContainer = frame
1108 | if lineSorting then
1109 | lineContainer = getFolder()
1110 | lineContainer.Name = tostring(lineIndex)
1111 | lineContainer.Parent = frame
1112 | end
1113 |
1114 | -- Create words.
1115 | for wordIndex, word in words do
1116 | if word then -- There may be empty words, caused by consecutive spaces. These we skip.
1117 | local wordContainer
1118 | if wordSorting then
1119 | wordContainer = getFolder()
1120 | -- Numerical naming.
1121 | if lineSorting then
1122 | wordContainer.Name = tostring(wordIndex)
1123 | else
1124 | globalWordCount += 1
1125 | wordContainer.Name = tostring(globalWordCount)
1126 | end
1127 | -- Parent.
1128 | wordContainer.Parent = lineContainer
1129 | else
1130 | wordContainer = lineContainer
1131 | end
1132 |
1133 | -- Create characters.
1134 | for characterIndex, characterData in word do
1135 | local width = characterData[2]
1136 |
1137 | local instance = createCharacter(characterData[1], x, y, width)
1138 | -- Numerical naming.
1139 | if not lineSorting and not wordSorting then
1140 | globalCharacterCount += 1
1141 | instance.Name = tostring(globalCharacterCount)
1142 | else
1143 | instance.Name = tostring(characterIndex)
1144 | end
1145 | -- Parent.
1146 | instance.Parent = wordContainer
1147 |
1148 | -- Add space before the next character.
1149 | x += width
1150 | end
1151 | end
1152 |
1153 | -- Add space before the next word.
1154 | x += spaceWidth + wordGap
1155 | end
1156 |
1157 | -- Add space before the next line.
1158 | y += lineHeight + lineGap
1159 | end
1160 |
1161 | -- Save text bounds.
1162 | frameTextBounds[frame] = Vector2.new(textWidth, textHeight)
1163 |
1164 | -- Fire update signal.
1165 | if Signal then frameUpdateSignals[frame]:Fire() end
1166 | end
1167 |
1168 | local function enableDynamic(frame, text)
1169 | frameSizeConnections[frame] = frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
1170 | -- Clear current text.
1171 | clear(frame)
1172 |
1173 | -- Render new text.
1174 | local text = frameText[frame]
1175 | if text == "" then
1176 | frameTextBounds[frame] = Vector2.zero
1177 | if Signal then frameUpdateSignals[frame]:Fire() end
1178 | else
1179 | render(frame, text, frameOptions[frame])
1180 | end
1181 | end)
1182 | end
1183 | local function create(frame, text, options)
1184 | -- Cache information.
1185 | frameText[frame] = text
1186 | frameOptions[frame] = options
1187 |
1188 | -- Render new text.
1189 | if text == "" then
1190 | frameTextBounds[frame] = Vector2.zero
1191 | if Signal then frameUpdateSignals[frame]:Fire() end
1192 | else
1193 | render(frame, text, options)
1194 | end
1195 | end
1196 |
1197 | --[[
1198 | Creates text in the specified frame.
1199 | If text is already present, it will overwrite text and merge options.
1200 |
1201 | frame: The container and bounding box.
1202 | ]]--
1203 | module.Create = function(frame: GuiObject, text: string, options: Options?)
1204 | -- Find current options.
1205 | local currentOptions = frameOptions[frame]
1206 |
1207 | -- Argument errors.
1208 | if not currentOptions and (typeof(frame) ~= "Instance" or not frame:IsA("GuiObject")) then error("Invalid frame.", 2) end
1209 | if type(text) ~= "string" then error("Invalid text.", 2) end
1210 |
1211 | -- Handle options.
1212 | if currentOptions then -- Text has been created before in this frame.
1213 | -- Clear current text.
1214 | clear(frame)
1215 |
1216 | -- Handle options.
1217 | if type(options) == "table" then
1218 | -- Merge options.
1219 | local newOptions = options
1220 | options = currentOptions
1221 | for key, value in newOptions do
1222 | if optionsList[key] then
1223 | if not value then
1224 | options[key] = nil
1225 | else
1226 | options[key] = value
1227 | end
1228 | else
1229 | warn("Invalid option '"..key.."'.")
1230 | end
1231 | end
1232 | -- Correct new (merged) options.
1233 | correctOptions(options)
1234 | else
1235 | options = currentOptions
1236 | end
1237 |
1238 | -- Handle dynamic, calculate size, and render.
1239 | if type(options.Dynamic) ~= "boolean" then
1240 | options.Dynamic = defaults.Dynamic
1241 | end
1242 | if options.Dynamic == true then
1243 | create(frame, text, options)
1244 | enableDynamic(frame, text, options)
1245 | else
1246 | -- Dynamic disabling.
1247 | if not options.Dynamic then
1248 | local connection = frameSizeConnections[frame]
1249 | if connection then connection:Disconnect() end
1250 | end
1251 |
1252 | -- Get rid of the non-true value.
1253 | options.Dynamic = nil
1254 |
1255 | -- Create.
1256 | create(frame, text, options)
1257 | end
1258 | else -- First text creation for this frame.
1259 | if Signal then frameUpdateSignals[frame] = Signal() end -- Create and save update signal.
1260 |
1261 | -- Correct options.
1262 | if type(options) == "table" then
1263 | for key in options do
1264 | if not optionsList[key] then
1265 | options[key] = nil
1266 | warn("Invalid option '"..key.."'.")
1267 | end
1268 | end
1269 | else
1270 | options = {}
1271 | end
1272 | correctOptions(options)
1273 |
1274 | -- Handle dynamic, calculate size, and render.
1275 | if type(options.Dynamic) ~= "boolean" then
1276 | options.Dynamic = defaults.Dynamic
1277 | end
1278 | if options.Dynamic == true then
1279 | create(frame, text, options)
1280 | enableDynamic(frame, text, options)
1281 | else
1282 | -- Dynamic disabling.
1283 | if not options.Dynamic then
1284 | local connection = frameSizeConnections[frame]
1285 | if connection then connection:Disconnect() end
1286 | end
1287 |
1288 | -- Get rid of the non-true value.
1289 | options.Dynamic = nil
1290 |
1291 | -- Create.
1292 | create(frame, text, options)
1293 | end
1294 |
1295 | -- Handle destroying.
1296 | frame.Destroying:Once(function()
1297 | -- Clear frame.
1298 | clear(frame)
1299 | -- Destroy signals.
1300 | if Signal then
1301 | frameUpdateSignals[frame]:Destroy()
1302 | frameUpdateSignals[frame] = nil
1303 | end
1304 | -- Remove connections.
1305 | frameSizeConnections[frame] = nil
1306 | -- Clear data.
1307 | frameText[frame] = nil
1308 | frameOptions[frame] = nil
1309 | frameTextBounds[frame] = nil
1310 | end)
1311 | end
1312 | end
1313 |
1314 | return table.freeze(module)
1315 |
--------------------------------------------------------------------------------