{"id":"biotoxin495/textsizer","name":"textsizer","scope":"biotoxin495","platform":"roblox","description":"A small, dependency-free Roblox utility module that sizes `TextLabel`, `TextButton`, and `TextBox` instances to fit their rendered text.","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"42151fef162da1284d9102cefbd09fed9374ae2aba8f31ba737e6974e029db5e","likes":0,"downloads":0,"install":"forest install biotoxin495/textsizer","url":"https://forest.dev/p/roblox/biotoxin495/textsizer","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/textsizer/files","readme":"\n# TextSizer — Automatic sizing for Roblox UI text\n\n**TextSizer**, a small, dependency-free Roblox utility module that sizes `TextLabel`, `TextButton`, and `TextBox` instances to fit their rendered text.\n\nText-driven UI often needs to grow or shrink as content changes. TextSizer handles this for buttons, tabs, badges, tags, counters, inventory labels, localized text, and other content-driven UI elements.\n\n## Quick example\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal TextSizer = require(\n\tReplicatedStorage:WaitForChild(\"TextSizer\")\n)\n\nlocal handle = TextSizer.Attach(script.Parent, {\n\tAxis = Enum.AutomaticSize.X,\n\tPadding = Vector2.new(16, 6),\n\tMinSize = Vector2.new(80, 32),\n\tMaxSize = Vector2.new(320, 64),\n})\n```\n\nTextSizer performs an initial resize and continues updating the object whenever its text or relevant text-rendering properties change.\n\nWhen the object is no longer needed, destroy the returned handle:\n\n```lua\nhandle:Destroy()\n```\n\n## 🚀 Features\n\n- Measures text without modifying the object\n- Resizes on the X, Y, or both axes\n- Reactively resizes changing text through `TextSizer.Attach()`\n- Supports `TextLabel`, `TextButton`, and `TextBox`\n- Supports minimum and maximum size constraints\n- Supports independent horizontal and vertical padding\n- Can include an existing `UIPadding` child\n- Uses rendered `TextBounds` when possible\n- Falls back to `TextService:GetTextBoundsAsync()` for width-constrained wrapped text\n- Accounts for `FontFace`, rich text, localization, and line-height changes\n- Batches rapid updates with deferred scheduling\n- Warns and leaves objects unchanged when `TextScaled` or `AutomaticSize` is active\n- Fully typed for Luau strict mode\n- No external dependencies\n\n## 🛠️ Installation\n\nPlace the `TextSizer` ModuleScript somewhere accessible to your client code, such as `ReplicatedStorage`.\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal TextSizer = require(\n\tReplicatedStorage:WaitForChild(\"TextSizer\")\n)\n```\n\nTextSizer is intended primarily for client-rendered Roblox UI.\n\n## 📖 Basic Usage\n\nCall `Resize` when a one-time measurement and resize is needed:\n\n```lua\nTextSizer.Resize(button, {\n\tAxis = Enum.AutomaticSize.X,\n\tPadding = Vector2.new(12, 0),\n\tMinSize = Vector2.new(100, 0),\n\tMaxSize = Vector2.new(280, math.huge),\n})\n```\n\nFor content that changes over time, call `Attach` once. The handle immediately resizes the object and continues watching it for relevant changes.\n\n```lua\nlocal handle = TextSizer.Attach(descriptionLabel, {\n\tAxis = Enum.AutomaticSize.Y,\n\tPadding = Vector2.new(0, 8),\n\tMinSize = Vector2.new(0, 24),\n\tMaxSize = Vector2.new(math.huge, 300),\n})\n```\n\nThe handle also destroys itself when its text object is destroyed. Call `Destroy` when the binding is no longer needed:\n\n```lua\nhandle:Destroy()\n```\n\n## Configuration\n\n`Measure`, `Resize`, and `Attach` accept an optional `Options` table.\n\n```lua\nTextSizer.Attach(label, {\n\tAxis = Enum.AutomaticSize.XY,\n\tPadding = Vector2.new(12, 8),\n\tMinSize = Vector2.new(80, 32),\n\tMaxSize = Vector2.new(360, 240),\n\tUseUIPadding = true,\n})\n```\n\n### `Axis`\n\n```lua\nAxis: Enum.AutomaticSize?\n```\n\nDetermines which axes `Resize` and `Attach` manage.\n\n```lua\nAxis = Enum.AutomaticSize.X\n```\n\nManages horizontal sizing and preserves the existing vertical `UDim` value.\n\n```lua\nAxis = Enum.AutomaticSize.Y\n```\n\nManages vertical sizing and preserves the existing horizontal `UDim` value. This is useful for wrapped text whose width is already determined by its layout.\n\n```lua\nAxis = Enum.AutomaticSize.XY\n```\n\nManages both axes.\n\nThe default value is `Enum.AutomaticSize.X`. `Enum.AutomaticSize.None` is not supported.\n\n### `Padding`\n\n```lua\nPadding: Vector2?\n```\n\nAdds pixel padding around the measured text. The X value is added to both the left and right sides, while the Y value is added to both the top and bottom sides.\n\nThe default value is `Vector2.zero`.\n\n```lua\nPadding = Vector2.new(16, 8)\n```\n\n### `MinSize`\n\n```lua\nMinSize: Vector2?\n```\n\nSets the minimum final pixel size on each axis.\n\nThe default value is `Vector2.zero`.\n\n```lua\nMinSize = Vector2.new(100, 40)\n```\n\n### `MaxSize`\n\n```lua\nMaxSize: Vector2?\n```\n\nSets the maximum final pixel size on each axis.\n\nThe default value is infinite on both axes.\n\nWhen `TextWrapped` is enabled and the X axis is being sized, a finite `MaxSize.X` is also used as the width for text measurement.\n\n```lua\nMaxSize = Vector2.new(320, 240)\n```\n\n### `UseUIPadding`\n\n```lua\nUseUIPadding: boolean?\n```\n\nIncludes the resolved values of the first direct `UIPadding` child in the measured size.\n\nThe default value is `false`.\n\n```lua\nTextSizer.Attach(button, {\n\tAxis = Enum.AutomaticSize.X,\n\tUseUIPadding = true,\n})\n```\n\n### `OnResize`\n\n```lua\nOnResize: ((newSize: Vector2, oldSize: Vector2) -> ())?\n```\n\nRuns after a resize changes the applied pixel size. Callback errors are caught and reported as warnings.\n\n```lua\nTextSizer.Attach(label, {\n\tOnResize = function(newSize, oldSize)\n\t\tprint(\"Resized from\", oldSize, \"to\", newSize)\n\tend,\n})\n```\n\n## Supported Text Objects\n\nTextSizer accepts the following Roblox UI classes:\n\n- `TextLabel`\n- `TextButton`\n- `TextBox`\n\nPassing another `Instance` to `Measure`, `Resize`, or `Attach` raises an error.\n\n## Measurement behavior\n\nTextSizer uses the object's rendered `TextBounds` whenever that result is suitable. This preserves behavior associated with the actual rendered object, including localization and line-height changes.\n\nWhen wrapped text must be measured against a width that the object does not currently have, TextSizer falls back to `TextService:GetTextBoundsAsync()` with `GetTextBoundsParams`. The fallback uses the object's `Text`, `FontFace`, `TextSize`, and `RichText` properties.\n\nAll final dimensions are rounded upward to reduce one-pixel clipping at text boundaries.\n\nThe selected `Axis` affects wrapped-text measurement, but `Measure` still returns both measured dimensions.\n\n## Axis Examples\n\n### Fit a button horizontally\n\n```lua\nlocal handle = TextSizer.Attach(button, {\n\tAxis = Enum.AutomaticSize.X,\n\tPadding = Vector2.new(18, 0),\n\tMinSize = Vector2.new(120, 44),\n\tMaxSize = Vector2.new(400, 44),\n})\n```\n\nThe existing Y component of `button.Size` is preserved.\n\n### Grow a wrapped description vertically\n\n```lua\nlabel.TextWrapped = true\nlabel.Size = UDim2.fromOffset(320, 20)\n\nlocal handle = TextSizer.Attach(label, {\n\tAxis = Enum.AutomaticSize.Y,\n\tPadding = Vector2.new(0, 6),\n\tMinSize = Vector2.new(0, 20),\n\tMaxSize = Vector2.new(math.huge, 260),\n})\n```\n\nThe label retains its current width and grows or shrinks vertically.\n\n### Fit a wrapped tooltip within a maximum width\n\n```lua\ntooltip.TextWrapped = true\n\nlocal handle = TextSizer.Attach(tooltip, {\n\tAxis = Enum.AutomaticSize.XY,\n\tPadding = Vector2.new(12, 8),\n\tMinSize = Vector2.new(80, 32),\n\tMaxSize = Vector2.new(320, 220),\n})\n```\n\nA finite `MaxSize.X` supplies the width used to measure wrapped text.\n\n## Manual Updates\n\nThe module automatically responds to supported UI changes when using `Attach`, but a recalculation can also be requested manually:\n\n```lua\nhandle:Refresh()\n```\n\n`Refresh` immediately measures and resizes the attached object.\n\nFor a one-time measurement without changing the object, use `Measure`:\n\n```lua\nlocal measuredSize = TextSizer.Measure(label, {\n\tPadding = Vector2.new(8, 4),\n\tMaxSize = Vector2.new(300, math.huge),\n})\n\nprint(measuredSize)\n```\n\n## Handle Methods\n\n### `handle:Refresh() -> Vector2`\n\nImmediately measures and resizes the attached object.\n\n### `handle:SetOptions(options?)`\n\nReplaces the current options, reconnects any axis-dependent observers, and schedules a refresh.\n\n```lua\nhandle:SetOptions({\n\tAxis = Enum.AutomaticSize.XY,\n\tPadding = Vector2.new(12, 8),\n\tMaxSize = Vector2.new(360, 240),\n})\n```\n\n### `handle:Destroy()`\n\nDisconnects every observer owned by the handle. Calling it more than once is safe.\n\n## 🛡️ TextScaled and AutomaticSize\n\nTextSizer intentionally does not resize objects while `TextScaled` is enabled. `TextScaled` derives the rendered font size from the container size, while TextSizer derives the container size from the text, creating a circular sizing relationship.\n\nTextSizer also does not resize objects whose Roblox `AutomaticSize` property is active. Running both systems on the same object can cause conflicting size writes.\n\nIn either case, TextSizer emits a warning and leaves the object unchanged. The warning is suppressed until the incompatibility changes, preventing repeated warning spam from attached handles.\n\nRecommended setup:\n\n```lua\ntextObject.TextScaled = false\ntextObject.AutomaticSize = Enum.AutomaticSize.None\n```\n\n## UIPadding\n\n`Padding` is usually the simplest choice. Set `UseUIPadding = true` when a text object already has a `UIPadding` child and the measured size should include it.\n\n```lua\nlocal padding = Instance.new(\"UIPadding\")\npadding.PaddingLeft = UDim.new(0, 12)\npadding.PaddingRight = UDim.new(0, 12)\npadding.Parent = button\n\nTextSizer.Attach(button, {\n\tAxis = Enum.AutomaticSize.X,\n\tUseUIPadding = true,\n})\n```\n\nScale-based `UIPadding` values are resolved against the object's current `AbsoluteSize`. Pixel offsets are generally more predictable for content-driven sizing.\n\n## Lifecycle and performance\n\nAttached updates are batched with `task.defer()`. If several observed properties change in one task cycle, TextSizer performs at most one scheduled refresh for that handle.\n\nEach call to `Attach` owns its own connections. Store and destroy the returned handle when replacing UI, changing screens, or otherwise ending the binding's lifetime.\n\nAvoid attaching multiple handles to the same object unless they are deliberately coordinated.\n\n## ⚙️ API Reference\n\n### `TextSizer.Measure`\n\n```lua\nTextSizer.Measure(\n\ttextObject: Instance,\n\toptions: Options?\n): Vector2\n```\n\nMeasures the content and returns the desired pixel size after applying padding and constraints. It does not modify the instance.\n\n### `TextSizer.Resize`\n\n```lua\nTextSizer.Resize(\n\ttextObject: Instance,\n\toptions: Options?\n): Vector2\n```\n\nMeasures the object and applies the result to the selected axes of its `Size` property. Selected axes are written as pixel offsets; axes not selected by `Axis` retain their existing `UDim` values.\n\n### `TextSizer.Attach`\n\n```lua\nTextSizer.Attach(\n\ttextObject: Instance,\n\toptions: Options?\n): Handle\n```\n\nCreates a reactive binding and performs an initial resize.\n\nThe binding observes changes to:\n\n- `Text`\n- `TextSize`\n- `FontFace`\n- `RichText`\n- `TextWrapped`\n- `LineHeight`\n- `TextBounds`\n- `TextScaled`\n- `AutomaticSize`\n\nFor Y-only sizing, it also watches `AbsoluteSize` so changes to the available width can update wrapped text height.\n\n## Complete Example\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal TextSizer = require(\n\tReplicatedStorage:WaitForChild(\"TextSizer\")\n)\n\nlocal label = script.Parent:WaitForChild(\"Description\")\nlabel.TextWrapped = true\nlabel.Size = UDim2.fromOffset(320, 20)\n\nlocal handle = TextSizer.Attach(label, {\n\tAxis = Enum.AutomaticSize.Y,\n\tPadding = Vector2.new(0, 8),\n\tMinSize = Vector2.new(0, 24),\n\tMaxSize = Vector2.new(math.huge, 300),\n})\n\ntask.delay(2, function()\n\thandle:SetOptions({\n\t\tAxis = Enum.AutomaticSize.XY,\n\t\tMaxSize = Vector2.new(360, 300),\n\t})\nend)\n```\n\n## 📝 Notes\n\n- TextSizer is designed primarily for client-rendered Roblox UI.\n- For the most predictable initial measurement, parent the object into its intended GUI hierarchy before calling `Resize` or `Attach`.\n- `TextBounds` and `AbsoluteSize` are most meaningful after the object participates in client layout and rendering.\n- `TextSizer.Measure` returns both dimensions even when `Axis` selects only one axis.\n- Selected axes are applied as pixel offsets; unselected axes retain their existing `UDim` values.\n- Attached updates are deferred and coalesced per handle.\n- Avoid attaching multiple handles to the same object unless they are deliberately coordinated.\n\n## Roblox API references\n\n- [TextService](https://create.roblox.com/docs/reference/engine/classes/TextService)\n- [GetTextBoundsParams](https://create.roblox.com/docs/reference/engine/classes/GetTextBoundsParams)\n- [TextLabel](https://create.roblox.com/docs/reference/engine/classes/TextLabel)\n- [AutomaticSize](https://create.roblox.com/docs/reference/engine/enums/AutomaticSize)\n- [Font](https://create.roblox.com/docs/reference/engine/datatypes/Font)\n\n## License\n\nNo license has been selected in this package. Add the license you want to publish under before releasing the repository.\n\n---\n\nmade with ❤️ by biotoxin495\n","readmeTruncated":false}