{"id":"biotoxin495/scrollframescaler","name":"scrollframescaler","scope":"biotoxin495","platform":"roblox","description":"Automatically keeps a ScrollingFrame's CanvasSize fitted to its content.","version":"1.0.2","latest":"1.0.2","versions":["1.0.0","1.0.1","1.0.2"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"9a524df2fde1cee8e5514e6b21a24e911d6b4019cd2f3ccd2d5400efa72bf858","likes":0,"downloads":0,"install":"forest install biotoxin495/scrollframescaler","url":"https://forest.dev/p/roblox/biotoxin495/scrollframescaler","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/scrollframescaler/files","readme":"# ScrollFrameScaler — Automatic canvas sizing for ScrollingFrames\n\n**ScrollFrameScaler**, a lightweight Roblox utility module that automatically keeps a `ScrollingFrame`'s `CanvasSize` fitted to its content.\n\nKeeping a `ScrollingFrame`'s canvas in sync with dynamic content usually means manually recalculating sizes every time a child is added, removed, resized, repositioned, or hidden — or leaning on Roblox's built-in `AutomaticCanvasSize`, which doesn't account for wrapping layouts, responsive padding, or directly positioned content in a fully predictable way.\n\n**ScrollFrameScaler** handles this for you. It observes the contents of a `ScrollingFrame` and recalculates its canvas whenever anything relevant changes, with support for Roblox layout objects, responsive padding, directly positioned UI elements, and both horizontal and vertical scrolling.\n\n## Quick example\n\n```lua\nlocal ScrollFrameScaler = require(ReplicatedStorage.Modules.ScrollFrameScaler)\n\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n})\n```\n\nBuild your list, grid, or free-form UI normally inside the `ScrollingFrame`. ScrollFrameScaler watches it and keeps `CanvasSize` correct as things change.\n\n## 🚀 Features\n\n- Automatically updates a `ScrollingFrame`’s `CanvasSize`\n- Supports X, Y, or both scrolling axes\n- Supports `UIListLayout`\n- Supports wrapping `UIListLayout` configurations\n- Supports `UIGridLayout`\n- Supports scale- and offset-based `UIPadding`\n- Supports directly positioned `GuiObject` children without a layout\n- Reacts to child addition, removal, resizing, movement, visibility, and layout changes\n- Coalesces rapid changes into a single deferred update\n- Preserves the current `CanvasPosition` when content changes\n- Supports additional configurable canvas padding\n- Automatically disables Roblox’s built-in `AutomaticCanvasSize`\n- Restores the original `AutomaticCanvasSize` value during cleanup\n- Includes optional animated scrolling to a specific descendant\n- Cleans up connections automatically when the `ScrollingFrame` is destroyed\n- Fully typed for Luau strict mode\n\n## 🛠️ Installation\n\nPlace the `ScrollFrameScaler` ModuleScript somewhere accessible to your client code, such as `ReplicatedStorage`.\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal ScrollFrameScaler = require(\n\tReplicatedStorage:WaitForChild(\"ScrollFrameScaler\")\n)\n```\n\n`ScrollFrameScaler` is intended for client-side UI code.\n\n## 📖 Basic Usage\n\nCall `Setup` once for every `ScrollingFrame` that should be managed.\n\n```lua\nlocal ScrollFrameScaler = require(path.To.ScrollFrameScaler)\n\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n})\n```\n\nThe module immediately calculates the required canvas size and continues updating it as the interface changes.\n\nWhen the `ScrollingFrame` is no longer being managed, call `Cleanup`:\n\n```lua\nScrollFrameScaler.Cleanup(scrollingFrame)\n```\n\nCleanup is also performed automatically when the `ScrollingFrame` is destroyed.\n\n## Configuration\n\n`Setup` accepts an optional configuration table.\n\n```lua\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n\tIncludeInvisible = false,\n\tExtraPadding = Vector2.new(0, 16),\n\tPreserveCanvasPosition = true,\n})\n```\n\n### `Axis`\n\n```lua\nAxis: \"X\" | \"Y\" | \"XY\"?\n```\n\nDetermines which parts of `CanvasSize` the module manages.\n\n```lua\nAxis = \"X\"\n```\n\nManages horizontal canvas sizing.\n\n```lua\nAxis = \"Y\"\n```\n\nManages vertical canvas sizing.\n\n```lua\nAxis = \"XY\"\n```\n\nManages both axes.\n\nThe default value is `\"Y\"`.\n\nAn unmanaged axis retains its existing `CanvasSize` scale and offset values.\n\n### `IncludeInvisible`\n\n```lua\nIncludeInvisible: boolean?\n```\n\nDetermines whether invisible `GuiObject` children are included when measuring directly positioned content.\n\nThe default value is `false`.\n\n```lua\nIncludeInvisible = true\n```\n\nThis option primarily affects `ScrollingFrame`s without a supported layout. Layout-based sizing uses the layout’s resolved `AbsoluteContentSize`.\n\n### `ExtraPadding`\n\n```lua\nExtraPadding: Vector2?\n```\n\nAdds additional pixel space to the final calculated canvas size.\n\n```lua\nExtraPadding = Vector2.new(12, 24)\n```\n\nThe X value adds horizontal space, while the Y value adds vertical space.\n\nThe default value is `Vector2.zero`.\n\nThis is applied in addition to any `UIPadding` inside the `ScrollingFrame`.\n\n### `PreserveCanvasPosition`\n\n```lua\nPreserveCanvasPosition: boolean?\n```\n\nDetermines whether the existing scroll position should be preserved when the canvas changes.\n\nThe default value is `true`.\n\nThe preserved position is clamped to the new canvas bounds, preventing the frame from remaining scrolled beyond its available content.\n\n## Supported Layouts\n\n### UIListLayout\n\n`UIListLayout` content is measured using its resolved `AbsoluteContentSize`.\n\nThis supports:\n\n- Vertical lists\n- Horizontal lists\n- Wrapping lists\n- Different child sizes\n- Layout padding\n- Layout ordering\n- Responsive UI sizes\n\n```lua\nlocal layout = Instance.new(\"UIListLayout\")\nlayout.Padding = UDim.new(0, 8)\nlayout.Parent = scrollingFrame\n\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n})\n```\n\n### UIGridLayout\n\n`UIGridLayout` content is also measured using `AbsoluteContentSize`.\n\nThis allows the module to respect Roblox’s resolved grid behavior, including responsive cell sizes, padding, fill direction, and grid configuration.\n\n```lua\nlocal layout = Instance.new(\"UIGridLayout\")\nlayout.CellSize = UDim2.fromOffset(120, 80)\nlayout.CellPadding = UDim2.fromOffset(8, 8)\nlayout.Parent = scrollingFrame\n\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n})\n```\n\n### UIPadding\n\nA direct child `UIPadding` is included in the final canvas calculation.\n\nBoth scale and offset values are supported.\n\n```lua\nlocal padding = Instance.new(\"UIPadding\")\npadding.PaddingTop = UDim.new(0, 12)\npadding.PaddingBottom = UDim.new(0, 12)\npadding.PaddingLeft = UDim.new(0.025, 0)\npadding.PaddingRight = UDim.new(0.025, 0)\npadding.Parent = scrollingFrame\n```\n\n### Directly Positioned Content\n\nWhen no supported layout is present, the module measures the bounds of the direct `GuiObject` children inside the `ScrollingFrame`.\n\n```lua\nlocal item = Instance.new(\"Frame\")\nitem.Position = UDim2.fromOffset(20, 300)\nitem.Size = UDim2.fromOffset(200, 80)\nitem.Parent = scrollingFrame\n\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n})\n```\n\nThe canvas will extend far enough to contain the child’s position and size.\n\nOnly direct `GuiObject` children are used for this calculation. Nested UI should generally be placed inside a direct content container or managed through a layout.\n\n## Manual Updates\n\nThe module automatically responds to supported UI changes, but a recalculation can also be requested manually.\n\n```lua\nScrollFrameScaler.Update(scrollingFrame)\n```\n\nThe `ScrollingFrame` must already have been passed to `Setup`.\n\nA manual update may be useful after a custom UI operation whose final visual state is not immediately represented by one of the watched properties.\n\n## Cleanup\n\nCall `Cleanup` to stop managing a `ScrollingFrame`.\n\n```lua\nScrollFrameScaler.Cleanup(scrollingFrame)\n```\n\nThis:\n\n- Disconnects all event connections\n- Cancels any active focus tween\n- Removes the frame’s internal state\n- Restores its previous `AutomaticCanvasSize` value\n\nCalling `Cleanup` on an unmanaged frame safely does nothing.\n\nCalling `Setup` again on the same frame automatically cleans up the previous configuration before applying the new one.\n\n## Focusing an Object\n\n`ScrollFrameScaler.Focus` scrolls the frame to reveal and align a descendant `GuiObject`.\n\n```lua\nScrollFrameScaler.Focus(scrollingFrame, targetObject)\n```\n\nBy default, the object is centered horizontally and vertically.\n\nIf the object is already fully visible, no scrolling occurs unless `Force` is enabled.\n\n### Focus Options\n\n```lua\nScrollFrameScaler.Focus(scrollingFrame, targetObject, {\n\tTweenInfo = TweenInfo.new(\n\t\t0.3,\n\t\tEnum.EasingStyle.Quint,\n\t\tEnum.EasingDirection.Out\n\t),\n\n\tHorizontalAlignment = Enum.HorizontalAlignment.Center,\n\tVerticalAlignment = Enum.VerticalAlignment.Center,\n\tForce = false,\n})\n```\n\n### `TweenInfo`\n\n```lua\nTweenInfo: TweenInfo?\n```\n\nControls the focus animation.\n\nThe default is:\n\n```lua\nTweenInfo.new(\n\t0.3,\n\tEnum.EasingStyle.Quint,\n\tEnum.EasingDirection.Out\n)\n```\n\n### `HorizontalAlignment`\n\n```lua\nHorizontalAlignment: Enum.HorizontalAlignment?\n```\n\nDetermines the target’s horizontal alignment inside the visible area.\n\nSupported values:\n\n- `Enum.HorizontalAlignment.Left`\n- `Enum.HorizontalAlignment.Center`\n- `Enum.HorizontalAlignment.Right`\n\nThe default is `Center`.\n\n### `VerticalAlignment`\n\n```lua\nVerticalAlignment: Enum.VerticalAlignment?\n```\n\nDetermines the target’s vertical alignment inside the visible area.\n\nSupported values:\n\n- `Enum.VerticalAlignment.Top`\n- `Enum.VerticalAlignment.Center`\n- `Enum.VerticalAlignment.Bottom`\n\nThe default is `Center`.\n\n### `Force`\n\n```lua\nForce: boolean?\n```\n\nWhen `false`, the function does nothing if the target is already fully visible.\n\nWhen `true`, the target is aligned even when it is already within the viewport.\n\nThe default value is `false`.\n\n### Returned Tween\n\n`Focus` returns the created `Tween`.\n\n```lua\nlocal tween = ScrollFrameScaler.Focus(\n\tscrollingFrame,\n\ttargetObject\n)\n\nif tween then\n\ttween.Completed:Wait()\nend\n```\n\nIt returns `nil` when the target is already visible and forced alignment is not enabled.\n\nStarting another focus operation on a managed frame cancels its previous focus tween.\n\n## ⚙️ API Reference\n\n### `ScrollFrameScaler.Setup`\n\n```lua\nScrollFrameScaler.Setup(\n\tscrollFrame: ScrollingFrame,\n\toptions: SetupOptions?\n)\n```\n\nBegins automatically managing the supplied `ScrollingFrame`.\n\n### `ScrollFrameScaler.Update`\n\n```lua\nScrollFrameScaler.Update(\n\tscrollFrame: ScrollingFrame\n)\n```\n\nImmediately recalculates the canvas size of a managed frame.\n\nThrows an error if `Setup` has not been called for that frame.\n\n### `ScrollFrameScaler.Cleanup`\n\n```lua\nScrollFrameScaler.Cleanup(\n\tscrollFrame: ScrollingFrame\n)\n```\n\nStops managing the frame and disconnects all associated listeners.\n\n### `ScrollFrameScaler.Focus`\n\n```lua\nScrollFrameScaler.Focus(\n\tscrollFrame: ScrollingFrame,\n\tobject: GuiObject,\n\toptions: FocusOptions?\n): Tween?\n```\n\nAnimates the frame’s `CanvasPosition` to reveal and align a descendant object.\n\n## Complete Example\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal ScrollFrameScaler = require(\n\tReplicatedStorage:WaitForChild(\"ScrollFrameScaler\")\n)\n\nlocal scrollingFrame = script.Parent:WaitForChild(\"ScrollingFrame\")\nlocal targetItem = scrollingFrame:WaitForChild(\"ImportantItem\")\n\nScrollFrameScaler.Setup(scrollingFrame, {\n\tAxis = \"Y\",\n\tExtraPadding = Vector2.new(0, 12),\n\tPreserveCanvasPosition = true,\n})\n\ntask.delay(2, function()\n\tScrollFrameScaler.Focus(scrollingFrame, targetItem, {\n\t\tVerticalAlignment = Enum.VerticalAlignment.Center,\n\t\tForce = true,\n\t})\nend)\n```\n\n## 📝 Notes\n\n- The module sets `AutomaticCanvasSize` to `Enum.AutomaticSize.None` while managing a frame.\n- The previous `AutomaticCanvasSize` value is restored during cleanup.\n- A `ScrollingFrame` should generally contain only one supported layout object.\n- Layout objects and `UIPadding` should be direct children of the managed frame.\n- Direct content measurement includes direct `GuiObject` children rather than every nested descendant.\n- Canvas dimensions are rounded up to whole pixels.\n- The final canvas size is prevented from becoming negative.\n- The module is intended to be required and used from client-side UI code.\n\n## License\n\nUse and modify the module according to the license included with the release.\n\n---\n\nmade with ❤️ by biotoxin495\n","readmeTruncated":false}