├── LICENSE ├── README.md ├── images ├── CollapsibleTitledSection.gif ├── CustomTextButton.gif ├── ImageButtonWithText.gif ├── LabeledCheckbox.gif ├── LabeledMultiChoice.gif ├── LabeledRadioButton.PNG ├── LabeledSlider.gif ├── LabeledTextInput.gif ├── StatefulImageButton.gif └── VerticalScrollingFrame.gif └── src ├── CollapsibleTitledSection.lua ├── CustomTextButton.lua ├── GuiUtilities.lua ├── ImageButtonWithText.lua ├── LabeledCheckbox.lua ├── LabeledMultiChoice.lua ├── LabeledRadioButton.lua ├── LabeledSlider.lua ├── LabeledTextInput.lua ├── RbxGui.lua ├── StatefulImageButton.lua ├── VerticalScrollingFrame.lua └── VerticallyScalingListFrame.lua /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Studio Widgets

2 | 3 |
4 | A set of GUI elements to use in Roblox Plugins hosted in PluginGUIs. Widgets have a standard "Studio" look & feel. 5 |
6 | 7 |
 
8 | 9 | ## Overview 10 | With PluginGuis, your RobloxPlugins can create GUIs hosted in dockable widgets (as opposed to being hosted in 3d viewport). 11 | 12 | We encourage plugin developers to use this library so that your GUIs have a standardized look & feel: colors, spacing, layout, etc. 13 | 14 | We will keep these libraries up to date as Studio look & feel changes (e.g. automatic support for Dark Theme, when that happens). 15 | 16 | ## Please contribute! 17 | We will work hard to keep this library up to date, bug-free, etc. 18 | 19 | That said, we have a small team and many competing priorities, so your efforts to improve this library are welcome and invited. Feel free to fork the repository and make fixes and improvements as you see fit. We will be happy to merge in any updates that fit within our vision of the library. 20 | 21 | ## Coding conventions 22 | Class and function names are CamelCase, starting with caps. 23 | Variable and member names are CamelCase, starting with lowercase. 24 | Members and methods of classes that begin with '_' are considered "private": should not be read or written outside the class. 25 | 26 | ### Files 27 | 28 | * [CollapsibleTitledSection.lua](#collapsibletitledsectionlua) 29 | * [CustomTextButton.lua](#customtextbuttonlua) 30 | * [GuiUtilities.lua](#customtextbuttonlua) 31 | * [ImageButtonWithText.lua](#imagebuttonwithtextlua) 32 | * [LabeledCheckbox.lua](#labeledcheckboxlua) 33 | * [LabeledMultiChoice.lua](#labeledmultichoicelua) 34 | * [LabeledSlider.lua](#labeledsliderlua) 35 | * [LabeledTextInput.lua](#labeledtextinputlua) 36 | * [RbxGui.lua](#rbxguilua) 37 | * [StatefulImageButton.lua](#statefulimagebuttonlua) 38 | * [VerticallyScalingListFrame.lua](#verticallyscalinglistframelua) 39 | * [VerticalScrollingFrame.lua](#verticalscrollingframelua) 40 | 41 | #### CollapsibleTitledSection.lua 42 | A "Section" containing one or more widgets, with titlebar. Title bar includes rotating arrow widget which can be used to collapse/expand the section. 43 | 44 | ![CollapsibleTitledSection](images/CollapsibleTitledSection.gif) 45 | 46 | ```Lua 47 | local collapse = CollapsibleTitledSection.new( 48 | "suffix", -- name suffix of the gui object 49 | "titleText", -- the text displayed beside the collapsible arrow 50 | true, -- have the content frame auto-update its size? 51 | true, -- minimizable? 52 | false -- minimized by default? 53 | ) 54 | 55 | -- put things we want to be "collapsed" under the frame returned by the :GetContentsFrame() method 56 | local label = Instance.new("TextLabel") 57 | label.Text = "Peekaboo!" 58 | label.Size = UDim2.new(0, 60, 0, 20) 59 | label.BackgroundTransparency = 1 60 | label.BorderSizePixel = 0 61 | label.Parent = collapse:GetContentsFrame() 62 | 63 | -- set the parent of the collapse object by setting the parent of the frame returned by the :GetSectionFrame() method 64 | collapse:GetSectionFrame().Parent = widgetGui 65 | ``` 66 | 67 | #### CustomTextButton.lua 68 | A text button contained in an image (rounded rect). Button and frame highlight appropriately on hover and click. 69 | 70 | ![CustomTextButton](images/CustomTextButton.gif) 71 | 72 | ```Lua 73 | local button = CustomTextButton.new( 74 | "button", -- name of the gui object 75 | "labelText" -- the text displayed on the button 76 | ) 77 | 78 | -- use the :getButton() method to return the ImageButton gui object 79 | local buttonObject = button:GetButton() 80 | buttonObject.Size = UDim2.new(0, 70, 0, 25) 81 | 82 | buttonObject.MouseButton1Click:Connect(function() 83 | print("I was clicked!") 84 | end) 85 | 86 | buttonObject.Parent = widgetGui 87 | ``` 88 | 89 | #### GuiUtilities.lua 90 | Grab bag of functions and definitions used by the rest of the code: colors, spacing, etc. 91 | 92 | #### ImageButtonWithText.lua 93 | A button comprising an image above text. Button highlights appropriately on hover and click. 94 | ![ImageButtonWithText](images/ImageButtonWithText.gif) 95 | 96 | ```Lua 97 | local button = ImageButtonWithText.new( 98 | "imgButton", -- name of the gui object 99 | 1, -- sets the sorting order for use with a UIGridStyleLayout object 100 | "rbxassetid://924320031", -- the asset id of the image 101 | "text", -- button text 102 | UDim2.new(0, 100, 0, 100), -- button size 103 | UDim2.new(0, 70, 0, 70), -- image size 104 | UDim2.new(0, 15, 0, 15), -- image position 105 | UDim2.new(0, 60, 0, 20), -- text size 106 | UDim2.new(0, 20, 0, 80) -- text position 107 | ) 108 | 109 | -- use the :getButton() method to return an ImageButton gui object 110 | local buttonObject = button:getButton() 111 | 112 | buttonObject.MouseButton1Click:Connect(function() 113 | -- use the :setSelected() method to highlight the button 114 | -- use the :getSelected() method to return a boolean that defines if the button is selected or not 115 | button:setSelected(not button:getSelected()) 116 | end) 117 | 118 | buttonObject.Parent = widgetGui 119 | ``` 120 | 121 | #### LabeledCheckbox.lua 122 | A widget comprising a text label and a checkbox. Can be configured in normal or "small" sizing. Layout and spacing change depending on size. 123 | 124 | ![LabeledCheckbox](images/LabeledCheckbox.gif) 125 | 126 | ```Lua 127 | local checkbox = LabeledCheckbox.new( 128 | "suffix", -- name suffix of gui object 129 | "labelText", -- text beside the checkbox 130 | false, -- initial value 131 | false -- initially disabled? 132 | ) 133 | 134 | -- get/set current value of the checkbox 135 | checkbox:SetValue(true) 136 | print(checkbox:GetValue()) 137 | 138 | -- disables and forces a checkbox value 139 | checkbox:DisableWithOverrideValue(false) 140 | if (checkbox:GetDisabled()) then 141 | checkbox:SetDisabled(false) 142 | end 143 | 144 | -- return the label or button frames 145 | print(checkbox:GetLabel()) 146 | print(checkbox:GetButton()) 147 | 148 | -- fire function when checkbox value changes 149 | checkbox:SetValueChangedFunction(function(newValue) 150 | print(newValue); 151 | end) 152 | 153 | -- use :GetFrame() to set the parent of the LabeledCheckbox 154 | checkbox:GetFrame().Parent = widgetGui 155 | ``` 156 | 157 | #### LabeledMultiChoice.lua 158 | A widget comprising a top-level label and a family of radio buttons. Exactly one radio button is always selected. Buttons are in a grid layout and will adjust to flood-fill parent. Height updates based on content. 159 | 160 | ![LabeledMultiChoice](images/LabeledMultiChoice.gif) 161 | 162 | ```Lua 163 | -- each choice must have an Id and Text 164 | local choices = { 165 | {Id = "choice1", Text = "a"}, 166 | {Id = "choice2", Text = "b"}, 167 | {Id = "choice3", Text = "c"} 168 | } 169 | 170 | local multiChoice = LabeledMultiChoice.new( 171 | "suffix", -- name suffix of gui object 172 | "labelText", -- title text of the multi choice 173 | choices, -- choices array 174 | 1 -- the starting index of the selection (in this case choice 1) 175 | ) 176 | 177 | -- get/set selection index 178 | multiChoice:SetSelectedIndex(3) 179 | print(multiChoice:GetSelectedIndex()) 180 | 181 | -- fire function when index value changes 182 | multiChoice:SetValueChangedFunction(function(newIndex) 183 | print(choices[newIndex].Id, choices[newIndex].Text) 184 | end) 185 | 186 | -- use :GetFrame() to set the parent of the LabeledMultiChoice 187 | multiChoice:GetFrame().Parent = widgetGui 188 | ``` 189 | 190 | #### LabeledSlider.lua 191 | A widget comprising a label and a slider control. 192 | 193 | ![LabeledSlider](images/LabeledSlider.gif) 194 | 195 | ```Lua 196 | -- note: the slider is clamped between [0, intervals] 197 | local slider = LabeledSlider.new( 198 | "suffix", -- name suffix of gui object 199 | "labelText", -- title text of the multi choice 200 | 100, -- how many intervals to split the slider into 201 | 50 -- the starting value of the slider 202 | ) 203 | 204 | -- get/set values 205 | slider:SetValue(0) 206 | print(slider:GetValue()) 207 | 208 | -- fire function when slider value changes 209 | slider:SetValueChangedFunction(function(newValue) 210 | print(newValue) 211 | end) 212 | 213 | -- use :GetFrame() to set the parent of the LabeledSlider 214 | slider:GetFrame().Parent = widgetGui 215 | ``` 216 | 217 | #### LabeledTextInput.lua 218 | A widget comprising a label and text edit control. 219 | 220 | ![LabeledTextInput](images/LabeledTextInput.gif) 221 | 222 | ```Lua 223 | local input = LabeledTextInput.new( 224 | "suffix", -- name suffix of gui object 225 | "labelText", -- title text of the multi choice 226 | "Hello world!" -- default value 227 | ) 228 | 229 | -- set/get graphemes which is essentially text character limit but grapemes measure things like emojis too 230 | input:SetMaxGraphemes(20) 231 | input:GetMaxGraphemes() 232 | 233 | -- set/get values methods 234 | input:SetValue("Hello world again...") 235 | print(input:GetValue()) 236 | 237 | -- fire function when input value changes 238 | input:SetValueChangedFunction(function(newValue) 239 | print(newValue) 240 | end) 241 | 242 | -- use :GetFrame() to set the parent of the LabeledTextInput 243 | input:GetFrame().Parent = widgetGui 244 | ``` 245 | 246 | #### RbxGui.lua 247 | Helper functions to support the slider control. 248 | 249 | #### StatefulImageButton.lua 250 | An image button with "on" and "off" states. 251 | 252 | ![StatefulImageButton](images/StatefulImageButton.gif) 253 | 254 | ```Lua 255 | local button = StatefulImageButton.new( 256 | "imgButton", -- name of the gui object 257 | "rbxassetid://924320031", -- image asset id 258 | UDim2.new(0, 100, 0, 100) -- size of the button 259 | ) 260 | 261 | -- set if the StatefulImageButton is selected or not 262 | local selected = false 263 | button:setSelected(selected) 264 | 265 | -- use the :getButton() method to return the ImageButton gui object 266 | local buttonObject = button:getButton() 267 | buttonObject.MouseButton1Click:Connect(function() 268 | selected = not selected 269 | button:setSelected(selected) 270 | end) 271 | buttonObject.Parent = widgetGui 272 | ``` 273 | 274 | #### VerticallyScalingListFrame.lua 275 | A frame that contains a list of sub-widgets. Will grow to accomodate size of children. 276 | 277 | ```Lua 278 | local listFrame = VerticallyScalingListFrame.new( 279 | "suffix" -- name suffix of gui object 280 | ) 281 | 282 | local label = Instance.new("TextLabel") 283 | label.Text = "labelText" 284 | label.Size = UDim2.new(0, 60, 0, 20) 285 | label.BackgroundTransparency = 1 286 | label.BorderSizePixel = 0 287 | local label2 = label:Clone() 288 | local label3 = label:Clone() 289 | 290 | -- fire function when the listFrame resizes 291 | listFrame:SetCallbackOnResize(function() 292 | print("Frame was resized!") 293 | end) 294 | 295 | -- add a gui element to the VerticallyScalingListFrame 296 | listFrame:AddChild(label) 297 | listFrame:AddChild(label2) 298 | listFrame:AddChild(label3) 299 | 300 | -- add padding to the VerticallyScalingListFrame 301 | listFrame:AddBottomPadding() 302 | 303 | -- use :GetFrame() to set the parent of the VerticallyScalingListFrame 304 | listFrame:GetFrame().Parent = widgetGui 305 | ``` 306 | 307 | #### VerticalScrollingFrame.lua 308 | A frame that holds sub-widgets and gives the user the ability to scroll through them over a fixed space. 309 | 310 | ![VerticalScrollingFrame](images/VerticalScrollingFrame.gif) 311 | 312 | ```Lua 313 | local choices = { 314 | {Id = "choice1", Text = "a"}, 315 | {Id = "choice2", Text = "b"}, 316 | {Id = "choice3", Text = "c"} 317 | } 318 | 319 | local scrollFrame = ScrollingFrame.new("suffix") 320 | 321 | local listFrame = VerticallyScalingListFrame.new("suffix") 322 | local collapse = CollapsibleTitledSection.new("suffix", "titleText", true, true, true) 323 | local multiChoice = LabeledMultiChoice.new("suffix", "labelText", choices, 1) 324 | local multiChoice2 = LabeledMultiChoice.new("suffix", "labelText", choices, 2) 325 | 326 | multiChoice:GetFrame().Parent = collapse:GetContentsFrame() 327 | multiChoice2:GetFrame().Parent = collapse:GetContentsFrame() 328 | listFrame:AddChild(collapse:GetSectionFrame()) -- add child to expanding VerticallyScalingListFrame 329 | 330 | local collapse = CollapsibleTitledSection.new("suffix", "titleText", true, false, false) 331 | local multiChoice = LabeledMultiChoice.new("suffix", "labelText", choices, 1) 332 | local multiChoice2 = LabeledMultiChoice.new("suffix", "labelText", choices, 2) 333 | 334 | multiChoice:GetFrame().Parent = collapse:GetContentsFrame() 335 | multiChoice2:GetFrame().Parent = collapse:GetContentsFrame() 336 | listFrame:AddChild(collapse:GetSectionFrame()) -- add child to expanding VerticallyScalingListFrame 337 | 338 | listFrame:AddBottomPadding() -- add padding to VerticallyScalingListFrame 339 | 340 | listFrame:GetFrame().Parent = scrollFrame:GetContentFrame() -- scroll content will be the VerticallyScalingListFrame 341 | scrollFrame:GetSectionFrame().Parent = widgetGui -- set the section parent 342 | ``` 343 | 344 | ### Bringing the project into studio 345 | The easiest way to bring the project into studio is to use the [HttpService](https://www.robloxdev.com/api-reference/class/HttpService) to pull the contents directly from this github project into module scripts. After enabling the http service from `Game Settings` the following code can be run in the command bar. 346 | 347 | ```Lua 348 | local http = game:GetService("HttpService") 349 | local req = http:GetAsync("https://api.github.com/repos/Roblox/StudioWidgets/contents/src") 350 | local json = http:JSONDecode(req) 351 | 352 | local targetFolder = Instance.new("Folder") 353 | targetFolder.Name = "StudioWidgets" 354 | targetFolder.Parent = game.Workspace 355 | 356 | for i = 1, #json do 357 | local file = json[i] 358 | if (file.type == "file") then 359 | local name = file.name:sub(1, #file.name-4) 360 | local module = targetFolder:FindFirstChild(name) or Instance.new("ModuleScript") 361 | module.Name = name 362 | module.Source = http:GetAsync(file.download_url) 363 | module.Parent = targetFolder 364 | end 365 | end 366 | ``` 367 | 368 | ## License 369 | Available under the Apache 2.0 license. See [LICENSE](LICENSE) for details. 370 | -------------------------------------------------------------------------------- /images/CollapsibleTitledSection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/CollapsibleTitledSection.gif -------------------------------------------------------------------------------- /images/CustomTextButton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/CustomTextButton.gif -------------------------------------------------------------------------------- /images/ImageButtonWithText.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/ImageButtonWithText.gif -------------------------------------------------------------------------------- /images/LabeledCheckbox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/LabeledCheckbox.gif -------------------------------------------------------------------------------- /images/LabeledMultiChoice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/LabeledMultiChoice.gif -------------------------------------------------------------------------------- /images/LabeledRadioButton.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/LabeledRadioButton.PNG -------------------------------------------------------------------------------- /images/LabeledSlider.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/LabeledSlider.gif -------------------------------------------------------------------------------- /images/LabeledTextInput.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/LabeledTextInput.gif -------------------------------------------------------------------------------- /images/StatefulImageButton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/StatefulImageButton.gif -------------------------------------------------------------------------------- /images/VerticalScrollingFrame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/StudioWidgets/c2be29292d3035b1d99e0d2b360d9594df97e9fc/images/VerticalScrollingFrame.gif -------------------------------------------------------------------------------- /src/CollapsibleTitledSection.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- CollapsibleTitledSectionClass 4 | -- 5 | -- Creates a section with a title label: 6 | -- 7 | -- "SectionXXX" 8 | -- "TitleBarVisual" 9 | -- "Contents" 10 | -- 11 | -- Requires "parent" and "sectionName" parameters and returns the section and its contentsFrame 12 | -- The entire frame will resize dynamically as contents frame changes size. 13 | -- 14 | -- "autoScalingList" is a boolean that defines wheter or not the content frame automatically resizes when children are added. 15 | -- This is important for cases when you want minimize button to push or contract what is below it. 16 | -- 17 | -- Both "minimizeable" and "minimizedByDefault" are false by default 18 | -- These parameters define if the section will have an arrow button infront of the title label, 19 | -- which the user may use to hide the section's contents 20 | -- 21 | ---------------------------------------- 22 | GuiUtilities = require(script.Parent.GuiUtilities) 23 | 24 | local kRightButtonAsset = "rbxasset://textures/TerrainTools/button_arrow.png" 25 | local kDownButtonAsset = "rbxasset://textures/TerrainTools/button_arrow_down.png" 26 | 27 | local kArrowSize = 9 28 | local kDoubleClickTimeSec = 0.5 29 | 30 | CollapsibleTitledSectionClass = {} 31 | CollapsibleTitledSectionClass.__index = CollapsibleTitledSectionClass 32 | 33 | 34 | function CollapsibleTitledSectionClass.new(nameSuffix, titleText, autoScalingList, minimizable, minimizedByDefault) 35 | local self = {} 36 | setmetatable(self, CollapsibleTitledSectionClass) 37 | 38 | self._minimized = minimizedByDefault 39 | self._minimizable = minimizable 40 | 41 | self._titleBarHeight = GuiUtilities.kTitleBarHeight 42 | 43 | local frame = Instance.new('Frame') 44 | frame.Name = 'CTSection' .. nameSuffix 45 | frame.BackgroundTransparency = 1 46 | self._frame = frame 47 | 48 | local uiListLayout = Instance.new('UIListLayout') 49 | uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder 50 | uiListLayout.Parent = frame 51 | self._uiListLayout = uiListLayout 52 | 53 | local contentsFrame = Instance.new('Frame') 54 | contentsFrame.Name = 'Contents' 55 | contentsFrame.BackgroundTransparency = 1 56 | contentsFrame.Size = UDim2.new(1, 0, 0, 1) 57 | contentsFrame.Position = UDim2.new(0, 0, 0, titleBarSize) 58 | contentsFrame.Parent = frame 59 | contentsFrame.LayoutOrder = 2 60 | GuiUtilities.syncGuiElementBackgroundColor(contentsFrame) 61 | 62 | self._contentsFrame = contentsFrame 63 | 64 | uiListLayout:GetPropertyChangedSignal('AbsoluteContentSize'):connect(function() 65 | self:_UpdateSize() 66 | end) 67 | self:_UpdateSize() 68 | 69 | self:_CreateTitleBar(titleText) 70 | self:SetCollapsedState(self._minimized) 71 | 72 | if (autoScalingList) then 73 | GuiUtilities.MakeFrameAutoScalingList(self:GetContentsFrame()) 74 | end 75 | 76 | return self 77 | end 78 | 79 | 80 | function CollapsibleTitledSectionClass:GetSectionFrame() 81 | return self._frame 82 | end 83 | 84 | function CollapsibleTitledSectionClass:GetContentsFrame() 85 | return self._contentsFrame 86 | end 87 | 88 | function CollapsibleTitledSectionClass:_UpdateSize() 89 | local totalSize = self._uiListLayout.AbsoluteContentSize.Y 90 | self._frame.Size = UDim2.new(1, 0, 0, totalSize) 91 | end 92 | 93 | function CollapsibleTitledSectionClass:_UpdateMinimizeButton() 94 | -- We can't rotate it because rotated images don't get clipped by parents. 95 | -- This is all in a scroll widget. 96 | -- :( 97 | if (self._minimized) then 98 | self._minimizeButton.Image = kRightButtonAsset 99 | else 100 | self._minimizeButton.Image = kDownButtonAsset 101 | end 102 | end 103 | 104 | function CollapsibleTitledSectionClass:SetCollapsedState(bool) 105 | self._minimized = bool 106 | self._contentsFrame.Visible = not bool 107 | self:_UpdateMinimizeButton() 108 | self:_UpdateSize() 109 | end 110 | 111 | function CollapsibleTitledSectionClass:_ToggleCollapsedState() 112 | self:SetCollapsedState(not self._minimized) 113 | end 114 | 115 | function CollapsibleTitledSectionClass:_CreateTitleBar(titleText) 116 | local titleTextOffset = self._titleBarHeight 117 | 118 | local titleBar = Instance.new('ImageButton') 119 | titleBar.AutoButtonColor = false 120 | titleBar.Name = 'TitleBarVisual' 121 | titleBar.BorderSizePixel = 0 122 | titleBar.Position = UDim2.new(0, 0, 0, 0) 123 | titleBar.Size = UDim2.new(1, 0, 0, self._titleBarHeight) 124 | titleBar.Parent = self._frame 125 | titleBar.LayoutOrder = 1 126 | GuiUtilities.syncGuiElementTitleColor(titleBar) 127 | 128 | local titleLabel = Instance.new('TextLabel') 129 | titleLabel.Name = 'TitleLabel' 130 | titleLabel.BackgroundTransparency = 1 131 | titleLabel.Font = Enum.Font.SourceSansBold --todo: input spec font 132 | titleLabel.TextSize = 15 --todo: input spec font size 133 | titleLabel.TextXAlignment = Enum.TextXAlignment.Left 134 | titleLabel.Text = titleText 135 | titleLabel.Position = UDim2.new(0, titleTextOffset, 0, 0) 136 | titleLabel.Size = UDim2.new(1, -titleTextOffset, 1, GuiUtilities.kTextVerticalFudge) 137 | titleLabel.Parent = titleBar 138 | GuiUtilities.syncGuiElementFontColor(titleLabel) 139 | 140 | self._minimizeButton = Instance.new('ImageButton') 141 | self._minimizeButton.Name = 'MinimizeSectionButton' 142 | self._minimizeButton.Image = kRightButtonAsset --todo: input arrow image from spec 143 | self._minimizeButton.Size = UDim2.new(0, kArrowSize, 0, kArrowSize) 144 | self._minimizeButton.AnchorPoint = Vector2.new(0.5, 0.5) 145 | self._minimizeButton.Position = UDim2.new(0, self._titleBarHeight*.5, 146 | 0, self._titleBarHeight*.5) 147 | self._minimizeButton.BackgroundTransparency = 1 148 | self._minimizeButton.Visible = self._minimizable -- only show when minimizable 149 | 150 | self._minimizeButton.MouseButton1Down:connect(function() 151 | self:_ToggleCollapsedState() 152 | end) 153 | self:_UpdateMinimizeButton() 154 | self._minimizeButton.Parent = titleBar 155 | 156 | self._latestClickTime = 0 157 | titleBar.MouseButton1Down:connect(function() 158 | local now = tick() 159 | if (now - self._latestClickTime < kDoubleClickTimeSec) then 160 | self:_ToggleCollapsedState() 161 | self._latestClickTime = 0 162 | else 163 | self._latestClickTime = now 164 | end 165 | end) 166 | end 167 | 168 | return CollapsibleTitledSectionClass -------------------------------------------------------------------------------- /src/CustomTextButton.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- CustomTextButton.lua 4 | -- 5 | -- Creates text button with custom look & feel, hover/click effects. 6 | -- 7 | ---------------------------------------- 8 | GuiUtilities = require(script.Parent.GuiUtilities) 9 | 10 | local kButtonImageIdDefault = "rbxasset://textures/TerrainTools/button_default.png" 11 | local kButtonImageIdHovered = "rbxasset://textures/TerrainTools/button_hover.png" 12 | local kButtonImageIdPressed = "rbxasset://textures/TerrainTools/button_pressed.png" 13 | 14 | CustomTextButtonClass = {} 15 | CustomTextButtonClass.__index = CustomTextButtonClass 16 | 17 | function CustomTextButtonClass.new(buttonName, labelText) 18 | local self = {} 19 | setmetatable(self, CustomTextButtonClass) 20 | 21 | local button = Instance.new('ImageButton') 22 | button.Name = buttonName 23 | button.Image = kButtonImageIdDefault 24 | button.BackgroundTransparency = 1 25 | button.ScaleType = Enum.ScaleType.Slice 26 | button.SliceCenter = Rect.new(7, 7, 156, 36) 27 | button.AutoButtonColor = false 28 | 29 | local label = Instance.new('TextLabel') 30 | label.Text = labelText 31 | label.BackgroundTransparency = 1 32 | label.Size = UDim2.new(1, 0, 1, GuiUtilities.kButtonVerticalFudge) 33 | label.Font = Enum.Font.SourceSans 34 | label.TextSize = 15 35 | label.Parent = button 36 | 37 | self._label = label 38 | self._button = button 39 | 40 | self._clicked = false 41 | self._hovered = false 42 | 43 | button.InputBegan:connect(function(input) 44 | if (input.UserInputType == Enum.UserInputType.MouseMovement) then 45 | self._hovered = true 46 | self:_updateButtonVisual() 47 | end 48 | end) 49 | 50 | 51 | button.InputEnded:connect(function(input) 52 | if (input.UserInputType == Enum.UserInputType.MouseMovement) then 53 | self._hovered = false 54 | self._clicked = false 55 | self:_updateButtonVisual() 56 | end 57 | end) 58 | 59 | button.MouseButton1Down:connect(function() 60 | self._clicked = true 61 | self:_updateButtonVisual() 62 | end) 63 | 64 | button.MouseButton1Up:connect(function() 65 | self._clicked = false 66 | self:_updateButtonVisual() 67 | end) 68 | 69 | self:_updateButtonVisual() 70 | 71 | return self 72 | end 73 | 74 | function CustomTextButtonClass:_updateButtonVisual() 75 | if (self._clicked) then 76 | self._button.Image = kButtonImageIdPressed 77 | self._label.TextColor3 = GuiUtilities.kPressedButtonTextColor 78 | elseif (self._hovered) then 79 | self._button.Image = kButtonImageIdHovered 80 | self._label.TextColor3 = GuiUtilities.kStandardButtonTextColor 81 | else 82 | self._button.Image = kButtonImageIdDefault 83 | self._label.TextColor3 = GuiUtilities.kStandardButtonTextColor 84 | end 85 | end 86 | 87 | -- Backwards compatibility (should be removed in the future) 88 | -- CustomTextButtonClass.getButton = CustomTextButtonClass.GetButton 89 | 90 | function CustomTextButtonClass:GetButton() 91 | return self._button 92 | end 93 | 94 | return CustomTextButtonClass 95 | -------------------------------------------------------------------------------- /src/GuiUtilities.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | 4 | module.kTitleBarHeight = 27 5 | module.kInlineTitleBarHeight = 24 6 | 7 | module.kStandardContentAreaWidth = 180 8 | 9 | module.kStandardPropertyHeight = 30 10 | module.kSubSectionLabelHeight = 30 11 | 12 | module.kStandardVMargin = 7 13 | module.kStandardHMargin = 16 14 | 15 | module.kCheckboxMinLabelWidth = 52 16 | module.kCheckboxMinMargin = 12 17 | module.kCheckboxWidth = 12 18 | 19 | module.kRadioButtonsHPadding = 24 20 | 21 | module.StandardLineLabelLeftMargin = module.kTitleBarHeight 22 | module.StandardLineElementLeftMargin = (module.StandardLineLabelLeftMargin + module.kCheckboxMinLabelWidth 23 | + module.kCheckboxMinMargin + module.kCheckboxWidth + module.kRadioButtonsHPadding) 24 | module.StandardLineLabelWidth = (module.StandardLineElementLeftMargin - module.StandardLineLabelLeftMargin - 10 ) 25 | 26 | module.kDropDownHeight = 55 27 | 28 | module.kBottomButtonsFrameHeight = 50 29 | module.kBottomButtonsHeight = 28 30 | 31 | module.kShapeButtonSize = 32 32 | module.kTextVerticalFudge = -3 33 | module.kButtonVerticalFudge = -5 34 | 35 | module.kBottomButtonsWidth = 100 36 | 37 | module.kDisabledTextColor = Color3.new(.4, .4, .4) --todo: input spec disabled text color 38 | module.kStandardButtonTextColor = Color3.new(0, 0, 0) --todo: input spec disabled text color 39 | module.kPressedButtonTextColor = Color3.new(1, 1, 1) --todo: input spec disabled text color 40 | 41 | module.kButtonStandardBackgroundColor = Color3.new(1, 1, 1) --todo: sync with spec 42 | module.kButtonStandardBorderColor = Color3.new(.4,.4,.4) --todo: sync with spec 43 | module.kButtonDisabledBackgroundColor = Color3.new(.7,.7,.7) --todo: sync with spec 44 | module.kButtonDisabledBorderColor = Color3.new(.6,.6,.6) --todo: sync with spec 45 | 46 | module.kButtonBackgroundTransparency = 0.5 47 | module.kButtonBackgroundIntenseTransparency = 0.4 48 | 49 | module.kMainFrame = nil 50 | 51 | function module.ShouldUseIconsForDarkerBackgrounds() 52 | local mainColor = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground) 53 | return (mainColor.r + mainColor.g + mainColor.b) / 3 < 0.5 54 | end 55 | 56 | function module.SetMainFrame(frame) 57 | module.kMainFrame = frame 58 | end 59 | 60 | function module.syncGuiElementTitleColor(guiElement) 61 | local function setColors() 62 | guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Titlebar) 63 | end 64 | settings().Studio.ThemeChanged:connect(setColors) 65 | setColors() 66 | end 67 | 68 | function module.syncGuiElementInputFieldColor(guiElement) 69 | local function setColors() 70 | guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground) 71 | end 72 | settings().Studio.ThemeChanged:connect(setColors) 73 | setColors() 74 | end 75 | 76 | function module.syncGuiElementBackgroundColor(guiElement) 77 | local function setColors() 78 | guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground) 79 | end 80 | settings().Studio.ThemeChanged:connect(setColors) 81 | setColors() 82 | end 83 | 84 | function module.syncGuiElementStripeColor(guiElement) 85 | local function setColors() 86 | if ((guiElement.LayoutOrder + 1) % 2 == 0) then 87 | guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground) 88 | else 89 | guiElement.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.CategoryItem) 90 | end 91 | end 92 | settings().Studio.ThemeChanged:connect(setColors) 93 | setColors() 94 | end 95 | 96 | function module.syncGuiElementBorderColor(guiElement) 97 | local function setColors() 98 | guiElement.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border) 99 | end 100 | settings().Studio.ThemeChanged:connect(setColors) 101 | setColors() 102 | end 103 | 104 | function module.syncGuiElementFontColor(guiElement) 105 | local function setColors() 106 | guiElement.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainText) 107 | end 108 | settings().Studio.ThemeChanged:connect(setColors) 109 | setColors() 110 | end 111 | 112 | function module.syncGuiElementScrollColor(guiElement) 113 | local function setColors() 114 | guiElement.ScrollBarImageColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar) 115 | end 116 | settings().Studio.ThemeChanged:connect(setColors) 117 | setColors() 118 | end 119 | 120 | -- A frame with standard styling. 121 | function module.MakeFrame(name) 122 | local frame = Instance.new("Frame") 123 | frame.Name = name 124 | frame.BackgroundTransparency = 0 125 | frame.BorderSizePixel = 0 126 | 127 | module.syncGuiElementBackgroundColor(frame) 128 | 129 | return frame 130 | end 131 | 132 | 133 | -- A frame that is a whole line, containing some arbitrary sized widget. 134 | function module.MakeFixedHeightFrame(name, height) 135 | local frame = module.MakeFrame(name) 136 | frame.Size = UDim2.new(1, 0, 0, height) 137 | 138 | return frame 139 | end 140 | 141 | -- A frame that is one standard-sized line, containing some standard-sized widget (label, edit box, dropdown, 142 | -- checkbox) 143 | function module.MakeStandardFixedHeightFrame(name) 144 | return module.MakeFixedHeightFrame(name, module.kStandardPropertyHeight) 145 | end 146 | 147 | function module.AdjustHeightDynamicallyToLayout(frame, uiLayout, optPadding) 148 | if (not optPadding) then 149 | optPadding = 0 150 | end 151 | 152 | local function updateSizes() 153 | frame.Size = UDim2.new(1, 0, 0, uiLayout.AbsoluteContentSize.Y + optPadding) 154 | end 155 | uiLayout:GetPropertyChangedSignal("AbsoluteContentSize"):connect(updateSizes) 156 | updateSizes() 157 | end 158 | 159 | -- Assumes input frame has a List layout with sort order layout order. 160 | -- Add frames in order as siblings of list layout, they will be laid out in order. 161 | -- Color frame background accordingly. 162 | function module.AddStripedChildrenToListFrame(listFrame, frames) 163 | for index, frame in ipairs(frames) do 164 | frame.Parent = listFrame 165 | frame.LayoutOrder = index 166 | frame.BackgroundTransparency = 0 167 | frame.BorderSizePixel = 1 168 | module.syncGuiElementStripeColor(frame) 169 | module.syncGuiElementBorderColor(frame) 170 | end 171 | end 172 | 173 | local function MakeSectionInternal(parentGui, name, title, contentHeight) 174 | local frame = Instance.new("Frame") 175 | frame.Name = name 176 | frame.BackgroundTransparency = 1 177 | frame.Parent = parentGui 178 | frame.BackgroundTransparency = 1 179 | frame.BorderSizePixel = 0 180 | 181 | -- If title is "nil', no title bar. 182 | local contentYOffset = 0 183 | local titleBar = nil 184 | if (title ~= nil) then 185 | local titleBarFrame = Instance.new("Frame") 186 | titleBarFrame.Name = "TitleBarFrame" 187 | titleBarFrame.Parent = frame 188 | titleBarFrame.Position = UDim2.new(0, 0, 0, 0) 189 | titleBarFrame.LayoutOrder = 0 190 | 191 | local titleBar = Instance.new("TextLabel") 192 | titleBar.Name = "TitleBarLabel" 193 | titleBar.Text = title 194 | titleBar.Parent = titleBarFrame 195 | titleBar.BackgroundTransparency = 1 196 | titleBar.Position = UDim2.new(0, module.kStandardHMargin, 0, 0) 197 | 198 | module.syncGuiElementFontColor(titleBar) 199 | 200 | contentYOffset = contentYOffset + module.kTitleBarHeight 201 | end 202 | 203 | frame.Size = UDim2.new(1, 0, 0, contentYOffset + contentHeight) 204 | 205 | return frame 206 | end 207 | 208 | function module.MakeStandardPropertyLabel(text, opt_ignoreThemeUpdates) 209 | local label = Instance.new('TextLabel') 210 | label.Name = 'Label' 211 | label.BackgroundTransparency = 1 212 | label.Font = Enum.Font.SourceSans --todo: input spec font 213 | label.TextSize = 15 --todo: input spec font size 214 | label.TextXAlignment = Enum.TextXAlignment.Left 215 | label.Text = text 216 | label.AnchorPoint = Vector2.new(0, 0.5) 217 | label.Position = UDim2.new(0, module.StandardLineLabelLeftMargin, 0.5, module.kTextVerticalFudge) 218 | label.Size = UDim2.new(0, module.StandardLineLabelWidth, 1, 0) 219 | 220 | if (not opt_ignoreThemeUpdates) then 221 | module.syncGuiElementFontColor(label) 222 | end 223 | 224 | return label 225 | end 226 | 227 | function module.MakeFrameWithSubSectionLabel(name, text) 228 | local row = module.MakeFixedHeightFrame(name, module.kSubSectionLabelHeight) 229 | row.BackgroundTransparency = 1 230 | 231 | local label = module.MakeStandardPropertyLabel(text) 232 | label.BackgroundTransparency = 1 233 | label.Parent = row 234 | 235 | return row 236 | end 237 | 238 | function module.MakeFrameAutoScalingList(frame) 239 | local uiListLayout = Instance.new("UIListLayout") 240 | uiListLayout.Parent = frame 241 | uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder 242 | 243 | module.AdjustHeightDynamicallyToLayout(frame, uiListLayout) 244 | end 245 | 246 | 247 | return module -------------------------------------------------------------------------------- /src/ImageButtonWithText.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- ImageButtonWithText.lua 4 | -- 5 | -- An image button with text underneath. Standardized hover, clicked, and 6 | -- selected states. 7 | -- 8 | ---------------------------------------- 9 | GuiUtilities = require(script.Parent.GuiUtilities) 10 | 11 | ImageButtonWithTextClass = {} 12 | ImageButtonWithTextClass.__index = ImageButtonWithTextClass 13 | 14 | local kSelectedBaseTransparency = 0.2 15 | local kAdditionalTransparency = 0.1 16 | 17 | function ImageButtonWithTextClass.new(name, 18 | layoutOrder, 19 | icon, 20 | text, 21 | buttonSize, 22 | imageSize, 23 | imagePos, 24 | textSize, 25 | textPos) 26 | local self = {} 27 | setmetatable(self, ImageButtonWithTextClass) 28 | 29 | local button = Instance.new("ImageButton") 30 | button.Name = name 31 | button.AutoButtonColor = false 32 | button.Size = buttonSize 33 | button.BorderSizePixel = 1 34 | -- Image-with-text button has translucent background and "selected" background color. 35 | -- When selected we set transluency to not-zero so we see selected color. 36 | button.BackgroundTransparency = 1 37 | 38 | button.LayoutOrder = layoutOrder 39 | 40 | local buttonIcon = Instance.new("ImageLabel") 41 | buttonIcon.BackgroundTransparency = 1 42 | buttonIcon.Image = icon or "" 43 | buttonIcon.Size = imageSize 44 | buttonIcon.Position = imagePos 45 | buttonIcon.Parent = button 46 | 47 | local textLabel = Instance.new("TextLabel") 48 | textLabel.BackgroundTransparency = 1 49 | textLabel.Text = text 50 | textLabel.Size = textSize 51 | textLabel.Position = textPos 52 | textLabel.TextScaled = true 53 | textLabel.Font = Enum.Font.SourceSans 54 | textLabel.Parent = button 55 | 56 | GuiUtilities.syncGuiElementFontColor(textLabel) 57 | 58 | local uiTextSizeConstraint = Instance.new("UITextSizeConstraint") 59 | -- Spec asks for fontsize of 12 pixels, but in Roblox the text font sizes look smaller than the mock 60 | --Note: For this font the Roblox text size is 25.7% larger than the design spec. 61 | uiTextSizeConstraint.MaxTextSize = 15 62 | uiTextSizeConstraint.Parent = textLabel 63 | 64 | self._button = button 65 | self._clicked = false 66 | self._hovered = false 67 | self._selected = false 68 | 69 | button.InputBegan:Connect(function(input) 70 | if (input.UserInputType == Enum.UserInputType.MouseMovement) then 71 | self._hovered = true 72 | self:_updateButtonVisual() 73 | end 74 | end) 75 | 76 | 77 | button.InputEnded:Connect(function(input) 78 | if (input.UserInputType == Enum.UserInputType.MouseMovement) then 79 | self._hovered = false 80 | self._clicked = false 81 | self:_updateButtonVisual() 82 | end 83 | end) 84 | 85 | button.MouseButton1Down:Connect(function() 86 | self._clicked = true 87 | self:_updateButtonVisual() 88 | end) 89 | 90 | button.MouseButton1Up:Connect(function() 91 | self._clicked = false 92 | self:_updateButtonVisual() 93 | end) 94 | 95 | function updateButtonVisual() 96 | self:_updateButtonVisual() 97 | end 98 | settings().Studio.ThemeChanged:connect(updateButtonVisual) 99 | 100 | self:_updateButtonVisual() 101 | 102 | return self 103 | end 104 | 105 | function ImageButtonWithTextClass:_updateButtonVisual() 106 | -- Possibilties: 107 | if (self._clicked) then 108 | -- This covers 'clicked and selected' or 'clicked' 109 | self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button, 110 | Enum.StudioStyleGuideModifier.Selected) 111 | self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border, 112 | Enum.StudioStyleGuideModifier.Selected) 113 | if (self._selected) then 114 | self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundIntenseTransparency 115 | else 116 | self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundTransparency 117 | end 118 | elseif (self._hovered) then 119 | -- This covers 'hovered and selected' or 'hovered' 120 | self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button, 121 | Enum.StudioStyleGuideModifier.Hover) 122 | self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border, 123 | Enum.StudioStyleGuideModifier.Hover) 124 | if (self._selected) then 125 | self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundIntenseTransparency 126 | else 127 | self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundTransparency 128 | end 129 | elseif (self._selected) then 130 | -- This covers 'selected' 131 | self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button, 132 | Enum.StudioStyleGuideModifier.Selected) 133 | self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border, 134 | Enum.StudioStyleGuideModifier.Selected) 135 | self._button.BackgroundTransparency = GuiUtilities.kButtonBackgroundTransparency 136 | else 137 | -- This covers 'no special state' 138 | self._button.BackgroundColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Button) 139 | self._button.BorderColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Border) 140 | self._button.BackgroundTransparency = 1 141 | end 142 | end 143 | 144 | function ImageButtonWithTextClass:GetButton() 145 | return self._button 146 | end 147 | 148 | function ImageButtonWithTextClass:SetSelected(selected) 149 | self._selected = selected 150 | self:_updateButtonVisual() 151 | end 152 | 153 | function ImageButtonWithTextClass:GetSelected() 154 | return self._selected 155 | end 156 | 157 | 158 | return ImageButtonWithTextClass 159 | -------------------------------------------------------------------------------- /src/LabeledCheckbox.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- LabeledCheckbox.lua 4 | -- 5 | -- Creates a frame containing a label and a checkbox. 6 | -- 7 | ---------------------------------------- 8 | GuiUtilities = require(script.Parent.GuiUtilities) 9 | 10 | local kCheckboxWidth = GuiUtilities.kCheckboxWidth 11 | 12 | local kMinTextSize = 14 13 | local kMinHeight = 24 14 | local kMinLabelWidth = GuiUtilities.kCheckboxMinLabelWidth 15 | local kMinMargin = GuiUtilities.kCheckboxMinMargin 16 | local kMinButtonWidth = kCheckboxWidth; 17 | 18 | local kMinLabelSize = UDim2.new(0, kMinLabelWidth, 0, kMinHeight) 19 | local kMinLabelPos = UDim2.new(0, kMinButtonWidth + kMinMargin, 0, kMinHeight/2) 20 | 21 | local kMinButtonSize = UDim2.new(0, kMinButtonWidth, 0, kMinButtonWidth) 22 | local kMinButtonPos = UDim2.new(0, 0, 0, kMinHeight/2) 23 | 24 | local kCheckImageWidth = 8 25 | local kMinCheckImageWidth = kCheckImageWidth 26 | 27 | local kCheckImageSize = UDim2.new(0, kCheckImageWidth, 0, kCheckImageWidth) 28 | local kMinCheckImageSize = UDim2.new(0, kMinCheckImageWidth, 0, kMinCheckImageWidth) 29 | 30 | local kEnabledCheckImage = "rbxasset://textures/TerrainTools/icon_tick.png" 31 | local kDisabledCheckImage = "rbxasset://textures/TerrainTools/icon_tick_grey.png" 32 | local kCheckboxFrameImage = "rbxasset://textures/TerrainTools/checkbox_square.png" 33 | LabeledCheckboxClass = {} 34 | LabeledCheckboxClass.__index = LabeledCheckboxClass 35 | 36 | LabeledCheckboxClass.kMinFrameSize = UDim2.new(0, kMinLabelWidth + kMinMargin + kMinButtonWidth, 0, kMinHeight) 37 | 38 | 39 | function LabeledCheckboxClass.new(nameSuffix, labelText, initValue, initDisabled) 40 | local self = {} 41 | setmetatable(self, LabeledCheckboxClass) 42 | 43 | local initValue = not not initValue 44 | local initDisabled = not not initDisabled 45 | 46 | local frame = GuiUtilities.MakeStandardFixedHeightFrame("CBF" .. nameSuffix) 47 | 48 | local fullBackgroundButton = Instance.new("TextButton") 49 | fullBackgroundButton.Name = "FullBackground" 50 | fullBackgroundButton.Parent = frame 51 | fullBackgroundButton.BackgroundTransparency = 1 52 | fullBackgroundButton.Size = UDim2.new(1, 0, 1, 0) 53 | fullBackgroundButton.Position = UDim2.new(0, 0, 0, 0) 54 | fullBackgroundButton.Text = "" 55 | 56 | local label = GuiUtilities.MakeStandardPropertyLabel(labelText, true) 57 | label.Parent = fullBackgroundButton 58 | 59 | local button = Instance.new('ImageButton') 60 | button.Name = 'Button' 61 | button.Size = UDim2.new(0, kCheckboxWidth, 0, kCheckboxWidth) 62 | button.AnchorPoint = Vector2.new(0, .5) 63 | button.BackgroundTransparency = 0 64 | button.Position = UDim2.new(0, GuiUtilities.StandardLineElementLeftMargin, .5, 0) 65 | button.Parent = fullBackgroundButton 66 | button.Image = kCheckboxFrameImage 67 | button.BorderSizePixel = 0 68 | button.AutoButtonColor = false 69 | 70 | local checkImage = Instance.new("ImageLabel") 71 | checkImage.Name = "CheckImage" 72 | checkImage.Parent = button 73 | checkImage.Image = kEnabledCheckImage 74 | checkImage.Visible = false 75 | checkImage.Size = kCheckImageSize 76 | checkImage.AnchorPoint = Vector2.new(0.5, 0.5) 77 | checkImage.Position = UDim2.new(0.5, 0, 0.5, 0) 78 | checkImage.BackgroundTransparency = 1 79 | checkImage.BorderSizePixel = 0 80 | 81 | self._frame = frame 82 | self._button = button 83 | self._label = label 84 | self._checkImage = checkImage 85 | self._fullBackgroundButton = fullBackgroundButton 86 | self._useDisabledOverride = false 87 | self._disabledOverride = false 88 | self:SetDisabled(initDisabled) 89 | 90 | self._value = not initValue 91 | self:SetValue(initValue) 92 | 93 | self:_SetupMouseClickHandling() 94 | 95 | local function updateFontColors() 96 | self:UpdateFontColors() 97 | end 98 | settings().Studio.ThemeChanged:connect(updateFontColors) 99 | updateFontColors() 100 | 101 | return self 102 | end 103 | 104 | 105 | function LabeledCheckboxClass:_MaybeToggleState() 106 | if not self._disabled then 107 | self:SetValue(not self._value) 108 | end 109 | end 110 | 111 | function LabeledCheckboxClass:_SetupMouseClickHandling() 112 | self._button.MouseButton1Down:connect(function() 113 | self:_MaybeToggleState() 114 | end) 115 | 116 | self._fullBackgroundButton.MouseButton1Down:connect(function() 117 | self:_MaybeToggleState() 118 | end) 119 | end 120 | 121 | function LabeledCheckboxClass:_HandleUpdatedValue() 122 | self._checkImage.Visible = self:GetValue() 123 | 124 | if (self._valueChangedFunction) then 125 | self._valueChangedFunction(self:GetValue()) 126 | end 127 | end 128 | 129 | -- Small checkboxes are a different entity. 130 | -- All the bits are smaller. 131 | -- Fixed width instead of flood-fill. 132 | -- Box comes first, then label. 133 | function LabeledCheckboxClass:UseSmallSize() 134 | self._label.TextSize = kMinTextSize 135 | self._label.Size = kMinLabelSize 136 | self._label.Position = kMinLabelPos 137 | self._label.TextXAlignment = Enum.TextXAlignment.Left 138 | 139 | self._button.Size = kMinButtonSize 140 | self._button.Position = kMinButtonPos 141 | 142 | self._checkImage.Size = kMinCheckImageSize 143 | 144 | self._frame.Size = LabeledCheckboxClass.kMinFrameSize 145 | self._frame.BackgroundTransparency = 1 146 | end 147 | 148 | function LabeledCheckboxClass:GetFrame() 149 | return self._frame 150 | end 151 | 152 | function LabeledCheckboxClass:GetValue() 153 | -- If button is disabled, and we should be using a disabled override, 154 | -- use the disabled override. 155 | if (self._disabled and self._useDisabledOverride) then 156 | return self._disabledOverride 157 | else 158 | return self._value 159 | end 160 | end 161 | 162 | function LabeledCheckboxClass:GetLabel() 163 | return self._label 164 | end 165 | 166 | function LabeledCheckboxClass:GetButton() 167 | return self._button 168 | end 169 | 170 | function LabeledCheckboxClass:SetValueChangedFunction(vcFunction) 171 | self._valueChangedFunction = vcFunction 172 | end 173 | 174 | function LabeledCheckboxClass:SetDisabled(newDisabled) 175 | local newDisabled = not not newDisabled 176 | 177 | local originalValue = self:GetValue() 178 | 179 | if newDisabled ~= self._disabled then 180 | self._disabled = newDisabled 181 | 182 | -- if we are no longer disabled, then we don't need or want 183 | -- the override any more. Forget it. 184 | if (not self._disabled) then 185 | self._useDisabledOverride = false 186 | end 187 | 188 | if (newDisabled) then 189 | self._checkImage.Image = kDisabledCheckImage 190 | else 191 | self._checkImage.Image = kEnabledCheckImage 192 | end 193 | 194 | self:UpdateFontColors() 195 | self._button.BackgroundColor3 = self._disabled and GuiUtilities.kButtonDisabledBackgroundColor or GuiUtilities.kButtonStandardBackgroundColor 196 | self._button.BorderColor3 = self._disabled and GuiUtilities.kButtonDisabledBorderColor or GuiUtilities.kButtonStandardBorderColor 197 | if self._disabledChangedFunction then 198 | self._disabledChangedFunction(self._disabled) 199 | end 200 | end 201 | 202 | local newValue = self:GetValue() 203 | if (newValue ~= originalValue) then 204 | self:_HandleUpdatedValue() 205 | end 206 | end 207 | 208 | function LabeledCheckboxClass:UpdateFontColors() 209 | if self._disabled then 210 | self._label.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.DimmedText) 211 | else 212 | self._label.TextColor3 = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainText) 213 | end 214 | end 215 | 216 | function LabeledCheckboxClass:DisableWithOverrideValue(overrideValue) 217 | -- Disable this checkbox. While disabled, force value to override 218 | -- value. 219 | local oldValue = self:GetValue() 220 | self._useDisabledOverride = true 221 | self._disabledOverride = overrideValue 222 | self:SetDisabled(true) 223 | local newValue = self:GetValue() 224 | if (oldValue ~= newValue) then 225 | self:_HandleUpdatedValue() 226 | end 227 | end 228 | 229 | function LabeledCheckboxClass:GetDisabled() 230 | return self._disabled 231 | end 232 | 233 | function LabeledCheckboxClass:SetValue(newValue) 234 | local newValue = not not newValue 235 | 236 | if newValue ~= self._value then 237 | self._value = newValue 238 | 239 | self:_HandleUpdatedValue() 240 | end 241 | end 242 | 243 | return LabeledCheckboxClass -------------------------------------------------------------------------------- /src/LabeledMultiChoice.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- LabeledMultiChoice.lua 4 | -- 5 | -- Creates a frame containing a label and list of choices, of which exactly one 6 | -- is always selected. 7 | -- 8 | ---------------------------------------- 9 | GuiUtilities = require(script.Parent.GuiUtilities) 10 | LabeledRadioButton = require(script.Parent.LabeledRadioButton) 11 | LabeledCheckbox = require(script.Parent.LabeledCheckbox) 12 | VerticallyScalingListFrame = require(script.Parent.VerticallyScalingListFrame) 13 | 14 | local kRadioButtonsHPadding = GuiUtilities.kRadioButtonsHPadding 15 | 16 | LabeledMultiChoiceClass = {} 17 | LabeledMultiChoiceClass.__index = LabeledMultiChoiceClass 18 | 19 | 20 | -- Note: 21 | -- "choices" is an array of entries. 22 | -- each entry must have at least 2 fields: 23 | -- "Id" - a unique (in the scope of choices) string id. Not visible to user. 24 | -- "Text" - user-facing string: the label for the choice. 25 | function LabeledMultiChoiceClass.new(nameSuffix, labelText, choices, initChoiceIndex) 26 | local self = {} 27 | setmetatable(self, LabeledMultiChoiceClass) 28 | 29 | self._buttonObjsByIndex = {} 30 | 31 | if (not initChoiceIndex ) then 32 | initChoiceIndex = 1 33 | end 34 | if (initChoiceIndex > #choices) then 35 | initChoiceIndex = #choices 36 | end 37 | 38 | 39 | local vsl = VerticallyScalingListFrame.new("MCC_" .. nameSuffix) 40 | vsl:AddBottomPadding() 41 | 42 | local titleLabel = GuiUtilities.MakeFrameWithSubSectionLabel("Title", labelText) 43 | vsl:AddChild(titleLabel) 44 | 45 | -- Container for cells. 46 | local cellFrame = self:_MakeRadioButtons(choices) 47 | vsl:AddChild(cellFrame) 48 | 49 | self._vsl = vsl 50 | 51 | self:SetSelectedIndex(initChoiceIndex) 52 | 53 | return self 54 | end 55 | 56 | function LabeledMultiChoiceClass:SetSelectedIndex(selectedIndex) 57 | self._selectedIndex = selectedIndex 58 | for i = 1, #self._buttonObjsByIndex do 59 | self._buttonObjsByIndex[i]:SetValue(i == selectedIndex) 60 | end 61 | 62 | if (self._valueChangedFunction) then 63 | self._valueChangedFunction(self._selectedIndex) 64 | end 65 | end 66 | 67 | function LabeledMultiChoiceClass:GetSelectedIndex() 68 | return self._selectedIndex 69 | end 70 | 71 | function LabeledMultiChoiceClass:SetValueChangedFunction(vcf) 72 | self._valueChangedFunction = vcf 73 | end 74 | 75 | function LabeledMultiChoiceClass:GetFrame() 76 | return self._vsl:GetFrame() 77 | end 78 | 79 | 80 | -- Small checkboxes are a different entity. 81 | -- All the bits are smaller. 82 | -- Fixed width instead of flood-fill. 83 | -- Box comes first, then label. 84 | function LabeledMultiChoiceClass:_MakeRadioButtons(choices) 85 | local frame = GuiUtilities.MakeFrame("RadioButtons") 86 | frame.BackgroundTransparency = 1 87 | 88 | local padding = Instance.new("UIPadding") 89 | padding.PaddingLeft = UDim.new(0, GuiUtilities.StandardLineLabelLeftMargin) 90 | padding.PaddingRight = UDim.new(0, GuiUtilities.StandardLineLabelLeftMargin) 91 | padding.Parent = frame 92 | 93 | -- Make a grid to put checkboxes in. 94 | local uiGridLayout = Instance.new("UIGridLayout") 95 | uiGridLayout.CellSize = LabeledCheckbox.kMinFrameSize 96 | uiGridLayout.CellPadding = UDim2.new(0, 97 | kRadioButtonsHPadding, 98 | 0, 99 | GuiUtilities.kStandardVMargin) 100 | uiGridLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left 101 | uiGridLayout.VerticalAlignment = Enum.VerticalAlignment.Top 102 | uiGridLayout.Parent = frame 103 | uiGridLayout.SortOrder = Enum.SortOrder.LayoutOrder 104 | 105 | for i, choiceData in ipairs(choices) do 106 | self:_AddRadioButton(frame, i, choiceData) 107 | end 108 | 109 | -- Sync size with content size. 110 | GuiUtilities.AdjustHeightDynamicallyToLayout(frame, uiGridLayout) 111 | 112 | return frame 113 | end 114 | 115 | function LabeledMultiChoiceClass:_AddRadioButton(parentFrame, index, choiceData) 116 | local radioButtonObj = LabeledRadioButton.new(choiceData.Id, choiceData.Text) 117 | self._buttonObjsByIndex[index] = radioButtonObj 118 | 119 | radioButtonObj:SetValueChangedFunction(function(value) 120 | -- If we notice the button going from off to on, and it disagrees with 121 | -- our current notion of selection, update selection. 122 | if (value and self._selectedIndex ~= index) then 123 | self:SetSelectedIndex(index) 124 | end 125 | end) 126 | 127 | radioButtonObj:GetFrame().LayoutOrder = index 128 | radioButtonObj:GetFrame().Parent = parentFrame 129 | end 130 | 131 | 132 | return LabeledMultiChoiceClass -------------------------------------------------------------------------------- /src/LabeledRadioButton.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- LabeledRadioButton.lua 4 | -- 5 | -- Creates a frame containing a label and a radio button. 6 | -- 7 | ---------------------------------------- 8 | GuiUtilities = require(script.Parent.GuiUtilities) 9 | LabeledCheckbox = require(script.Parent.LabeledCheckbox) 10 | 11 | local kButtonImage = "rbxasset://textures/TerrainTools/radio_button_frame.png" 12 | local kBulletImage = "rbxasset://textures/TerrainTools/radio_button_bullet.png" 13 | 14 | local kButtonImageDark = "rbxasset://textures/TerrainTools/radio_button_frame_dark.png" 15 | local kBulletImageDark = "rbxasset://textures/TerrainTools/radio_button_bullet_dark.png" 16 | 17 | local kFrameSize = 12 18 | local kBulletSize = 14 19 | 20 | LabeledRadioButtonClass = {} 21 | LabeledRadioButtonClass.__index = LabeledRadioButtonClass 22 | setmetatable(LabeledRadioButtonClass, LabeledCheckbox) 23 | 24 | function LabeledRadioButtonClass.new(nameSuffix, labelText) 25 | local newButton = LabeledCheckbox.new(nameSuffix, labelText, false) 26 | setmetatable(newButton, LabeledRadioButtonClass) 27 | 28 | newButton:UseSmallSize() 29 | newButton._checkImage.Position = UDim2.new(0.5, 0, 0.5, 0) 30 | newButton._checkImage.Image = kBulletImage 31 | newButton._checkImage.Size = UDim2.new(0, kBulletSize, 0, kBulletSize) 32 | 33 | newButton._button.Image = kButtonImage 34 | newButton._button.Size = UDim2.new(0, kFrameSize, 0, kFrameSize) 35 | newButton._button.BackgroundTransparency = 1 36 | 37 | local function updateImages() 38 | if (GuiUtilities:ShouldUseIconsForDarkerBackgrounds()) then 39 | newButton._checkImage.Image = kBulletImageDark 40 | newButton._button.Image = kButtonImageDark 41 | else 42 | newButton._checkImage.Image = kBulletImage 43 | newButton._button.Image = kButtonImage 44 | end 45 | end 46 | settings().Studio.ThemeChanged:connect(updateImages) 47 | updateImages() 48 | 49 | return newButton 50 | end 51 | 52 | function LabeledRadioButtonClass:_MaybeToggleState() 53 | -- A checkbox can never be toggled off. 54 | -- Only turns off because another one turns on. 55 | if (not self._disabled and not self._value) then 56 | self:SetValue(not self._value) 57 | end 58 | end 59 | 60 | return LabeledRadioButtonClass -------------------------------------------------------------------------------- /src/LabeledSlider.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- LabeledSlider.lua 4 | -- 5 | -- Creates a frame containing a label and a slider control. 6 | -- 7 | ---------------------------------------- 8 | GuiUtilities = require(script.Parent.GuiUtilities) 9 | rbxGuiLibrary = require(script.Parent.RbxGui) 10 | 11 | local kSliderWidth = 100 12 | 13 | local kSliderThumbImage = "rbxasset://textures/TerrainTools/sliderbar_button.png" 14 | local kPreThumbImage = "rbxasset://textures/TerrainTools/sliderbar_blue.png" 15 | local kPostThumbImage = "rbxasset://textures/TerrainTools/sliderbar_grey.png" 16 | 17 | local kThumbSize = 13 18 | 19 | local kSteps = 100 20 | 21 | LabeledSliderClass = {} 22 | LabeledSliderClass.__index = LabeledSliderClass 23 | 24 | function LabeledSliderClass.new(nameSuffix, labelText, sliderIntervals, defaultValue) 25 | local self = {} 26 | setmetatable(self, LabeledSliderClass) 27 | 28 | self._valueChangedFunction = nil 29 | 30 | local sliderIntervals = sliderIntervals or 100 31 | local defaultValue = defaultValue or 1 32 | 33 | local frame = GuiUtilities.MakeStandardFixedHeightFrame('Slider' .. nameSuffix) 34 | self._frame = frame 35 | 36 | local label = GuiUtilities.MakeStandardPropertyLabel(labelText) 37 | label.Parent = frame 38 | self._label = label 39 | 40 | self._value = defaultValue 41 | 42 | --steps, width, position 43 | local slider, sliderValue = rbxGuiLibrary.CreateSlider(sliderIntervals, 44 | kSteps, 45 | UDim2.new(0, 0, .5, -3)) 46 | self._slider = slider 47 | self._sliderValue = sliderValue 48 | -- Some tweaks to make slider look nice. 49 | -- Hide the existing bar. 50 | slider.Bar.BackgroundTransparency = 1 51 | -- Replace slider thumb image. 52 | self._thumb = slider.Bar.Slider 53 | self._thumb.Image = kSliderThumbImage 54 | self._thumb.AnchorPoint = Vector2.new(0.5, 0.5) 55 | self._thumb.Size = UDim2.new(0, kThumbSize, 0, kThumbSize) 56 | 57 | -- Add images on bar. 58 | self._preThumbImage = Instance.new("ImageLabel") 59 | self._preThumbImage.Name = "PreThumb" 60 | self._preThumbImage.Parent = slider.Bar 61 | self._preThumbImage.Size = UDim2.new(1, 0, 1, 0) 62 | self._preThumbImage.Position = UDim2.new(0, 0, 0, 0) 63 | self._preThumbImage.Image = kPreThumbImage 64 | self._preThumbImage.BorderSizePixel = 0 65 | 66 | self._postThumbImage = Instance.new("ImageLabel") 67 | self._postThumbImage.Name = "PostThumb" 68 | self._postThumbImage.Parent = slider.Bar 69 | self._postThumbImage.Size = UDim2.new(1, 0, 1, 0) 70 | self._postThumbImage.Position = UDim2.new(0, 0, 0, 0) 71 | self._postThumbImage.Image = kPostThumbImage 72 | self._postThumbImage.BorderSizePixel = 0 73 | 74 | sliderValue.Changed:connect(function() 75 | self._value = sliderValue.Value 76 | 77 | -- Min value is 1. 78 | -- Max value is sliderIntervals. 79 | -- So scale is... 80 | local scale = (self._value - 1)/(sliderIntervals-1) 81 | 82 | self._preThumbImage.Size = UDim2.new(scale, 0, 1, 0) 83 | self._postThumbImage.Size = UDim2.new(1 - scale, 0, 1, 0) 84 | self._postThumbImage.Position = UDim2.new(scale, 0, 0, 0) 85 | 86 | self._thumb.Position = UDim2.new(scale, 0, 87 | 0.5, 0) 88 | 89 | if self._valueChangedFunction then 90 | self._valueChangedFunction(self._value) 91 | end 92 | end) 93 | 94 | self:SetValue(defaultValue) 95 | slider.AnchorPoint = Vector2.new(0, 0.5) 96 | slider.Size = UDim2.new(0, kSliderWidth, 1, 0) 97 | slider.Position = UDim2.new(0, GuiUtilities.StandardLineElementLeftMargin, 0, GuiUtilities.kStandardPropertyHeight/2) 98 | slider.Parent = frame 99 | 100 | return self 101 | end 102 | 103 | function LabeledSliderClass:SetValueChangedFunction(vcf) 104 | self._valueChangedFunction = vcf 105 | end 106 | 107 | function LabeledSliderClass:GetFrame() 108 | return self._frame 109 | end 110 | 111 | function LabeledSliderClass:SetValue(newValue) 112 | if self._sliderValue.Value ~= newValue then 113 | self._sliderValue.Value = newValue 114 | end 115 | end 116 | 117 | function LabeledSliderClass:GetValue() 118 | return self._sliderValue.Value 119 | end 120 | 121 | 122 | return LabeledSliderClass -------------------------------------------------------------------------------- /src/LabeledTextInput.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- LabeledTextInput.lua 4 | -- 5 | -- Creates a frame containing a label and a text input control. 6 | -- 7 | ---------------------------------------- 8 | GuiUtilities = require(script.Parent.GuiUtilities) 9 | 10 | local kTextInputWidth = 100 11 | local kTextBoxInternalPadding = 4 12 | 13 | LabeledTextInputClass = {} 14 | LabeledTextInputClass.__index = LabeledTextInputClass 15 | 16 | function LabeledTextInputClass.new(nameSuffix, labelText, defaultValue) 17 | local self = {} 18 | setmetatable(self, LabeledTextInputClass) 19 | 20 | -- Note: we are using "graphemes" instead of characters. 21 | -- In modern text-manipulation-fu, what with internationalization, 22 | -- emojis, etc, it's not enough to count characters, particularly when 23 | -- concerned with "how many am I rendering?". 24 | -- We are using the 25 | self._MaxGraphemes = 10 26 | 27 | self._valueChangedFunction = nil 28 | 29 | local defaultValue = defaultValue or "" 30 | 31 | local frame = GuiUtilities.MakeStandardFixedHeightFrame('TextInput ' .. nameSuffix) 32 | self._frame = frame 33 | 34 | local label = GuiUtilities.MakeStandardPropertyLabel(labelText) 35 | label.Parent = frame 36 | self._label = label 37 | 38 | self._value = defaultValue 39 | 40 | -- Dumb hack to add padding to text box, 41 | local textBoxWrapperFrame = Instance.new("Frame") 42 | textBoxWrapperFrame.Name = "Wrapper" 43 | textBoxWrapperFrame.Size = UDim2.new(0, kTextInputWidth, 0.6, 0) 44 | textBoxWrapperFrame.Position = UDim2.new(0, GuiUtilities.StandardLineElementLeftMargin, .5, 0) 45 | textBoxWrapperFrame.AnchorPoint = Vector2.new(0, .5) 46 | textBoxWrapperFrame.Parent = frame 47 | GuiUtilities.syncGuiElementInputFieldColor(textBoxWrapperFrame) 48 | GuiUtilities.syncGuiElementBorderColor(textBoxWrapperFrame) 49 | 50 | local textBox = Instance.new("TextBox") 51 | textBox.Parent = textBoxWrapperFrame 52 | textBox.Name = "TextBox" 53 | textBox.Text = defaultValue 54 | textBox.Font = Enum.Font.SourceSans 55 | textBox.TextSize = 15 56 | textBox.BackgroundTransparency = 1 57 | textBox.TextXAlignment = Enum.TextXAlignment.Left 58 | textBox.Size = UDim2.new(1, -kTextBoxInternalPadding, 1, GuiUtilities.kTextVerticalFudge) 59 | textBox.Position = UDim2.new(0, kTextBoxInternalPadding, 0, 0) 60 | textBox.ClipsDescendants = true 61 | 62 | GuiUtilities.syncGuiElementFontColor(textBox) 63 | 64 | textBox:GetPropertyChangedSignal("Text"):connect(function() 65 | -- Never let the text be too long. 66 | -- Careful here: we want to measure number of graphemes, not characters, 67 | -- in the text, and we want to clamp on graphemes as well. 68 | if (utf8.len(self._textBox.Text) > self._MaxGraphemes) then 69 | local count = 0 70 | for start, stop in utf8.graphemes(self._textBox.Text) do 71 | count = count + 1 72 | if (count > self._MaxGraphemes) then 73 | -- We have gone one too far. 74 | -- clamp just before the beginning of this grapheme. 75 | self._textBox.Text = string.sub(self._textBox.Text, 1, start-1) 76 | break 77 | end 78 | end 79 | -- Don't continue with rest of function: the resetting of "Text" field 80 | -- above will trigger re-entry. We don't need to trigger value 81 | -- changed function twice. 82 | return 83 | end 84 | 85 | self._value = self._textBox.Text 86 | if (self._valueChangedFunction) then 87 | self._valueChangedFunction(self._value) 88 | end 89 | end) 90 | 91 | self._textBox = textBox 92 | 93 | return self 94 | end 95 | 96 | function LabeledTextInputClass:SetValueChangedFunction(vcf) 97 | self._valueChangedFunction = vcf 98 | end 99 | 100 | function LabeledTextInputClass:GetFrame() 101 | return self._frame 102 | end 103 | 104 | function LabeledTextInputClass:GetValue() 105 | return self._value 106 | end 107 | 108 | function LabeledTextInputClass:GetMaxGraphemes() 109 | return self._MaxGraphemes 110 | end 111 | 112 | function LabeledTextInputClass:SetMaxGraphemes(newValue) 113 | self._MaxGraphemes = newValue 114 | end 115 | 116 | function LabeledTextInputClass:SetValue(newValue) 117 | if self._value ~= newValue then 118 | self._textBox.Text = newValue 119 | end 120 | end 121 | 122 | return LabeledTextInputClass 123 | -------------------------------------------------------------------------------- /src/StatefulImageButton.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- StatefulImageButton.lua 4 | -- 5 | -- Image button. 6 | -- Has custom image for when "selected" 7 | -- Uses shading to indicate hover and click states. 8 | -- 9 | ---------------------------------------- 10 | GuiUtilities = require(script.Parent.GuiUtilities) 11 | 12 | StatefulImageButtonClass = {} 13 | StatefulImageButtonClass.__index = StatefulImageButtonClass 14 | 15 | function StatefulImageButtonClass.new(buttonName, imageAsset, buttonSize) 16 | local self = {} 17 | setmetatable(self, StatefulImageButtonClass) 18 | 19 | local button = Instance.new("ImageButton") 20 | button.Parent = parent 21 | button.Image = imageAsset 22 | button.BackgroundTransparency = 1 23 | button.BorderSizePixel = 0 24 | button.Size = buttonSize 25 | button.Name = buttonName 26 | 27 | self._button = button 28 | 29 | self._hovered = false 30 | self._clicked = false 31 | self._selected = false 32 | 33 | button.InputBegan:connect(function(input) 34 | if (input.UserInputType == Enum.UserInputType.MouseMovement) then 35 | self._hovered = true 36 | self:_updateButtonVisual() 37 | end 38 | end) 39 | 40 | 41 | button.InputEnded:connect(function(input) 42 | if (input.UserInputType == Enum.UserInputType.MouseMovement) then 43 | self._hovered = false 44 | self._clicked = false 45 | self:_updateButtonVisual() 46 | end 47 | end) 48 | 49 | button.MouseButton1Down:connect(function() 50 | self._clicked = true 51 | self:_updateButtonVisual() 52 | end) 53 | 54 | button.MouseButton1Up:connect(function() 55 | self._clicked = false 56 | self:_updateButtonVisual() 57 | end) 58 | 59 | self:_updateButtonVisual() 60 | 61 | return self 62 | end 63 | 64 | function StatefulImageButtonClass:_updateButtonVisual() 65 | if (self._selected) then 66 | self._button.ImageTransparency = 0 67 | self._button.ImageColor3 = Color3.new(1,1,1) 68 | else 69 | self._button.ImageTransparency = 0.5 70 | self._button.ImageColor3 = Color3.new(.5,.5,.5) 71 | end 72 | 73 | if (self._clicked) then 74 | self._button.BackgroundTransparency = 0.8 75 | elseif (self._hovered) then 76 | self._button.BackgroundTransparency = 0.9 77 | else 78 | self._button.BackgroundTransparency = 1 79 | end 80 | end 81 | 82 | function StatefulImageButtonClass:setSelected(selected) 83 | self._selected = selected 84 | self:_updateButtonVisual() 85 | end 86 | 87 | function StatefulImageButtonClass:getSelected() 88 | return self._selected 89 | end 90 | 91 | function StatefulImageButtonClass:getButton() 92 | return self._button 93 | end 94 | 95 | return StatefulImageButtonClass -------------------------------------------------------------------------------- /src/VerticalScrollingFrame.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- VerticalScrollingFrame.lua 4 | -- 5 | -- Creates a scrolling frame that automatically updates canvas size 6 | -- 7 | ---------------------------------------- 8 | 9 | local GuiUtilities = require(script.Parent.GuiUtilities) 10 | 11 | local VerticalScrollingFrame = {} 12 | VerticalScrollingFrame.__index = VerticalScrollingFrame 13 | 14 | function VerticalScrollingFrame.new(suffix) 15 | local self = {} 16 | setmetatable(self, VerticalScrollingFrame) 17 | 18 | local section = Instance.new("Frame") 19 | section.BorderSizePixel = 0 20 | section.Size = UDim2.new(1, 0, 1, 0) 21 | section.Position = UDim2.new(0, 0, 0, 0) 22 | section.BackgroundTransparency = 1 23 | section.Name = "VerticalScrollFrame" .. suffix 24 | 25 | local scrollBackground = Instance.new("Frame") 26 | scrollBackground.Name = "ScrollbarBackground" 27 | scrollBackground.BackgroundColor3 = Color3.fromRGB(238, 238, 238) 28 | scrollBackground.BorderColor3 = Color3.fromRGB(182, 182, 182) 29 | scrollBackground.Size = UDim2.new(0, 15, 1, -2) 30 | scrollBackground.Position = UDim2.new(1, -16, 0, 1) 31 | scrollBackground.Parent = section 32 | scrollBackground.ZIndex = 2; 33 | 34 | local scrollFrame = Instance.new("ScrollingFrame") 35 | scrollFrame.Name = "ScrollFrame" .. suffix 36 | scrollFrame.VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right 37 | scrollFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar 38 | scrollFrame.ElasticBehavior = Enum.ElasticBehavior.Never 39 | scrollFrame.ScrollBarThickness = 17 40 | scrollFrame.BorderSizePixel = 0 41 | scrollFrame.BackgroundTransparency = 1 42 | scrollFrame.ZIndex = 2 43 | scrollFrame.TopImage = "http://www.roblox.com/asset/?id=1533255544" 44 | scrollFrame.MidImage = "http://www.roblox.com/asset/?id=1535685612" 45 | scrollFrame.BottomImage = "http://www.roblox.com/asset/?id=1533256504" 46 | scrollFrame.Size = UDim2.new(1, 0, 1, 0) 47 | scrollFrame.Position = UDim2.new(0, 0, 0, 0) 48 | scrollFrame.Parent = section 49 | 50 | local uiListLayout = Instance.new("UIListLayout") 51 | uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder 52 | uiListLayout.Parent = scrollFrame 53 | 54 | self._section = section 55 | self._scrollFrame = scrollFrame 56 | self._scrollBackground = scrollBackground 57 | self._uiListLayout = uiListLayout 58 | 59 | scrollFrame:GetPropertyChangedSignal("AbsoluteSize"):connect(function() self:_updateScrollingFrameCanvas() end) 60 | uiListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):connect(function() self:_updateScrollingFrameCanvas() end) 61 | self:_updateScrollingFrameCanvas() 62 | 63 | GuiUtilities.syncGuiElementScrollColor(scrollFrame) 64 | GuiUtilities.syncGuiElementBorderColor(scrollBackground) 65 | GuiUtilities.syncGuiElementTitleColor(scrollBackground) 66 | 67 | return self 68 | end 69 | 70 | function VerticalScrollingFrame:_updateScrollbarBackingVisibility() 71 | self._scrollBackground.Visible = self._scrollFrame.AbsoluteSize.y < self._uiListLayout.AbsoluteContentSize.y 72 | end 73 | 74 | function VerticalScrollingFrame:_updateScrollingFrameCanvas() 75 | self._scrollFrame.CanvasSize = UDim2.new(0, 0, 0, self._uiListLayout.AbsoluteContentSize.Y) 76 | self:_updateScrollbarBackingVisibility() 77 | end 78 | 79 | function VerticalScrollingFrame:GetContentsFrame() 80 | return self._scrollFrame 81 | end 82 | 83 | function VerticalScrollingFrame:GetSectionFrame() 84 | return self._section 85 | end 86 | 87 | return VerticalScrollingFrame -------------------------------------------------------------------------------- /src/VerticallyScalingListFrame.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | -- 3 | -- VerticallyScalingListFrame 4 | -- 5 | -- Creates a frame that organizes children into a list layout. 6 | -- Will scale dynamically as children grow. 7 | -- 8 | ---------------------------------------- 9 | GuiUtilities = require(script.Parent.GuiUtilities) 10 | 11 | VerticallyScalingListFrameClass = {} 12 | VerticallyScalingListFrameClass.__index = VerticallyScalingListFrameClass 13 | 14 | local kBottomPadding = 10 15 | 16 | function VerticallyScalingListFrameClass.new(nameSuffix) 17 | local self = {} 18 | setmetatable(self, VerticallyScalingListFrameClass) 19 | 20 | self._resizeCallback = nil 21 | 22 | local frame = Instance.new('Frame') 23 | frame.Name = 'VSLFrame' .. nameSuffix 24 | frame.Size = UDim2.new(1, 0, 0, height) 25 | frame.BackgroundTransparency = 0 26 | frame.BorderSizePixel = 0 27 | GuiUtilities.syncGuiElementBackgroundColor(frame) 28 | 29 | self._frame = frame 30 | 31 | local uiListLayout = Instance.new('UIListLayout') 32 | uiListLayout.SortOrder = Enum.SortOrder.LayoutOrder 33 | uiListLayout.Parent = frame 34 | self._uiListLayout = uiListLayout 35 | 36 | local function updateSizes() 37 | self._frame.Size = UDim2.new(1, 0, 0, uiListLayout.AbsoluteContentSize.Y) 38 | if (self._resizeCallback) then 39 | self._resizeCallback() 40 | end 41 | end 42 | self._uiListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):connect(updateSizes) 43 | updateSizes() 44 | 45 | self._childCount = 0 46 | 47 | return self 48 | end 49 | 50 | function VerticallyScalingListFrameClass:AddBottomPadding() 51 | local frame = Instance.new("Frame") 52 | frame.Name = "BottomPadding" 53 | frame.BackgroundTransparency = 1 54 | frame.Size = UDim2.new(1, 0, 0, kBottomPadding) 55 | frame.LayoutOrder = 1000 56 | frame.Parent = self._frame 57 | end 58 | 59 | function VerticallyScalingListFrameClass:GetFrame() 60 | return self._frame 61 | end 62 | 63 | function VerticallyScalingListFrameClass:AddChild(childFrame) 64 | childFrame.LayoutOrder = self._childCount 65 | self._childCount = self._childCount + 1 66 | childFrame.Parent = self._frame 67 | end 68 | 69 | function VerticallyScalingListFrameClass:SetCallbackOnResize(callback) 70 | self._resizeCallback = callback 71 | end 72 | 73 | return VerticallyScalingListFrameClass --------------------------------------------------------------------------------