├── 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 | Demonstration of full sorting\ 42 | &#xNAN;_(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 | &#xNAN;_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 | &#xNAN;_**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 | &#xNAN;_Default:_ `Font.new("rbxasset://fonts/families/SourceSansPro.json")` 36 | 37 | ​ 38 | * **Size:** number\ 39 | &#xNAN;_Default:_ `14`\ 40 | ​ 41 | * **ScaleSize:** "RootX" | "RootY" | "RootXY" |\ 42 | "FrameX" | "FrameY" | "FrameXY"\ 43 | &#xNAN;_Default:_ `nil` 44 | * **MinimumSize:** number\ 45 | &#xNAN;_Default:_ `nil` 46 | * **MaximumSize:** number\ 47 | &#xNAN;_Default:_ `nil`\ 48 | ​ 49 | * **Color:** Color3\ 50 | &#xNAN;_Default:_ `Color3.fromRGB(0, 0, 0)` 51 | * **Transparency:** number\ 52 | &#xNAN;_Default:_ `0`\ 53 | ​ 54 | * **Pixelated:** boolean\ 55 | &#xNAN;_Default:_ `false`\ 56 | ​ 57 | * **Offset:** Vector2\ 58 | &#xNAN;_Default:_ `Vector2.new(0, 0)` 59 | * **Rotation:** number\ 60 | &#xNAN;_Default:_ `0`\ 61 | ​ 62 | * **StrokeSize:** number\ 63 | &#xNAN;_Default:_ `5` 64 | * **StrokeColor:** Color3\ 65 | &#xNAN;_Default:_ `Color3.fromRGB(0, 0, 0)` 66 | * **StrokeTransparency:** number\ 67 | &#xNAN;_Default:_ _**`Transparency`**_\ 68 | ​ 69 | * **ShadowOffset:** Vector2\ 70 | &#xNAN;_Default:_ `Vector2.new(0, 20)` 71 | * **ShadowColor:** Color3\ 72 | &#xNAN;_Default:_ `Color3.fromRGB(50, 50, 50)` 73 | * **ShadowTransparency:** number\ 74 | &#xNAN;_Default:_ _**`Transparency`**_\ 75 | ​ 76 | * **LineHeight:** number\ 77 | &#xNAN;_Default:_ `1` 78 | * **CharacterSpacing:** number\ 79 | &#xNAN;_Default:_ `1`\ 80 | ​ 81 | * **Truncate:** boolean\ 82 | &#xNAN;_Default:_ `false`\ 83 | ​ 84 | * **XAlignment:** "Left" | "Center" | "Right" | "Justified"\ 85 | &#xNAN;_Default:_ `"Left"` 86 | * **YAlignment:** "Top" | "Center" | "Bottom" | "Justified"\ 87 | &#xNAN;_Default:_ `"Top"`\ 88 | ​ 89 | * **WordSorting:** boolean\ 90 | &#xNAN;_Default:_ `false` 91 | * **LineSorting:** boolean\ 92 | &#xNAN;_Default:_ `false`\ 93 | ​ 94 | * **Dynamic:** boolean\ 95 | &#xNAN;_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 | --------------------------------------------------------------------------------